diff --git a/MODULE.bazel b/MODULE.bazel index 9ecdff3970..ec7a92b69a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -140,6 +140,10 @@ use_repo(perfetto_ext, "perfetto_cfg") # Perfetto requires this to get through bazel analysis, but seems unused for our use. bazel_dep(name = "rules_android", version = "0.6.0") +# SPIN model checker — used by xls/spin to verify Promela models generated from XLS IR. +spin_ext = use_extension("//dependency_support/spin:extension.bzl", "spin_extension") +use_repo(spin_ext, "spin") + # Pin this as otherwise we encounter some bazel analysis failure. bazel_dep(name = "rules_jvm_external", version = "6.8") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 5fc27c31ba..1e6acd1d03 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -298,9 +298,6 @@ "https://bcr.bazel.build/modules/gazelle/0.46.0/source.json": "f255441117f6c63a3cbc0d4fd84c09c047e54a9bdaaf6aedf66e3b4218ddebd4", "https://bcr.bazel.build/modules/glpk/5.0.bcr.2/MODULE.bazel": "d539aa4b146ae743e51eba2e42851e27580a34edd51d82fec4738edc20e3d1d8", "https://bcr.bazel.build/modules/glpk/5.0.bcr.2/source.json": "624382187f52acfb739e560922ec6c3b6d7ca0036c82c8333c810505273970cf", - "https://bcr.bazel.build/modules/gmp/6.3.0.bcr.1/MODULE.bazel": "f9a5442767c9082f25d05c01e20040ea06302d654f2ca9da50674d15b3d1a2dd", - "https://bcr.bazel.build/modules/gmp/6.3.0.bcr.1/source.json": "a98a4d4944ca1d45d4c860bfe41d217fb0dc9734af357ed78d8b522c5bba4b37", - "https://bcr.bazel.build/modules/gmp/6.3.0/MODULE.bazel": "81e36099789b2a1160372f73155be37ae382c1e593b4a2ce51348bb722735a59", "https://bcr.bazel.build/modules/google_bazel_common/0.0.1/MODULE.bazel": "5b364f7ca35cc87851b625cd9ec2b86bd6d3fbe9724646f28a5254d213f1c05b", "https://bcr.bazel.build/modules/google_bazel_common/0.0.1/source.json": "399a40d8acfb3a592f38717d27457900cc9540da8ddcff936584d0b590e3386d", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", @@ -365,12 +362,8 @@ "https://bcr.bazel.build/modules/linenoise/2.0.0/source.json": "046b7e7d9f384a1d7fd9b65e0aa97a12d2835f351d5fab918f91b30262d37633", "https://bcr.bazel.build/modules/lz4/1.9.4/MODULE.bazel": "e3d307b1d354d70f6c809167eafecf5d622c3f27e3971ab7273410f429c7f83a", "https://bcr.bazel.build/modules/lz4/1.9.4/source.json": "233f0bdfc21f254e3dda14683ddc487ca68c6a3a83b7d5db904c503f85bd089b", - "https://bcr.bazel.build/modules/m4/1.4.21.bcr.1/MODULE.bazel": "d89395ff2ea032b3aaf0a6c08bbddbccdbbfd6644dbafd946f65c487cdf060b3", - "https://bcr.bazel.build/modules/m4/1.4.21.bcr.1/source.json": "6693ed9c3380e29bbe5200c0e2fa4635b38b2b7cf8c1cb527fc3109456d3ca69", "https://bcr.bazel.build/modules/mbedtls/3.6.0/MODULE.bazel": "8e380e4698107c5f8766264d4df92e36766248447858db28187151d884995a09", "https://bcr.bazel.build/modules/mbedtls/3.6.0/source.json": "1dbe7eb5258050afcc3806b9d43050f71c6f539ce0175535c670df606790b30c", - "https://bcr.bazel.build/modules/mpfr/4.2.2/MODULE.bazel": "a27d7763535094f9fb14c9d8e7e559e2b5a66b8beaa1b361e46da79a0783616f", - "https://bcr.bazel.build/modules/mpfr/4.2.2/source.json": "23c8cc076a26015225ec2da01b963df77181e1bff134a9b30f93d259f443bff6", "https://bcr.bazel.build/modules/nlohmann_json/3.11.3.bcr.1/MODULE.bazel": "83bbe365b1eb640ef903df2240f11e7df8f70563199bc17085816033bc36da89", "https://bcr.bazel.build/modules/nlohmann_json/3.11.3/MODULE.bazel": "87023db2f55fc3a9949c7b08dc711fae4d4be339a80a99d04453c4bb3998eefc", "https://bcr.bazel.build/modules/nlohmann_json/3.12.0.bcr.1/MODULE.bazel": "a1c8bb07b5b91d971727c635f449d05623ac9608f6fe4f5f04254ea12f08e349", @@ -491,9 +484,6 @@ "https://bcr.bazel.build/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162", "https://bcr.bazel.build/modules/rules_cc/0.2.11/MODULE.bazel": "e94f24f065bf2191dba2dace951814378b66a94bb3bcc48077492fe0508059b5", "https://bcr.bazel.build/modules/rules_cc/0.2.11/source.json": "4d555dc20c9c135b21b2e403cf0ce8393fb65711b2305979ce053df4ee3e78de", - "https://bcr.bazel.build/modules/rules_cc_autoconf/0.10.1/MODULE.bazel": "9d437cf6abd311a8cbb464bdb987411ed441714074444ae3963ee94baffb3522", - "https://bcr.bazel.build/modules/rules_cc_autoconf/0.10.3/MODULE.bazel": "936257270b147ea6c17633a7f682a87a915782c45059263811ed895375f2dba9", - "https://bcr.bazel.build/modules/rules_cc_autoconf/0.10.3/source.json": "a025eecbbaceb6b8d1d578b382b6f2ebd4b108cd1e02c375d22397b422747b5f", "https://bcr.bazel.build/modules/rules_closure/0.15.0/MODULE.bazel": "2346f8f2daefe805bb12d5a97218d33cd02fd10732eb2de8e94ca88b00efdd68", "https://bcr.bazel.build/modules/rules_closure/0.15.0/source.json": "c477734278ed50eef6d698fbbb2ac0ef8583257a7c0b05ea55d145da4acadb4e", "https://bcr.bazel.build/modules/rules_flex/0.3/MODULE.bazel": "7a8d6cfb459c0368014a8305c406de69d18a3f7d0c1ae45df829af7da6d4c195", @@ -694,70 +684,6 @@ }, "selectedYankedVersions": {}, "moduleExtensions": { - "//dependency_support/bitwuzla:extension.bzl%bitwuzla_extension": { - "general": { - "bzlTransitiveDigest": "+F1jQc3G+VvtEAnQFxQK5+xH7M3KMtbGSDJfvEDbjDo=", - "usagesDigest": "PN9ToO8V5otkYkA5UnAzURX+LBirU5veji+2wrYsjK4=", - "recordedFileInputs": {}, - "recordedDirentsInputs": {}, - "envVariables": {}, - "generatedRepoSpecs": { - "cadical": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/arminbiere/cadical/archive/3ff42f04384489916f017acd6d5e7cbfa7257be7.tar.gz" - ], - "strip_prefix": "cadical-3ff42f04384489916f017acd6d5e7cbfa7257be7", - "integrity": "sha256-+w7OK9Q2I6/HOPqUXFfRMM7ipu9lFoNcUQWoUDGSogU=", - "build_file": "@@//dependency_support/bitwuzla:cadical.BUILD.bazel" - } - }, - "symfpu": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/martin-cs/symfpu/archive/8fbe139bf0071cbe0758d2f6690a546c69ff0053.tar.gz" - ], - "strip_prefix": "symfpu-8fbe139bf0071cbe0758d2f6690a546c69ff0053", - "integrity": "sha256-2guu77hPsArUStaZPZfC5sw1aPmu0NnoaTqmtZl1Lls=", - "build_file": "@@//dependency_support/bitwuzla:symfpu.BUILD.bazel" - } - }, - "bitwuzla": { - "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", - "ruleClassName": "http_archive", - "attributes": { - "urls": [ - "https://github.com/bitwuzla/bitwuzla/archive/3f5d9cd11dda80626b23bc2b353768abd6906f58.tar.gz" - ], - "strip_prefix": "bitwuzla-3f5d9cd11dda80626b23bc2b353768abd6906f58", - "build_file": "@@//dependency_support/bitwuzla:bundled.BUILD.bazel", - "integrity": "sha256-8EoJYNCds0Fkuw9jKEHL47LsdCJ9d03750w+TFHSquA=", - "patches": [ - "@@//dependency_support/bitwuzla:config.patch", - "@@//dependency_support/bitwuzla:msan.patch" - ], - "patch_args": [ - "-p1" - ], - "patch_cmds": [ - "\n find . -type f \\( -name \"*.cpp\" -o -name \"*.h\" \\) | while read -r file; do\n # Avoid self-patching config.h\n if [ \"$file\" = \"./src/config.h\" ]; then continue; fi\n \n if grep -q '#include \"config.h\"' \"$file\"; then\n # Construct the relative path to the repository root:\n # strip \"./\", then create a string with a \"../\" for each slash in the path.\n clean_path=${file#./}\n relative_root=$(echo \"$clean_path\" | tr -cd '/' | sed 's|/|../|g')\n \n sed 's|#include \"config.h\"|#include \"'\"${relative_root}\"'src/config.h\"|g' \"$file\" > \"$file.tmp\" && mv \"$file.tmp\" \"$file\"\n fi\n done\n" - ] - } - } - }, - "recordedRepoMappingEntries": [ - [ - "", - "bazel_tools", - "bazel_tools" - ] - ] - } - }, "//dependency_support/com_github_facebook_zstd:extension.bzl%zstd_extension": { "general": { "bzlTransitiveDigest": "3Gk4jrDeuAvwC/9h5g6ke9hYB6B8hmi3VlRx/Fvx5+U=", @@ -820,7 +746,7 @@ }, "//dependency_support/llvm:extension.bzl%llvm_raw_ext": { "general": { - "bzlTransitiveDigest": "yDyvvV65EuVA/p2C1UAFCbxOdUmKwZQP8HvCzi66dYE=", + "bzlTransitiveDigest": "KfR/V9I3k/JetM989sGJAWNutJY8koUY2+xHpIoWNT0=", "usagesDigest": "RRrequOfgwo6eIf1sG5DKJ5RMOK/Amf3nxP0UZMhsHE=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -831,18 +757,19 @@ "ruleClassName": "http_archive", "attributes": { "build_file_content": "# empty", - "sha256": "de1e67f7290d93340a408ba5841966d129127de2104c304d7f761a4de101ece2", + "sha256": "dd594a2c2a1af4713349d103d83fae765843ff9874e3d37cbd3b05f9da48a0df", "patches": [ "@@//dependency_support/llvm:llvm.patch", "@@//dependency_support/llvm:zlib-header.patch", - "@@//dependency_support/llvm:run_lit.patch" + "@@//dependency_support/llvm:run_lit.patch", + "@@//dependency_support/llvm:llvm_repo_metadata.patch" ], "patch_args": [ "-p1" ], - "strip_prefix": "llvm-project-93b1f81e03ef4c717a9d222b46f78c38bcff0a3f", + "strip_prefix": "llvm-project-140fc5aa2a0a754db87c68b2e3861c70dd94360b", "urls": [ - "https://github.com/llvm/llvm-project/archive/93b1f81e03ef4c717a9d222b46f78c38bcff0a3f.tar.gz" + "https://github.com/llvm/llvm-project/archive/140fc5aa2a0a754db87c68b2e3861c70dd94360b.tar.gz" ] } } @@ -928,6 +855,44 @@ ] } }, + "//dependency_support/spin:extension.bzl%spin_extension": { + "general": { + "bzlTransitiveDigest": "TZBQCPtSdzsd/B9tRxi1SR7clp415rear01/48ymQlA=", + "usagesDigest": "OY+qzTHfABqiBjmwfd5DPWD0TSZmD+oOuML4yDuyEco=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "spin": { + "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", + "ruleClassName": "http_archive", + "attributes": { + "sha256": "e2ba8cfebf963cb524be2a3caee68821472602f9c4f0e557f2550d8e382eab99", + "strip_prefix": "Spin-master", + "urls": [ + "https://github.com/nimble-code/Spin/archive/refs/heads/master.tar.gz" + ], + "build_file": "@@//dependency_support/spin:bundled.BUILD.bazel", + "patches": [ + "@@//dependency_support/spin/patches:spin_json_trace.patch", + "@@//dependency_support/spin/patches:spin_trail_and_exit.patch" + ], + "patch_args": [ + "-p1", + "--fuzz=3" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "//dependency_support/z3:extension.bzl%z3_extension": { "general": { "bzlTransitiveDigest": "qu/tfQRAZhNUvlq6u2XDvfhIcWFScGpF3KrIx02SlwM=", diff --git a/dependency_support/spin/BUILD.bazel b/dependency_support/spin/BUILD.bazel new file mode 100644 index 0000000000..87e73ced80 --- /dev/null +++ b/dependency_support/spin/BUILD.bazel @@ -0,0 +1,17 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Support files for the SPIN model-checker dependency. + +exports_files(["bundled.BUILD.bazel"]) diff --git a/dependency_support/spin/bundled.BUILD.bazel b/dependency_support/spin/bundled.BUILD.bazel new file mode 100644 index 0000000000..dcb4761001 --- /dev/null +++ b/dependency_support/spin/bundled.BUILD.bazel @@ -0,0 +1,94 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build rules for the SPIN model checker. +# https://spinroot.com / https://github.com/nimble-code/Spin + +load("@rules_cc//cc:defs.bzl", "cc_binary") + +licenses(["notice"]) # BSD-like, see Src/LICENSE + +exports_files(["Src/LICENSE"]) + +package(default_visibility = ["//visibility:public"]) + +# Process spin.y with bison; produces y.tab.c and y.tab.h. +genrule( + name = "spin_yacc_gen", + srcs = ["Src/spin.y"], + outs = [ + "y.tab.c", + "y.tab.h", + ], + cmd = "M4=$(M4) $(BISON) --yacc --defines=$(location y.tab.h) -o $(location y.tab.c) $(location Src/spin.y) 2>/dev/null", + toolchains = [ + "@rules_bison//bison:current_bison_toolchain", + "@rules_m4//m4:current_m4_toolchain", + ], + visibility = ["//visibility:private"], +) + +cc_binary( + name = "spin", + srcs = [ + "Src/dstep.c", + "Src/flow.c", + "Src/guided.c", + "Src/main.c", + "Src/mesg.c", + "Src/msc_tcl.c", + "Src/pangen1.c", + "Src/pangen1.h", + "Src/pangen2.c", + "Src/pangen2.h", + "Src/pangen3.c", + "Src/pangen3.h", + "Src/pangen4.c", + "Src/pangen4.h", + "Src/pangen5.c", + "Src/pangen5.h", + "Src/pangen6.c", + "Src/pangen6.h", + "Src/pangen7.c", + "Src/pangen7.h", + "Src/reprosrc.c", + "Src/run.c", + "Src/sched.c", + "Src/spin.h", + "Src/spinlex.c", + "Src/structs.c", + "Src/sym.c", + "Src/tl.h", + "Src/tl_buchi.c", + "Src/tl_cache.c", + "Src/tl_lex.c", + "Src/tl_main.c", + "Src/tl_mem.c", + "Src/tl_parse.c", + "Src/tl_rewrt.c", + "Src/tl_trans.c", + "Src/vars.c", + "Src/version.h", + ":spin_yacc_gen", + ], + copts = [ + "-O2", + "-DNXT", + "-w", + ], + includes = [ + ".", + "Src", + ], +) diff --git a/dependency_support/spin/extension.bzl b/dependency_support/spin/extension.bzl new file mode 100644 index 0000000000..35412f8a6e --- /dev/null +++ b/dependency_support/spin/extension.bzl @@ -0,0 +1,39 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module extension for the SPIN model checker (https://spinroot.com).""" + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +def _spin_extension_impl(_ctx): + http_archive( + name = "spin", + sha256 = "e2ba8cfebf963cb524be2a3caee68821472602f9c4f0e557f2550d8e382eab99", + strip_prefix = "Spin-master", + urls = ["https://github.com/nimble-code/Spin/archive/refs/heads/master.tar.gz"], + build_file = Label("//dependency_support/spin:bundled.BUILD.bazel"), + patches = [ + # Adds -Q : emits one JSON line per channel event during + # simulation, used by xls/spin/ to verify equivalence of channel + # communication between the Promela model and the DSLX source proc. + # SPIN has no built-in machine-readable channel output. + Label("//dependency_support/spin/patches:spin_json_trace.patch"), + # Adds -K: pan writes .trail files into instead of cwd. + # Adds -H: pan exits with code 1 when errors > 0 (default is 0). + Label("//dependency_support/spin/patches:spin_trail_and_exit.patch"), + ], + patch_args = ["-p1", "--fuzz=3"], + ) + +spin_extension = module_extension(implementation = _spin_extension_impl) diff --git a/dependency_support/spin/patches/BUILD.bazel b/dependency_support/spin/patches/BUILD.bazel new file mode 100644 index 0000000000..273769c48b --- /dev/null +++ b/dependency_support/spin/patches/BUILD.bazel @@ -0,0 +1,15 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +exports_files(["spin_json_trace.patch"]) diff --git a/dependency_support/spin/patches/spin_json_trace.patch b/dependency_support/spin/patches/spin_json_trace.patch new file mode 100644 index 0000000000..e4de47568a --- /dev/null +++ b/dependency_support/spin/patches/spin_json_trace.patch @@ -0,0 +1,98 @@ +--- a/Src/main.c 2025-09-18 20:24:08.000000000 +0200 ++++ b/Src/main.c 2026-06-17 15:50:14.726424007 +0200 +@@ -50,6 +50,7 @@ + int old_scope_rules; /* use pre 5.3.0 rules */ + int old_priority_rules; /* use pre 6.2.0 rules */ + int product, Strict; ++FILE *json_trace_fp = NULL; /* output file for -Q JSON channel trace */ + short replay; + + int merger = 1, deadvar = 1, implied_semis = 1; +@@ -267,6 +268,10 @@ + #else + struct stat x; + #endif ++ if (json_trace_fp) ++ { fclose(json_trace_fp); ++ json_trace_fp = NULL; ++ } + if (preprocessonly == 0 && strlen(out1) > 0) + { (void) unlink((const char *) out1); + } +@@ -941,6 +946,18 @@ + alldone(0); + } + verbose += 4; break; ++ case 'Q': if (argc < 3) ++ { fprintf(stderr, ++ "spin: -Q requires an output filename\n"); ++ alldone(1); ++ } ++ json_trace_fp = fopen(argv[2], "w"); ++ if (!json_trace_fp) ++ { fprintf(stderr, ++ "spin: cannot open '%s'\n", argv[2]); ++ alldone(1); ++ } ++ argc--; argv++; break; + case 'q': if (isdigit((int) argv[1][2])) + qhide(atoi(&argv[1][2])); + break; +--- a/Src/spin.h 2025-09-18 20:24:08.000000000 +0200 ++++ b/Src/spin.h 2026-06-17 15:50:14.726424007 +0200 +@@ -102,6 +102,7 @@ + int *contents; /* the values stored */ + int *stepnr; /* depth when each msg was sent */ + char **mtp; /* if mtype, name of list, else 0 */ ++ char *cname; /* physical channel name, set in qmake */ + struct Queue *nxt; /* linked list */ + } Queue; + +--- a/Src/mesg.c 2025-09-18 20:24:08.000000000 +0200 ++++ b/Src/mesg.c 2026-06-17 21:55:41.781648379 +0200 +@@ -20,6 +20,7 @@ + extern int verbose, TstOnly, s_trail, analyze, columns; + extern int lineno, depth, xspin, m_loss, jumpsteps; + extern int nproc, nstop; ++extern FILE *json_trace_fp; /* set by -Q flag; emit JSON channel trace */ + extern short Have_claim; + + QH *qh_lst; +@@ -78,6 +79,7 @@ + q->nslots = s->ini->val; + q->nflds = cnt_mpars(s->ini->rgt); + q->setat = depth; ++ q->cname = s->name; + + i = max(1, q->nslots); /* 0-slot qs get 1 slot minimum */ + j = q->nflds * i; +@@ -337,6 +339,14 @@ + /* may 30, 2023: the field types are 0..nflds, not i+0..nflds */ + typ_ck(q->fld_width[j], Sym_typ(m->lft), "send"); + } ++ if (json_trace_fp && q->nflds == 1) ++ { fprintf(json_trace_fp, ++ "{\"channel_name\":\"%s\"," ++ "\"direction\":\"SEND\",\"value\":%d}\n", ++ q->cname ? q->cname : "", ++ q->contents[i]); ++ fflush(json_trace_fp); ++ } + + if ((verbose&16) && depth >= jumpsteps) + { for (i = j; i < q->nflds; i++) +@@ -429,6 +439,14 @@ + } + + oi = q->stepnr[i]; ++ if (json_trace_fp && full && n->val < 2 && q->nflds == 1 && !Rvous) ++ { fprintf(json_trace_fp, ++ "{\"channel_name\":\"%s\"," ++ "\"direction\":\"RECV\",\"value\":%d}\n", ++ q->cname ? q->cname : "", ++ q->contents[i]); ++ fflush(json_trace_fp); ++ } + for (m = n->rgt, j = 0; m && j < q->nflds; m = m->rgt, j++) + { if (columns && !full) /* was columns == 1 */ + continue; diff --git a/dependency_support/spin/patches/spin_trail_and_exit.patch b/dependency_support/spin/patches/spin_trail_and_exit.patch new file mode 100644 index 0000000000..64bbd19514 --- /dev/null +++ b/dependency_support/spin/patches/spin_trail_and_exit.patch @@ -0,0 +1,42 @@ +--- a/Src/pangen2.c 2025-09-18 20:24:08.000000000 +0200 ++++ b/Src/pangen2.c 2026-06-22 10:00:00.000000000 +0200 +@@ -474,6 +474,8 @@ + if (separate != 2) + { fprintf(fd_tc, "char *TrailFile = PanSource; /* default */\n"); + fprintf(fd_tc, "char *trailfilename;\n"); ++ fprintf(fd_tc, "char *trail_output_dir = NULL;\n"); ++ fprintf(fd_tc, "int halt_on_error = 0;\n"); + } + + fprintf(fd_tc, "#ifdef LOOPSTATE\n"); +--- a/Src/pangen1.h 2025-09-18 20:24:08.000000000 +0200 ++++ b/Src/pangen1.h 2026-06-22 10:00:00.000000000 +0200 +@@ -996,6 +996,11 @@ + " sprintf(fnm, \"%%s.%%s\", MyFile, tprefix);", + "#endif", + " }", ++ " if (trail_output_dir)", ++ " { char tmp[1024];", ++ " snprintf(tmp, sizeof(tmp), \"%%s/%%s\", trail_output_dir, fnm);", ++ " strcpy(fnm, tmp);", ++ " }", + " if ((fd = open(fnm, w_flags, TMODE)) < 0)", + " { if ((q = strchr(MyFile, '.')))", + " { *q = '\\0';", +@@ -4686,7 +4691,7 @@ + "#if NCORE>1 && defined(T_ALERT)", + " crash_report();", + "#endif", + "#ifndef BFS_PAR", +- " pan_exit(0);", ++ " pan_exit(errors > 0 && halt_on_error ? 1 : 0);", + "#endif", + "}\n", +@@ -5468,6 +5473,8 @@ + " break;", + "#endif", ++ " case 'H': halt_on_error = 1; break;", ++ " case 'K': trail_output_dir = &argv[1][2]; break;", + " case 'q': strict = 1; break;", + " case 'R':", + " if (argv[1][2] == 'S') /* e.g., -RS76842 */", diff --git a/xls/build_rules/xls_dslx_test.bzl b/xls/build_rules/xls_dslx_test.bzl index 3d1b520b1e..54984958a1 100644 --- a/xls/build_rules/xls_dslx_test.bzl +++ b/xls/build_rules/xls_dslx_test.bzl @@ -57,6 +57,24 @@ xls_dslx_test_common_attrs = { "'compare' dslx_test_arg is only available with the default 'dslx-interpreter' " + "evaluator.", ), + "spin_guided": attr.bool( + default = False, + doc = "Runs SPIN guided simulation on the DSLX design. " + + "Converts DSLX to SPIN model, executes one concrete " + + "simulation path and compares the resulting channel trace with " + + "the DSLX interpreter trace." + ), + "spin_exhaustive": attr.bool( + default = False, + doc = "Runs SPIN exhaustive verification on the DSLX design. " + + "Converts DSLX to SPIN model and performs full state-space " + + "exploration. Fails on any SPIN assertion violation. " + ), + "trace_channels": attr.bool( + default = False, + doc = "Enables logging of channel send/receive events in the DSLX " + + "interpreter during execution." + ), } def get_dslx_test_cmd(ctx, src_files_to_test): @@ -76,10 +94,14 @@ def get_dslx_test_cmd(ctx, src_files_to_test): dslx_interpreter_tool_runfiles = ( ctx.attr._xls_dslx_interpreter_tool[DefaultInfo].default_runfiles ) + extra_runfiles = [dslx_interpreter_tool_runfiles] + needs_spin = ctx.attr.spin_guided or ctx.attr.spin_exhaustive + if needs_spin: + extra_runfiles.append(ctx.attr._spin[DefaultInfo].default_runfiles) runfiles = get_runfiles_for_xls( ctx, - [dslx_interpreter_tool_runfiles], - src_files_to_test, + extra_runfiles, + src_files_to_test + ([ctx.executable._spin] if needs_spin else []), ) cmds = [] @@ -148,11 +170,19 @@ def _get_dslx_test_cmdline(ctx, src, all_srcs, append_cmd_line_args = True): dslx_test_args["evaluator"] = ctx.attr.evaluator my_args = args_to_string(dslx_test_args) - cmd = "{} {} {}".format( + extra_flags = [] + if ctx.attr.trace_channels and not ctx.attr.spin_guided: + extra_flags.append("--trace_channels") + if ctx.attr.spin_guided: + extra_flags.append("--spin_guided") + if ctx.attr.spin_exhaustive: + extra_flags.append("--spin_exhaustive") + cmd = "{} {} {} {}".format( dslx_interpreter_tool.short_path, src.short_path, my_args, - ) + " ".join(extra_flags), + ).strip() # Append command-line arguments. if append_cmd_line_args: @@ -203,6 +233,13 @@ xls_dslx_test = rule( xls_dslx_library_as_input_attrs, xls_dslx_test_common_attrs, dicts.pick(xls_toolchain_attrs, ["_xls_dslx_interpreter_tool"]), + { + "_spin": attr.label( + default = Label("@spin//:spin"), + executable = True, + cfg = "target", + ), + }, ), test = True, ) diff --git a/xls/dslx/BUILD b/xls/dslx/BUILD index c7a3283e77..3e198305be 100644 --- a/xls/dslx/BUILD +++ b/xls/dslx/BUILD @@ -640,6 +640,7 @@ cc_binary( "//xls/dslx/run_routines:test_xml", "//xls/ir:evaluator_result_cc_proto", "//xls/ir:format_preference", + "//xls/spin:spin_runner", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/log", "@com_google_absl//absl/log:check", diff --git a/xls/dslx/bytecode/BUILD b/xls/dslx/bytecode/BUILD index 5ba98bec50..e2b003c8bf 100644 --- a/xls/dslx/bytecode/BUILD +++ b/xls/dslx/bytecode/BUILD @@ -292,6 +292,7 @@ cc_library( "//xls/ir:evaluator_result_cc_proto", "//xls/ir:format_preference", "//xls/ir:format_strings", + "@com_google_absl//absl/base", "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/log", "@com_google_absl//absl/log:check", diff --git a/xls/dslx/bytecode/bytecode_emitter.cc b/xls/dslx/bytecode/bytecode_emitter.cc index f3b8498230..5795df59ca 100644 --- a/xls/dslx/bytecode/bytecode_emitter.cc +++ b/xls/dslx/bytecode/bytecode_emitter.cc @@ -819,9 +819,10 @@ absl::Status BytecodeEmitter::HandleChannelDecl(const ChannelDecl* node) { XLS_RET_CHECK(tuple_type != nullptr); XLS_RET_CHECK_EQ(tuple_type->size(), 2); - XLS_ASSIGN_OR_RETURN(auto in_out_channels, - CreateChannelReferencePair(&tuple_type->GetMemberType(0), - channel_instance_allocator_)); + XLS_ASSIGN_OR_RETURN( + auto in_out_channels, + CreateChannelReferencePair(&tuple_type->GetMemberType(0), + channel_instance_allocator_, node)); Add(Bytecode::MakeLiteral( node->span(), diff --git a/xls/dslx/bytecode/bytecode_interpreter.cc b/xls/dslx/bytecode/bytecode_interpreter.cc index ff9a78a384..4033676f08 100644 --- a/xls/dslx/bytecode/bytecode_interpreter.cc +++ b/xls/dslx/bytecode/bytecode_interpreter.cc @@ -27,6 +27,7 @@ #include #include +#include "absl/base/casts.h" #include "absl/log/check.h" #include "absl/log/die_if_null.h" #include "absl/log/log.h" @@ -69,6 +70,23 @@ namespace xls::dslx { namespace { +std::string_view ChannelNameFromRef(const InterpValue::ChannelReference& ref) { + if (!ref.GetDefiner().has_value() || *ref.GetDefiner() == nullptr) { + return ""; + } + const AstNode* definer = *ref.GetDefiner(); + if (definer->kind() == AstNodeKind::kChannelDecl) { + const auto* decl = absl::down_cast(definer); + if (decl->channel_name_expr().kind() == AstNodeKind::kString) { + return absl::down_cast(&decl->channel_name_expr())->text(); + } + } + if (definer->kind() == AstNodeKind::kParam) { + return absl::down_cast(definer)->identifier(); + } + return ""; +} + // Returns the given InterpValue formatted using the given format descriptor (if // it is not null). absl::StatusOr ToStringMaybeFormatted( @@ -235,9 +253,13 @@ void DslxInterpreterEvents::AddTraceChannelMessage( const char* verb = direction == ChannelDirection::kIn ? "Received" : "Sent"; std::string msg = absl::StrFormat("%s data on channel `%s`:\n%s", verb, channel_name, formatted_data); - // TODO(meheff): Add trace channel proto variant and use here. TraceMessageProto* tm = proto_.add_trace_msgs(); tm->set_message(msg); + tm->mutable_channel()->set_channel_name(std::string{channel_name}); + tm->mutable_channel()->set_direction(direction == ChannelDirection::kIn + ? TraceChannelProto::RECV + : TraceChannelProto::SEND); + *tm->mutable_channel()->mutable_value() = value.AsProto().value(); SetLocationProto(tm->mutable_location(), file_table, source_location); NoteTraceMessageString(file_table, source_location, std::move(msg)); } @@ -1330,8 +1352,8 @@ absl::Status BytecodeInterpreter::EvalRecvNonBlocking( if (options_.trace_channels() && events_.has_value()) { (*events_)->AddTraceChannelMessage( import_data_->file_table(), bytecode.source_span(), - FormatChannelNameForTracing(*channel_data), value, - ChannelDirection::kIn, channel_data->value_fmt_desc()); + FormatChannelNameForTracing(ChannelNameFromRef(channel_reference)), + value, ChannelDirection::kIn, channel_data->value_fmt_desc()); } stack_.Push(InterpValue::MakeTuple( {token, std::move(value), InterpValue::MakeBool(true)})); @@ -1364,7 +1386,7 @@ absl::Status BytecodeInterpreter::EvalRecv(const Bytecode& bytecode) { stack_.Push(condition); stack_.Push(default_value); blocked_channel_info_ = BlockedChannelInfo{ - .name = FormatChannelNameForTracing(*channel_data), + .name = FormatChannelNameForTracing(channel_data->channel_name()), .span = bytecode.source_span(), }; return absl::UnavailableError("Channel is empty."); @@ -1375,8 +1397,8 @@ absl::Status BytecodeInterpreter::EvalRecv(const Bytecode& bytecode) { if (options_.trace_channels() && events_.has_value()) { (*events_)->AddTraceChannelMessage( import_data_->file_table(), bytecode.source_span(), - FormatChannelNameForTracing(*channel_data), value, - ChannelDirection::kIn, channel_data->value_fmt_desc()); + FormatChannelNameForTracing(ChannelNameFromRef(channel_reference)), + value, ChannelDirection::kIn, channel_data->value_fmt_desc()); } stack_.Push(InterpValue::MakeTuple({token, std::move(value)})); } else { @@ -1406,8 +1428,8 @@ absl::Status BytecodeInterpreter::EvalSend(const Bytecode& bytecode) { bytecode.channel_data()); (*events_)->AddTraceChannelMessage( import_data_->file_table(), bytecode.source_span(), - FormatChannelNameForTracing(*channel_data), payload, - ChannelDirection::kOut, channel_data->value_fmt_desc()); + FormatChannelNameForTracing(ChannelNameFromRef(channel_reference)), + payload, ChannelDirection::kOut, channel_data->value_fmt_desc()); } channel.Write(payload); } @@ -1832,11 +1854,8 @@ absl::Status BytecodeInterpreter::RunBuiltinMap(const Bytecode& bytecode) { } std::string BytecodeInterpreter::FormatChannelNameForTracing( - const Bytecode::ChannelData& channel) { + std::string_view local_name) { if (proc_id().has_value()) { - // Include a string describing the instantiation path to this channel - // instance. For example: - // my_top->my_proc#1->your_proc#2::the_channel std::string result; for (auto [proc, instance] : proc_id()->proc_instance_stack) { if (result.empty()) { @@ -1846,9 +1865,9 @@ std::string BytecodeInterpreter::FormatChannelNameForTracing( instance); } } - return absl::StrCat(result, "::", channel.channel_name()); + return absl::StrCat(result, "::", local_name); } - return std::string(channel.channel_name()); + return std::string(local_name); } absl::Status BytecodeInterpreter::EvalRead(const Bytecode& bytecode) { diff --git a/xls/dslx/bytecode/bytecode_interpreter.h b/xls/dslx/bytecode/bytecode_interpreter.h index e727fdd820..89da14f57d 100644 --- a/xls/dslx/bytecode/bytecode_interpreter.h +++ b/xls/dslx/bytecode/bytecode_interpreter.h @@ -145,6 +145,7 @@ class InterpValueChannel { InterpValueChannel(InterpValueChannel&&) = default; int64_t GetId() const { return id_; } + bool IsEmpty() const { return queue_.empty(); } int64_t GetSize() const { return queue_.size(); } InterpValue Read() { @@ -328,7 +329,7 @@ class BytecodeInterpreter { // Formats the name of the given `channel` for logging via the trace hook. The // name is qualified with the proc instantiation context. - std::string FormatChannelNameForTracing(const Bytecode::ChannelData& channel); + std::string FormatChannelNameForTracing(std::string_view local_name); private: // Runs the next instruction in the current frame. Returns an error if called diff --git a/xls/dslx/bytecode/proc_hierarchy_interpreter.cc b/xls/dslx/bytecode/proc_hierarchy_interpreter.cc index 1310cb6b18..fdb0e8ce5e 100644 --- a/xls/dslx/bytecode/proc_hierarchy_interpreter.cc +++ b/xls/dslx/bytecode/proc_hierarchy_interpreter.cc @@ -282,8 +282,8 @@ ProcHierarchyInterpreter::Create(ImportData* import_data, TypeInfo* type_info, } XLS_RETURN_IF_ERROR(hierarchy_interpreter->AddInterfaceChannel( - param->identifier(), param_type.value(), - &channel_type->payload_type())); + param->identifier(), param_type.value(), &channel_type->payload_type(), + static_cast(param))); } XLS_RETURN_IF_ERROR(ProcConfigBytecodeInterpreter::EvalSpawn( @@ -483,7 +483,8 @@ absl::Span ProcHierarchyInterpreter::proc_instances() { } absl::Status ProcHierarchyInterpreter::AddInterfaceChannel( - std::string_view name, const Type* arg_type, const Type* payload_type) { + std::string_view name, const Type* arg_type, const Type* payload_type, + std::optional definer) { const ChannelType* channel_type = dynamic_cast(arg_type); if (channel_type == nullptr) { return absl::UnimplementedError(absl::StrFormat( @@ -492,18 +493,18 @@ absl::Status ProcHierarchyInterpreter::AddInterfaceChannel( name, arg_type->ToString())); } - InterpValueChannel* channel = nullptr; auto channel_instance_allocator = [&]() -> int64_t { - channel = &channel_manager_->AllocateLegacyChannel(); - return channel->GetId(); + return channel_manager_->AllocateLegacyChannel().GetId(); }; XLS_ASSIGN_OR_RETURN( InterpValue channel_reference, CreateChannelReference(channel_type->direction(), channel_type, - channel_instance_allocator)); + channel_instance_allocator, definer)); - XLS_RET_CHECK(channel != nullptr); + XLS_ASSIGN_OR_RETURN(InterpValue::ChannelReference cr, + channel_reference.GetChannelReference()); + InterpValueChannel* channel = &channel_manager_->GetChannel(nullptr, cr); interface_args_.push_back(channel_reference); interface_channels_.push_back( InterfaceChannel{.name = std::string{name}, diff --git a/xls/dslx/bytecode/proc_hierarchy_interpreter.h b/xls/dslx/bytecode/proc_hierarchy_interpreter.h index 1f0236a42f..aa417e353c 100644 --- a/xls/dslx/bytecode/proc_hierarchy_interpreter.h +++ b/xls/dslx/bytecode/proc_hierarchy_interpreter.h @@ -168,7 +168,9 @@ class ProcHierarchyInterpreter { InterpValueChannelManager& channel_manager() { return *channel_manager_; } absl::Status AddInterfaceChannel(std::string_view name, const Type* arg_type, - const Type* payload_type); + const Type* payload_type, + std::optional definer = + std::nullopt); absl::Status AddProcDefInterfaceChannel(std::string_view name, const ChannelType& channel_type, const InterpValue& channel_reference); diff --git a/xls/dslx/interp_value_utils.cc b/xls/dslx/interp_value_utils.cc index efa48cee76..961abc219e 100644 --- a/xls/dslx/interp_value_utils.cc +++ b/xls/dslx/interp_value_utils.cc @@ -399,7 +399,8 @@ absl::StatusOr InterpValueAsString(const InterpValue& v) { absl::StatusOr CreateChannelReference( ChannelDirection direction, const Type* type, - std::optional> channel_instance_allocator) { + std::optional> channel_instance_allocator, + std::optional definer) { if (auto* array_type = dynamic_cast(type)) { XLS_ASSIGN_OR_RETURN(int dim_int, array_type->size().GetAsInt64()); std::vector elements; @@ -408,7 +409,7 @@ absl::StatusOr CreateChannelReference( XLS_ASSIGN_OR_RETURN( InterpValue element, CreateChannelReference(direction, &array_type->element_type(), - channel_instance_allocator)); + channel_instance_allocator, definer)); elements.push_back(element); } return InterpValue::MakeArray(elements); @@ -421,7 +422,8 @@ absl::StatusOr CreateChannelReference( channel_instance_allocator.has_value() ? std::make_optional((*channel_instance_allocator)()) : std::nullopt; - return InterpValue::MakeChannelReference(direction, channel_instance_id); + return InterpValue::MakeChannelReference(direction, channel_instance_id, + definer); } absl::StatusOr> CreateChannelReferencePair( diff --git a/xls/dslx/interp_value_utils.h b/xls/dslx/interp_value_utils.h index b98cb0870f..6acdbd4640 100644 --- a/xls/dslx/interp_value_utils.h +++ b/xls/dslx/interp_value_utils.h @@ -117,7 +117,8 @@ absl::StatusOr InterpValueAsString(const InterpValue& v); absl::StatusOr CreateChannelReference( ChannelDirection direction, const Type* type, std::optional> channel_instance_allocator = - std::nullopt); + std::nullopt, + std::optional definer = std::nullopt); // Creates a pair of ChannelReference InterpValues. The first element has // channel direction "out" while the second element has channel direction diff --git a/xls/dslx/interpreter_main.cc b/xls/dslx/interpreter_main.cc index 3b6398a461..4fd3979b57 100644 --- a/xls/dslx/interpreter_main.cc +++ b/xls/dslx/interpreter_main.cc @@ -52,6 +52,7 @@ #include "xls/dslx/warning_kind.h" #include "xls/ir/evaluator_result.pb.h" #include "xls/ir/format_preference.h" +#include "xls/spin/spin_runner.h" // LINT.IfChange ABSL_FLAG(std::string, dslx_path, "", @@ -120,6 +121,20 @@ ABSL_FLAG(std::optional, convert_tests, false, // LINT.ThenChange(//xls/build_rules/xls_dslx_rules.bzl) +ABSL_FLAG(bool, spin_guided, false, + "When true, converts the DSLX source to Promela, runs SPIN guided " + "simulation, and compares per-channel event sequences against the " + "DSLX interpreter trace. The spin binary is located via Bazel " + "runfiles or PATH. The top-level test proc is inferred from the " + "module; a warning is emitted when multiple #[test_proc] entries " + "are present. Implies --trace_channels."); +ABSL_FLAG(bool, spin_exhaustive, false, + "When true, converts the DSLX source to Promela and runs SPIN " + "exhaustive state-space search (spin -search). Fails if SPIN " + "reports any assertion violations. The expected post-terminator " + "deadlock is not treated as an error. Does not require " + "--trace_channels."); + namespace xls::dslx { namespace { @@ -179,6 +194,10 @@ absl::StatusOr RealMain( absl::GetFlag(FLAGS_disable_warnings))); std::optional type_inference_v2_flag = absl::GetFlag(FLAGS_type_inference_v2); + const bool spin_guided = absl::GetFlag(FLAGS_spin_guided); + const bool spin_exhaustive = absl::GetFlag(FLAGS_spin_exhaustive); + // --spin_guided requires channel tracing to build the DSLX side of the trace. + if (spin_guided) trace_channels = true; std::optional type_inference_version = type_inference_v2_flag.has_value() ? std::make_optional(*type_inference_v2_flag @@ -262,11 +281,12 @@ absl::StatusOr RealMain( .max_ticks = max_ticks, }; - // Create a results proto if requested and plumb it through options. + // Create a results proto if requested or needed for SPIN trace comparison. xls::EvaluatorResultsProto results_proto; - options.results_out = !absl::GetFlag(FLAGS_output_results_proto).empty() - ? &results_proto - : nullptr; + options.results_out = + (!absl::GetFlag(FLAGS_output_results_proto).empty() || spin_guided) + ? &results_proto + : nullptr; std::unique_ptr test_runner = GetTestRunner(evaluator); XLS_ASSIGN_OR_RETURN(TestResultData test_result, @@ -282,7 +302,7 @@ absl::StatusOr RealMain( } // If requested, write the results proto to the given file in text format. - if (options.results_out != nullptr) { + if (!absl::GetFlag(FLAGS_output_results_proto).empty()) { std::string text; QCHECK(google::protobuf::TextFormat::PrintToString(results_proto, &text)); XLS_RETURN_IF_ERROR( @@ -356,6 +376,19 @@ absl::StatusOr RealMain( } } + if (spin_guided || spin_exhaustive) { + spin::SpinRunOptions spin_opts; + spin_opts.dslx_stdlib_path = dslx_stdlib_path.string(); + spin_opts.dslx_paths.assign(dslx_paths.begin(), dslx_paths.end()); + spin_opts.test_filter = test_filter; + spin_opts.type_inference_v2 = type_inference_v2_flag.value_or(false); + spin_opts.results_proto = spin_guided ? &results_proto : nullptr; + spin_opts.exec_type = spin_guided ? spin::SpinExecutionType::kGuided + : spin::SpinExecutionType::kExhaustive; + XLS_RETURN_IF_ERROR( + spin::RunSpinCheck(program, entry_module_path, module_name, spin_opts)); + } + return test_result.result(); } diff --git a/xls/dslx/run_routines/ir_test_runner.cc b/xls/dslx/run_routines/ir_test_runner.cc index c2e91095ac..0d41257abc 100644 --- a/xls/dslx/run_routines/ir_test_runner.cc +++ b/xls/dslx/run_routines/ir_test_runner.cc @@ -155,8 +155,8 @@ class IrRunner : public AbstractParsedTestRunner { } absl::StatusOr RunTestProc( - std::string_view name, - const BytecodeInterpreterOptions& options) override { + std::string_view name, const BytecodeInterpreterOptions& options, + EvaluatorEventsProto* /*events_out*/ = nullptr) override { if (options.trace_channels()) { LOG(WARNING) << "Unable to trace channels with IR testing"; } diff --git a/xls/dslx/run_routines/run_routines.cc b/xls/dslx/run_routines/run_routines.cc index 38a5087355..d318fda2ea 100644 --- a/xls/dslx/run_routines/run_routines.cc +++ b/xls/dslx/run_routines/run_routines.cc @@ -188,7 +188,8 @@ template absl::Status RunDslxTestProc(ImportData* import_data, const Module* module, ProcType* tp, TypeInfo* ti, std::optional expected_fail_label, - const BytecodeInterpreterOptions& options) { + const BytecodeInterpreterOptions& options, + EvaluatorEventsProto* events_out = nullptr) { auto cache = std::make_unique(); import_data->SetBytecodeCache(std::move(cache)); @@ -229,6 +230,11 @@ absl::Status RunDslxTestProc(ImportData* import_data, const Module* module, return FailureErrorStatus(tp->span(), "Proc reported failure upon exit.", import_data->file_table()); } + if (events_out != nullptr) { + for (const ProcInstance& pi : hierarchy_interpreter->proc_instances()) { + events_out->MergeFrom(pi.events().AsProto()); + } + } return absl::OkStatus(); } @@ -301,14 +307,15 @@ absl::StatusOr DslxInterpreterParsedTestRunner::RunTestFunction( } absl::StatusOr DslxInterpreterParsedTestRunner::RunTestProc( - std::string_view name, const BytecodeInterpreterOptions& options) { + std::string_view name, const BytecodeInterpreterOptions& options, + EvaluatorEventsProto* events_out) { if (std::optional tp = entry_module_->GetMember(name); tp.has_value()) { XLS_ASSIGN_OR_RETURN(TypeInfo * ti, type_info_->GetTopLevelProcTypeInfo((*tp)->proc())); - return RunResult{ - .result = RunDslxTestProc(import_data_, entry_module_, (*tp)->proc(), - ti, (*tp)->expected_fail_label(), options)}; + return RunResult{.result = RunDslxTestProc( + import_data_, entry_module_, (*tp)->proc(), ti, + (*tp)->expected_fail_label(), options, events_out)}; } XLS_ASSIGN_OR_RETURN(ProcDef * proc_def, @@ -322,7 +329,8 @@ absl::StatusOr DslxInterpreterParsedTestRunner::RunTestProc( return RunResult{ .result = RunDslxTestProc(import_data_, entry_module_, proc_def, initializers[0].next_type_info, - /*expected_fail_label=*/std::nullopt, options)}; + /*expected_fail_label=*/std::nullopt, options, + /*events_out=*/nullptr)}; } TestResultData::TestResultData(absl::Time start_time, @@ -1069,23 +1077,18 @@ absl::StatusOr AbstractTestRunner::ParseAndTest( XLS_ASSIGN_OR_RETURN( out, runner->RunTestFunction(test_name, interpreter_options, /*events=*/&test_events)); - } else { - if (options.results_out != nullptr) { - return absl::UnimplementedError( - "Collecting EvaluatorResultsProto for proc tests is not yet " - "implemented"); + if (result_proto != nullptr) { + *result_proto->mutable_events() = test_events.AsProto(); } - XLS_ASSIGN_OR_RETURN(out, - runner->RunTestProc(test_name, interpreter_options)); + } else { + XLS_ASSIGN_OR_RETURN( + out, runner->RunTestProc(test_name, interpreter_options, + result_proto != nullptr + ? result_proto->mutable_events() + : nullptr)); } auto test_case_end = absl::Now(); - // If collecting results, copy the events into the result proto for this - // test invocation. - if (result_proto != nullptr) { - *result_proto->mutable_events() = test_events.AsProto(); - } - if (out.result.ok()) { // Add to the tracking data. result.AddTestCase(test_xml::TestCase{ diff --git a/xls/dslx/run_routines/run_routines.h b/xls/dslx/run_routines/run_routines.h index fed71d2576..98823fea85 100644 --- a/xls/dslx/run_routines/run_routines.h +++ b/xls/dslx/run_routines/run_routines.h @@ -238,7 +238,8 @@ class AbstractParsedTestRunner { public: virtual ~AbstractParsedTestRunner() = default; virtual absl::StatusOr RunTestProc( - std::string_view name, const BytecodeInterpreterOptions& options) = 0; + std::string_view name, const BytecodeInterpreterOptions& options, + EvaluatorEventsProto* events_out = nullptr) = 0; virtual absl::StatusOr RunTestFunction( std::string_view name, const BytecodeInterpreterOptions& options, std::optional events) = 0; @@ -260,8 +261,8 @@ class DslxInterpreterParsedTestRunner : public AbstractParsedTestRunner { entry_module_(entry_module) {} absl::StatusOr RunTestProc( - std::string_view name, - const BytecodeInterpreterOptions& options) override; + std::string_view name, const BytecodeInterpreterOptions& options, + EvaluatorEventsProto* events_out = nullptr) override; absl::StatusOr RunTestFunction( std::string_view name, const BytecodeInterpreterOptions& options, std::optional events) override; diff --git a/xls/ir/evaluator_result.proto b/xls/ir/evaluator_result.proto index e4e8d72623..ad00febbca 100644 --- a/xls/ir/evaluator_result.proto +++ b/xls/ir/evaluator_result.proto @@ -57,6 +57,18 @@ message TraceNodeProto { optional ValueProto value_changes = 3; } +// Structured metadata for a channel send or receive event. +message TraceChannelProto { + enum Direction { + INVALID = 0; + SEND = 1; + RECV = 2; + } + optional string channel_name = 1; + optional Direction direction = 2; + optional ValueProto value = 3; +} + // A trace message emitted during evaluation. message TraceMessageProto { optional string message = 1; @@ -66,6 +78,7 @@ message TraceMessageProto { TraceCallProto call = 4; TraceNodeProto node = 5; TraceCallReturnProto call_return = 6; + TraceChannelProto channel = 7; } } diff --git a/xls/spin/BUILD b/xls/spin/BUILD new file mode 100644 index 0000000000..a63005ded2 --- /dev/null +++ b/xls/spin/BUILD @@ -0,0 +1,237 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build rules for the XLS -> Promela code generator. +# Promela is the input language for the SPIN model checker (https://spinroot.com). + +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//xls:xls_users"], + licenses = ["notice"], +) + +cc_library( + name = "trace_compare", + srcs = ["trace_compare.cc"], + hdrs = ["trace_compare.h"], + deps = [ + "//xls/ir:bits", + "//xls/ir:evaluator_result_cc_proto", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_protobuf//:protobuf", + "@re2", + ], +) + +cc_library( + name = "promela_generator", + srcs = ["promela_generator.cc"], + hdrs = ["promela_generator.h"], + deps = [ + "//xls/common/status:status_macros", + "//xls/ir", + "//xls/ir:channel", + "//xls/ir:op", + "//xls/ir:source_location", + "//xls/ir:state_element", + "//xls/ir:type", + "//xls/ir:value", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + ], +) + +cc_test( + name = "promela_generator_test", + size = "medium", + srcs = ["promela_generator_test.cc"], + data = [ + "//xls/spin/testdata:assertion.ir", + "//xls/spin/testdata:blocking_receive.ir", + "//xls/spin/testdata:channel_naming.x", + "//xls/spin/testdata:conditional_io.ir", + "//xls/spin/testdata:conditional_io.x", + "//xls/spin/testdata:counter.ir", + "//xls/spin/testdata:counter.x", + "//xls/spin/testdata:deadlock.ir", + "//xls/spin/testdata:deadlock.x", + "//xls/spin/testdata:func.ir", + "//xls/spin/testdata:multi_proc.ir", + "//xls/spin/testdata:non_blocking_receive.ir", + "//xls/spin/testdata:passthrough.ir", + "//xls/spin/testdata:passthrough.x", + "//xls/spin/testdata:predicated_receive.ir", + "//xls/spin/testdata:predicated_send.ir", + "//xls/spin/testdata:proc.ir", + "//xls/spin/testdata:state_counter.ir", + "//xls/spin/testdata:unconditional_send.ir", + "//xls/spin/testdata:xr_hints.ir", + "@spin", + ], + deps = [ + ":promela_generator", + ":trace_compare", + "//xls/common:subprocess", + "//xls/common:undeclared_outputs", + "//xls/common:xls_gunit_main", + "//xls/common/file:filesystem", + "//xls/common/file:get_runfile_path", + "//xls/common/file:temp_directory", + "//xls/common/status:matchers", + "//xls/dslx/ir_convert:ir_converter", + "//xls/dslx/run_routines", + "//xls/ir", + "//xls/ir:evaluator_result_cc_proto", + "//xls/ir:ir_test_base", + "//xls/passes:optimization_pass_pipeline", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_protobuf//:protobuf", + "@googletest//:gtest", + ], +) + +cc_test( + name = "spin_runner_test", + size = "medium", + srcs = ["spin_runner_test.cc"], + data = [ + "//xls/spin/testdata:passthrough.x", + "@spin", + ], + deps = [ + ":spin_runner", + "//xls/common:xls_gunit_main", + "//xls/common/file:filesystem", + "//xls/common/file:get_runfile_path", + "//xls/common/file:temp_directory", + "//xls/common/status:matchers", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:scoped_mock_log", + "@googletest//:gtest", + ], +) + +cc_test( + name = "trace_compare_test", + srcs = ["trace_compare_test.cc"], + deps = [ + ":trace_compare", + "//xls/common:xls_gunit_main", + "@googletest//:gtest", + ], +) + +cc_library( + name = "spin_runner", + srcs = ["spin_runner.cc"], + hdrs = ["spin_runner.h"], + deps = [ + ":promela_generator", + ":trace_compare", + "//xls/common:subprocess", + "//xls/common/file:filesystem", + "//xls/common/file:get_runfile_path", + "//xls/common/file:temp_directory", + "//xls/common/status:status_macros", + "//xls/dslx:create_import_data", + "//xls/dslx:parse_and_typecheck", + "//xls/dslx:virtualizable_file_system", + "//xls/dslx:warning_kind", + "//xls/dslx/frontend:proc", + "//xls/dslx/ir_convert:ir_converter", + "//xls/dslx/run_routines", + "//xls/estimators", + "//xls/ir", + "//xls/ir:evaluator_result_cc_proto", + "//xls/passes", + "//xls/passes:optimization_pass_pipeline", + "@com_google_absl//absl/log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_protobuf//:protobuf", + "@re2", + ], +) + +cc_binary( + name = "dslx_trace_filter", + srcs = ["dslx_trace_filter_main.cc"], + visibility = ["//xls:xls_users"], + deps = [ + "//xls/common:exit_status", + "//xls/common:init_xls", + "//xls/common/file:filesystem", + "//xls/common/status:status_macros", + "//xls/ir:evaluator_result_cc_proto", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_protobuf//:protobuf", + ], +) + +cc_binary( + name = "promela_trace_compare", + srcs = ["promela_trace_compare_main.cc"], + visibility = ["//xls:xls_users"], + deps = [ + ":trace_compare", + "//xls/common:init_xls", + "//xls/common/file:filesystem", + "//xls/common/status:status_macros", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/status", + ], +) + +cc_binary( + name = "promela_main", + srcs = ["promela_main.cc"], + visibility = ["//xls:xls_users"], + deps = [ + ":promela_generator", + "//xls/common:exit_status", + "//xls/common:init_xls", + "//xls/common/file:filesystem", + "//xls/common/status:status_macros", + "//xls/ir:ir_parser", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/log", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + ], +) diff --git a/xls/spin/README.md b/xls/spin/README.md new file mode 100644 index 0000000000..84888bdac9 --- /dev/null +++ b/xls/spin/README.md @@ -0,0 +1,97 @@ +# XLS Spin Integration + +This toolset integrates the [SPIN](https://spinroot.com) model checker into +the XLS build system to formally verify DSLX hardware designs. It is an +experimental feature and is subject to change. + +## Motivation + +The XLS DSLX interpreter simulates hardware designs by running all operations +in a fixed, sequential order. This works correctly for a class of designs +called Kahn Process Networks (KPNs), where the result depends only on the +input data and not on the timing or order of execution. However, many designs +written in DSLX break this property by using constructs such as bounded FIFOs +that can fill up and block, or non-blocking reads that behave differently +depending on whether data happens to be present. For these designs, the order +in which operations execute matters, and different orderings can produce +different results or expose bugs. Because the interpreter always follows one +fixed order, it can miss entire classes of failures: deadlocks where the +system stalls permanently, livelocks where processes keep running but make no +useful progress, and race conditions where two processes interfere with each +other in a timing-dependent way. A design can pass all interpreter tests and +still fail in real hardware, because the interpreter never explored the +problematic execution order. + +## How it works + +The toolset translates XLS designs to Promela, the input language of SPIN, +and runs SPIN on the resulting model. Unlike the interpreter, SPIN does not +follow a single execution order. It supports two operating modes: a guided +simulation that follows one specific execution path, and an exhaustive search +that explores all possible interleavings of concurrent processes. SPIN detects +deadlocks by default, and can optionally detect livelocks and channel overflow +with additional options. A trace-comparison layer allows the channel events +produced by a SPIN simulation to be compared against those from the DSLX +interpreter, surfacing any divergence as a test failure. + +## Directory layout + +| File / dir | Role | +|---|---| +| `promela_generator.h/.cc` | Converts XLS IR to a Promela model that SPIN can verify. | +| `spin_runner.h/.cc` | Runs the full DSLX-to-SPIN verification pipeline programmatically. | +| `trace_compare.h/.cc` | Checks that SPIN and the DSLX interpreter produce the same channel events. | +| `promela_main.cc` | CLI tool for generating a Promela model from an IR file. | +| `dslx_trace_filter_main.cc` | CLI tool for preparing a DSLX interpreter trace for comparison. | +| `promela_trace_compare_main.cc` | CLI tool for comparing a SPIN trace against a DSLX trace. | +| `defs.bzl` | Public Bazel rules for integrating SPIN verification into a build. | +| `priv-defs.bzl` | Private Bazel rules used internally for testing and development. | +| `testdata/` | Checked-in fixtures used by the test suite. | +| `examples/` | Working DSLX proc examples with full verification target suites. | + +## Getting started + +SPIN verification is enabled via the `spin_guided` and `spin_exhaustive` +attributes on `xls_dslx_test`: + +```python +xls_dslx_test( + name = "my_proc_test", + srcs = ["my_proc.x"], + spin_guided = True, # guided simulation, compares traces with DSLX + spin_exhaustive = True, # exhaustive search over all interleavings +) +``` + +The `examples/` directory contains working designs with both modes enabled +and is the best place to see how to structure a proc for verification. + +For cases where the test rule is not sufficient, `defs.bzl` provides dedicated +Bazel rules that give more control over the pipeline: + +| Rule | Purpose | +|---|---| +| `xls_dslx_spin` | DSLX -> Promela build action | +| `spin_guided_test` | Guided simulation as `bazel test` | +| `spin_exhaustive_test` | Exhaustive verification as `bazel test` | + +These rules expose additional options such as livelock detection, throughput +modeling, and channel overflow assertions. See `defs.bzl` for the full list. +Additional private rules intended for development are available in `priv-defs.bzl`. + +## Known limitations + +- **Interleaving at channel boundaries only.** SPIN explores all interleavings + between concurrent procs, but within a single proc the operations execute in + the same fixed order as in DSLX. The scheduler may reorder those operations + in real hardware, so interleavings between operations within a proc are not + covered. + +- **32-bit integer limit.** Promela represents all scalar types as 32-bit + signed integers. XLS `bits[N]` with N > 32 is rejected at generation time. + Sub-32-bit types are emulated by masking after every arithmetic operation. + +- **One test proc per run.** The verification pipeline targets a single + `#[test_proc]` at a time. When a DSLX file contains multiple test procs, one + is selected via `--spin_top` or by picking the first match. Each proc must be + verified in a separate `bazel test` target. diff --git a/xls/spin/defs.bzl b/xls/spin/defs.bzl new file mode 100644 index 0000000000..5e761d0da1 --- /dev/null +++ b/xls/spin/defs.bzl @@ -0,0 +1,374 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Bazel rules for XLS -> SPIN model-checker integration. + +Public rules: + xls_ir_spin -- generate a SPIN model from an XLS IR file (build) + xls_dslx_spin -- generate a SPIN model from a DSLX source file (build) + spin_run -- interactive simulation; use for exploration (run) + spin_guided_test -- single-path simulation; fast failure detection (test) + spin_exhaustive_test -- exhaustive state-space search (test) + +Internal rules and macros are in //xls/spin:priv-defs.bzl. +""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load( + "//xls/build_rules:xls_ir_rules.bzl", + "xls_dslx_ir_attrs", + "xls_dslx_ir_impl", + "xls_ir_opt_ir_attrs", + "xls_ir_opt_ir_impl", +) +load("//xls/build_rules:xls_toolchains.bzl", "xls_toolchain_attrs") + +# Attrs shared by xls_ir_spin and xls_dslx_spin. +_promela_attrs = { + "emit_source_locations": attr.bool( + default = False, + doc = "Annotate each Promela statement with a /* filename:line:col */ " + + "comment derived from the XLS IR source location.", + ), + "emit_source_hints": attr.bool( + default = False, + doc = "Annotate each Promela statement with a /* ir: */ " + + "comment showing the original IR operation.", + ), + "channel_depth": attr.int( + default = 8, + doc = "Buffer depth for every declared Promela channel " + + "(chan x = [N] of {T}). Defaults to 8.", + ), + "emit_termination_hook": attr.bool( + default = False, + doc = "Append a blocking receive on the terminator channel to " + + "the init block so the simulation trace shows test completion.", + ), + "assert_send_on_full_channel": attr.bool( + default = False, + doc = "Prefix every send with assert(len(ch) < DEPTH). " + + "Turns a blocked-on-full-channel situation into an explicit " + + "SPIN assertion violation during exhaustive verification.", + ), + "worst_case_throughput": attr.string_dict( + default = {}, + doc = "Per-proc worst-case throughput: maps a sanitised Promela " + + "proctype name to N (string). The proc does useful work at " + + "most once every N loop iterations; all other iterations are " + + "idle stalls. Procs not listed run every iteration (N=1).", + ), + "emit_progress_labels": attr.bool( + default = False, + doc = "Prefix each channel send/receive with a SPIN progress label " + + "(progress_recv_: or progress_send_:). " + + "Use with spin -search -DNP to detect livelocks.", + ), + "_promela_main": attr.label( + default = Label("//xls/spin:promela_main"), + executable = True, + cfg = "exec", + ), +} + +def _xls_ir_spin_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".pml") + args = ["--output=" + out.path] + if ctx.attr.emit_source_locations: + args.append("--emit_source_locations") + if ctx.attr.emit_source_hints: + args.append("--emit_source_hints") + args.append("--channel_depth=" + str(ctx.attr.channel_depth)) + if ctx.attr.emit_termination_hook: + args.append("--emit_termination_hook") + if ctx.attr.assert_send_on_full_channel: + args.append("--assert_send_on_full_channel") + if ctx.attr.worst_case_throughput: + args.append("--worst_case_throughput=" + ",".join([ + k + ":" + v + for k, v in ctx.attr.worst_case_throughput.items() + ])) + if ctx.attr.emit_progress_labels: + args.append("--emit_progress_labels") + args.append(ctx.file.src.path) + ctx.actions.run( + executable = ctx.executable._promela_main, + arguments = args, + inputs = [ctx.file.src], + outputs = [out], + mnemonic = "IrToPromela", + progress_message = "Generating Promela from %{input}", + ) + return [DefaultInfo(files = depset([out]))] + +xls_ir_spin = rule( + implementation = _xls_ir_spin_impl, + attrs = dicts.add( + { + "src": attr.label( + allow_single_file = [".ir"], + mandatory = True, + doc = "XLS IR source file.", + ), + }, + _promela_attrs, + ), + doc = "Generates a SPIN Promela model from an XLS IR file.", +) + +def _xls_dslx_spin_impl(ctx): + # Step 1: DSLX -> IR - fully delegated to the standard XLS implementation. + ir_file_info, _dslx_info, ir_conv_info, _ir_files, _runfiles = ( + xls_dslx_ir_impl(ctx) + ) + + # Step 2 (optional): IR -> optimised IR. + if ctx.attr.opt: + opt_ir_file_info, _opt_info, _opt_files, _opt_runfiles = xls_ir_opt_ir_impl( + ctx, + ir_file_info, + ir_conv_info.original_input_files, + ) + ir_for_promela = opt_ir_file_info + else: + ir_for_promela = ir_file_info + + # Step 3: IR -> Promela + pml_out = ctx.actions.declare_file(ctx.label.name + ".pml") + args = ["--output=" + pml_out.path] + if ctx.attr.emit_source_locations: + args.append("--emit_source_locations") + if ctx.attr.emit_source_hints: + args.append("--emit_source_hints") + args.append("--channel_depth=" + str(ctx.attr.channel_depth)) + if ctx.attr.emit_termination_hook: + args.append("--emit_termination_hook") + if ctx.attr.assert_send_on_full_channel: + args.append("--assert_send_on_full_channel") + if ctx.attr.worst_case_throughput: + args.append("--worst_case_throughput=" + ",".join([ + k + ":" + v + for k, v in ctx.attr.worst_case_throughput.items() + ])) + if ctx.attr.emit_progress_labels: + args.append("--emit_progress_labels") + args.append(ir_for_promela.ir_file.path) + ctx.actions.run( + executable = ctx.executable._promela_main, + inputs = [ir_for_promela.ir_file], + outputs = [pml_out], + arguments = args, + mnemonic = "IrToPromela", + progress_message = "Generating Promela from IR: %{input}", + ) + + return [DefaultInfo(files = depset([pml_out]))] + +_xls_dslx_spin_attrs = dicts.add( + {k: v for k, v in xls_dslx_ir_attrs.items() if k != "ir_conv_args"}, + xls_ir_opt_ir_attrs, + { + "ir_conv_args": attr.string_dict( + default = {"lower_to_proc_scoped_channels": "true", "convert_tests": "true"}, + doc = "Arguments forwarded to ir_converter_main. " + + "lower_to_proc_scoped_channels defaults to 'true'; " + + "promela_main requires proc-scoped channels.", + ), + "opt": attr.bool( + default = True, + doc = "Run IR optimisation before Promela generation (default True). " + + "Set to False to use the raw unoptimised IR.", + ), + }, + _promela_attrs, + dicts.pick(xls_toolchain_attrs, ["_xls_ir_converter_tool", "_xls_opt_ir_tool"]), +) + +xls_dslx_spin = rule( + implementation = _xls_dslx_spin_impl, + attrs = _xls_dslx_spin_attrs, + doc = """\ +Converts a DSLX source file to a SPIN Promela model in one step. + +Follows the same combined-rule convention as xls_dslx_opt_ir: the DSLX->IR +step is handled by xls_dslx_ir_impl; by default the IR is then optimised via +xls_ir_opt_ir_impl before being passed to promela_main. Set opt = False to +skip optimisation and use the raw IR directly. + +Example: + xls_dslx_spin( + name = "my_proc_pml", + srcs = ["my_proc.x"], + dslx_top = "MyProc", + ) +""", +) + +def _spin_run_impl(ctx): + script = ctx.actions.declare_file(ctx.label.name + "_runner.sh") + ctx.actions.write( + output = script, + is_executable = True, + content = "\n".join([ + "#!/usr/bin/env bash", + "set -e", + "{spin} {args} {src} \"$@\"".format( + spin = ctx.executable._spin.short_path, + args = " ".join(ctx.attr.spin_args), + src = ctx.file.src.short_path, + ), + "exit 0", + ]), + ) + return [DefaultInfo( + executable = script, + runfiles = ctx.runfiles(files = [ctx.executable._spin, ctx.file.src]), + )] + +spin_run = rule( + implementation = _spin_run_impl, + executable = True, + attrs = { + "src": attr.label( + allow_single_file = [".pml"], + mandatory = True, + doc = "Promela source file.", + ), + "spin_args": attr.string_list( + default = ["-c"], + doc = "Flags forwarded to spin before the model path (default: -c).", + ), + "_spin": attr.label( + default = Label("@spin//:spin"), + executable = True, + cfg = "target", + ), + }, + doc = "Runs an interactive SPIN simulation trace on a Promela model (`bazel run`).", +) + +def _spin_guided_test_impl(ctx): + script = ctx.actions.declare_file(ctx.label.name + "_runner.sh") + # Spin simulation already exits non-zero on assertion violations; no wrapper + # logic needed. Deadlocks surface as exit 0 ("timeout") in simulation -- + # use spin_exhaustive_test to catch those. + ctx.actions.write( + output = script, + is_executable = True, + content = "#!/usr/bin/env bash\n{spin} {args} {src} \"$@\"\n".format( + spin = ctx.executable._spin.short_path, + args = " ".join(ctx.attr.spin_args), + src = ctx.file.src.short_path, + ), + ) + runfiles = ctx.runfiles(files = [ctx.executable._spin, ctx.file.src]) + return [DefaultInfo(executable = script, runfiles = runfiles)] + +def _spin_exhaustive_test_impl(ctx): + script = ctx.actions.declare_file(ctx.label.name + "_runner.sh") + # -H: pan exits 1 when errors > 0 (patched via pangen1.h). + # -K$DIR: pan writes the .trail file directly into the Bazel undeclared + # outputs directory so Bazel preserves it in test.outputs/outputs.zip. + ctx.actions.write( + output = script, + is_executable = True, + content = "\n".join([ + "#!/usr/bin/env bash", + "TRAIL_DIR=\"${TEST_UNDECLARED_OUTPUTS_DIR:-.}\"", + "{spin} {args} \"-K$TRAIL_DIR\" -H {src} \"$@\"".format( + spin = ctx.executable._spin.short_path, + args = " ".join(ctx.attr.spin_args), + src = ctx.file.src.short_path, + ), + ]), + ) + runfiles = ctx.runfiles(files = [ctx.executable._spin, ctx.file.src]) + return [DefaultInfo(executable = script, runfiles = runfiles)] + +spin_guided_test = rule( + implementation = _spin_guided_test_impl, + test = True, + attrs = { + "src": attr.label( + allow_single_file = [".pml"], + mandatory = True, + doc = "Promela source file.", + ), + "spin_args": attr.string_list( + default = ["-c"], + doc = "Flags forwarded to spin before the model path (default: -c).", + ), + "_spin": attr.label( + default = Label("@spin//:spin"), + executable = True, + cfg = "target", + ), + }, + doc = """\ +Single-path SPIN simulation as a Bazel test. + +Runs `spin -c` on the Promela model (one simulation path, not exhaustive). +Fails if SPIN reports any errors or assertion violations. Faster than +spin_exhaustive_test; use to catch obvious failures during development. + + spin_guided_test( + name = "my_guided", + src = ":my_pml", + ) +""", +) + +spin_exhaustive_test = rule( + implementation = _spin_exhaustive_test_impl, + test = True, + attrs = { + "src": attr.label( + allow_single_file = [".pml"], + mandatory = True, + doc = "Promela source file.", + ), + "spin_args": attr.string_list( + default = ["-search"], + doc = "Flags forwarded to spin before the model path (default: -search).", + ), + "_spin": attr.label( + default = Label("@spin//:spin"), + executable = True, + cfg = "target", + ), + }, + doc = """\ +Exhaustive SPIN verification as a Bazel test. + +The test fails if SPIN reports any errors (deadlocks, assertion violations, +or liveness violations). When SPIN finds an error it writes a counterexample +.trail file directly into the Bazel undeclared outputs directory, retrievable +from: + + bazel-testlogs/...//test.outputs/outputs.zip + +Replay the counterexample: + + unzip bazel-testlogs/...//test.outputs/outputs.zip -d /tmp/replay + spin -t -p -g /tmp/replay/model.pml # .trail must be alongside the .pml + +To detect livelocks in addition to deadlocks, pass -DNP: + + spin_exhaustive_test( + name = "my_verify", + src = ":my_pml", + spin_args = ["-search", "-DNP"], + ) +""", +) diff --git a/xls/spin/dslx_trace_filter_main.cc b/xls/spin/dslx_trace_filter_main.cc new file mode 100644 index 0000000000..dc27ef8ae6 --- /dev/null +++ b/xls/spin/dslx_trace_filter_main.cc @@ -0,0 +1,198 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Filters an EvaluatorResultsProto textproto to JSON channel events. +// Usage: dslx_trace_filter --input=trace.textproto [--output=out.json] +// [--format=hex] + +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_format.h" +#include "google/protobuf/text_format.h" +#include "xls/common/exit_status.h" +#include "xls/common/file/filesystem.h" +#include "xls/common/init_xls.h" +#include "xls/common/status/status_macros.h" +#include "xls/ir/evaluator_result.pb.h" + +enum class TraceValueFormat { kHex, kDecimal, kBinary, kBase64 }; + +const std::string kHex = "hex"; +const std::string kDecimal = "decimal"; +const std::string kBinary = "binary"; +const std::string kBase64 = "base64"; +const std::string kUnknown = "unknown"; + +bool AbslParseFlag(absl::string_view text, TraceValueFormat* mode, std::string* error) { + if (text == kHex) { *mode = TraceValueFormat::kHex; return true; } + if (text == kDecimal) { *mode = TraceValueFormat::kDecimal; return true; } + if (text == kBinary) { *mode = TraceValueFormat::kBinary; return true; } + if (text == kBase64) { *mode = TraceValueFormat::kBase64; return true; } + + *error = absl::StrFormat("--format must be '%s', '%s', '%s', or '%s'.", kHex, kDecimal, kBinary, kBase64); + return false; +} + +std::string AbslUnparseFlag(TraceValueFormat mode) { + switch (mode) { + case TraceValueFormat::kHex: return kHex; + case TraceValueFormat::kDecimal: return kDecimal; + case TraceValueFormat::kBinary: return kBinary; + case TraceValueFormat::kBase64: return kBase64; + } + return kUnknown; +} + +ABSL_FLAG(std::string, input, "", + "Input EvaluatorResultsProto text proto file."); +ABSL_FLAG(std::string, output, "-", + "Output file path, or '-' for stdout (default)."); +ABSL_FLAG(TraceValueFormat, format, TraceValueFormat::kHex, + "Value encoding: 'hex' (default), 'decimal', 'binary', or 'base64'."); + +namespace xls::spin { +namespace { + +// Encode the little-endian bytes of a BitsProto as a 0x-prefixed big-endian +// hex string padded to the full byte width. +std::string BitsToHex(const std::string& data) { + std::string result = "0x"; + for (size_t i = data.size(); i-- > 0;) { + // char is signed; cast to uint8_t to prevent sign extension. + uint8_t byte_value = static_cast(data[i]); + absl::StrAppendFormat(&result, "%02x", byte_value); + } + return result; +} + +// Encode little-endian bytes as a 0b-prefixed binary string, MSB first, +// trimmed to bit_count significant bits. +std::string BitsToBinary(const std::string& data, int64_t bit_count) { + std::string result = "0b"; + for (int64_t i = bit_count - 1; i >= 0; --i) { + int64_t byte_idx = i / 8; + int64_t bit_idx = i % 8; + char byte = (byte_idx < data.size()) ? data[byte_idx] : 0; + result += ((byte >> bit_idx) & 1) ? '1' : '0'; + } + return result; +} + +// Decode little-endian bytes to an unsigned 64-bit integer. +uint64_t BitsToUint(const std::string& data) { + uint64_t value = 0; + for (size_t i = data.size(); i-- > 0;) { + // char is signed; cast to uint8_t to prevent sign extension into value. + uint8_t byte_value = static_cast(data[i]); + value = (value << 8) | byte_value; + } + return value; +} + +// Format a ValueProto as a JSON object: {"bit_count": N, "data": }. +std::string FormatValue(const ValueProto& value_proto, + const TraceValueFormat format) { + if (!value_proto.has_bits()) { + return "{\"bit_count\": 0, \"data\": \"(non-bits)\"}"; + } + const std::string& data = value_proto.bits().data(); + int64_t bit_count = value_proto.bits().bit_count(); + std::string encoded; + if (format == TraceValueFormat::kDecimal) { + encoded = absl::StrFormat("%u", BitsToUint(data)); + } else if (format == TraceValueFormat::kBinary) { + encoded = absl::StrFormat("\"%s\"", BitsToBinary(data, bit_count)); + } else if (format == TraceValueFormat::kBase64) { + encoded = absl::StrFormat("\"%s\"", absl::Base64Escape(data)); + } else { + encoded = absl::StrFormat("\"%s\"", BitsToHex(data)); + } + return absl::StrFormat("{\"bit_count\": %d, \"data\": %s}", bit_count, + encoded); +} + +std::string DirectionString(TraceChannelProto::Direction dir) { + switch (dir) { + case TraceChannelProto::SEND: + return "SEND"; + case TraceChannelProto::RECV: + return "RECV"; + default: + return "UNKNOWN"; + } +} + +std::string FormatTrace(const EvaluatorResultsProto& proto, + const TraceValueFormat format) { + std::vector entries; + + for (const EvaluatorResultProto& result : proto.results()) { + for (const TraceMessageProto& trace_message : result.events().trace_msgs()) { + if (!trace_message.has_channel()) { + continue; + } + const TraceChannelProto& trace_channel = trace_message.channel(); + entries.push_back(absl::StrCat( + " {\"channel_name\": \"", trace_channel.channel_name(), + "\", \"direction\": \"", DirectionString(trace_channel.direction()), + "\", \"value\": ", FormatValue(trace_channel.value(), format), "}")); + } + } + + return absl::StrCat("[\n", absl::StrJoin(entries, ",\n"), "\n]\n"); +} + +absl::Status Run() { + std::string input_path = absl::GetFlag(FLAGS_input); + if (input_path.empty()) { + return absl::InvalidArgumentError("--input is required."); + } + TraceValueFormat format = absl::GetFlag(FLAGS_format); + + LOG(INFO) << "Filtering DSLX trace from '" << input_path << "'"; + std::string content; + XLS_ASSIGN_OR_RETURN(content, GetFileContents(input_path)); + + EvaluatorResultsProto proto; + if (!google::protobuf::TextFormat::ParseFromString(content, &proto)) { + return absl::InvalidArgumentError("Failed to parse input text proto."); + } + + std::string json = FormatTrace(proto, format); + + std::string output_path = absl::GetFlag(FLAGS_output); + if (output_path == "-") { + LOG(INFO) << "Writing filtered trace to stdout"; + std::cout << json; + } else { + LOG(INFO) << "Writing filtered trace to '" << output_path << "'"; + XLS_RETURN_IF_ERROR(SetFileContents(output_path, json)); + } + return absl::OkStatus(); +} + +} // namespace +} // namespace xls::spin + +int main(int argc, char** argv) { + xls::InitXls(argv[0], argc, argv); + return xls::ExitStatus(xls::spin::Run()); +} diff --git a/xls/spin/examples/BUILD b/xls/spin/examples/BUILD new file mode 100644 index 0000000000..87e0374567 --- /dev/null +++ b/xls/spin/examples/BUILD @@ -0,0 +1,67 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Build rules for XLS spin examples. + +load( + "//xls/build_rules:xls_build_defs.bzl", + "xls_dslx_library", + "xls_dslx_test", +) + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//xls:xls_internal"], + licenses = ["notice"], +) + +exports_files(glob(include = ["*.x"])) + +# counter + +xls_dslx_library( + name = "counter_dslx", + srcs = ["counter.x"], +) + +xls_dslx_test( + name = "counter_dslx_guided_test", + library = ":counter_dslx", + spin_guided = True, +) + +xls_dslx_test( + name = "counter_dslx_exhaustive_test", + library = ":counter_dslx", + spin_exhaustive = True, +) + +# order_dependence + +xls_dslx_library( + name = "order_dependence_dslx", + srcs = ["order_dependence.x"], +) + +xls_dslx_test( + name = "order_dependence_guided_test", + library = ":order_dependence_dslx", +) + +xls_dslx_test( + name = "order_dependence_exhaustive_test", + library = ":order_dependence_dslx", + spin_exhaustive = True, + tags = ["manual"], # expected to fail: demonstrates SPIN finding the race +) diff --git a/xls/spin/examples/README.md b/xls/spin/examples/README.md new file mode 100644 index 0000000000..626c4a7c0b --- /dev/null +++ b/xls/spin/examples/README.md @@ -0,0 +1,63 @@ +# XLS / SPIN examples + +The DSLX interpreter executes one fixed schedule per run, so it can't catch +bugs that only appear under a different interleaving of a concurrent design. +These examples translate DSLX procs to +[Promela](https://spinroot.com/spin/Man/Intro.html) and check them with +[SPIN](https://spinroot.com/spin/whatispin.html) +instead, which can reason about all schedules. + +Each example has two test targets: + +| Target | What it does | +|---|---| +| `*_dslx_guided_test` | DSLX interpreter + one guided SPIN simulation; trace comparison | +| `*_dslx_exhaustive_test` | DSLX interpreter + exhaustive SPIN state-space search | + +## counter + +`counter.x` sends incrementing u32 values on a channel. Well-behaved example +that produces the same output in DSLX and SPIN. + +``` +bazel test //xls/spin/examples:counter_dslx_guided_test +bazel test //xls/spin/examples:counter_dslx_exhaustive_test +``` + +Expected result: both pass. + +## order_dependence + +An `Arbiter` forwards requests to two parametric `Worker` procs and collects +their responses via a `Receiver` using non-blocking receives. The two workers +run concurrently, making the response non-deterministic. The DSLX interpreter +misses this because it runs one fixed schedule; SPIN finds it under exhaustive +search. + +``` +bazel test //xls/spin/examples:order_dependence_guided_test # passes +bazel test //xls/spin/examples:order_dependence_exhaustive_test --tags= # fails +``` + +`order_dependence_guided_test` is a plain DSLX test (no SPIN). It passes because +the interpreter runs one fixed schedule where the assertion holds. A guided SPIN +simulation would also pass for the same reason: it replays the DSLX trace and +sees the same schedule. Only the exhaustive search explores the schedules where +the assertion fails. + +`order_dependence_exhaustive_test` is tagged `manual` (it intentionally fails) +and reports 1 error: exhaustive search proves the assertion fails under some +schedule. + +## Test artifacts + +All test targets write to Bazel's undeclared test outputs: + +``` +bazel-testlogs/xls/spin/examples//test.outputs/outputs.zip +``` + +- Guided tests: `spin_guided/model.pml`, `spin_trace.json`, + `dslx_trace.textproto`, `spin_output.log`. +- Exhaustive tests: the counterexample `.pml.trail` on failure. To + replay it: put it alongside `.pml` and run `spin -t -p -g .pml`. diff --git a/xls/spin/examples/counter.x b/xls/spin/examples/counter.x new file mode 100644 index 0000000000..4964242f21 --- /dev/null +++ b/xls/spin/examples/counter.x @@ -0,0 +1,47 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![feature(type_inference_v2)] + +// Counter sends values 0..9 then becomes a no-op; this bounds SPIN iterations. +proc Counter { + data_s: chan out; + config(data_s: chan out) { (data_s,) } + init { u32:0 } + next(state: u32) { + send_if(join(), data_s, state < u32:10, state); + state + u32:1 + } +} + +#[test_proc] +proc CounterTest { + terminator: chan out; + data_r: chan in; + config(terminator: chan out) { + let (data_s, data_r) = chan("data"); + spawn Counter(data_s); + (terminator, data_r) + } + + init { u32:10 } + + next(count: u32) { + let tok = join(); + let (tok, _value) = recv_if(tok, data_r, count > u32:0, u32:0); + send_if(tok, terminator, count == u32:1, true); + + if count > u32:0 { count - u32:1 } else { u32:0 } + } +} diff --git a/xls/spin/examples/order_dependence.x b/xls/spin/examples/order_dependence.x new file mode 100644 index 0000000000..611ff5b87f --- /dev/null +++ b/xls/spin/examples/order_dependence.x @@ -0,0 +1,103 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![feature(type_inference_v2)] + +proc Worker { + req_r: chan in; + resp_s: chan out; + + config(req_r: chan in, resp_s: chan out) { + (req_r, resp_s) + } + + init { } + + next(state: ()) { + let (tok, req) = recv(join(), req_r); + let tok = send(tok, resp_s, req + VALUE); + } +} + +proc Receiver { + req_r: chan[2] in; + resp_s: chan out; + + config(req_r: chan[2] in, resp_s: chan out) { + (req_r, resp_s) + } + + init { } + + next(state: ()) { + let (tok0, req0, valid0) = recv_non_blocking(join(), req_r[0], u32:0); + let (tok1, req1, valid1) = recv_non_blocking(join(), req_r[1], u32:0); + let tok = send_if(join(tok0, tok1), resp_s, valid0 || valid1, req0 + req1); + } +} + +proc Arbiter { + req_r: chan in; + resp_s: chan out; + worker_req_s: chan[2] out; + receiver_resp_r: chan in; + + config(req_r: chan in, resp_s: chan out) { + let (worker_req_s, worker_req_r) = chan[2]("worker_req"); + let (receiver_req_s, receiver_req_r) = chan[2]("receiver_req"); + let (receiver_resp_s, receiver_resp_r) = chan("receiver_resp"); + + spawn Worker(worker_req_r[0], receiver_req_s[0]); + spawn Receiver(receiver_req_r, receiver_resp_s); + spawn Worker(worker_req_r[1], receiver_req_s[1]); + + (req_r, resp_s, worker_req_s, receiver_resp_r) + } + + init { } + + next(state: ()) { + let (tok, req, req_valid) = recv_non_blocking(join(), req_r, u32:0); + let tok0 = send_if(tok, worker_req_s[0], req_valid, req); + let tok1 = send_if(tok, worker_req_s[1], req_valid, req); + + let (tok, resp, valid) = recv_non_blocking(join(tok0, tok1), receiver_resp_r, u32:0); + let tok = send_if(tok, resp_s, valid, resp); + } +} + +#[test_proc] +proc Tester { + req_s: chan out; + resp_r: chan in; + terminator: chan out; + + config(terminator: chan out) { + let (req_s, req_r) = chan("req"); + let (resp_s, resp_r) = chan("resp"); + spawn Arbiter(req_r, resp_s); + + (req_s, resp_r, terminator) + } + + init { } + + next(_: ()) { + let tok = send(join(), req_s, u32:16); + + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, u32:21); + let tok = send(tok, terminator, true); + } +} diff --git a/xls/spin/priv-defs.bzl b/xls/spin/priv-defs.bzl new file mode 100644 index 0000000000..ba3c954de6 --- /dev/null +++ b/xls/spin/priv-defs.bzl @@ -0,0 +1,319 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Internal Bazel rules for xls/spin -- not part of the public API. + +Used by testdata/BUILD. +For the public-facing rules see //xls/spin:defs.bzl. + +Rules: + spin_syntax_check -- validate Promela syntax at build time (build action) + spin_trace -- run SPIN as a build action, write JSON artifact (build action) + spin_trace_test -- compare SPIN and DSLX traces per channel (bazel test) + dslx_trace -- generate a DSLX channel-event trace textproto (build action) + +Macros: + spin_ir_golden_test -- compile DSLX to IR and diff against a checked-in golden + (builds {name}_gen_ir; tests with {name}; updates with + {name}_update_golden) +""" + +load( + "@bazel_tools//tools/build_defs/cc:action_names.bzl", + "ACTION_NAMES", +) +load( + "@bazel_tools//tools/cpp:toolchain_utils.bzl", + "find_cpp_toolchain", + "use_cpp_toolchain", +) +load( + "//xls/build_rules:xls_build_defs.bzl", + "xls_dslx_ir", +) +load("//xls/build_rules:xls_diff_test.bzl", "diff_test") + +def _spin_syntax_check_impl(ctx): + stamp = ctx.actions.declare_file(ctx.label.name + ".syntax_ok") + + # spin -a hardcodes its preprocessor; -P overrides it at runtime so we + # pass clang in preprocessor-only mode instead of a system gcc. + cc_toolchain = find_cpp_toolchain(ctx) + feature_configuration = cc_common.configure_features( + ctx = ctx, + cc_toolchain = cc_toolchain, + requested_features = ctx.features, + unsupported_features = ctx.disabled_features, + ) + c_compiler = cc_common.get_tool_for_action( + feature_configuration = feature_configuration, + action_name = ACTION_NAMES.c_compile, + ) + + ctx.actions.run_shell( + inputs = depset( + [ctx.file.src, ctx.executable._spin], + transitive = [cc_toolchain.all_files], + ), + outputs = [stamp], + command = "{spin} -a -P\"{compiler} -E -x c\" {pml} && touch {stamp}".format( + spin = ctx.executable._spin.path, + compiler = c_compiler, + pml = ctx.file.src.path, + stamp = stamp.path, + ), + mnemonic = "PromelaSyntax", + progress_message = "Checking Promela syntax of %{input}", + ) + return [DefaultInfo(files = depset([stamp]))] + +spin_syntax_check = rule( + implementation = _spin_syntax_check_impl, + attrs = { + "src": attr.label( + allow_single_file = [".pml"], + mandatory = True, + doc = "Promela source file to validate.", + ), + "_spin": attr.label( + default = Label("@spin//:spin"), + executable = True, + cfg = "exec", + ), + }, + toolchains = use_cpp_toolchain(), + fragments = ["cpp"], + doc = """\ +Validates Promela syntax at build time (spin -a). + +The build fails with SPIN's diagnostics if the model is syntactically invalid. +Unlike a test, this is enforced on every `bazel build`. +""", +) + +def _spin_trace_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".json") + cc_toolchain = find_cpp_toolchain(ctx) + feature_configuration = cc_common.configure_features( + ctx = ctx, + cc_toolchain = cc_toolchain, + requested_features = ctx.features, + unsupported_features = ctx.disabled_features, + ) + c_compiler = cc_common.get_tool_for_action( + feature_configuration = feature_configuration, + action_name = ACTION_NAMES.c_compile, + ) + ctx.actions.run_shell( + inputs = depset( + [ctx.file.src, ctx.executable._spin], + transitive = [cc_toolchain.all_files], + ), + outputs = [out], + command = "{spin} -Q {out} -P\"{compiler} -E -x c\" {args} {src}".format( + spin = ctx.executable._spin.path, + out = out.path, + compiler = c_compiler, + args = " ".join(ctx.attr.spin_args), + src = ctx.file.src.path, + ), + mnemonic = "PromelaSimTrace", + progress_message = "SPIN simulation trace of %{input}", + ) + return [DefaultInfo(files = depset([out]))] + +spin_trace = rule( + implementation = _spin_trace_impl, + attrs = { + "src": attr.label( + allow_single_file = [".pml"], + mandatory = True, + doc = "Promela source file.", + ), + "spin_args": attr.string_list( + default = ["-c"], + doc = "Flags forwarded to spin before the model path (default: -c).", + ), + "_spin": attr.label( + default = Label("@spin//:spin"), + executable = True, + cfg = "exec", + ), + }, + toolchains = use_cpp_toolchain(), + fragments = ["cpp"], + doc = """\ +Runs SPIN as a build action and writes the channel-event JSON artifact. + + spin_trace( + name = "my_trace", + src = ":my_pml", + ) + + bazel build //path/to:my_trace # produces my_trace.json in bazel-bin/ +""", +) + +def _spin_trace_test_impl(ctx): + spin_json = ctx.attr.spin_trace[DefaultInfo].files.to_list()[0] + dslx_json = ctx.attr.dslx_trace[DefaultInfo].files.to_list()[0] + compare = ctx.executable._compare + + script = ctx.actions.declare_file(ctx.label.name + "_compare_test.sh") + ctx.actions.write( + output = script, + is_executable = True, + content = "\n".join([ + "#!/usr/bin/env bash", + "set -e", + "{compare} --spin_trace={spin} --dslx_trace={dslx}".format( + compare = compare.short_path, + spin = spin_json.short_path, + dslx = dslx_json.short_path, + ), + ]), + ) + runfiles = ctx.runfiles(files = [compare, spin_json, dslx_json]) + runfiles = runfiles.merge(ctx.attr._compare[DefaultInfo].default_runfiles) + return [DefaultInfo(executable = script, runfiles = runfiles)] + +spin_trace_test = rule( + implementation = _spin_trace_test_impl, + test = True, + attrs = { + "spin_trace": attr.label( + mandatory = True, + doc = "Target producing the SPIN trace JSON (spin_trace).", + ), + "dslx_trace": attr.label( + mandatory = True, + doc = "Target producing the DSLX trace JSON (dslx_trace).", + ), + "_compare": attr.label( + default = Label("//xls/spin:promela_trace_compare"), + executable = True, + cfg = "target", + ), + }, + doc = """\ +Compares SPIN and DSLX channel-event traces per channel as a Bazel test. + +The test passes when every channel's event sequence (values and direction) +is identical in both traces. Events on different channels may interleave +differently; only the relative order within each channel is checked. + + spin_trace_test( + name = "my_trace_compare", + spin_trace = ":my_spin_trace", # spin_trace + dslx_trace = ":my_dslx_trace", # dslx_trace + ) + + bazel test //path/to:my_trace_compare +""", +) + +def _dslx_trace_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".textproto") + src = ctx.file.src + args = [ + src.path, + "--trace_channels", + "--output_results_proto=" + out.path, + "--dslx_path=" + src.dirname, + ] + for p in ctx.attr.dslx_paths: + args.append("--dslx_path=" + p) + ctx.actions.run( + executable = ctx.executable._interpreter, + arguments = args, + inputs = [src], + outputs = [out], + mnemonic = "DslxTrace", + progress_message = "Generating DSLX channel-event trace from %{input}", + ) + return [DefaultInfo(files = depset([out]))] + +dslx_trace = rule( + implementation = _dslx_trace_impl, + attrs = { + "src": attr.label( + allow_single_file = [".x"], + mandatory = True, + doc = "DSLX source file to interpret.", + ), + "dslx_paths": attr.string_list( + default = [], + doc = "Additional --dslx_path entries for resolving imports.", + ), + "_interpreter": attr.label( + default = Label("//xls/dslx:interpreter_main"), + executable = True, + cfg = "exec", + ), + }, + doc = """\ +Generates a DSLX channel-event trace as a build artifact. + +Runs the DSLX interpreter with --trace_channels --output_results_proto and +writes the EvaluatorResultsProto textproto to .textproto. Suitable +as the dslx_trace input to spin_trace_test. +""", +) + +def spin_ir_golden_test(name, src, golden, dslx_top, convert_tests = False, tags = []): + """Generates IR from a DSLX source and diffs it against a golden .ir file. + + Creates two targets: + {name}_gen_ir -- xls_dslx_ir build action; produces the .ir artifact. + {name} -- diff_test; fails if the generated IR differs from the + golden. A companion {name}_update_golden target is + created automatically for refreshing the golden locally. + + Typical usage in testdata/BUILD: + + spin_ir_golden_test( + name = "counter_ir_golden", + src = "counter.x", + golden = "counter.ir", + dslx_top = "CounterTest", + convert_tests = True, + ) + + Args: + name: Base name prefix for generated targets. + src: DSLX source file (e.g. "counter.x"). + golden: Checked-in golden IR file (e.g. "counter.ir"). + dslx_top: Top-level proc name passed to ir_converter_main. + convert_tests: Pass --convert_tests to ir_converter_main so #[test_proc] + procs are included in the converted IR. Required when + dslx_top names a test proc. + tags: Extra tags forwarded to the diff_test target. + """ + ir_conv_args = {"lower_to_proc_scoped_channels": "true"} + if convert_tests: + ir_conv_args["convert_tests"] = "true" + + xls_dslx_ir( + name = name + "_gen_ir", + srcs = [src], + dslx_top = dslx_top, + ir_conv_args = ir_conv_args, + ) + + diff_test( + name = name, + file = ":" + name + "_gen_ir.ir", + golden = golden, + tags = tags, + ) diff --git a/xls/spin/promela_generator.cc b/xls/spin/promela_generator.cc new file mode 100644 index 0000000000..ecb1b86695 --- /dev/null +++ b/xls/spin/promela_generator.cc @@ -0,0 +1,1013 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "xls/spin/promela_generator.h" + +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/btree_map.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_replace.h" +#include "absl/strings/substitute.h" +#include "xls/common/status/status_macros.h" +#include "xls/ir/channel.h" +#include "xls/ir/dfs_visitor.h" +#include "xls/ir/function.h" +#include "xls/ir/function_base.h" +#include "xls/ir/node.h" +#include "xls/ir/nodes.h" +#include "xls/ir/op.h" +#include "xls/ir/package.h" +#include "xls/ir/proc.h" +#include "xls/ir/proc_instantiation.h" +#include "xls/ir/source_location.h" +#include "xls/ir/state_element.h" +#include "xls/ir/type.h" +#include "xls/ir/value.h" + +namespace xls::spin { +namespace { + +bool IsUnitType(Type* type) { + return type->IsTuple() && type->AsTupleOrDie()->size() == 0; +} + +std::string_view PromelaType(Type* type) { + if (type->IsToken() || IsUnitType(type)) { + return ""; + } + if (type->IsBits()) { + const int64_t bits = type->AsBitsOrDie()->bit_count(); + if (bits == 1) { + return "bit"; + } + if (bits <= 8) { + return "byte"; + } + if (bits <= 16) { + return "short"; + } + return "int"; + } + LOG(WARNING) << "Type '" << type->ToString() + << "' has no Promela equivalent; approximated as int (lossy)."; + return "int"; +} + +std::string SanitizeName(std::string_view name) { + std::string result; + result.reserve(name.size()); + if (!result.empty() && absl::ascii_isdigit(result[0])) { + result.insert(result.begin(), '_'); + } + for (char c : name) { + result.push_back((absl::ascii_isalnum(c) || c == '_') ? c : '_'); + } + return result; +} + +std::string BitsLiteralStr(const Value& value) { + if (!value.IsBits()) { + return "0"; + } + auto uint_value = value.bits().ToUint64(); + return uint_value.ok() ? absl::StrCat(*uint_value) : "0 /* oversized */"; +} + +bool HasBodyOps(Proc* proc) { + return absl::c_any_of(proc->nodes(), + [](Node* n) { + return n->op() == Op::kReceive || + n->op() == Op::kSend || n->op() == Op::kAssert; + }) || + absl::c_any_of(proc->StateElements(), [](StateElement* se) { + return !se->type()->IsToken() && !IsUnitType(se->type()); + }); +} + +std::vector ProcParams(Proc* proc) { + std::vector params; + absl::flat_hash_set seen; + for (ChannelInterface* iface : proc->interface()) { + if (seen.emplace(iface->name()).second) { + params.push_back(std::string(iface->name())); + } + } + return params; +} + +} // namespace + +PromelaGenerator::PromelaGenerator(Package* package, + const PromelaGeneratorOptions& options) + : package_(package), options_(options), emit_(out_) {} + +absl::StatusOr PromelaGenerator::Generate( + Package* package, const PromelaGeneratorOptions& options) { + PromelaGenerator gen(package, options); + + LOG(INFO) << "Generating Promela for package '" << package->name() + << "': " << package->functions().size() << " function(s), " + << package->procs().size() << " proc(s)"; + + if (!package->procs().empty() && !package->ChannelsAreProcScoped()) { + return absl::InvalidArgumentError( + absl::StrCat("Package '", package->name(), + "' has procs with old-style (package-level) channels. " + "Re-generate the IR with proc-scoped channels enabled " + "(lower_to_proc_scoped_channels pass).")); + } + + XLS_RETURN_IF_ERROR(gen.ValidateTypes()); + + gen.emit_.Line("/* Promela model generated from XLS IR package: $0 */", + package->name()); + gen.emit_.Blank(); + + if (options.emit_termination_hook) { + gen.emit_.Line("bit __terminated = 0;"); + gen.emit_.Blank(); + } + + for (const auto& fn : package->functions()) { + XLS_RETURN_IF_ERROR(gen.EmitFunction(fn.get())); + } + + for (const auto& proc : package->procs()) { + if (!proc->is_new_style_proc()) { + continue; + } + if (!HasBodyOps(proc.get()) && proc->channels().empty() && + proc->proc_instantiations().empty()) { + continue; + } + XLS_RETURN_IF_ERROR(gen.EmitProc(proc.get())); + } + + std::vector roots = gen.FindRootProcs(); + if (!roots.empty()) { + gen.EmitInit(roots); + } + + LOG(INFO) << "Promela generation complete"; + return gen.out_; +} + +// Rejects any node or channel type wider than 32 bits (Promela int limit). +absl::Status PromelaGenerator::ValidateTypes() { + auto check = [](Type* type, std::string_view ctx) -> absl::Status { + if (type->IsBits() && type->AsBitsOrDie()->bit_count() > 32) { + return absl::InvalidArgumentError( + absl::StrCat(ctx, " has bit width ", type->AsBitsOrDie()->bit_count(), + " which exceeds Promela's maximum of 32 (int).")); + } + return absl::OkStatus(); + }; + for (const auto& fn : package_->functions()) { + for (Node* node : fn->nodes()) { + XLS_RETURN_IF_ERROR(check( + node->GetType(), absl::StrCat("node '", node->GetName(), + "' in function '", fn->name(), "'"))); + } + } + for (const auto& proc : package_->procs()) { + for (Node* node : proc->nodes()) { + XLS_RETURN_IF_ERROR(check( + node->GetType(), absl::StrCat("node '", node->GetName(), + "' in proc '", proc->name(), "'"))); + } + for (ChannelInterface* i : proc->interface()) { + XLS_RETURN_IF_ERROR( + check(i->type(), absl::StrCat("channel '", i->name(), "' in proc '", + proc->name(), "'"))); + } + for (Channel* c : proc->channels()) { + XLS_RETURN_IF_ERROR( + check(c->type(), absl::StrCat("channel '", c->name(), "' in proc '", + proc->name(), "'"))); + } + } + return absl::OkStatus(); +} + +// Returns procs that are not instantiated by any other proc (top-level roots). +std::vector PromelaGenerator::FindRootProcs() const { + absl::flat_hash_set is_child; + for (const auto& proc : package_->procs()) { + for (const auto& instantiation : proc->proc_instantiations()) { + is_child.insert(instantiation->proc()); + } + } + std::vector roots; + for (const auto& proc : package_->procs()) { + if (proc->is_new_style_proc() && !is_child.contains(proc.get())) { + roots.push_back(proc.get()); + } + } + LOG(INFO) << "Found " << roots.size() << " root proc(s)"; + return roots; +} + +// Emits a Promela inline macro for `fn`; result is passed via an out-parameter. +absl::Status PromelaGenerator::EmitFunction(Function* fn) { + LOG(INFO) << "Emitting function '" << fn->name() << "'"; + std::vector param_parts; + for (Param* param : fn->params()) { + param_parts.push_back(SanitizeName(param->name())); + } + param_parts.push_back("_ret"); + + emit_.Line("inline fn_$0($1) {", SanitizeName(fn->name()), + absl::StrJoin(param_parts, ", ")); + emit_.Indent(); + + next_nodes_.clear(); + tuple_components_.clear(); + XLS_RETURN_IF_ERROR(fn->Accept(this)); + + std::string_view ret_var = Ref(fn->return_value()); + if (!ret_var.empty()) { + emit_.Line("_ret = $0;", ret_var); + } + emit_.Dedent(); + emit_.Line("}"); + emit_.Blank(); + return absl::OkStatus(); +} + +// Emits a Promela proctype for `proc`, including channel decls, child spawns, +// xr/xs hints, state variables, and the do/od main loop. +absl::Status PromelaGenerator::EmitProc(Proc* proc) { + LOG(INFO) << "Emitting proc '" << proc->name() << "' (" << proc->node_count() + << " node(s))"; + absl::flat_hash_set send_channels, recv_channels; + for (Node* node : proc->nodes()) { + if (node->op() == Op::kSend) { + send_channels.insert(std::string(node->As()->channel_name())); + } else if (node->op() == Op::kReceive) { + recv_channels.insert(std::string(node->As()->channel_name())); + } + } + + std::vector param_parts; + for (const std::string& param_name : ProcParams(proc)) { + param_parts.push_back(absl::StrCat("chan ", SanitizeName(param_name))); + } + emit_.Line("proctype $0($1) {", SanitizeName(proc->name()), + absl::StrJoin(param_parts, "; ")); + emit_.Indent(); + + for (Channel* channel : proc->channels()) { + std::string_view elem_type = PromelaType(channel->type()); + if (elem_type.empty()) { + elem_type = "bit"; + } + emit_.Line("chan $0 = [$1] of { $2 };", SanitizeName(channel->name()), + options_.channel_depth, elem_type); + } + if (!proc->channels().empty()) { + emit_.Blank(); + } + + for (const auto& instantiation : proc->proc_instantiations()) { + std::vector args; + for (ChannelInterface* channel_interface : instantiation->channel_args()) { + args.push_back(SanitizeName(channel_interface->name())); + } + emit_.Line("run $0($1);", SanitizeName(instantiation->proc()->name()), + absl::StrJoin(args, ", ")); + } + if (!proc->proc_instantiations().empty()) { + emit_.Blank(); + } + + // TODO: emit xs for send channels that are never polled (non-blocking receive) + // by another proc; safe xs improves POR but requires proc-graph analysis. + for (const std::string& channel_name : recv_channels) { + emit_.Line("xr $0;", SanitizeName(channel_name)); + } + if (!recv_channels.empty()) { + emit_.Blank(); + } + + if (!HasBodyOps(proc)) { + emit_.Dedent(); + emit_.Line("}"); + emit_.Blank(); + return absl::OkStatus(); + } + + for (StateElement* state_element : proc->StateElements()) { + const std::string_view state_type = PromelaType(state_element->type()); + if (state_type.empty()) { + continue; + } + emit_.Line("$0 s_$1 = $2;", state_type, SanitizeName(state_element->name()), + BitsLiteralStr(state_element->initial_value())); + } + + const std::string proc_key = SanitizeName(proc->name()); + auto throughput_it = options_.worst_case_throughput.find(proc_key); + const int64_t throughput = + (throughput_it != options_.worst_case_throughput.end()) + ? throughput_it->second + : 1; + const bool use_throughput = throughput > 1; + + if (use_throughput) { + emit_.Line("$0 __thr = 0;", (throughput <= 256) ? "byte" : "short"); + } + if (proc->GetStateElementCount() > 0 || use_throughput) { + emit_.Blank(); + } + + emit_.Line("do"); + if (options_.emit_termination_hook) { + emit_.Line(":: (__terminated) -> break"); + } + if (use_throughput) { + emit_.Line(":: (__thr > 0) -> __thr--;"); + emit_.Line(":: (__thr == 0) ->"); + emit_.Line("__thr = $0;", throughput - 1); + } else { + emit_.Line("::"); + } + + next_nodes_.clear(); + tuple_components_.clear(); + XLS_RETURN_IF_ERROR(proc->Accept(this)); + + for (Node* node : next_nodes_) { + auto* next_node = node->As(); + if (next_node->state_element()->type()->IsToken() || + IsUnitType(next_node->state_element()->type())) { + continue; + } + const std::string state_var = + absl::StrCat("s_", SanitizeName(next_node->state_element()->name())); + std::string_view next_value = Ref(next_node->value()); + if (next_value.empty()) { + next_value = "0"; + } + if (next_node->predicate().has_value()) { + std::string_view predicate = Ref(*next_node->predicate()); + if (predicate.empty()) { + predicate = "0"; + } + emit_.Line("if"); + emit_.Line(":: ($0 != 0) -> $1 = $2;", predicate, state_var, next_value); + emit_.Line(":: else -> skip;"); + emit_.Line("fi"); + } else { + emit_.Line("$0 = $1;", state_var, next_value); + } + } + + emit_.Line("od"); + emit_.Dedent(); + emit_.Line("}"); + emit_.Blank(); + return absl::OkStatus(); +} + +// Emits the Promela init block: declares interface channels and spawns roots. +void PromelaGenerator::EmitInit(const std::vector& root_procs) { + LOG(INFO) << "Emitting init block for " << root_procs.size() + << " root proc(s)"; + emit_.Line("init {"); + emit_.Indent(); + + // absl::btree_map gives deterministic (sorted) iteration for channel decls. + absl::btree_map interface_channels; + for (Proc* root_proc : root_procs) { + for (ChannelInterface* i : root_proc->interface()) { + interface_channels.emplace(SanitizeName(i->name()), i->type()); + } + } + for (const auto& [name, type] : interface_channels) { + std::string_view elem_type = PromelaType(type); + if (elem_type.empty()) { + elem_type = "bit"; + } + emit_.Line("chan $0 = [$1] of { $2 };", name, options_.channel_depth, + elem_type); + } + if (!interface_channels.empty()) { + emit_.Blank(); + } + + for (Proc* root_proc : root_procs) { + std::vector args; + for (const std::string& param_name : ProcParams(root_proc)) { + args.push_back(SanitizeName(param_name)); + } + emit_.Line("run $0($1);", SanitizeName(root_proc->name()), + absl::StrJoin(args, ", ")); + } + + if (options_.emit_termination_hook) { + for (const auto& [name, unused] : interface_channels) { + if (absl::StrContains(name, "terminator")) { + emit_.Line("bit __term_val;"); + emit_.Line("$0 ? __term_val;", name); + emit_.Line("__terminated = 1;"); + break; + } + } + } + + emit_.Dedent(); + emit_.Line("}"); +} + +// Associates `name` with `node` for later Ref() lookup. +absl::Status PromelaGenerator::SetName(Node* node, std::string name) { + names_[node] = std::move(name); + return absl::OkStatus(); +} + +// Returns the Promela variable name recorded for `node`, or "" if none. +std::string_view PromelaGenerator::Ref(Node* node) const { + auto it = names_.find(node); + return (it != names_.end()) ? std::string_view(it->second) : ""; +} + +// Returns the canonical Promela variable name for `node` (v_). +std::string PromelaGenerator::Var(Node* node) const { + return absl::StrCat("v_", SanitizeName(node->GetName())); +} + +// Declares a typed Promela variable initialised to `expr`; masks narrow ints. +absl::Status PromelaGenerator::Assign(Node* node, std::string_view expr) { + MaybeEmitLocComment(node); + MaybeEmitIrHintComment(node); + const std::string var = Var(node); + if (node->GetType()->IsBits()) { + const int64_t bit_count = node->GetType()->AsBitsOrDie()->bit_count(); + if (bit_count > 1 && bit_count < 32) { + const uint64_t mask = (uint64_t{1} << bit_count) - 1; + Emit("int $0 = ($1) & $2;", var, expr, mask); + return SetName(node, var); + } + } + Emit("$0 $1 = $2;", PromelaType(node->GetType()), var, expr); + return SetName(node, var); +} + +// Emits an if/fi block that sets a bit variable to 1 if `lhs cmp_op rhs`. +absl::Status PromelaGenerator::EmitCompare(CompareOp* node, + std::string_view cmp_op) { + MaybeEmitLocComment(node); + MaybeEmitIrHintComment(node); + const std::string var = Var(node); + Emit("bit $0;", var); + Emit("if"); + Emit(":: ($0 $1 $2) -> $3 = 1;", Ref(node->operand(0)), cmp_op, + Ref(node->operand(1)), var); + Emit(":: else -> $0 = 0;", var); + Emit("fi"); + return SetName(node, var); +} + +// Emits an n-ary bitwise expression joined by `op_sym`, optionally inverted. +absl::Status PromelaGenerator::EmitNaryBitwise(NaryOp* node, + std::string_view op_sym, + bool invert) { + std::vector parts; + for (Node* operand : node->operands()) { + parts.emplace_back(Ref(operand)); + } + const std::string expr = absl::StrJoin(parts, absl::StrCat(" ", op_sym, " ")); + if (invert) { + bool is_bit = node->GetType()->IsBits() && + node->GetType()->AsBitsOrDie()->bit_count() == 1; + return Assign(node, absl::StrCat(is_bit ? "!" : "~", "(", expr, ")")); + } + return Assign(node, expr); +} + +// Emits an if/fi that sets a bit to 1 when `operand cmp_op sentinel`. +absl::Status PromelaGenerator::EmitBitwiseReduce(BitwiseReductionOp* node, + std::string_view cmp_op, + std::string_view sentinel) { + MaybeEmitLocComment(node); + MaybeEmitIrHintComment(node); + const std::string var = Var(node); + Emit("bit $0;", var); + Emit("if"); + Emit(":: ($0 $1 $2) -> $3 = 1;", Ref(node->operand(0)), cmp_op, sentinel, + var); + Emit(":: else -> $0 = 0;", var); + Emit("fi"); + return SetName(node, var); +} + +// Emits a multiply; shared by HandleUMul and HandleSMul. +absl::Status PromelaGenerator::EmitMul(ArithOp* mul) { + return Assign( + mul, absl::StrCat(Ref(mul->operand(0)), " * ", Ref(mul->operand(1)))); +} + +// Returns "progress__: " when emit_progress_labels is set, else "". +std::string PromelaGenerator::ProgressLabel(std::string_view dir, + std::string_view chan) const { + if (!options_.emit_progress_labels) { + return ""; + } + return absl::StrCat("progress_", dir, "_", chan, ": "); +} + +// Emits /* file:line:col */ when emit_source_locations is set and node has loc. +void PromelaGenerator::MaybeEmitLocComment(Node* node) { + if (!options_.emit_source_locations) { + return; + } + const SourceInfo& info = node->loc(); + if (info.Empty()) { + return; + } + const SourceLocation& loc = info.locations.front(); + std::optional filename = package_->GetFilename(loc.fileno()); + const std::string file_part = + filename.has_value() ? *filename + : absl::StrCat("file:", loc.fileno().value()); + Emit("/* $0:$1:$2 */", file_part, loc.lineno().value(), loc.colno().value()); +} + +// Emits /* ir: */ when emit_source_hints is set. +void PromelaGenerator::MaybeEmitIrHintComment(Node* node) { + if (!options_.emit_source_hints) { + return; + } + std::string repr = node->ToString(); + for (char& c : repr) { + if (c == '\n' || c == '\r') { + c = ' '; + } + } + Emit("/* ir: $0 */", repr); +} + +absl::Status PromelaGenerator::DefaultHandler(Node* node) { + if (node->GetType()->IsToken() || IsUnitType(node->GetType())) { + return SetName(node, ""); + } + return absl::UnimplementedError( + absl::StrCat("unsupported op: ", OpToString(node->op()))); +} + +absl::Status PromelaGenerator::HandleAfterAll(AfterAll* after_all) { + return SetName(after_all, ""); +} +absl::Status PromelaGenerator::HandleMinDelay(MinDelay* min_delay) { + return SetName(min_delay, ""); +} +absl::Status PromelaGenerator::HandleTrace(Trace* trace_op) { + return SetName(trace_op, ""); +} +absl::Status PromelaGenerator::HandleCover(Cover* cover) { + return SetName(cover, ""); +} +absl::Status PromelaGenerator::HandleNewChannel(NewChannel* new_channel) { + return SetName(new_channel, ""); +} +absl::Status PromelaGenerator::HandleRecvChannelEnd(RecvChannelEnd* rce) { + return SetName(rce, ""); +} +absl::Status PromelaGenerator::HandleSendChannelEnd(SendChannelEnd* sce) { + return SetName(sce, ""); +} +absl::Status PromelaGenerator::HandleParam(Param* param) { + return SetName(param, SanitizeName(param->name())); +} + +absl::Status PromelaGenerator::HandleStateRead(StateRead* state_read) { + if (state_read->GetType()->IsToken() || IsUnitType(state_read->GetType())) { + return SetName(state_read, ""); + } + return SetName( + state_read, + absl::StrCat("s_", SanitizeName(state_read->state_element()->name()))); +} + +absl::Status PromelaGenerator::HandleNext(Next* next) { + next_nodes_.push_back(next); + return SetName(next, ""); +} + +absl::Status PromelaGenerator::HandleLiteral(Literal* literal) { + if (literal->GetType()->IsToken() || IsUnitType(literal->GetType())) { + return SetName(literal, ""); + } + return Assign(literal, literal->value().IsBits() + ? BitsLiteralStr(literal->value()) + : "0"); +} + +absl::Status PromelaGenerator::HandleAdd(BinOp* add) { + return Assign( + add, absl::StrCat(Ref(add->operand(0)), " + ", Ref(add->operand(1)))); +} +absl::Status PromelaGenerator::HandleSub(BinOp* sub) { + return Assign( + sub, absl::StrCat(Ref(sub->operand(0)), " - ", Ref(sub->operand(1)))); +} +absl::Status PromelaGenerator::HandleUMul(ArithOp* mul) { return EmitMul(mul); } +absl::Status PromelaGenerator::HandleSMul(ArithOp* mul) { return EmitMul(mul); } +absl::Status PromelaGenerator::HandleUDiv(BinOp* div) { + return Assign( + div, absl::StrCat(Ref(div->operand(0)), " / ", Ref(div->operand(1)))); +} +absl::Status PromelaGenerator::HandleSDiv(BinOp* div) { + return Assign( + div, absl::StrCat(Ref(div->operand(0)), " / ", Ref(div->operand(1)))); +} +absl::Status PromelaGenerator::HandleUMod(BinOp* mod) { + return Assign( + mod, absl::StrCat(Ref(mod->operand(0)), " % ", Ref(mod->operand(1)))); +} +absl::Status PromelaGenerator::HandleSMod(BinOp* mod) { + return Assign( + mod, absl::StrCat(Ref(mod->operand(0)), " % ", Ref(mod->operand(1)))); +} +absl::Status PromelaGenerator::HandleNeg(UnOp* neg) { + return Assign(neg, absl::StrCat("-", Ref(neg->operand(0)))); +} +absl::Status PromelaGenerator::HandleIdentity(UnOp* identity) { + return Assign(identity, Ref(identity->operand(0))); +} + +absl::Status PromelaGenerator::HandleNot(UnOp* not_op) { + const int64_t bit_count = not_op->GetType()->AsBitsOrDie()->bit_count(); + std::string_view arg = Ref(not_op->operand(0)); + if (bit_count == 1) { + return Assign(not_op, absl::StrCat("!(", arg, ")")); + } + return Assign(not_op, absl::StrCat("~(", arg, ")")); +} + +absl::Status PromelaGenerator::HandleNaryAnd(NaryOp* and_op) { + return EmitNaryBitwise(and_op, "&"); +} +absl::Status PromelaGenerator::HandleNaryOr(NaryOp* or_op) { + return EmitNaryBitwise(or_op, "|"); +} +absl::Status PromelaGenerator::HandleNaryXor(NaryOp* xor_op) { + return EmitNaryBitwise(xor_op, "^"); +} +absl::Status PromelaGenerator::HandleNaryNand(NaryOp* nand_op) { + return EmitNaryBitwise(nand_op, "&", /*invert=*/true); +} +absl::Status PromelaGenerator::HandleNaryNor(NaryOp* nor_op) { + return EmitNaryBitwise(nor_op, "|", /*invert=*/true); +} + +absl::Status PromelaGenerator::HandleAndReduce(BitwiseReductionOp* and_reduce) { + const int64_t source_bit_count = + and_reduce->operand(0)->GetType()->AsBitsOrDie()->bit_count(); + const std::string sentinel = + (source_bit_count >= 32) + ? "-1" + : absl::StrCat((uint64_t{1} << source_bit_count) - 1); + return EmitBitwiseReduce(and_reduce, "==", sentinel); +} +absl::Status PromelaGenerator::HandleOrReduce(BitwiseReductionOp* or_reduce) { + return EmitBitwiseReduce(or_reduce, "!=", "0"); +} +absl::Status PromelaGenerator::HandleShll(BinOp* shll) { + return Assign(shll, absl::StrCat("(", Ref(shll->operand(0)), " << ", + Ref(shll->operand(1)), ")")); +} +absl::Status PromelaGenerator::HandleShrl(BinOp* shrl) { + return Assign(shrl, absl::StrCat("(", Ref(shrl->operand(0)), " >> ", + Ref(shrl->operand(1)), ")")); +} +absl::Status PromelaGenerator::HandleShra(BinOp* shra) { + return Assign(shra, absl::StrCat("(", Ref(shra->operand(0)), " >> ", + Ref(shra->operand(1)), ")")); +} + +absl::Status PromelaGenerator::HandleEq(CompareOp* eq) { + return EmitCompare(eq, "=="); +} +absl::Status PromelaGenerator::HandleNe(CompareOp* ne) { + return EmitCompare(ne, "!="); +} +absl::Status PromelaGenerator::HandleULt(CompareOp* lt) { + return EmitCompare(lt, "<"); +} +absl::Status PromelaGenerator::HandleULe(CompareOp* le) { + return EmitCompare(le, "<="); +} +absl::Status PromelaGenerator::HandleUGt(CompareOp* gt) { + return EmitCompare(gt, ">"); +} +absl::Status PromelaGenerator::HandleUGe(CompareOp* ge) { + return EmitCompare(ge, ">="); +} +absl::Status PromelaGenerator::HandleSLt(CompareOp* lt) { + return EmitCompare(lt, "<"); +} +absl::Status PromelaGenerator::HandleSLe(CompareOp* le) { + return EmitCompare(le, "<="); +} +absl::Status PromelaGenerator::HandleSGt(CompareOp* gt) { + return EmitCompare(gt, ">"); +} +absl::Status PromelaGenerator::HandleSGe(CompareOp* ge) { + return EmitCompare(ge, ">="); +} + +absl::Status PromelaGenerator::HandleBitSlice(BitSlice* bit_slice) { + const int64_t width = bit_slice->width(); + const uint64_t mask = + (width >= 64) ? ~uint64_t{0} : ((uint64_t{1} << width) - 1); + return Assign(bit_slice, + absl::StrFormat("(%s >> %d) & %u", Ref(bit_slice->operand(0)), + bit_slice->start(), mask)); +} + +absl::Status PromelaGenerator::HandleDynamicBitSlice( + DynamicBitSlice* dynamic_bit_slice) { + const int64_t width = dynamic_bit_slice->width(); + const uint64_t mask = + (width >= 64) ? ~uint64_t{0} : ((uint64_t{1} << width) - 1); + return Assign( + dynamic_bit_slice, + absl::StrFormat("(%s >> %s) & %u", Ref(dynamic_bit_slice->operand(0)), + Ref(dynamic_bit_slice->operand(1)), mask)); +} + +absl::Status PromelaGenerator::HandleSignExtend(ExtendOp* sign_ext) { + MaybeEmitLocComment(sign_ext); + MaybeEmitIrHintComment(sign_ext); + const int64_t src_bits = + sign_ext->operand(0)->GetType()->AsBitsOrDie()->bit_count(); + const uint64_t sign_bit = uint64_t{1} << (src_bits - 1); + const uint64_t low_mask = sign_bit - 1; + std::string_view arg = Ref(sign_ext->operand(0)); + const std::string var = Var(sign_ext); + Emit("int $0;", var); + Emit("if"); + Emit(":: (($0 & $1) != 0) -> $2 = $0 | (~$3);", arg, sign_bit, var, low_mask); + Emit(":: else -> $0 = $1;", var, arg); + Emit("fi"); + return SetName(sign_ext, var); +} + +absl::Status PromelaGenerator::HandleZeroExtend(ExtendOp* zero_ext) { + return Assign(zero_ext, Ref(zero_ext->operand(0))); +} + +absl::Status PromelaGenerator::HandleConcat(Concat* concat) { + const std::string var = Var(concat); + Emit("$0 $1 = 0;", PromelaType(concat->GetType()), var); + int64_t shift = 0; + for (int64_t i = concat->operand_count() - 1; i >= 0; --i) { + Node* operand = concat->operand(i); + const int64_t width = operand->GetType()->IsBits() + ? operand->GetType()->AsBitsOrDie()->bit_count() + : 8; + const uint64_t mask = + (width >= 64) ? ~uint64_t{0} : ((uint64_t{1} << width) - 1); + Emit("$0 = $0 | (($1 & $2) << $3);", var, Ref(operand), mask, shift); + shift += width; + } + return SetName(concat, var); +} + +absl::Status PromelaGenerator::HandleSel(Select* sel) { + MaybeEmitLocComment(sel); + MaybeEmitIrHintComment(sel); + const std::string var = Var(sel); + Emit("$0 $1;", PromelaType(sel->GetType()), var); + Emit("if"); + for (int64_t i = 0; i < sel->cases().size(); ++i) { + Emit(":: ($0 == $1) -> $2 = $3;", Ref(sel->selector()), i, var, + Ref(sel->get_case(i))); + } + if (sel->default_value().has_value()) { + Emit(":: else -> $0 = $1;", var, Ref(*sel->default_value())); + } else if (!sel->cases().empty()) { + Emit(":: else -> $0 = $1;", var, + Ref(sel->get_case(sel->cases().size() - 1))); + } + Emit("fi"); + return SetName(sel, var); +} + +absl::Status PromelaGenerator::HandleOneHotSel(OneHotSelect* sel) { + const std::string var = Var(sel); + Emit("$0 $1 = 0;", PromelaType(sel->GetType()), var); + for (int64_t i = 0; i < sel->cases().size(); ++i) { + Emit("if"); + Emit(":: ((($0 >> $1) & 1) == 1) -> $2 = $2 | $3;", Ref(sel->selector()), i, + var, Ref(sel->get_case(i))); + Emit(":: else -> skip;"); + Emit("fi"); + } + return SetName(sel, var); +} + +absl::Status PromelaGenerator::HandlePrioritySel(PrioritySelect* sel) { + const std::string var = Var(sel); + Emit("$0 $1 = $2;", PromelaType(sel->GetType()), var, + Ref(sel->default_value())); + for (int64_t i = sel->cases().size() - 1; i >= 0; --i) { + Emit("if"); + Emit(":: ((($0 >> $1) & 1) == 1) -> $2 = $3;", Ref(sel->selector()), i, var, + Ref(sel->get_case(i))); + Emit(":: else -> skip;"); + Emit("fi"); + } + return SetName(sel, var); +} + +absl::Status PromelaGenerator::HandleGate(Gate* gate) { + MaybeEmitLocComment(gate); + MaybeEmitIrHintComment(gate); + const std::string var = Var(gate); + Emit("$0 $1;", PromelaType(gate->GetType()), var); + Emit("if"); + Emit(":: ($0 != 0) -> $1 = $2;", Ref(gate->condition()), var, + Ref(gate->data())); + Emit(":: else -> $0 = 0;", var); + Emit("fi"); + return SetName(gate, var); +} + +absl::Status PromelaGenerator::HandleTuple(Tuple* tuple) { + if (IsUnitType(tuple->GetType())) { + return SetName(tuple, ""); + } + std::vector comps; + comps.reserve(tuple->operand_count()); + for (Node* op : tuple->operands()) { + comps.emplace_back(Ref(op)); + } + tuple_components_[tuple] = std::move(comps); + return SetName(tuple, ""); +} + +absl::Status PromelaGenerator::HandleTupleIndex(TupleIndex* index) { + if (index->GetType()->IsToken() || IsUnitType(index->GetType())) { + return SetName(index, ""); + } + Node* src = index->operand(0); + if (src->op() == Op::kReceive) { + if (index->index() == 1) { + return SetName(index, std::string(Ref(src))); + } + if (index->index() == 2 && !src->As()->is_blocking()) { + return SetName(index, absl::StrCat(Ref(src), "_valid")); + } + return SetName(index, ""); + } + auto it = tuple_components_.find(src); + if (it != tuple_components_.end()) { + const auto& comps = it->second; + if (index->index() < comps.size()) { + return SetName(index, comps[index->index()]); + } + return SetName(index, ""); + } + return Assign(index, absl::StrCat(Ref(src), " /* tuple_index[", + index->index(), "] */")); +} + +absl::Status PromelaGenerator::HandleReceive(Receive* receive) { + MaybeEmitLocComment(receive); + MaybeEmitIrHintComment(receive); + Type* payload = receive->GetPayloadType(); + const std::string_view payload_type = PromelaType(payload); + const std::string channel_name = SanitizeName(receive->channel_name()); + const std::string recv_progress_label = ProgressLabel("recv", channel_name); + + if (payload_type.empty()) { + if (receive->predicate().has_value()) { + Emit("if"); + Emit(":: ($0 != 0) -> $1$2 ? eval(0);", Ref(*receive->predicate()), + recv_progress_label, channel_name); + Emit(":: else -> skip;"); + Emit("fi"); + } else { + Emit("$0$1 ? eval(0);", recv_progress_label, channel_name); + } + return SetName(receive, ""); + } + + const std::string data_var = absl::StrCat(Var(receive), "_data"); + Emit("$0 $1;", payload_type, data_var); + + if (!receive->is_blocking()) { + const std::string valid_var = data_var + "_valid"; + Emit("bit $0;", valid_var); + Emit("atomic {"); + Emit("if"); + if (receive->predicate().has_value()) { + std::string_view pred = Ref(*receive->predicate()); + Emit(":: ($0 != 0) && $1?[$2] -> $3$1 ? $2; $4 = 1;", pred, channel_name, + data_var, recv_progress_label, valid_var); + } else { + Emit(":: $0?[$1] -> $2$0 ? $1; $3 = 1;", channel_name, data_var, + recv_progress_label, valid_var); + } + Emit(":: else -> $0 = 0;", valid_var); + Emit("fi"); + Emit("}"); + return SetName(receive, data_var); + } + + if (receive->predicate().has_value()) { + Emit("if"); + Emit(":: ($0 != 0) -> $1$2 ? $3;", Ref(*receive->predicate()), + recv_progress_label, channel_name, data_var); + Emit(":: else -> skip;"); + Emit("fi"); + } else { + Emit("$0$1 ? $2;", recv_progress_label, channel_name, data_var); + } + return SetName(receive, data_var); +} + +absl::Status PromelaGenerator::HandleSend(Send* send) { + MaybeEmitLocComment(send); + MaybeEmitIrHintComment(send); + const std::string channel_name = SanitizeName(send->channel_name()); + const std::string send_progress_label = ProgressLabel("send", channel_name); + if (send->predicate().has_value()) { + Emit("if"); + if (options_.assert_send_on_full_channel) { + Emit(":: ($0 != 0) -> assert(len($1) < $2); $3$1 ! $4;", + Ref(*send->predicate()), channel_name, options_.channel_depth, + send_progress_label, Ref(send->data())); + } else { + Emit(":: ($0 != 0) -> $1$2 ! $3;", Ref(*send->predicate()), + send_progress_label, channel_name, Ref(send->data())); + } + Emit(":: else -> skip;"); + Emit("fi"); + } else { + if (options_.assert_send_on_full_channel) { + Emit("assert(len($0) < $1);", channel_name, options_.channel_depth); + } + Emit("$0$1 ! $2;", send_progress_label, channel_name, Ref(send->data())); + } + return SetName(send, ""); +} + +absl::Status PromelaGenerator::HandleAssert(Assert* assert_op) { + // Print the IR label only when the condition is false so the spin runner can + // identify which assertion fired without noise on every passing iteration. + if (assert_op->label().has_value() && !assert_op->label()->empty()) { + std::string label = *assert_op->label(); + absl::StrReplaceAll({{"\\", "\\\\"}, {"\"", "\\\""}}, &label); + Emit("if"); + Emit(absl::StrCat(":: (", Ref(assert_op->condition()), + " == 0) -> printf(\"XLS_ASSERT:", label, "\\n\");")); + Emit(":: else -> skip;"); + Emit("fi"); + } + Emit("assert($0 != 0);", Ref(assert_op->condition())); + return SetName(assert_op, ""); +} + +absl::Status PromelaGenerator::HandleInvoke(Invoke* invoke) { + const std::string var = Var(invoke); + Emit("$0 $1;", PromelaType(invoke->GetType()), var); + std::vector arg_parts; + for (Node* operand : invoke->operands()) { + arg_parts.emplace_back(Ref(operand)); + } + arg_parts.push_back(var); + Emit("fn_$0($1);", SanitizeName(invoke->to_apply()->name()), + absl::StrJoin(arg_parts, ", ")); + return SetName(invoke, var); +} + +} // namespace xls::spin diff --git a/xls/spin/promela_generator.h b/xls/spin/promela_generator.h new file mode 100644 index 0000000000..24ffc1ef6f --- /dev/null +++ b/xls/spin/promela_generator.h @@ -0,0 +1,217 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef XLS_SPIN_PROMELA_GENERATOR_H_ +#define XLS_SPIN_PROMELA_GENERATOR_H_ + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/substitute.h" +#include "xls/ir/dfs_visitor.h" +#include "xls/ir/package.h" + +namespace xls::spin { + +struct PromelaGeneratorOptions { + // Annotate each Promela statement with a /* filename:line:col */ comment. + bool emit_source_locations = false; + + // Annotate each Promela statement with a /* ir: */ comment. + bool emit_source_hints = false; + + // Buffer depth N in `chan x = [N] of {T}`. Default 8. + int64_t channel_depth = 8; + + // Block init on the terminator channel (mirrors #[test_proc]). + bool emit_termination_hook = false; + + // Prefix every send with `assert(len(ch) < DEPTH)` to turn a full-channel + // block into an explicit SPIN assertion violation. + bool assert_send_on_full_channel = false; + + // Maps sanitised proc name to N: proc does real work at most once every N + // loop iterations; other iterations are idle stalls. Procs not listed use 1. + absl::flat_hash_map worst_case_throughput; + + // Prefix each channel send/receive with a SPIN progress label for livelock + // detection via `spin -search -DNP`. + bool emit_progress_labels = false; +}; + +// Translates an XLS IR package to Promela source text via DFS post-order +// traversal. Use the static Generate() factory; do not instantiate directly. +class PromelaGenerator : public DfsVisitorWithDefault { + public: + // Translates all functions and procs in `package` into Promela source text. + // Requires proc-scoped channels; returns an error for old-style packages. + static absl::StatusOr Generate( + Package* package, + const PromelaGeneratorOptions& options = PromelaGeneratorOptions{}); + + // DfsVisitorWithDefault overrides. + absl::Status DefaultHandler(Node* node) override; + absl::Status HandleAfterAll(AfterAll* after_all) override; + absl::Status HandleMinDelay(MinDelay* min_delay) override; + absl::Status HandleTrace(Trace* trace_op) override; + absl::Status HandleCover(Cover* cover) override; + absl::Status HandleNewChannel(NewChannel* new_channel) override; + absl::Status HandleRecvChannelEnd(RecvChannelEnd* rce) override; + absl::Status HandleSendChannelEnd(SendChannelEnd* sce) override; + absl::Status HandleParam(Param* param) override; + absl::Status HandleStateRead(StateRead* state_read) override; + absl::Status HandleNext(Next* next) override; + absl::Status HandleLiteral(Literal* literal) override; + absl::Status HandleAdd(BinOp* add) override; + absl::Status HandleSub(BinOp* sub) override; + absl::Status HandleUMul(ArithOp* mul) override; + absl::Status HandleSMul(ArithOp* mul) override; + absl::Status HandleUDiv(BinOp* div) override; + absl::Status HandleSDiv(BinOp* div) override; + absl::Status HandleUMod(BinOp* mod) override; + absl::Status HandleSMod(BinOp* mod) override; + absl::Status HandleNeg(UnOp* neg) override; + absl::Status HandleIdentity(UnOp* identity) override; + absl::Status HandleNot(UnOp* not_op) override; + absl::Status HandleNaryAnd(NaryOp* and_op) override; + absl::Status HandleNaryOr(NaryOp* or_op) override; + absl::Status HandleNaryXor(NaryOp* xor_op) override; + absl::Status HandleNaryNand(NaryOp* nand_op) override; + absl::Status HandleNaryNor(NaryOp* nor_op) override; + absl::Status HandleAndReduce(BitwiseReductionOp* and_reduce) override; + absl::Status HandleOrReduce(BitwiseReductionOp* or_reduce) override; + absl::Status HandleShll(BinOp* shll) override; + absl::Status HandleShrl(BinOp* shrl) override; + absl::Status HandleShra(BinOp* shra) override; + absl::Status HandleEq(CompareOp* eq) override; + absl::Status HandleNe(CompareOp* ne) override; + absl::Status HandleULt(CompareOp* lt) override; + absl::Status HandleULe(CompareOp* le) override; + absl::Status HandleUGt(CompareOp* gt) override; + absl::Status HandleUGe(CompareOp* ge) override; + absl::Status HandleSLt(CompareOp* lt) override; + absl::Status HandleSLe(CompareOp* le) override; + absl::Status HandleSGt(CompareOp* gt) override; + absl::Status HandleSGe(CompareOp* ge) override; + absl::Status HandleBitSlice(BitSlice* bit_slice) override; + absl::Status HandleDynamicBitSlice( + DynamicBitSlice* dynamic_bit_slice) override; + absl::Status HandleSignExtend(ExtendOp* sign_ext) override; + absl::Status HandleZeroExtend(ExtendOp* zero_ext) override; + absl::Status HandleConcat(Concat* concat) override; + absl::Status HandleSel(Select* sel) override; + absl::Status HandleOneHotSel(OneHotSelect* sel) override; + absl::Status HandlePrioritySel(PrioritySelect* sel) override; + absl::Status HandleGate(Gate* gate) override; + absl::Status HandleTuple(Tuple* tuple) override; + absl::Status HandleTupleIndex(TupleIndex* index) override; + absl::Status HandleReceive(Receive* receive) override; + absl::Status HandleSend(Send* send) override; + absl::Status HandleAssert(Assert* assert_op) override; + absl::Status HandleInvoke(Invoke* invoke) override; + + // Unhandled ops fall through to DefaultHandler (UnimplementedError): + // InputPort, OutputPort, XorReduce, Reverse, OneHot, Array*, Map, + // CountedFor, BitSliceUpdate, Encode, Decode, SMulp, UMulp. + + private: + // Indented-line emitter. Each indent level adds two spaces. + class Emitter { + public: + explicit Emitter(std::string& out, int64_t level = 0) + : out_(out), level_(level) {} + void Reset() { level_ = 0; } + void Indent() { ++level_; } + void Dedent() { + if (level_ > 0) { + --level_; + } + } + void Blank() { out_ += '\n'; } + void Line(std::string_view text) { + WriteIndent(); + absl::StrAppend(&out_, text, "\n"); + } + template + void Line(std::string_view fmt, const Args&... args) { + WriteIndent(); + absl::SubstituteAndAppend(&out_, fmt, args...); + out_ += '\n'; + } + + private: + void WriteIndent() { + for (int64_t i = 0; i < level_; ++i) { + absl::StrAppend(&out_, " "); + } + } + std::string& out_; + int64_t level_; + }; + + explicit PromelaGenerator(Package* package, + const PromelaGeneratorOptions& options); + + // Package-level orchestration. + absl::Status ValidateTypes(); + std::vector FindRootProcs() const; + absl::Status EmitFunction(Function* fn); + absl::Status EmitProc(Proc* proc); + void EmitInit(const std::vector& root_procs); + + // Node name tracking. + absl::Status SetName(Node* node, std::string name); + std::string_view Ref(Node* node) const; + std::string Var(Node* node) const; + + // Code emission helpers. + absl::Status Assign(Node* node, std::string_view expr); + absl::Status EmitCompare(CompareOp* node, std::string_view cmp_op); + absl::Status EmitNaryBitwise(NaryOp* node, std::string_view op_sym, + bool invert = false); + absl::Status EmitBitwiseReduce(BitwiseReductionOp* node, + std::string_view cmp_op, + std::string_view sentinel); + absl::Status EmitMul(ArithOp* mul); + std::string ProgressLabel(std::string_view dir, std::string_view chan) const; + void MaybeEmitLocComment(Node* node); + void MaybeEmitIrHintComment(Node* node); + + void Emit(std::string_view text) { emit_.Line(text); } + template + void Emit(std::string_view fmt, const Args&... args) { + emit_.Line(fmt, args...); + } + + Package* package_; + PromelaGeneratorOptions options_; + std::string out_; + Emitter emit_; + + // Per-FunctionBase visit state; set up in EmitFunction / EmitProc. + absl::flat_hash_map names_; + absl::flat_hash_map> tuple_components_; + // Collects Next nodes during proc visits for deferred state-update emit. + std::vector next_nodes_; +}; + +} // namespace xls::spin + +#endif // XLS_SPIN_PROMELA_GENERATOR_H_ diff --git a/xls/spin/promela_generator_test.cc b/xls/spin/promela_generator_test.cc new file mode 100644 index 0000000000..f10747b5db --- /dev/null +++ b/xls/spin/promela_generator_test.cc @@ -0,0 +1,1893 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "xls/spin/promela_generator.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_replace.h" +#include "gmock/gmock.h" +#include "google/protobuf/text_format.h" +#include "gtest/gtest.h" +#include "xls/common/file/filesystem.h" +#include "xls/common/file/get_runfile_path.h" +#include "xls/common/file/temp_directory.h" +#include "xls/common/status/matchers.h" +#include "xls/common/subprocess.h" +#include "xls/common/undeclared_outputs.h" +#include "xls/dslx/ir_convert/ir_converter.h" +#include "xls/dslx/run_routines/run_routines.h" +#include "xls/ir/evaluator_result.pb.h" +#include "xls/ir/ir_test_base.h" +#include "xls/ir/package.h" +#include "xls/passes/optimization_pass_pipeline.h" +#include "xls/spin/trace_compare.h" + +namespace xls::spin { +namespace { + +using ::absl_testing::StatusIs; +using ::testing::ElementsAre; +using ::testing::HasSubstr; +using ::testing::Not; + +struct DslxFunctionParam { + std::string name; // e.g. "a" + std::string type; // e.g. "u32" +}; + +/* Helper functions */ + +// Returns the text from the opening '{' to the matching '}' for the first +// block whose header contains `header`, or "" if not found. +std::string_view BlockBody(std::string_view out, std::string_view header) { + auto hpos = out.find(header); + if (hpos == std::string_view::npos) { + return ""; + } + auto open = out.find('{', hpos); + if (open == std::string_view::npos) { + return ""; + } + int64_t depth = 0; + for (size_t i = open; i < out.size(); ++i) { + if (out[i] == '{') { + ++depth; + } else if (out[i] == '}' && --depth == 0) { + return out.substr(open, i - open + 1); + } + } + return ""; +} + +std::string_view InitBody(std::string_view out) { + return BlockBody(out, "\ninit {"); +} + +std::string_view ProctypeBody(std::string_view out, std::string_view name) { + return BlockBody(out, absl::StrCat("proctype ", name, "(")); +} + +std::string_view InlineBody(std::string_view out, std::string_view name) { + return BlockBody(out, absl::StrCat("inline ", name, "(")); +} + +// Writes content to TEST_UNDECLARED_OUTPUTS_DIR/... +// No-op when TEST_UNDECLARED_OUTPUTS_DIR is not set. +void DumpArtifact(std::string_view filename, std::string_view content) { + std::optional dir = GetUndeclaredOutputDirectory(); + if (!dir) { + return; + } + const auto* info = testing::UnitTest::GetInstance()->current_test_info(); + std::string test_id = + info ? absl::StrCat(info->test_suite_name(), ".", info->name()) + : "unknown"; + (void)SetFileContents(*dir / absl::StrCat(test_id, ".", filename), content); +} + +// Generates Promela and dumps it as .pml; returns the text. +absl::StatusOr GenerateAndDump( + Package* package, const PromelaGeneratorOptions& options = {}) { + XLS_ASSIGN_OR_RETURN(std::string pml, + PromelaGenerator::Generate(package, options)); + DumpArtifact(absl::StrCat(package->name(), ".pml"), pml); + return pml; +} + +// Internal implementation: runs the full pipeline given a resolved source path +// and the directory to use for --dslx_path (so DSLX imports can be resolved). +testing::AssertionResult VerifyDslxPromelaTraces( + const std::filesystem::path& src_path, + const std::filesystem::path& dslx_path_dir, std::string_view dslx_top, + bool emit_termination_hook) { + auto spin_path = GetXlsRunfilePath("spin", "spin"); + if (!spin_path.ok()) { + return testing::AssertionFailure() << "spin: " << spin_path.status(); + } + + auto impl = [&]() -> absl::Status { + std::string module_name = src_path.stem().string(); + XLS_ASSIGN_OR_RETURN(std::string dslx_source, GetFileContents(src_path)); + + // Step 1: DSLX interpreter API -> EvaluatorResultsProto. + dslx::ParseAndTypecheckOptions ptc; + ptc.dslx_paths = {dslx_path_dir}; + EvaluatorResultsProto proto; + dslx::ParseAndTestOptions pts; + pts.parse_and_typecheck_options = ptc; + pts.trace_channels = true; + pts.results_out = &proto; + dslx::DslxInterpreterTestRunner runner; + XLS_RETURN_IF_ERROR( + runner.ParseAndTest(dslx_source, module_name, src_path.string(), pts) + .status()); + std::string proto_text; + if (!google::protobuf::TextFormat::PrintToString(proto, &proto_text)) { + return absl::InternalError("proto serialization failed"); + } + + // Steps 2+3: DSLX -> optimised IR (API). + dslx::ConvertOptions conv_opts; + conv_opts.emit_positions = true; + conv_opts.emit_assert = true; + conv_opts.convert_tests = true; + conv_opts.lower_to_proc_scoped_channels = true; + bool printed_error = false; + const std::string src_path_str = src_path.string(); + std::string_view module_paths[] = {src_path_str}; + XLS_ASSIGN_OR_RETURN( + dslx::PackageConversionData conv, + dslx::ConvertFilesToPackage( + module_paths, /*dslx_stdlib_path=*/"", {dslx_path_dir}, conv_opts, + std::string(dslx_top), module_name, &printed_error)); + XLS_RETURN_IF_ERROR( + RunOptimizationPassPipeline(conv.package.get()).status()); + + // Step 4: Optimised IR -> Promela (API). + PromelaGeneratorOptions pml_opts; + pml_opts.emit_termination_hook = emit_termination_hook; + pml_opts.emit_source_hints = true; + XLS_ASSIGN_OR_RETURN(std::string pml, PromelaGenerator::Generate( + conv.package.get(), pml_opts)); + + // Write Promela to a temp dir for SPIN. + XLS_ASSIGN_OR_RETURN(auto tmp, TempDirectory::Create("dslx_promela_trace")); + const std::filesystem::path dir = tmp.path(); + const std::filesystem::path pml_file = dir / "out.pml"; + const std::filesystem::path spin_trace = dir / "spin_trace.json"; + XLS_RETURN_IF_ERROR(SetFileContents(pml_file, pml)); + + // Step 5: SPIN guided simulation -> JSON trace (still a subprocess). + std::vector argv = {spin_path->string(), "-c", "-Q", + spin_trace.string(), pml_file.string()}; + XLS_ASSIGN_OR_RETURN(SubprocessResult r, InvokeSubprocess(argv, dir)); + if (r.exit_status != 0) { + return absl::InternalError(absl::StrFormat( + "spin failed (exit %d):\n%s", r.exit_status, r.stderr_content)); + } + + // Step 6: Parse both traces and compare. + std::string_view term_chan = emit_termination_hook ? "terminator" : ""; + TraceMap spin_events, dslx_events; + XLS_ASSIGN_OR_RETURN(std::string spin_json, GetFileContents(spin_trace)); + + // Dump artifacts. + DumpArtifact("harness.x", dslx_source); + DumpArtifact("out.pml", pml); + DumpArtifact("dslx_trace.textproto", proto_text); + DumpArtifact("spin_trace.json", spin_json); + XLS_RETURN_IF_ERROR(ParseSpinTrace(spin_json, spin_events, term_chan)); + XLS_RETURN_IF_ERROR(ParseDslxTrace(proto_text, dslx_events, term_chan)); + return CompareTraces(spin_events, dslx_events); + }; + + auto status = impl(); + if (!status.ok()) { + return testing::AssertionFailure() << status.message(); + } + return testing::AssertionSuccess(); +} + +// Generates a DSLX source file with the structure shown in the raw-string +// template at the bottom of this function. The variable sections below are +// pre-built and substituted into the $PLACEHOLDER tokens. +std::string GenerateFunctionHarnessDslx( + std::string_view fn_name, const std::vector& params, + std::string_view return_type, std::string_view fn_body, + const std::vector>& test_cases, + std::string_view fn_extras = "") { + // Per-param string sections (each appended once per param in the loop below). + std::string fn_params; // "a: u32, b: u32" for the fn declaration + std::string h_fields; // " chan_a: chan in;\n" x N + std::string cfg_params; // "chan_a: chan in, chan_b: chan in" + std::string cfg_tuple; // "chan_a, chan_b" + std::string recvs; // recv chain in next(state:()) + std::string call_args; // "a, b" + std::string t_fields; // " chan_a: chan out;\n" x N + std::string chan_decls; // "let (chan_a_s, chan_a_r) = chan("a");\n" x N + std::string spawn_args; // "chan_a_r, chan_b_r," (trailing comma consumed by + // $SPAWN_ARGS, key) + std::string ret_tuple; // "chan_a_s, chan_b_s," (trailing comma consumed by + // $RET_TUPLE, key) + std::string loop_body; // dispatch + send chain in next(i:u32) + + for (size_t i = 0; i < params.size(); ++i) { + const auto& p = params[i]; + absl::StrAppendFormat(&h_fields, " chan_%s: chan<%s> in;\n", p.name, + p.type); + if (i > 0) { + absl::StrAppend(&fn_params, ", "); + absl::StrAppend(&cfg_params, ", "); + absl::StrAppend(&cfg_tuple, ", "); + absl::StrAppend(&call_args, ", "); + absl::StrAppend(&spawn_args, ", "); + absl::StrAppend(&ret_tuple, ", "); + } + absl::StrAppendFormat(&fn_params, "%s: %s", p.name, p.type); + absl::StrAppendFormat(&cfg_params, "chan_%s: chan<%s> in", p.name, p.type); + absl::StrAppendFormat(&cfg_tuple, "chan_%s", p.name); + absl::StrAppendFormat(&recvs, + " let (tok, %s) = recv(%s, chan_%s);\n", + p.name, i == 0 ? "join()" : "tok", p.name); + absl::StrAppend(&call_args, p.name); + absl::StrAppendFormat(&t_fields, " chan_%s: chan<%s> out;\n", p.name, + p.type); + absl::StrAppendFormat( + &chan_decls, + " let (chan_%s_s, chan_%s_r) = chan<%s>(\"chan_%s\");\n", p.name, + p.name, p.type, p.name); + absl::StrAppendFormat(&spawn_args, "chan_%s_r", p.name); + absl::StrAppendFormat(&ret_tuple, "chan_%s_s", p.name); + absl::StrAppendFormat(&loop_body, + " let tok = send(%s, chan_%s, v_%s);\n", + i == 0 ? "join()" : "tok", p.name, p.name); + } + + // Build the full function declaration from components. + std::string fn_decl; + if (!fn_extras.empty()) { + absl::StrAppendFormat(&fn_decl, "%s\n", fn_extras); + } + absl::StrAppendFormat(&fn_decl, "fn %s(%s) -> %s { %s }", fn_name, fn_params, + return_type, fn_body); + + // Build the test-case dispatch + send block. + // One test case: simple bindings. Multiple cases: match expression. + std::string dispatch; + if (test_cases.size() == 1) { + for (size_t pi = 0; pi < params.size(); ++pi) { + absl::StrAppendFormat(&dispatch, " let v_%s = %s;\n", + params[pi].name, test_cases[0][pi]); + } + } else if (params.size() == 1) { + absl::StrAppendFormat(&dispatch, " let v_%s: %s = match i {\n", + params[0].name, params[0].type); + for (size_t tc = 0; tc < test_cases.size(); ++tc) { + if (tc + 1 < test_cases.size()) { + absl::StrAppendFormat(&dispatch, " u32:%zu => %s,\n", tc, + test_cases[tc][0]); + } else { + absl::StrAppendFormat(&dispatch, " _ => %s,\n", + test_cases[tc][0]); + } + } + absl::StrAppend(&dispatch, " };\n"); + } else { + absl::StrAppend(&dispatch, " let ("); + for (size_t pi = 0; pi < params.size(); ++pi) { + if (pi > 0) { + absl::StrAppend(&dispatch, ", "); + } + absl::StrAppendFormat(&dispatch, "v_%s", params[pi].name); + } + absl::StrAppend(&dispatch, "): ("); + for (size_t pi = 0; pi < params.size(); ++pi) { + if (pi > 0) { + absl::StrAppend(&dispatch, ", "); + } + absl::StrAppend(&dispatch, params[pi].type); + } + absl::StrAppend(&dispatch, ") = match i {\n"); + for (size_t tc = 0; tc < test_cases.size(); ++tc) { + if (tc + 1 < test_cases.size()) { + absl::StrAppendFormat(&dispatch, " u32:%zu => (", tc); + } else { + absl::StrAppend(&dispatch, " _ => ("); + } + for (size_t pi = 0; pi < params.size(); ++pi) { + if (pi > 0) { + absl::StrAppend(&dispatch, ", "); + } + absl::StrAppend(&dispatch, test_cases[tc][pi]); + } + absl::StrAppend(&dispatch, "),\n"); + } + absl::StrAppend(&dispatch, " };\n"); + } + loop_body = dispatch + loop_body; + + std::string last_idx = absl::StrCat(test_cases.size() - 1); + + // The raw string below is the exact DSLX that gets generated. + // $SPAWN_ARGS, and $RET_TUPLE, include the trailing comma so the template + // reads as natural DSLX syntax. + return absl::StrReplaceAll( + R"(#![feature(type_inference_v2)] + +$FN_BODY + +proc $NAME_Harness { +$H_FIELDS chan_result: chan<$RTYPE> out; + + config($CFG_PARAMS, chan_result: chan<$RTYPE> out) { + ($CFG_TUPLE, chan_result) + } + + init { () } + + next(state: ()) { +$RECVS let r = $NAME($CALL_ARGS); + send(tok, chan_result, r); + } +} + +#[test_proc] +proc $NAME_Test { +$T_FIELDS chan_result: chan<$RTYPE> in; + terminator: chan out; + + config(terminator: chan out) { +$CHAN_DECLS let (chan_result_s, chan_result_r) = chan<$RTYPE>("chan_result"); + spawn $NAME_Harness($SPAWN_ARGS, chan_result_s); + ($RET_TUPLE, chan_result_r, terminator) + } + + init { u32:0 } + + next(i: u32) { +$LOOP_BODY let (tok, _) = recv(tok, chan_result); + send_if(tok, terminator, i == u32:$LAST_IDX, true); + i + u32:1 + } +} +)", + { + {"$FN_BODY", fn_decl}, + {"$NAME", fn_name}, + {"$RTYPE", return_type}, + {"$H_FIELDS", h_fields}, + {"$CFG_PARAMS", cfg_params}, + {"$CFG_TUPLE", cfg_tuple}, + {"$RECVS", recvs}, + {"$CALL_ARGS", call_args}, + {"$T_FIELDS", t_fields}, + {"$CHAN_DECLS", chan_decls}, + {"$SPAWN_ARGS", spawn_args}, + {"$RET_TUPLE", ret_tuple}, + {"$LOOP_BODY", loop_body}, + {"$LAST_IDX", last_idx}, + }); +} + +/* Test fixture */ + +class PromelaGeneratorTest : public IrTestBase { + protected: + // Loads IR text from testdata/. + absl::StatusOr LoadTestIr(std::string_view filename); + + // Parses `ir_text` and runs the generator. + absl::StatusOr Generate(std::string_view ir_text); + + // Loads IR from testdata/, generates Promela, and returns it. + absl::StatusOr GenerateFromIrFile(std::string_view filename); + + // Generate Promela from `ir_text` and verify that `spin -a` accepts it. + testing::AssertionResult GenerateAndSpinCheck(std::string_view ir_text); + + // Loads IR from the given Bazel runfile path, generates Promela, and verifies + // that `spin -a` accepts it. + testing::AssertionResult GenerateAndSpinCheckFromFile( + std::string_view runfile_path); + + // Writes `promela` to a temp file and runs `spin -a` on it. + testing::AssertionResult SpinSyntaxOk(std::string_view promela); + + // Runs the full pipeline for inline DSLX source text and compares traces. + testing::AssertionResult DslxPromelaTracesMatch( + std::string_view dslx_source, std::string_view dslx_top, + bool emit_termination_hook = false); + + // Runs the full pipeline for a Bazel runfile path; uses the file's parent + // directory as --dslx_path so imports resolve correctly. + testing::AssertionResult DslxPromelaTracesMatchFile( + std::string_view runfile_path, std::string_view dslx_top, + bool emit_termination_hook = false); + + // Wraps a DSLX function in a harness + test proc, runs both DSLX interpreter + // and SPIN simulation, and verifies that per-channel event sequences match. + testing::AssertionResult DslxFunctionTracesMatch( + std::string_view fn_name, const std::vector& params, + std::string_view return_type, std::string_view fn_body, + const std::vector>& test_cases, + std::string_view fn_extras = ""); +}; + +absl::StatusOr PromelaGeneratorTest::Generate( + std::string_view ir_text) { + XLS_ASSIGN_OR_RETURN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(ir_text)); + return GenerateAndDump(pkg.get()); +} + +absl::StatusOr PromelaGeneratorTest::LoadTestIr( + std::string_view filename) { + XLS_ASSIGN_OR_RETURN( + std::filesystem::path path, + GetXlsRunfilePath(absl::StrCat("xls/spin/testdata/", filename))); + return GetFileContents(path); +} + +absl::StatusOr PromelaGeneratorTest::GenerateFromIrFile( + std::string_view filename) { + XLS_ASSIGN_OR_RETURN(std::string ir_text, LoadTestIr(filename)); + return Generate(ir_text); +} + +testing::AssertionResult PromelaGeneratorTest::SpinSyntaxOk( + std::string_view promela) { + auto dir_or = TempDirectory::Create("spin_syntax"); + if (!dir_or.ok()) { + return testing::AssertionFailure() + << "TempDirectory::Create: " << dir_or.status(); + } + const std::filesystem::path pml = dir_or->path() / "model.pml"; + if (auto s = SetFileContents(pml, promela); !s.ok()) { + return testing::AssertionFailure() << "SetFileContents: " << s; + } + auto spin_or = GetXlsRunfilePath("spin", "spin"); + if (!spin_or.ok()) { + return testing::AssertionFailure() + << "GetXlsRunfilePath(spin): " << spin_or.status(); + } + std::vector argv = {spin_or->string(), "-a", pml.string()}; + auto result_or = InvokeSubprocess(argv, dir_or->path()); + if (!result_or.ok()) { + return testing::AssertionFailure() + << "InvokeSubprocess: " << result_or.status(); + } + if (result_or->exit_status != 0) { + return testing::AssertionFailure() + << "spin -a exited " << result_or->exit_status << "\n" + << "stdout:\n" + << result_or->stdout_content << "stderr:\n" + << result_or->stderr_content; + } + return testing::AssertionSuccess(); +} + +testing::AssertionResult PromelaGeneratorTest::GenerateAndSpinCheck( + std::string_view ir_text) { + auto pkg_or = IrTestBase::ParsePackageNoVerify(ir_text); + if (!pkg_or.ok()) { + return testing::AssertionFailure() + << "ParsePackageNoVerify: " << pkg_or.status(); + } + auto pml_or = GenerateAndDump(pkg_or->get()); + if (!pml_or.ok()) { + return testing::AssertionFailure() + << "PromelaGenerator::Generate: " << pml_or.status(); + } + return SpinSyntaxOk(*pml_or); +} + +testing::AssertionResult PromelaGeneratorTest::GenerateAndSpinCheckFromFile( + std::string_view runfile_path) { + auto ir_path_or = GetXlsRunfilePath(runfile_path); + if (!ir_path_or.ok()) { + return testing::AssertionFailure() + << "GetXlsRunfilePath: " << ir_path_or.status(); + } + auto ir_text_or = GetFileContents(*ir_path_or); + if (!ir_text_or.ok()) { + return testing::AssertionFailure() + << "GetFileContents: " << ir_text_or.status(); + } + return GenerateAndSpinCheck(*ir_text_or); +} + +testing::AssertionResult PromelaGeneratorTest::DslxPromelaTracesMatch( + std::string_view dslx_source, std::string_view dslx_top, + bool emit_termination_hook) { + auto tmp = TempDirectory::Create("dslx_src"); + if (!tmp.ok()) + return testing::AssertionFailure() << "TempDirectory: " << tmp.status(); + const std::filesystem::path src = tmp->path() / "src.x"; + if (auto s = SetFileContents(src, dslx_source); !s.ok()) + return testing::AssertionFailure() << "writing source: " << s; + return VerifyDslxPromelaTraces(src, tmp->path(), dslx_top, + emit_termination_hook); +} + +testing::AssertionResult PromelaGeneratorTest::DslxPromelaTracesMatchFile( + std::string_view runfile_path, std::string_view dslx_top, + bool emit_termination_hook) { + auto src = GetXlsRunfilePath(runfile_path); + if (!src.ok()) + return testing::AssertionFailure() << "source file: " << src.status(); + return VerifyDslxPromelaTraces(*src, src->parent_path(), dslx_top, + emit_termination_hook); +} + +testing::AssertionResult PromelaGeneratorTest::DslxFunctionTracesMatch( + std::string_view fn_name, const std::vector& params, + std::string_view return_type, std::string_view fn_body, + const std::vector>& test_cases, + std::string_view fn_extras) { + std::string dslx_source = GenerateFunctionHarnessDslx( + fn_name, params, return_type, fn_body, test_cases, fn_extras); + std::string top = absl::StrCat(fn_name, "_Test"); + return DslxPromelaTracesMatch(dslx_source, top, + /*emit_termination_hook=*/true); +} + +/* Init block and package header */ + +TEST_F(PromelaGeneratorTest, InitBlockRunsAllProcs) { + // The init block spawns a proctype for every proc in the package. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, Generate(R"( +package multi_proc +top proc __multi_proc__ProcA_0_next<>(__state: bits[1], init={0}) { + __state: bits[1] = state_read(state_element=__state, id=2) + __token: token = literal(value=token, id=1) + literal.3: bits[1] = literal(value=1, id=3) + tuple.4: () = tuple(id=4) + next_value.5: () = next_value(param=__state, value=__state, id=5) +} +proc __multi_proc__ProcB_0_next<>(__state: bits[1], init={0}) { + __state: bits[1] = state_read(state_element=__state, id=7) + __token: token = literal(value=token, id=6) + literal.8: bits[1] = literal(value=1, id=8) + tuple.9: () = tuple(id=9) + next_value.10: () = next_value(param=__state, value=__state, id=10) +} +)")); + std::string_view init = InitBody(out); + ASSERT_THAT(init, Not(testing::IsEmpty())); + EXPECT_THAT(init, HasSubstr("run __multi_proc__ProcA_0_next();")); + EXPECT_THAT(init, HasSubstr("run __multi_proc__ProcB_0_next();")); +} + +TEST_F(PromelaGeneratorTest, NoInitBlockForFunctionPackage) { + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("func.ir")); + EXPECT_THAT(out, Not(HasSubstr("init {"))); +} + +TEST_F(PromelaGeneratorTest, HeaderContainsPackageName) { + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("func.ir")); + EXPECT_THAT(out, HasSubstr("func")); +} + +/* Function structure */ + +TEST_F(PromelaGeneratorTest, FuncStructure_Body) { + // A function becomes a Promela inline macro with params and a _ret out-param. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("func.ir")); + EXPECT_THAT(out, HasSubstr("inline fn___func__myfunc(a, b, _ret)")); + EXPECT_THAT(out, Not(HasSubstr("proctype"))); + EXPECT_THAT(out, Not(HasSubstr("init {"))); +} + +TEST_F(PromelaGeneratorTest, FuncStructure_ArithmeticOp) { + // Add emits as a typed variable assignment `int v_ = lhs + rhs;`. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("func.ir")); + std::string_view fn = InlineBody(out, "fn___func__myfunc"); + ASSERT_THAT(fn, Not(testing::IsEmpty())); + EXPECT_THAT(fn, HasSubstr("int v_add_3 = a + b;")); +} + +TEST_F(PromelaGeneratorTest, FuncStructure_ResultViaRetParam) { + // The return value is assigned to _ret; ordering: computation before assign. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("func.ir")); + std::string_view fn = InlineBody(out, "fn___func__myfunc"); + ASSERT_THAT(fn, Not(testing::IsEmpty())); + EXPECT_THAT(fn, HasSubstr("_ret = v_add_3;")); + EXPECT_LT(fn.find("v_add_3 = a + b"), fn.find("_ret = v_add_3")); +} + +/* Proc structure */ + +TEST_F(PromelaGeneratorTest, ProcStructure_Body) { + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("proc.ir")); + EXPECT_THAT( + out, + HasSubstr("proctype __proc__MyProc_0_next(chan _req_r; chan _resp_s)")); + std::string_view pt = ProctypeBody(out, "__proc__MyProc_0_next"); + ASSERT_THAT(pt, Not(testing::IsEmpty())); + EXPECT_THAT(pt, HasSubstr("short s___state = 0;")); + EXPECT_THAT(pt, HasSubstr("do\n ::")); + EXPECT_THAT(pt, HasSubstr("od")); +} + +TEST_F(PromelaGeneratorTest, ProcStructure_StateInitialValue) { + // Patch proc.ir to change initial state value from 0 to 7. + XLS_ASSERT_OK_AND_ASSIGN(std::string ir_text, LoadTestIr("proc.ir")); + XLS_ASSERT_OK_AND_ASSIGN( + std::string out, + Generate(absl::StrReplaceAll(ir_text, {{"init={0}", "init={7}"}}))); + std::string_view pt = ProctypeBody(out, "__proc__MyProc_0_next"); + ASSERT_THAT(pt, Not(testing::IsEmpty())); + EXPECT_THAT(pt, HasSubstr("short s___state = 7;")); +} + +TEST_F(PromelaGeneratorTest, ProcStructure_StateUpdateAfterComputation) { + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("proc.ir")); + std::string_view pt = ProctypeBody(out, "__proc__MyProc_0_next"); + ASSERT_THAT(pt, Not(testing::IsEmpty())); + EXPECT_THAT(pt, HasSubstr("s___state = v_add_15;")); + // State update must appear after the add that computes the new value. + EXPECT_LT(pt.find("v_add_15 = (s___state +"), + pt.find("s___state = v_add_15")); +} + +TEST_F(PromelaGeneratorTest, ProcStructure_TokenStateNotEmitted) { + // The token state element must not produce a Promela variable or assignment. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("proc.ir")); + std::string_view pt = ProctypeBody(out, "__proc__MyProc_0_next"); + ASSERT_THAT(pt, Not(testing::IsEmpty())); + EXPECT_THAT(pt, Not(HasSubstr("token"))); + EXPECT_THAT(pt, Not(HasSubstr("s___tkn"))); +} + +TEST_F(PromelaGeneratorTest, ProcStructure_XrForRecv) { + // TODO: also check xs for send channels not polled via non-blocking receive. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("proc.ir")); + std::string_view pt = ProctypeBody(out, "__proc__MyProc_0_next"); + ASSERT_THAT(pt, Not(testing::IsEmpty())); + EXPECT_THAT(pt, HasSubstr("xr _req_r;")); + EXPECT_THAT(pt, Not(HasSubstr("xs"))); + EXPECT_THAT(pt, Not(HasSubstr("xr _resp_s"))); +} + +TEST_F(PromelaGeneratorTest, ProcStructure_ChannelDeclarations) { + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("proc.ir")); + std::string_view init = InitBody(out); + ASSERT_THAT(init, Not(testing::IsEmpty())); + EXPECT_THAT(init, HasSubstr("chan _req_r = [8] of { byte };")); + EXPECT_THAT(init, HasSubstr("chan _resp_s = [8] of { int };")); +} + +// I have to check this +TEST_F(PromelaGeneratorTest, ProcNoBodyOps) { + // parent owns a channel and spawns child but has no sends/receives/state: + // EmitProc returns early after emitting the channel declaration and run stmt. + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +proc child<_ch: bits[8] in>(__tkn: token, __s: bits[8], init={token, 0}) { + chan_interface _ch(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[8] = state_read(state_element=__s, id=2) + receive.3: (token, bits[8]) = receive(__tkn, channel=_ch, id=3) + tuple_index.4: token = tuple_index(receive.3, index=0, id=4) + next_value.5: () = next_value(param=__tkn, value=tuple_index.4, id=5) + next_value.6: () = next_value(param=__s, value=__s, id=6) +} +top proc parent<>(__tkn: token, init={token}) { + chan _ch(bits[8], id=0, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _ch(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _ch(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + proc_instantiation child_inst(_ch, proc=child) + __tkn: token = state_read(state_element=__tkn, id=7) + next_value.8: () = next_value(param=__tkn, value=__tkn, id=8) +})")); + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get())); + // parent emits channel decl and run, but no do/od loop. + EXPECT_THAT(out, HasSubstr("chan _ch = [")); + EXPECT_THAT(out, HasSubstr("run child(_ch);")); + // Parent's proctype closes right after the run statement (no loop). + EXPECT_THAT(out, HasSubstr("run child(_ch);\n\n}")); +} + +/* Channel operations */ + +TEST_F(PromelaGeneratorTest, ProcReceive) { + // Receive emits `chan ? data_var` inside a predicated if/fi block. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, + GenerateFromIrFile("blocking_receive.ir")); + EXPECT_THAT(out, HasSubstr("_in_r ? v_receive_6_data")); +} + +TEST_F(PromelaGeneratorTest, ProcSend) { + // Send emits `chan ! payload` inside a predicated if/fi; payload is the state + // var. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, + GenerateFromIrFile("unconditional_send.ir")); + EXPECT_THAT(out, HasSubstr("_out_s ! s___state")); +} + +TEST_F(PromelaGeneratorTest, ProcPredicatedSend) { + // Conditional send wraps the `!` in `if :: (pred) -> chan ! val :: else -> + // skip fi`. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, + GenerateFromIrFile("predicated_send.ir")); + EXPECT_THAT(out, HasSubstr("(v_and_7 != 0) -> _out_s ! s___state")); + EXPECT_THAT(out, HasSubstr(":: else -> skip;")); +} + +TEST_F(PromelaGeneratorTest, ProcNonBlockingReceive) { + // Non-blocking receive must use an atomic poll+receive pair (?[]/?) so that + // no other process can consume the message between the check and the read + // (guide section 4). + XLS_ASSERT_OK_AND_ASSIGN(std::string out, + GenerateFromIrFile("non_blocking_receive.ir")); + EXPECT_THAT(out, HasSubstr("atomic {")); + EXPECT_THAT(out, HasSubstr("_in_r?[v_receive_7_data]")); + EXPECT_THAT(out, HasSubstr("_in_r ? v_receive_7_data")); + EXPECT_THAT(out, HasSubstr("v_receive_7_data_valid = 1")); + EXPECT_THAT(out, HasSubstr(":: else -> v_receive_7_data_valid = 0")); +} + +TEST_F(PromelaGeneratorTest, ProcNonBlockingReceivePredicated) { + // Predicated non-blocking receive: predicate is ANDed with the poll inside + // the same atomic block. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, Generate(R"(package p +proc nb_cond_reader(__tkn: token, __s: bits[1], init={token, 0}) { + chan_interface in(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[1] = state_read(state_element=__s, id=2) + literal.3: bits[1] = literal(value=1, id=3) + receive.4: (token, bits[32], bits[1]) = receive(__tkn, predicate=literal.3, blocking=false, channel=in, id=4) + recv_tok: token = tuple_index(receive.4, index=0, id=5) + next_value.6: () = next_value(param=__tkn, value=recv_tok, id=6) + next_value.7: () = next_value(param=__s, value=__s, id=7) +})")); + EXPECT_THAT(out, HasSubstr("atomic {")); + EXPECT_THAT(out, HasSubstr("(v_literal_3 != 0) && in?[v_receive_4_data]")); + EXPECT_THAT(out, Not(HasSubstr("nempty"))); +} + +TEST_F(PromelaGeneratorTest, ProcPredicatedReceive) { + // Conditional receive wraps the `?` in `if :: (pred) -> chan ? var :: else -> + // skip fi`. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, + GenerateFromIrFile("predicated_receive.ir")); + EXPECT_THAT(out, HasSubstr("(v_and_8 != 0) -> _in_r ? v_receive_9_data")); + EXPECT_THAT(out, HasSubstr(":: else -> skip;")); +} +/* Operations */ + +TEST_F(PromelaGeneratorTest, TraceMatch_Add) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a + b", + /*test_cases=*/ + {{"u32:1", "u32:2"}, {"u32:10", "u32:20"}, {"u32:100", "u32:200"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Sub) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a - b", + /*test_cases=*/ + {{"u32:10", "u32:3"}, {"u32:5", "u32:5"}, {"u32:100", "u32:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Neg) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s32"}}, /*return_type=*/"s32", + /*fn_body=*/"-a", + /*test_cases=*/{{"s32:5"}, {"s32:0"}, {"s32:-3"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_UMul) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a * b", + /*test_cases=*/ + {{"u32:3", "u32:4"}, {"u32:100", "u32:200"}, {"u32:0", "u32:5"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_SMul) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s32"}, {"b", "s32"}}, + /*return_type=*/"s32", /*fn_body=*/"a * b", + /*test_cases=*/{{"s32:3", "s32:-4"}, {"s32:-2", "s32:-5"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_UDiv) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a / b", + /*test_cases=*/{{"u32:10", "u32:3"}, {"u32:100", "u32:4"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_SDiv) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s32"}, {"b", "s32"}}, + /*return_type=*/"s32", /*fn_body=*/"a / b", + /*test_cases=*/{{"s32:-10", "s32:3"}, {"s32:10", "s32:-2"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_UMod) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a % b", + /*test_cases=*/{{"u32:10", "u32:3"}, {"u32:7", "u32:4"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_SMod) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s32"}, {"b", "s32"}}, + /*return_type=*/"s32", /*fn_body=*/"a % b", + /*test_cases=*/{{"s32:10", "s32:3"}, {"s32:-10", "s32:3"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_And) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a & b", + /*test_cases=*/{{"u32:255", "u32:15"}, {"u32:170", "u32:85"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Or) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a | b", + /*test_cases=*/{{"u32:240", "u32:15"}, {"u32:0", "u32:0"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Xor) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a ^ b", + /*test_cases=*/{{"u32:255", "u32:15"}, {"u32:170", "u32:170"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Not) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u8"}}, /*return_type=*/"u8", + /*fn_body=*/"!a", + /*test_cases=*/{{"u8:0"}, {"u8:255"}, {"u8:170"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_AndReduce) { + // and_reduce requires unsigned input. Use !a to compute all-ones at runtime + // (avoiding a large literal that would overflow SPIN's signed int). + // !u32:0 = all_ones -> and_reduce = 1; !u32:1 = ...1110 -> 0. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}}, /*return_type=*/"u1", + /*fn_body=*/"and_reduce(!a)", + /*test_cases=*/ + {{"u32:0"}, // !0 = all_ones -> 1 + {"u32:1"}, // !1 = ...1110 -> 0 + {"u32:2147483647"}})); // !0x7FFFFFFF = 0x80000000 -> 0 +} + +TEST_F(PromelaGeneratorTest, TraceMatch_OrReduce) { + // or_reduce requires unsigned input; use values that fit in SPIN's int range. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}}, /*return_type=*/"u1", + /*fn_body=*/"or_reduce(a)", + /*test_cases=*/{{"u32:0"}, {"u32:1"}, {"u32:1073741824"}})); // bit 30 + // set, fits + // in signed + // int +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Shll) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"n", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a << n", + /*test_cases=*/{{"u32:1", "u32:4"}, {"u32:3", "u32:8"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Shrl) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"n", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a >> n", + /*test_cases=*/{{"u32:256", "u32:4"}, {"u32:768", "u32:8"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Shra) { + // Arithmetic right shift on signed type: sign bit is replicated. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s32"}, {"n", "u32"}}, + /*return_type=*/"s32", /*fn_body=*/"a >> n", + /*test_cases=*/{{"s32:-256", "u32:4"}, {"s32:256", "u32:4"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Eq) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u1", /*fn_body=*/"a == b", + /*test_cases=*/{{"u32:5", "u32:5"}, {"u32:5", "u32:6"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Ne) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u1", /*fn_body=*/"a != b", + /*test_cases=*/{{"u32:5", "u32:6"}, {"u32:5", "u32:5"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_ULt) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u1", /*fn_body=*/"a < b", + /*test_cases=*/ + {{"u32:3", "u32:5"}, {"u32:5", "u32:5"}, {"u32:5", "u32:3"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_ULe) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u1", /*fn_body=*/"a <= b", + /*test_cases=*/ + {{"u32:3", "u32:5"}, {"u32:5", "u32:5"}, {"u32:6", "u32:5"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_UGt) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u1", /*fn_body=*/"a > b", + /*test_cases=*/ + {{"u32:5", "u32:3"}, {"u32:5", "u32:5"}, {"u32:3", "u32:5"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_UGe) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u1", /*fn_body=*/"a >= b", + /*test_cases=*/ + {{"u32:5", "u32:3"}, {"u32:5", "u32:5"}, {"u32:3", "u32:5"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_SLt) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s32"}, {"b", "s32"}}, + /*return_type=*/"u1", /*fn_body=*/"a < b", + /*test_cases=*/ + {{"s32:-1", "s32:0"}, {"s32:0", "s32:0"}, {"s32:1", "s32:-1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_SLe) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s32"}, {"b", "s32"}}, + /*return_type=*/"u1", /*fn_body=*/"a <= b", + /*test_cases=*/ + {{"s32:-1", "s32:0"}, {"s32:0", "s32:0"}, {"s32:1", "s32:-1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_SGt) { + EXPECT_TRUE(DslxFunctionTracesMatch(/*fn_name=*/"op", + /*params=*/{{"a", "s32"}, {"b", "s32"}}, + /*return_type=*/"u1", /*fn_body=*/"a > b", + /*test_cases=*/ + {{"s32:0", "s32:-1"}, // 0 > -1 -> 1 + {"s32:0", "s32:0"}, // 0 > 0 -> 0 + {"s32:-1", "s32:0"}})); // -1 > 0 -> 0 +} + +TEST_F(PromelaGeneratorTest, TraceMatch_SGe) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s32"}, {"b", "s32"}}, + /*return_type=*/"u1", /*fn_body=*/"a >= b", + /*test_cases=*/ + {{"s32:0", "s32:-1"}, {"s32:-1", "s32:0"}, {"s32:-1", "s32:-1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_BitSlice) { + // a[4:8] extracts bits 4..7 (a 4-bit nibble from position 4). + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u16"}}, /*return_type=*/"u4", + /*fn_body=*/"a[4:8]", + /*test_cases=*/ + {{"u16:240"}, // 0x00F0 -> nibble[4:8] = 0xF = 15 + {"u16:15"}, // 0x000F -> nibble[4:8] = 0x0 = 0 + {"u16:4080"}})); // 0x0FF0 -> nibble[4:8] = 0xF = 15 +} + +TEST_F(PromelaGeneratorTest, TraceMatch_DynamicBitSlice) { + // a[start +: u4] extracts 4 bits starting at runtime index `start`. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u16"}, {"start", "u32"}}, + /*return_type=*/"u4", /*fn_body=*/"a[start +: u4]", + /*test_cases=*/ + {{"u16:240", "u32:4"}, // 0x00F0, bits[4:8] = 0xF = 15 + {"u16:240", "u32:0"}, // 0x00F0, bits[0:4] = 0x0 = 0 + {"u16:15", "u32:0"}})); // 0x000F, bits[0:4] = 0xF = 15 +} + +TEST_F(PromelaGeneratorTest, TraceMatch_SignExtend) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "s8"}}, /*return_type=*/"s32", + /*fn_body=*/"a as s32", + /*test_cases=*/ + {{"s8:127"}, // positive: no sign bit -> 127 + {"s8:-1"}, // 0xFF -> sign-extended to 0xFFFFFFFF = -1 + {"s8:-128"}})); // 0x80 -> sign-extended to 0xFFFFFF80 = -128 +} + +TEST_F(PromelaGeneratorTest, TraceMatch_ZeroExtend) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u8"}}, /*return_type=*/"u32", + /*fn_body=*/"a as u32", + /*test_cases=*/{{"u8:0"}, {"u8:127"}, {"u8:255"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Concat) { + // a ++ b: a in high byte, b in low byte of u16, then zero-extended to u32. + // Single test case avoids the multi-case if/else chains that the optimizer + // converts to sign-extend patterns, which would cause SPIN truncation + // warnings when the resulting int is assigned to a byte variable. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u8"}, {"b", "u8"}}, + /*return_type=*/"u32", /*fn_body=*/"(a ++ b) as u32", + /*test_cases=*/{{"u8:1", "u8:2"}})); // 0x0102 = 258 +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Sel) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"s", "u1"}, {"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"if s == u1:0 { a } else { b }", + /*test_cases=*/ + {{"u1:0", "u32:10", "u32:20"}, {"u1:1", "u32:10", "u32:20"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_OneHotSel) { + // one_hot_sel: bit i of selector picks cases[i]; multiple bits OR the cases. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"s", "u2"}, {"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"one_hot_sel(s, [a, b])", + /*test_cases=*/ + {{"u2:1", "u32:10", "u32:20"}, // bit 0 -> a = 10 + {"u2:2", "u32:10", "u32:20"}})); // bit 1 -> b = 20 +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Gate) { + // gate!(cond, value): passes value through when cond is true, else 0. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"c", "u1"}, {"a", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"gate!(c as bool, a)", + /*test_cases=*/{{"u1:1", "u32:42"}, {"u1:0", "u32:42"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Invoke) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}}, /*return_type=*/"u32", + /*fn_body=*/"helper(a)", + /*test_cases=*/{{"u32:5"}, {"u32:21"}, {"u32:0"}}, + /*fn_extras=*/"fn helper(x: u32) -> u32 { x + x }")); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_AddMul_U32) { + // a + b * c: two operations chained, u32 (no overflow). + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}, {"c", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"a + b * c", + /*test_cases=*/ + {{"u32:10", "u32:3", "u32:7"}, // 10 + 3*7 = 31 + {"u32:100", "u32:20", "u32:5"}})); // 100 + 20*5 = 200 +} + +TEST_F(PromelaGeneratorTest, TraceMatch_AddMul_U8_Overflow) { + // b*c = 10*30 = 300 -> u8:44; a + 44 = 244+44 = 288 -> u8:32. + // Both the intermediate multiply and the final add overflow u8. + // Single case to avoid optimizer sign-extend patterns on sub-byte types. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u8"}, {"b", "u8"}, {"c", "u8"}}, + /*return_type=*/"u8", /*fn_body=*/"a + b * c", + /*test_cases=*/{{"u8:244", "u8:10", "u8:30"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_CompareAfterOverflow_U8) { + // a + b overflows u8: 200+100 = 300 -> u8:44. + // The comparison then sees the wrapped value: u8:44 > u8:100 -> false -> + // u8:0. Without intermediate masking the Promela would compare 300 > 100 -> + // true -> u8:1 (wrong). + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u8"}, {"b", "u8"}, {"t", "u8"}}, + /*return_type=*/"u8", /*fn_body=*/"if a + b > t { u8:1 } else { u8:0 }", + /*test_cases=*/{{"u8:200", "u8:100", "u8:100"}})); +} + +// Mixed-width operands +// Shifts: result width = value width (amount width is independent). +// Concat: result width = sum of part widths. + +TEST_F(PromelaGeneratorTest, TraceMatch_Shll_WideValue_NarrowAmount) { + // u8 value, u4 shift amount -> result u8. 200 << 2 = 800, wraps to 32. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u8"}, {"n", "u4"}}, + /*return_type=*/"u8", /*fn_body=*/"a << n", + /*test_cases=*/{{"u8:200", "u4:2"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Shll_NarrowValue_WideAmount) { + // u4 value, u8 shift amount -> result u4. 3 << 3 = 24, wraps to 8. + // (Mirrors TraceMatch_Shll_Overflow_U4 but with a u8 shift amount.) + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u4"}, {"n", "u8"}}, + /*return_type=*/"u4", /*fn_body=*/"a << n", + /*test_cases=*/{{"u4:3", "u8:3"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Shrl_WideValue_NarrowAmount) { + // u12 value, u4 shift amount -> result u12. 4095 >> 4 = 255. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u12"}, {"n", "u4"}}, + /*return_type=*/"u12", /*fn_body=*/"a >> n", + /*test_cases=*/{{"u12:4095", "u4:4"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Shll_WideValue_NarrowAmount_Overflow) { + // u12 value, u4 shift amount -> result u12. 2048 << 1 = 4096, wraps to 0. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u12"}, {"n", "u4"}}, + /*return_type=*/"u12", /*fn_body=*/"a << n", + /*test_cases=*/{{"u12:2048", "u4:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Concat_DifferentWidth) { + // u4 ++ u8 -> u12. 0xA ++ 0xBC = 0xABC = 2748. + // Result width = 4 + 8 = 12; value fits in short channel (2748 < 32768). + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u4"}, {"b", "u8"}}, + /*return_type=*/"u12", /*fn_body=*/"a ++ b", + /*test_cases=*/{{"u4:10", "u8:188"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Concat_ThreeParts) { + // u4 ++ u4 ++ u8 -> u16. 0x1 ++ 0x2 ++ 0x34 = 0x1234 = 4660. + // Operand widths 4, 4, 8 all differ from result width 16. + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u4"}, {"b", "u4"}, {"c", "u8"}}, + /*return_type=*/"u16", /*fn_body=*/"a ++ b ++ c", + /*test_cases=*/{{"u4:1", "u4:2", "u8:52"}})); +} + +// Type-width-aware wrapping (sub-int arithmetic must wrap at declared width) +// Each test uses a single input tuple so the optimizer emits constant +// assignments instead of if/else chains that would trigger sign-extend +// patterns. + +// u4 (4-bit -> byte channel) +TEST_F(PromelaGeneratorTest, TraceMatch_Add_Overflow_U4) { + // 15 + 1 = 16, wraps to 0 in u4 + EXPECT_TRUE(DslxFunctionTracesMatch(/*fn_name=*/"op", + /*params=*/{{"a", "u4"}, {"b", "u4"}}, + /*return_type=*/"u4", /*fn_body=*/"a + b", + /*test_cases=*/{{"u4:15", "u4:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Sub_Underflow_U4) { + // 0 - 1 = -1, wraps to 15 in u4 + EXPECT_TRUE(DslxFunctionTracesMatch(/*fn_name=*/"op", + /*params=*/{{"a", "u4"}, {"b", "u4"}}, + /*return_type=*/"u4", /*fn_body=*/"a - b", + /*test_cases=*/{{"u4:0", "u4:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Mul_Overflow_U4) { + // 5 * 4 = 20, wraps to 4 in u4 (20 & 0xF = 4) + EXPECT_TRUE(DslxFunctionTracesMatch(/*fn_name=*/"op", + /*params=*/{{"a", "u4"}, {"b", "u4"}}, + /*return_type=*/"u4", /*fn_body=*/"a * b", + /*test_cases=*/{{"u4:5", "u4:4"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Shll_Overflow_U4) { + // 3 << 3 = 24, wraps to 8 in u4 (24 & 0xF = 8) + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u4"}, {"n", "u32"}}, + /*return_type=*/"u4", /*fn_body=*/"a << n", + /*test_cases=*/{{"u4:3", "u32:3"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Not_U4) { + // bitwise NOT: ~0101 = 1010, i.e. !u4:5 = u4:10 + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u4"}}, /*return_type=*/"u4", + /*fn_body=*/"!a", /*test_cases=*/{{"u4:5"}})); +} + +// u6 (6-bit, non-power-of-2 -> byte channel) +TEST_F(PromelaGeneratorTest, TraceMatch_Add_Overflow_U6) { + // 63 + 1 = 64, wraps to 0 in u6 (mask = 0x3F) + EXPECT_TRUE(DslxFunctionTracesMatch(/*fn_name=*/"op", + /*params=*/{{"a", "u6"}, {"b", "u6"}}, + /*return_type=*/"u6", /*fn_body=*/"a + b", + /*test_cases=*/{{"u6:63", "u6:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Sub_Underflow_U6) { + // 0 - 1 = -1, wraps to 63 in u6 + EXPECT_TRUE(DslxFunctionTracesMatch(/*fn_name=*/"op", + /*params=*/{{"a", "u6"}, {"b", "u6"}}, + /*return_type=*/"u6", /*fn_body=*/"a - b", + /*test_cases=*/{{"u6:0", "u6:1"}})); +} + +// u8 (8-bit -> byte channel) +TEST_F(PromelaGeneratorTest, TraceMatch_Add_Overflow_U8) { + // 255 + 1 = 256, wraps to 0 in u8 + EXPECT_TRUE(DslxFunctionTracesMatch(/*fn_name=*/"op", + /*params=*/{{"a", "u8"}, {"b", "u8"}}, + /*return_type=*/"u8", /*fn_body=*/"a + b", + /*test_cases=*/{{"u8:255", "u8:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Sub_Underflow_U8) { + // 0 - 1 = -1, wraps to 255 in u8 + EXPECT_TRUE(DslxFunctionTracesMatch(/*fn_name=*/"op", + /*params=*/{{"a", "u8"}, {"b", "u8"}}, + /*return_type=*/"u8", /*fn_body=*/"a - b", + /*test_cases=*/{{"u8:0", "u8:1"}})); +} + +// u12 (12-bit, non-power-of-2 -> short channel; max value 4095 fits in short) +TEST_F(PromelaGeneratorTest, TraceMatch_Add_Overflow_U12) { + // 4095 + 1 = 4096, wraps to 0 in u12 (mask = 0xFFF) + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u12"}, {"b", "u12"}}, + /*return_type=*/"u12", /*fn_body=*/"a + b", + /*test_cases=*/{{"u12:4095", "u12:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Sub_Underflow_U12) { + // 0 - 1 = -1, wraps to 4095 in u12 + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u12"}, {"b", "u12"}}, + /*return_type=*/"u12", /*fn_body=*/"a - b", + /*test_cases=*/{{"u12:0", "u12:1"}})); +} + +// u20 (20-bit, non-power-of-2 -> int channel) +TEST_F(PromelaGeneratorTest, TraceMatch_Add_Overflow_U20) { + // 1048575 + 1 = 1048576, wraps to 0 in u20 (mask = 0xFFFFF) + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u20"}, {"b", "u20"}}, + /*return_type=*/"u20", /*fn_body=*/"a + b", + /*test_cases=*/{{"u20:1048575", "u20:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Sub_Underflow_U20) { + // 0 - 1 = -1, wraps to 1048575 in u20 + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u20"}, {"b", "u20"}}, + /*return_type=*/"u20", /*fn_body=*/"a - b", + /*test_cases=*/{{"u20:0", "u20:1"}})); +} + +// u31 (31-bit -> int channel; max value is 2^31-1 = INT_MAX) +TEST_F(PromelaGeneratorTest, TraceMatch_Add_Overflow_U31) { + // 2147483647 + 1 overflows signed int to -2147483648; (-2147483648) & + // 0x7FFFFFFF = 0 + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u31"}, {"b", "u31"}}, + /*return_type=*/"u31", /*fn_body=*/"a + b", + /*test_cases=*/{{"u31:2147483647", "u31:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Sub_Underflow_U31) { + // 0 - 1 = -1, wraps to 2147483647 in u31 + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u31"}, {"b", "u31"}}, + /*return_type=*/"u31", /*fn_body=*/"a - b", + /*test_cases=*/{{"u31:0", "u31:1"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Nand) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"!(a & b)", + /*test_cases=*/ + {{"u32:0", "u32:0"}, {"u32:5", "u32:3"}, {"u32:0xff", "u32:0xff"}})); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Nor) { + EXPECT_TRUE(DslxFunctionTracesMatch( + /*fn_name=*/"op", /*params=*/{{"a", "u32"}, {"b", "u32"}}, + /*return_type=*/"u32", /*fn_body=*/"!(a | b)", + /*test_cases=*/ + {{"u32:0", "u32:0"}, {"u32:5", "u32:3"}, {"u32:0xff", "u32:0x00"}})); +} + +TEST_F(PromelaGeneratorTest, PrioritySel_DirectIR) { + XLS_ASSERT_OK_AND_ASSIGN(std::string out, Generate(R"(package p +fn f(sel: bits[2], a: bits[32], b: bits[32], d: bits[32]) -> bits[32] { + ret priority_sel.1: bits[32] = priority_sel(sel, cases=[a, b], default=d, id=1) +})")); + // Default value is assigned first; each case overwrites if its bit is set. + EXPECT_THAT(out, HasSubstr("v_priority_sel_1 = d")); +} + +// No TraceMatch_PrioritySel: priority_sel lowers to array_index which is not +// yet supported by promela_main. + +/* Side-effect operations */ + +TEST_F(PromelaGeneratorTest, MinDelay_DirectIR) { + XLS_ASSERT_OK_AND_ASSIGN(std::string out, Generate(R"(package p +fn f() -> token { + after_all.1: token = after_all(id=1) + ret min_delay.2: token = min_delay(after_all.1, delay=3, id=2) +})")); + // min_delay is token-only: no variable emitted, function still generates. + EXPECT_THAT(out, HasSubstr("fn_f")); +} + +TEST_F(PromelaGeneratorTest, Cover_DirectIR) { + XLS_ASSERT_OK_AND_ASSIGN(std::string out, Generate(R"(package p +fn f(cond: bits[1]) -> () { + ret cover.1: () = cover(cond, label="my_label", id=1) +})")); + // cover is unit-typed: no variable emitted, function still generates. + EXPECT_THAT(out, HasSubstr("fn_f")); +} + +TEST_F(PromelaGeneratorTest, ProcAssert) { + // assert() emits a Promela assert() guarded by a conditional printf that + // prints the label only when the condition is false. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("assertion.ir")); + EXPECT_THAT( + out, HasSubstr(R"(:: (v_or_9 == 0) -> printf("XLS_ASSERT:nonzero\n");)")); + EXPECT_THAT(out, HasSubstr("assert(v_or_9 != 0)")); +} + +/* Unimplemented operations */ + +TEST_F(PromelaGeneratorTest, Unimplemented_XorReduce) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[8]) -> bits[1] { + ret xor_reduce.1: bits[1] = xor_reduce(a, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: xor_reduce"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_Reverse) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32]) -> bits[32] { + ret reverse.1: bits[32] = reverse(a, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: reverse"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_OneHot) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[4]) -> bits[5] { + ret one_hot.1: bits[5] = one_hot(a, lsb_prio=true, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: one_hot"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_Array) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32], b: bits[32]) -> bits[32][2] { + ret array.1: bits[32][2] = array(a, b, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: array"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_ArrayConcat) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32][2], b: bits[32][2]) -> bits[32][4] { + ret array_concat.1: bits[32][4] = array_concat(a, b, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: array_concat"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_ArrayIndex) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32][4], idx: bits[2]) -> bits[32] { + ret array_index.1: bits[32] = array_index(a, indices=[idx], id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: array_index"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_ArraySlice) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32][4], idx: bits[2]) -> bits[32][2] { + ret array_slice.1: bits[32][2] = array_slice(a, idx, width=2, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: array_slice"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_ArrayUpdate) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32][4], val: bits[32], idx: bits[2]) -> bits[32][4] { + ret array_update.1: bits[32][4] = array_update(a, val, indices=[idx], id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: array_update"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_Map) { + EXPECT_THAT(Generate(R"(package p +fn double(x: bits[32]) -> bits[32] { + ret add.1: bits[32] = add(x, x, id=1) +} +fn f(a: bits[32][3]) -> bits[32][3] { + ret map.2: bits[32][3] = map(a, to_apply=double, id=2) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: map"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_CountedFor) { + EXPECT_THAT(Generate(R"(package p +fn body(i: bits[32], acc: bits[32]) -> bits[32] { + ret add.1: bits[32] = add(i, acc, id=1) +} +fn f(x: bits[32]) -> bits[32] { + ret counted_for.2: bits[32] = counted_for(x, trip_count=4, stride=1, body=body, id=2) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: counted_for"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_DynamicCountedFor) { + EXPECT_THAT(Generate(R"(package p +fn body(i: bits[32], acc: bits[32]) -> bits[32] { + ret add.1: bits[32] = add(i, acc, id=1) +} +fn f(x: bits[32], tc: bits[16], st: bits[16]) -> bits[32] { + ret dynamic_counted_for.2: bits[32] = dynamic_counted_for(x, tc, st, body=body, id=2) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: dynamic_counted_for"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_BitSliceUpdate) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32], start: bits[5], val: bits[8]) -> bits[32] { + ret bit_slice_update.1: bits[32] = bit_slice_update(a, start, val, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: bit_slice_update"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_Encode) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[16]) -> bits[4] { + ret encode.1: bits[4] = encode(a, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: encode"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_Decode) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[3]) -> bits[8] { + ret decode.1: bits[8] = decode(a, width=8, id=1) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: decode"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_SMulp) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32], b: bits[32]) -> bits[32] { + smulp.1: (bits[32], bits[32]) = smulp(a, b, id=1) + ret add.2: bits[32] = add(a, b, id=2) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: smulp"))); +} + +TEST_F(PromelaGeneratorTest, Unimplemented_UMulp) { + EXPECT_THAT(Generate(R"(package p +fn f(a: bits[32], b: bits[32]) -> bits[32] { + umulp.1: (bits[32], bits[32]) = umulp(a, b, id=1) + ret add.2: bits[32] = add(a, b, id=2) +})"), + StatusIs(absl::StatusCode::kUnimplemented, + HasSubstr("unsupported op: umulp"))); +} + +/* Error cases */ + +TEST_F(PromelaGeneratorTest, ErrorOnOldStyleGlobalChannels) { + // A package with procs that use package-level channels (not proc-scoped) + // must be rejected with a clear INVALID_ARGUMENT error. + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package old_style +chan step_in(bits[32], id=0, kind=streaming, ops=receive_only, + flow_control=ready_valid, strictness=proven_mutually_exclusive) +chan count_out(bits[32], id=1, kind=streaming, ops=send_only, + flow_control=ready_valid, strictness=proven_mutually_exclusive) +top proc counter(__count: bits[32], init={0}) { + after_all.2: token = after_all(id=2) + receive.3: (token, bits[32]) = receive(after_all.2, channel=step_in, id=3) + tuple_index.4: token = tuple_index(receive.3, index=0, id=4) + step: bits[32] = tuple_index(receive.3, index=1, id=5) + __count_r: bits[32] = state_read(state_element=__count, id=6) + new_count: bits[32] = add(__count_r, step, id=7) + send.8: token = send(tuple_index.4, new_count, channel=count_out, id=8) + next_value.9: () = next_value(param=__count, value=new_count, id=9) +})")); + EXPECT_THAT(GenerateAndDump(pkg.get()), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("proc-scoped channels"))); +} + +TEST_F(PromelaGeneratorTest, ErrorOnBitWidthOver32) { + EXPECT_THAT( + Generate(R"(package wide +fn f(x: bits[64]) -> bits[64] { + ret identity.1: bits[64] = identity(x, id=1) +})"), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("bit width 64"))); +} + +TEST_F(PromelaGeneratorTest, FunctionOnlyPackageNotRejected) { + // A package with only functions and no procs has no channels at all; + // it must be accepted even though ChannelsAreProcScoped() returns false. + XLS_ASSERT_OK_AND_ASSIGN(std::string out, Generate(R"(package fns_only +fn add(a: bits[32], b: bits[32]) -> bits[32] { + ret add.1: bits[32] = add(a, b, id=1) +})")); + EXPECT_THAT(out, HasSubstr("inline fn_add")); + EXPECT_THAT(out, Not(HasSubstr("init {"))); +} + +TEST_F(PromelaGeneratorTest, ErrorOnBitWidthOver32_ProcNode) { + EXPECT_THAT( + Generate(R"(package p +top proc my_proc<>(__tkn: token, init={token}) { + __tkn: token = state_read(state_element=__tkn, id=1) + lit: bits[64] = literal(value=0, id=2) + next_value.3: () = next_value(param=__tkn, value=__tkn, id=3) +})"), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("bit width 64"))); +} + +TEST_F(PromelaGeneratorTest, ErrorOnBitWidthOver32_ProcStateElement) { + // State element bits[64]: the corresponding Param node fires the check. + EXPECT_THAT( + Generate(R"(package p +top proc my_proc<>(__tkn: token, __state: bits[64], init={token, 0}) { + __tkn: token = state_read(state_element=__tkn, id=1) + next_value.2: () = next_value(param=__tkn, value=__tkn, id=2) +})"), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("bit width 64"))); +} + +TEST_F(PromelaGeneratorTest, ErrorOnBitWidthOver32_ProcInterfaceChannel) { + // Interface channel bits[64] with no bits[64] nodes (no receive in body). + EXPECT_THAT(Generate(R"(package p +top proc my_proc<_big: bits[64] in>(__tkn: token, init={token}) { + chan_interface _big(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + next_value.2: () = next_value(param=__tkn, value=__tkn, id=2) +})"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("channel '_big'"))); +} + +TEST_F(PromelaGeneratorTest, ErrorOnBitWidthOver32_ProcOwnedChannel) { + // Owned channel bits[64]: chan_interface entries carry the same type, + // so the interface check fires and rejects the package. + EXPECT_THAT(Generate(R"(package p +top proc my_proc<>(__tkn: token, init={token}) { + chan _big(bits[64], id=0, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _big(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _big(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + next_value.2: () = next_value(param=__tkn, value=__tkn, id=2) +})"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("channel '_big'"))); +} + +/* Generator options */ + +TEST_F(PromelaGeneratorTest, SourceLocationsDisabledByDefault) { + // Without the option, no /* file:line:col */ comments should appear. + // (The package header comment "/* Promela model ... */" is always present; + // per-node location comments use the pattern "/* file:line:col */".) + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateFromIrFile("proc.ir")); + EXPECT_THAT(out, Not(HasSubstr("/* file:"))); +} + +TEST_F(PromelaGeneratorTest, SourceLocationsEmitted) { + // IR with an explicit source location on the add node. The location is + // encoded as (fileno, lineno, colno); fileno 1 maps to "counter.x". + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +file_number 1 "counter.x" +fn add(a: bits[32], b: bits[32]) -> bits[32] { + ret add.1: bits[32] = add(a, b, id=1, pos=[(1,10,5)]) +})")); + PromelaGeneratorOptions opts; + opts.emit_source_locations = true; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("/* counter.x:10:5 */")); + EXPECT_THAT(out, HasSubstr("v_add_1 = a + b")); +} + +TEST_F(PromelaGeneratorTest, SourceLocationsNodeWithoutLoc) { + // Nodes with no recorded location must not produce a comment even when the + // option is enabled. (The package header is allowed; per-node location + // comments use the pattern "/* file:line:col */".) + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +fn add(a: bits[32], b: bits[32]) -> bits[32] { + ret add.1: bits[32] = add(a, b, id=1) +})")); + PromelaGeneratorOptions opts; + opts.emit_source_locations = true; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, Not(HasSubstr("/* file:"))); + EXPECT_THAT(out, HasSubstr("v_add_1 = a + b")); +} + +TEST_F(PromelaGeneratorTest, OptionChannelDepth) { + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +proc sender(__tkn: token, __s: bits[32], init={token, 0}) { + chan_interface out_ch(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[32] = state_read(state_element=__s, id=2) + send.3: token = send(__tkn, __s, channel=out_ch, id=3) + next_value.4: () = next_value(param=__tkn, value=send.3, id=4) + next_value.5: () = next_value(param=__s, value=__s, id=5) +})")); + PromelaGeneratorOptions opts; + opts.channel_depth = 4; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("[4] of {")); + EXPECT_THAT(out, Not(HasSubstr("[8] of {"))); +} + +TEST_F(PromelaGeneratorTest, OptionEmitSourceHints) { + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +fn add(a: bits[32], b: bits[32]) -> bits[32] { + ret add.1: bits[32] = add(a, b, id=1) +})")); + PromelaGeneratorOptions opts; + opts.emit_source_hints = true; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("/* ir:")); + EXPECT_THAT(out, HasSubstr("v_add_1 = a + b")); +} + +TEST_F(PromelaGeneratorTest, OptionEmitTerminationHook) { + // emit_termination_hook adds a __terminated guard to each proc loop and + // a global flag that init sets after the terminator channel fires. + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +proc sender(__tkn: token, __s: bits[32], init={token, 0}) { + chan_interface out_ch(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[32] = state_read(state_element=__s, id=2) + send.3: token = send(__tkn, __s, channel=out_ch, id=3) + next_value.4: () = next_value(param=__tkn, value=send.3, id=4) + next_value.5: () = next_value(param=__s, value=__s, id=5) +})")); + PromelaGeneratorOptions opts; + opts.emit_termination_hook = true; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("bit __terminated = 0;")); + EXPECT_THAT(out, HasSubstr("(__terminated) -> break")); +} + +TEST_F(PromelaGeneratorTest, OptionAssertSendOnFullChannel) { + // assert_send_on_full_channel turns a blocked-on-full-channel situation into + // an explicit SPIN assertion violation rather than a deadlock. + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +proc sender(__tkn: token, __s: bits[32], init={token, 0}) { + chan_interface out_ch(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[32] = state_read(state_element=__s, id=2) + send.3: token = send(__tkn, __s, channel=out_ch, id=3) + next_value.4: () = next_value(param=__tkn, value=send.3, id=4) + next_value.5: () = next_value(param=__s, value=__s, id=5) +})")); + PromelaGeneratorOptions opts; + opts.assert_send_on_full_channel = true; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("assert(len(out_ch) < 8)")); +} + +TEST_F(PromelaGeneratorTest, OptionWorstCaseThroughput) { + // worst_case_throughput adds an idle-stall counter __thr so that SPIN + // explores the case where the proc does real work at most once every N + // iterations. + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +proc sender(__tkn: token, __s: bits[32], init={token, 0}) { + chan_interface out_ch(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[32] = state_read(state_element=__s, id=2) + send.3: token = send(__tkn, __s, channel=out_ch, id=3) + next_value.4: () = next_value(param=__tkn, value=send.3, id=4) + next_value.5: () = next_value(param=__s, value=__s, id=5) +})")); + PromelaGeneratorOptions opts; + opts.worst_case_throughput["sender"] = 3; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("byte __thr = 0;")); + EXPECT_THAT(out, HasSubstr(":: (__thr > 0) -> __thr--;")); + EXPECT_THAT(out, HasSubstr(":: (__thr == 0) ->")); + EXPECT_THAT(out, HasSubstr("__thr = 2;")); // throughput - 1 +} + +TEST_F(PromelaGeneratorTest, OptionEmitProgressLabels) { + // emit_progress_labels prefixes each channel op with a SPIN progress label + // for livelock detection via spin -search -DNP. + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +proc reader(__tkn: token, __s: bits[1], init={token, 0}) { + chan_interface in_ch(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[1] = state_read(state_element=__s, id=2) + receive.3: (token, bits[32]) = receive(__tkn, channel=in_ch, id=3) + tuple_index.4: token = tuple_index(receive.3, index=0, id=4) + next_value.5: () = next_value(param=__tkn, value=tuple_index.4, id=5) + next_value.6: () = next_value(param=__s, value=__s, id=6) +})")); + PromelaGeneratorOptions opts; + opts.emit_progress_labels = true; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("progress_recv_in_ch: in_ch ?")); +} + +TEST_F(PromelaGeneratorTest, OptionWorstCaseThroughputLarge) { + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +proc sender(__tkn: token, __s: bits[32], init={token, 0}) { + chan_interface out_ch(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[32] = state_read(state_element=__s, id=2) + send.3: token = send(__tkn, __s, channel=out_ch, id=3) + next_value.4: () = next_value(param=__tkn, value=send.3, id=4) + next_value.5: () = next_value(param=__s, value=__s, id=5) +})")); + PromelaGeneratorOptions opts; + opts.worst_case_throughput["sender"] = 257; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("short __thr = 0;")); +} + +TEST_F(PromelaGeneratorTest, EmitInitTerminatorChannel) { + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +top proc my_proc<_terminator: bits[1] out>(__tkn: token, __s: bits[1], init={token, 0}) { + chan_interface _terminator(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __tkn: token = state_read(state_element=__tkn, id=1) + __s: bits[1] = state_read(state_element=__s, id=2) + send.3: token = send(__tkn, __s, channel=_terminator, id=3) + next_value.4: () = next_value(param=__tkn, value=send.3, id=4) + next_value.5: () = next_value(param=__s, value=__s, id=5) +})")); + PromelaGeneratorOptions opts; + opts.emit_termination_hook = true; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("bit __term_val;")); + EXPECT_THAT(out, HasSubstr("_terminator ? __term_val;")); + EXPECT_THAT(out, HasSubstr("__terminated = 1;")); +} + +TEST_F(PromelaGeneratorTest, SourceLocationsUnregisteredFile) { + // fileno 99 is not registered; falls back to "file:99:line:col" format. + XLS_ASSERT_OK_AND_ASSIGN(std::unique_ptr pkg, + IrTestBase::ParsePackageNoVerify(R"(package p +fn add(a: bits[32], b: bits[32]) -> bits[32] { + ret add.1: bits[32] = add(a, b, id=1, pos=[(99,10,5)]) +})")); + PromelaGeneratorOptions opts; + opts.emit_source_locations = true; + XLS_ASSERT_OK_AND_ASSIGN(std::string out, GenerateAndDump(pkg.get(), opts)); + EXPECT_THAT(out, HasSubstr("/* file:99:10:5 */")); +} + +/* Spin syntax validation */ + +TEST_F(PromelaGeneratorTest, SpinSyntax_Proc) { + EXPECT_TRUE(GenerateAndSpinCheckFromFile("xls/spin/testdata/proc.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_ProcReceive) { + EXPECT_TRUE( + GenerateAndSpinCheckFromFile("xls/spin/testdata/blocking_receive.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_ProcSend) { + EXPECT_TRUE( + GenerateAndSpinCheckFromFile("xls/spin/testdata/unconditional_send.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_PredicatedSend) { + EXPECT_TRUE( + GenerateAndSpinCheckFromFile("xls/spin/testdata/predicated_send.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_PredicatedReceive) { + EXPECT_TRUE( + GenerateAndSpinCheckFromFile("xls/spin/testdata/predicated_receive.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_ProcAssert) { + EXPECT_TRUE(GenerateAndSpinCheckFromFile("xls/spin/testdata/assertion.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_MultiProc) { + EXPECT_TRUE(GenerateAndSpinCheckFromFile("xls/spin/testdata/multi_proc.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_NonBlockingReceive) { + EXPECT_TRUE(GenerateAndSpinCheckFromFile( + "xls/spin/testdata/non_blocking_receive.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_XrHints) { + // TODO: add xs coverage once the generator emits xs for safe send channels. + EXPECT_TRUE(GenerateAndSpinCheckFromFile("xls/spin/testdata/xr_hints.ir")); +} + +TEST_F(PromelaGeneratorTest, SpinSyntax_Deadlock) { + // deadlock.x has a circular send/receive dependency between procs A and B. + // The generated Promela must be syntactically valid even though exhaustive + // search (spin -search) will find a deadlock at run time. + EXPECT_TRUE(GenerateAndSpinCheckFromFile("xls/spin/testdata/deadlock.ir")); +} + +/* End-to-end proc tests */ + +TEST_F(PromelaGeneratorTest, TraceMatch_Passthrough) { + EXPECT_TRUE(DslxPromelaTracesMatchFile("xls/spin/testdata/passthrough.x", + "PassthroughTest", + /*emit_termination_hook=*/true)); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_Counter) { + EXPECT_TRUE(DslxPromelaTracesMatchFile("xls/spin/testdata/counter.x", + "CounterTest", + /*emit_termination_hook=*/true)); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_ConditionalIo) { + EXPECT_TRUE(DslxPromelaTracesMatchFile("xls/spin/testdata/conditional_io.x", + "ConditionalIoTest", + /*emit_termination_hook=*/true)); +} + +TEST_F(PromelaGeneratorTest, TraceMatch_ChannelNaming) { + EXPECT_TRUE(DslxPromelaTracesMatchFile("xls/spin/testdata/channel_naming.x", + "MultiplyBy4Test", + /*emit_termination_hook=*/true)); +} + +} // namespace +} // namespace xls::spin diff --git a/xls/spin/promela_main.cc b/xls/spin/promela_main.cc new file mode 100644 index 0000000000..9c5e455e99 --- /dev/null +++ b/xls/spin/promela_main.cc @@ -0,0 +1,178 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// XLS IR -> Promela code generator for the SPIN model checker. +// Usage: promela_main [--output=PATH] IR_FILE (stdout if --output omitted) + +#include +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_split.h" +#include "xls/common/exit_status.h" +#include "xls/common/file/filesystem.h" +#include "xls/common/init_xls.h" +#include "xls/common/status/status_macros.h" +#include "xls/ir/ir_parser.h" +#include "xls/spin/promela_generator.h" + +ABSL_FLAG(std::string, output, "", + "Path for the generated Promela file. Writes to stdout if empty."); + +ABSL_FLAG(std::string, top, "", + "Name of the top function or proc. Required when the package " + "contains multiple entry points and no top is already set."); + +ABSL_FLAG(bool, emit_source_locations, false, + "Annotate each generated Promela statement with a comment of the " + "form /* filename:line:col */ derived from the XLS IR source " + "location. Useful for tracing generated Promela back to the " + "original DSLX source."); + +ABSL_FLAG(bool, emit_source_hints, false, + "Annotate each generated Promela statement with a comment " + "containing the original IR node expression, e.g.: " + "/* ir: add.1: bits[32] = add(a, b, id=1) */"); + +ABSL_FLAG(int64_t, channel_depth, 8, + "Buffer depth for every declared Promela channel " + "(chan x = [N] of {T}). Should match the FIFO depth of the " + "corresponding XLS channel."); + +ABSL_FLAG(bool, emit_termination_hook, false, + "Append a blocking receive on the terminator channel to the init " + "block. init blocks until the test proc signals completion, making " + "the test end point visible in the simulation trace."); + +ABSL_FLAG(bool, assert_send_on_full_channel, false, + "Prefix every send operation with assert(len(ch) < DEPTH). " + "Turns a blocked-on-full-channel situation into an explicit SPIN " + "assertion violation during exhaustive verification. Useful for " + "proving that no execution path sends to a saturated channel."); + +ABSL_FLAG(std::string, worst_case_throughput, "", + "Comma-separated list of ProcName:N pairs specifying the worst-case " + "throughput for individual procs (e.g. 'SlowProc:4,OtherProc:2'). " + "ProcName must match the sanitised Promela proctype name as emitted " + "in the generated 'proctype () {' header. N is the number of " + "loop iterations between productive steps; procs not listed run " + "every iteration (N=1)."); + +ABSL_FLAG(bool, emit_progress_labels, false, + "Prefix each channel send and receive with a SPIN progress label " + "(progress_recv_: or progress_send_:). " + "Progress labels are used with spin -search -DNP for non-progress " + "cycle (livelock) detection: a cycle that visits no progress-labeled " + "state is flagged as a livelock."); + +namespace { +constexpr std::string_view kUsage = R"( +Generates Promela source code from an XLS IR file for model checking with +the SPIN verifier (https://spinroot.com). + +XLS functions are emitted as Promela inline procedures. +XLS procs are emitted as Promela proctypes. +XLS channels are emitted as Promela chan declarations. + +Example: + promela_main my_design.ir + promela_main --output=model.pml my_design.ir +)"; +} // namespace + +namespace xls { +namespace { + +absl::Status RealMain(std::string_view ir_path) { + if (ir_path == "-") { + ir_path = "/dev/stdin"; + } + + LOG(INFO) << "Parsing IR from '" << ir_path << "'"; + XLS_ASSIGN_OR_RETURN(std::string ir_contents, GetFileContents(ir_path)); + XLS_ASSIGN_OR_RETURN(std::unique_ptr package, + Parser::ParsePackage(ir_contents, ir_path)); + + const std::string top_name = absl::GetFlag(FLAGS_top); + if (!top_name.empty() && !package->GetTop().has_value()) { + XLS_RETURN_IF_ERROR(package->SetTopByName(top_name)); + } + + spin::PromelaGeneratorOptions options; + options.emit_source_locations = absl::GetFlag(FLAGS_emit_source_locations); + options.emit_source_hints = absl::GetFlag(FLAGS_emit_source_hints); + options.channel_depth = absl::GetFlag(FLAGS_channel_depth); + options.emit_termination_hook = absl::GetFlag(FLAGS_emit_termination_hook); + options.assert_send_on_full_channel = + absl::GetFlag(FLAGS_assert_send_on_full_channel); + options.emit_progress_labels = absl::GetFlag(FLAGS_emit_progress_labels); + + const std::string throughput_flag = + absl::GetFlag(FLAGS_worst_case_throughput); + if (!throughput_flag.empty()) { + for (std::string_view entry : absl::StrSplit(throughput_flag, ',')) { + std::vector parts = absl::StrSplit(entry, ':'); + if (parts.size() != 2) { + return absl::InvalidArgumentError(absl::StrFormat( + "--worst_case_throughput: expected 'Name:N', got '%s'", entry)); + } + int64_t throughput_value; + if (!absl::SimpleAtoi(parts[1], &throughput_value) || + throughput_value < 1) { + return absl::InvalidArgumentError(absl::StrFormat( + "--worst_case_throughput: N must be a positive integer, got '%s'", + parts[1])); + } + options.worst_case_throughput[std::string(parts[0])] = throughput_value; + } + } + + LOG(INFO) << "Generating Promela for package '" << package->name() << "'"; + XLS_ASSIGN_OR_RETURN( + std::string promela_text, + spin::PromelaGenerator::Generate(package.get(), options)); + + const std::string output_path = absl::GetFlag(FLAGS_output); + if (output_path.empty()) { + LOG(INFO) << "Writing Promela to stdout"; + std::cout << promela_text; + } else { + LOG(INFO) << "Writing Promela to '" << output_path << "'"; + XLS_RETURN_IF_ERROR(SetFileContents(output_path, promela_text)); + } + + return absl::OkStatus(); +} + +} // namespace +} // namespace xls + +int main(int argc, char** argv) { + std::vector positional_arguments = + xls::InitXls(kUsage, argc, argv); + + if (positional_arguments.size() != 1) { + LOG(QFATAL) << absl::StreamFormat("Expected invocation: %s IR_FILE", + argv[0]); + } + std::string_view ir_path = positional_arguments[0]; + return xls::ExitStatus(xls::RealMain(ir_path)); +} diff --git a/xls/spin/promela_trace_compare_main.cc b/xls/spin/promela_trace_compare_main.cc new file mode 100644 index 0000000000..3bc3ee1073 --- /dev/null +++ b/xls/spin/promela_trace_compare_main.cc @@ -0,0 +1,79 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Compares a SPIN trace against a DSLX trace per channel (order must match). +// Usage: promela_trace_compare --spin_trace= +// --dslx_trace= + +#include +#include + +#include "absl/flags/flag.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "xls/common/file/filesystem.h" +#include "xls/common/init_xls.h" +#include "xls/common/status/status_macros.h" +#include "xls/spin/trace_compare.h" + +ABSL_FLAG(std::string, spin_trace, "", "Path to SPIN simulation trace JSON."); +ABSL_FLAG(std::string, dslx_trace, "", + "Path to DSLX interpreter trace textproto."); +ABSL_FLAG(std::string, terminator_channel, "", + "Channel name used as a termination signal. When non-empty, " + "both traces are truncated after the first SEND on this channel."); + +namespace xls::spin { +namespace { + +absl::Status Run() { + std::string spin_path = absl::GetFlag(FLAGS_spin_trace); + std::string dslx_path = absl::GetFlag(FLAGS_dslx_trace); + if (spin_path.empty() || dslx_path.empty()) { + return absl::InvalidArgumentError( + "--spin_trace and --dslx_trace are required."); + } + + LOG(INFO) << "Comparing SPIN trace '" << spin_path << "' against DSLX trace '" + << dslx_path << "'"; + std::string spin_json, dslx_json; + XLS_ASSIGN_OR_RETURN(spin_json, GetFileContents(spin_path)); + XLS_ASSIGN_OR_RETURN(dslx_json, GetFileContents(dslx_path)); + + const std::string terminator_channel = + absl::GetFlag(FLAGS_terminator_channel); + TraceMap spin_events, dslx_events; + XLS_RETURN_IF_ERROR( + ParseSpinTrace(spin_json, spin_events, terminator_channel)); + XLS_RETURN_IF_ERROR( + ParseDslxTrace(dslx_json, dslx_events, terminator_channel)); + absl::Status result = CompareTraces(spin_events, dslx_events); + if (result.ok()) { + LOG(INFO) << "Trace comparison passed"; + } + return result; +} + +} // namespace +} // namespace xls::spin + +int main(int argc, char** argv) { + xls::InitXls(argv[0], argc, argv); + absl::Status status = xls::spin::Run(); + if (!status.ok()) { + std::cerr << status.message() << "\n"; + return 1; + } + return 0; +} diff --git a/xls/spin/spin_runner.cc b/xls/spin/spin_runner.cc new file mode 100644 index 0000000000..fc24f3143c --- /dev/null +++ b/xls/spin/spin_runner.cc @@ -0,0 +1,372 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "xls/spin/spin_runner.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/strings/match.h" +#include "absl/strings/str_format.h" +#include "google/protobuf/text_format.h" +#include "re2/re2.h" +#include "xls/common/file/filesystem.h" +#include "xls/common/file/get_runfile_path.h" +#include "xls/common/file/temp_directory.h" +#include "xls/common/status/status_macros.h" +#include "xls/common/subprocess.h" +#include "xls/dslx/create_import_data.h" +#include "xls/dslx/frontend/proc.h" +#include "xls/dslx/ir_convert/ir_converter.h" +#include "xls/dslx/parse_and_typecheck.h" +#include "xls/dslx/run_routines/run_routines.h" +#include "xls/dslx/virtualizable_file_system.h" +#include "xls/dslx/warning_kind.h" +#include "xls/ir/evaluator_result.pb.h" +#include "xls/ir/function_base.h" +#include "xls/ir/nodes.h" +#include "xls/ir/package.h" +#include "xls/passes/optimization_pass_pipeline.h" +#include "xls/spin/promela_generator.h" +#include "xls/spin/trace_compare.h" + +namespace xls::spin { + +namespace { + +// Output dir for one SPIN run: TEST_UNDECLARED_OUTPUTS_DIR/ if set, +// otherwise a temp directory deleted on destruction. Use operator/ for paths. +class OutputDir { + public: + static absl::StatusOr Create(std::string_view subdir) { + OutputDir result; + if (const char* base = std::getenv("TEST_UNDECLARED_OUTPUTS_DIR"); + base != nullptr && *base != '\0') { + result.path_ = std::filesystem::path(base) / subdir; + XLS_RETURN_IF_ERROR(RecursivelyCreateDir(result.path_)); + } else { + XLS_ASSIGN_OR_RETURN(result.tmp_, TempDirectory::Create(subdir)); + result.path_ = result.tmp_->path(); + } + return result; + } + + std::filesystem::path operator/(std::string_view name) const { + return path_ / name; + } + const std::filesystem::path& path() const { return path_; } + + private: + std::filesystem::path path_; + std::optional tmp_; +}; + +// Returns the #[test_proc] identifier to use as SPIN top, or "" if none. +// Uses test_filter to disambiguate multiple test procs; falls back to first. +std::string SelectTestProc(dslx::Module* module, + std::string_view entry_module_path, + const std::optional& test_filter) { + std::vector test_procs = module->GetTestProcs(); + LOG(INFO) << "Found " << test_procs.size() << " test proc(s) in " + << entry_module_path; + if (test_procs.empty()) { + return ""; + } + if (test_procs.size() == 1) { + return test_procs[0]->identifier(); + } + + if (test_filter.has_value()) { + RE2 re(*test_filter); + for (dslx::TestProc* test_proc : test_procs) { + if (RE2::FullMatch(test_proc->identifier(), re)) { + return test_proc->identifier(); + } + } + } + std::string top = test_procs[0]->identifier(); + LOG(WARNING) << "multiple #[test_proc] entries; using " << top; + return top; +} + +// Generates Promela from `package` and writes it to `pml_path`. +absl::Status GenerateAndWritePml(Package* package, + const PromelaGeneratorOptions& options, + const std::filesystem::path& promela_path) { + XLS_ASSIGN_OR_RETURN(std::string pml, + PromelaGenerator::Generate(package, options)); + LOG(INFO) << "Generated Promela model"; + XLS_RETURN_IF_ERROR(SetFileContents(promela_path, pml)); + LOG(INFO) << "Wrote Promela model to " << promela_path.string(); + return absl::OkStatus(); +} + +// Returns the "LINE:COL" source location of the Assert node whose label +// matches spin_label, or "" if not found. +std::string FindAssertLocation(Package* package, std::string_view spin_label) { + for (FunctionBase* function_base : package->GetFunctionBases()) { + for (Node* node : function_base->nodes()) { + if (!node->Is()) { + continue; + } + const Assert* assert_op = node->As(); + if (assert_op->label().has_value() && *assert_op->label() == spin_label) { + std::string dslx_location; + RE2::PartialMatch(assert_op->message(), R"(:(\d+:\d+))", + &dslx_location); + if (!dslx_location.empty()) { + return dslx_location; + } + } + } + } + return ""; +} + +// Returns the XLS_ASSERT label if spin_out contains "assertion violated", +// or nullopt if no violation. The label may be empty for non-XLS assertions. +std::optional ExtractAssertionLabel(const std::string& spin_out) { + if (!absl::StrContains(spin_out, "assertion violated")) { + return std::nullopt; + } + std::string label; + RE2::PartialMatch(spin_out, R"(XLS_ASSERT:([^\n]+))", &label); + return label; +} + +// Cross-checks a SPIN assertion violation against the DSLX trace. +// Returns OK if DSLX fired the same assertion (consistent failure). +absl::Status CheckGuidedAssertion(Package* package, std::string_view spin_label, + const EvaluatorResultsProto& proto) { + LOG(WARNING) << "SPIN assertion violated: '" << spin_label << "'"; + const std::string assert_loc = FindAssertLocation(package, spin_label); + + bool had_assertion = false, same_location = false; + for (const auto& eval_result : proto.results()) { + for (const auto& assert_msg : eval_result.events().assert_msgs()) { + had_assertion = true; + std::string dslx_location; + RE2::PartialMatch(assert_msg.message(), R"(:(\d+:\d+))", &dslx_location); + if (!assert_loc.empty() && dslx_location == assert_loc) { + same_location = true; + } + } + } + if (same_location) { + LOG(INFO) << "SPIN assertion '" << spin_label + << "' matches DSLX assertion; consistent failure"; + return absl::OkStatus(); + } + if (had_assertion) { + return absl::FailedPreconditionError(absl::StrFormat( + "Promela assertion \"%s\" violated but does not match the " + "assertion fired by the DSLX interpreter.", + spin_label)); + } + return absl::FailedPreconditionError(absl::StrFormat( + "Promela assertion \"%s\" violated but the DSLX interpreter " + "reported no assertion failures.", + spin_label)); +} + +// Runs SPIN, logs exit status, writes stdout to dir/"spin_output.log". +absl::StatusOr RunSpin(const std::vector& argv, + const OutputDir& dir) { + XLS_ASSIGN_OR_RETURN(SubprocessResult result, + InvokeSubprocess(argv, dir.path())); + LOG(INFO) << "SPIN complete (exit status " << result.exit_status << ")"; + XLS_RETURN_IF_ERROR( + SetFileContents(dir / "spin_output.log", result.stdout_content)); + return result; +} + +// Runs the DSLX interpreter on `spin_top` with channel tracing and returns the +// EvaluatorResultsProto. Used by RunSpinCheck when no proto is pre-supplied. +absl::StatusOr CollectDslxTrace( + std::string_view dslx_source, std::string_view entry_module_path, + std::string_view module_name, std::string_view spin_top, + const SpinRunOptions& options) { + dslx::ParseAndTypecheckOptions typecheck_options; + typecheck_options.dslx_stdlib_path = options.dslx_stdlib_path; + typecheck_options.dslx_paths = options.dslx_paths; + if (options.type_inference_v2) { + typecheck_options.type_inference_version = + dslx::TypeInferenceVersion::kVersion2; + } + RE2 top_proc_regex{std::string(spin_top)}; + EvaluatorResultsProto proto; + dslx::ParseAndTestOptions test_options; + test_options.parse_and_typecheck_options = typecheck_options; + test_options.test_filter = &top_proc_regex; + test_options.trace_channels = true; + test_options.results_out = &proto; + dslx::DslxInterpreterTestRunner runner; + XLS_RETURN_IF_ERROR(runner + .ParseAndTest(dslx_source, module_name, + entry_module_path, test_options) + .status()); + return proto; +} + +absl::StatusOr FindSpinBinary() { + auto runfile = GetXlsRunfilePath("spin", "spin"); + if (runfile.ok() && std::filesystem::exists(*runfile)) { + LOG(INFO) << "Using SPIN binary: " << runfile->string(); + return *runfile; + } + LOG(WARNING) + << "SPIN binary not found in runfiles; falling back to PATH lookup"; + return std::filesystem::path("spin"); +} + +} // namespace + +absl::Status RunSpinCheck(std::string_view dslx_source, + std::string_view entry_module_path, + std::string_view module_name, + const SpinRunOptions& options) { + std::string_view mode = + options.exec_type == SpinExecutionType::kGuided ? "guided" : "exhaustive"; + LOG(INFO) << "Starting SPIN " << mode << " check for module '" << module_name + << "' (" << entry_module_path << ")"; + + dslx::ImportData import_data(dslx::CreateImportData( + options.dslx_stdlib_path, options.dslx_paths, dslx::kAllWarningsSet, + std::make_unique())); + XLS_ASSIGN_OR_RETURN(dslx::TypecheckedModule tm, + dslx::ParseAndTypecheck(dslx_source, entry_module_path, + module_name, &import_data)); + + std::string spin_top = + SelectTestProc(tm.module, entry_module_path, options.test_filter); + if (spin_top.empty()) { + LOG(WARNING) << "no #[test_proc] found in " << entry_module_path + << "; skipping " << mode << " check"; + return absl::OkStatus(); + } + + LOG(INFO) << "Converting test proc '" << spin_top << "' to optimized IR"; + bool printed_error = false; + dslx::ConvertOptions convert_options = { + .emit_positions = true, + .emit_assert = true, + .verify_ir = true, + .warnings_as_errors = false, + .warnings = dslx::kAllWarningsSet, + .convert_tests = true, + .type_inference_v2 = options.type_inference_v2, + .lower_to_proc_scoped_channels = true, + }; + std::array module_path{entry_module_path}; + XLS_ASSIGN_OR_RETURN( + dslx::PackageConversionData conversion_data, + dslx::ConvertFilesToPackage(module_path, options.dslx_stdlib_path, + options.dslx_paths, convert_options, spin_top, + module_name, &printed_error)); + XLS_RETURN_IF_ERROR( + RunOptimizationPassPipeline(conversion_data.package.get()).status()); + LOG(INFO) << "IR optimization complete; running Promela " << mode << " check"; + + XLS_ASSIGN_OR_RETURN(std::filesystem::path spin_bin, FindSpinBinary()); + PromelaGeneratorOptions promela_options; + promela_options.emit_source_hints = true; + promela_options.emit_termination_hook = true; + promela_options.emit_progress_labels = true; + Package* package = conversion_data.package.get(); + + if (options.exec_type == SpinExecutionType::kGuided) { + EvaluatorResultsProto internal_proto; + const EvaluatorResultsProto* proto_ptr = options.results_proto; + if (proto_ptr == nullptr) { + XLS_ASSIGN_OR_RETURN(internal_proto, + CollectDslxTrace(dslx_source, entry_module_path, + module_name, spin_top, options)); + proto_ptr = &internal_proto; + } + std::string proto_text; + if (!google::protobuf::TextFormat::PrintToString(*proto_ptr, &proto_text)) { + return absl::InternalError("Failed to serialize EvaluatorResultsProto"); + } + XLS_ASSIGN_OR_RETURN(OutputDir dir, OutputDir::Create("guided")); + const std::filesystem::path promela_path = dir / "model.pml"; + const std::filesystem::path spin_trace_path = dir / "spin_trace.json"; + XLS_RETURN_IF_ERROR( + GenerateAndWritePml(package, promela_options, promela_path)); + XLS_RETURN_IF_ERROR( + SetFileContents(dir / "dslx_trace.textproto", proto_text)); + LOG(INFO) << "Running SPIN simulation on " << promela_path.string(); + XLS_ASSIGN_OR_RETURN( + SubprocessResult spin_result, + RunSpin({spin_bin.string(), "-c", "-Q", spin_trace_path.string(), + promela_path.string()}, + dir)); + if (spin_result.exit_status != 0) { + if (auto label = ExtractAssertionLabel(spin_result.stdout_content); + label.has_value()) { + return CheckGuidedAssertion(package, *label, *proto_ptr); + } + return absl::InternalError(absl::StrFormat("spin failed (exit %d):\n%s", + spin_result.exit_status, + spin_result.stdout_content)); + } + const bool timed_out = + absl::StrContains(spin_result.stdout_content, "timeout"); + std::string_view terminator_channel = + promela_options.emit_termination_hook ? "terminator" : ""; + XLS_ASSIGN_OR_RETURN(std::string spin_json, + GetFileContents(spin_trace_path)); + LOG(INFO) << "Parsing SPIN trace from " << spin_trace_path.string(); + TraceMap spin_events, dslx_events; + bool saw_terminator = false; + XLS_RETURN_IF_ERROR(ParseSpinTrace(spin_json, spin_events, + terminator_channel, &saw_terminator)); + if (timed_out && !saw_terminator) { + return absl::DeadlineExceededError(absl::StrFormat( + "SPIN simulation reached a deadlock before the terminator fired.\n" + "The Promela model blocked with no enabled transitions.\n" + "SPIN output:\n%s", + spin_result.stdout_content)); + } + XLS_RETURN_IF_ERROR( + ParseDslxTrace(proto_text, dslx_events, terminator_channel)); + LOG(INFO) << "Comparing SPIN and DSLX traces"; + return CompareTraces(spin_events, dslx_events); + } else { + XLS_ASSIGN_OR_RETURN(OutputDir dir, OutputDir::Create("exhaustive")); + const std::filesystem::path promela_path = dir / "model.pml"; + XLS_RETURN_IF_ERROR( + GenerateAndWritePml(package, promela_options, promela_path)); + LOG(INFO) << "Running SPIN exhaustive search on " << promela_path.string(); + XLS_ASSIGN_OR_RETURN( + SubprocessResult spin_result, + RunSpin({spin_bin.string(), "-search", promela_path.string()}, dir)); + if (auto label = ExtractAssertionLabel(spin_result.stdout_content); + label.has_value()) { + return absl::FailedPreconditionError(absl::StrFormat( + "SPIN exhaustive search found assertion violation: \"%s\".\n" + "SPIN output:\n%s", + *label, spin_result.stdout_content)); + } + return absl::OkStatus(); + } +} + +} // namespace xls::spin diff --git a/xls/spin/spin_runner.h b/xls/spin/spin_runner.h new file mode 100644 index 0000000000..5d7016acc1 --- /dev/null +++ b/xls/spin/spin_runner.h @@ -0,0 +1,56 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Entry point for Promela generation + SPIN simulation + trace comparison. + +#ifndef XLS_SPIN_SPIN_RUNNER_H_ +#define XLS_SPIN_SPIN_RUNNER_H_ + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "xls/ir/evaluator_result.pb.h" +#include "xls/ir/package.h" +#include "xls/spin/promela_generator.h" + +namespace xls::spin { + +enum class SpinExecutionType { kGuided, kExhaustive }; + +// Options forwarded from interpreter_main to RunSpinCheck. +struct SpinRunOptions { + SpinExecutionType exec_type = SpinExecutionType::kGuided; + std::string dslx_stdlib_path; + std::vector dslx_paths; + std::optional test_filter; + bool type_inference_v2 = false; + // Required when exec_type == kGuided; ignored for kExhaustive. + xls::EvaluatorResultsProto* results_proto = nullptr; +}; + +// DSLX-level: full pipeline from source text -- picks a #[test_proc] (using +// options.test_filter if there are several), converts it, and verifies it. +absl::Status RunSpinCheck(std::string_view dslx_source, + std::string_view entry_module_path, + std::string_view module_name, + const SpinRunOptions& options = {}); + +} // namespace xls::spin + +#endif // XLS_SPIN_SPIN_RUNNER_H_ diff --git a/xls/spin/spin_runner_test.cc b/xls/spin/spin_runner_test.cc new file mode 100644 index 0000000000..0e812c873c --- /dev/null +++ b/xls/spin/spin_runner_test.cc @@ -0,0 +1,154 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "xls/spin/spin_runner.h" + +#include +#include +#include + +#include "absl/log/log.h" +#include "absl/log/scoped_mock_log.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "xls/common/file/filesystem.h" +#include "xls/common/file/get_runfile_path.h" +#include "xls/common/file/temp_directory.h" +#include "xls/common/status/matchers.h" + +namespace xls::spin { +namespace { + +using ::absl::ScopedMockLog; +using ::absl_testing::IsOk; +using ::testing::_; +using ::testing::HasSubstr; + +class PromelaSpinRunnerTest : public testing::Test {}; + +/* RunSpinCheck */ + +TEST_F(PromelaSpinRunnerTest, RunSpinCheck_NoTestProc) { + constexpr std::string_view kDslx = R"( +proc SimpleProc { + config() {} + init { () } + next(state: ()) {} +} +)"; + ScopedMockLog log; + EXPECT_CALL(log, Log(absl::LogSeverity::kWarning, _, + HasSubstr("no #[test_proc] found"))); + log.StartCapturingLogs(); + EXPECT_THAT(RunSpinCheck(kDslx, "simple.x", "simple"), IsOk()); + log.StopCapturingLogs(); +} + +TEST_F(PromelaSpinRunnerTest, RunSpinCheck_Passthrough) { + XLS_ASSERT_OK_AND_ASSIGN( + auto src, GetXlsRunfilePath("xls/spin/testdata/passthrough.x")); + XLS_ASSERT_OK_AND_ASSIGN(std::string dslx_source, GetFileContents(src)); + SpinRunOptions opts; + opts.exec_type = SpinExecutionType::kGuided; + opts.type_inference_v2 = true; + ScopedMockLog log; + EXPECT_CALL(log, Log(absl::LogSeverity::kInfo, _, + HasSubstr("Starting SPIN guided check"))); + EXPECT_CALL(log, Log(absl::LogSeverity::kInfo, _, + HasSubstr("Running SPIN simulation on"))); + log.StartCapturingLogs(); + EXPECT_THAT(RunSpinCheck(dslx_source, src.string(), "passthrough", opts), + IsOk()); + log.StopCapturingLogs(); +} + +TEST_F(PromelaSpinRunnerTest, RunSpinCheck_Exhaustive) { + XLS_ASSERT_OK_AND_ASSIGN( + auto src, GetXlsRunfilePath("xls/spin/testdata/passthrough.x")); + XLS_ASSERT_OK_AND_ASSIGN(std::string dslx_source, GetFileContents(src)); + SpinRunOptions opts; + opts.exec_type = SpinExecutionType::kExhaustive; + opts.type_inference_v2 = true; + ScopedMockLog log; + EXPECT_CALL(log, Log(absl::LogSeverity::kInfo, _, + HasSubstr("Starting SPIN exhaustive check"))); + EXPECT_CALL(log, Log(absl::LogSeverity::kInfo, _, + HasSubstr("Running SPIN exhaustive search on"))); + log.StartCapturingLogs(); + EXPECT_THAT(RunSpinCheck(dslx_source, src.string(), "passthrough", opts), + IsOk()); + log.StopCapturingLogs(); +} + +constexpr std::string_view kTwoTestProcsDslx = R"( +#![feature(type_inference_v2)] + +#[test_proc] +proc FirstTest { + terminator: chan out; + config(terminator: chan out) { (terminator,) } + init { () } + next(state: ()) { + let tok = send(join(), terminator, true); + } +} + +#[test_proc] +proc SecondTest { + terminator: chan out; + config(terminator: chan out) { (terminator,) } + init { () } + next(state: ()) { + let tok = send(join(), terminator, true); + } +} +)"; + +TEST_F(PromelaSpinRunnerTest, RunSpinCheck_MultipleProcs_WithFilter) { + XLS_ASSERT_OK_AND_ASSIGN(auto tmp, TempDirectory::Create("two_procs")); + const std::filesystem::path src = tmp.path() / "test.x"; + XLS_ASSERT_OK(SetFileContents(src, kTwoTestProcsDslx)); + SpinRunOptions opts; + opts.exec_type = SpinExecutionType::kGuided; + opts.type_inference_v2 = true; + opts.test_filter = "SecondTest"; + ScopedMockLog log; + EXPECT_CALL(log, Log(absl::LogSeverity::kInfo, _, + HasSubstr("Converting test proc 'SecondTest'"))); + log.StartCapturingLogs(); + EXPECT_THAT(RunSpinCheck(kTwoTestProcsDslx, src.string(), "test", opts), + IsOk()); + log.StopCapturingLogs(); +} + +TEST_F(PromelaSpinRunnerTest, RunSpinCheck_MultipleProcs_WithoutFilter) { + XLS_ASSERT_OK_AND_ASSIGN(auto tmp, TempDirectory::Create("two_procs")); + const std::filesystem::path src = tmp.path() / "test.x"; + XLS_ASSERT_OK(SetFileContents(src, kTwoTestProcsDslx)); + SpinRunOptions opts; + opts.exec_type = SpinExecutionType::kGuided; + opts.type_inference_v2 = true; + ScopedMockLog log; + EXPECT_CALL(log, Log(absl::LogSeverity::kWarning, _, + HasSubstr("multiple #[test_proc]"))); + EXPECT_CALL(log, Log(absl::LogSeverity::kInfo, _, + HasSubstr("Converting test proc 'FirstTest'"))); + log.StartCapturingLogs(); + EXPECT_THAT(RunSpinCheck(kTwoTestProcsDslx, src.string(), "test", opts), + IsOk()); + log.StopCapturingLogs(); +} + +} // namespace +} // namespace xls::spin diff --git a/xls/spin/testdata/BUILD b/xls/spin/testdata/BUILD new file mode 100644 index 0000000000..b7f7bb3113 --- /dev/null +++ b/xls/spin/testdata/BUILD @@ -0,0 +1,145 @@ +# Copyright 2026 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Golden IR diff tests for xls/spin testdata. + +load("//xls/spin:priv-defs.bzl", "spin_ir_golden_test") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//xls:xls_internal"], + licenses = ["notice"], +) + +exports_files(glob([ + "*.ir", + "*.x", +])) + +spin_ir_golden_test( + name = "assertion_ir_golden", + src = "assertion.x", + dslx_top = "Asserting", + golden = "assertion.ir", +) + +spin_ir_golden_test( + name = "blocking_receive_ir_golden", + src = "blocking_receive.x", + dslx_top = "Reader", + golden = "blocking_receive.ir", +) + +spin_ir_golden_test( + name = "conditional_io_ir_golden", + src = "conditional_io.x", + convert_tests = True, + dslx_top = "ConditionalIoTest", + golden = "conditional_io.ir", +) + +spin_ir_golden_test( + name = "counter_ir_golden", + src = "counter.x", + convert_tests = True, + dslx_top = "CounterTest", + golden = "counter.ir", +) + +spin_ir_golden_test( + name = "multi_proc_ir_golden", + src = "multi_proc.x", + dslx_top = "ProcA", + golden = "multi_proc.ir", +) + +spin_ir_golden_test( + name = "non_blocking_receive_ir_golden", + src = "non_blocking_receive.x", + dslx_top = "NbReader", + golden = "non_blocking_receive.ir", +) + +spin_ir_golden_test( + name = "passthrough_ir_golden", + src = "passthrough.x", + convert_tests = True, + dslx_top = "PassthroughTest", + golden = "passthrough.ir", +) + +spin_ir_golden_test( + name = "predicated_receive_ir_golden", + src = "predicated_receive.x", + dslx_top = "CondReader", + golden = "predicated_receive.ir", +) + +spin_ir_golden_test( + name = "predicated_send_ir_golden", + src = "predicated_send.x", + dslx_top = "CondWriter", + golden = "predicated_send.ir", +) + +spin_ir_golden_test( + name = "func_ir_golden", + src = "func.x", + dslx_top = "myfunc", + golden = "func.ir", +) + +spin_ir_golden_test( + name = "proc_ir_golden", + src = "proc.x", + dslx_top = "MyProc", + golden = "proc.ir", +) + +spin_ir_golden_test( + name = "state_counter_ir_golden", + src = "state_counter.x", + dslx_top = "Counter", + golden = "state_counter.ir", +) + +spin_ir_golden_test( + name = "unconditional_send_ir_golden", + src = "unconditional_send.x", + dslx_top = "Writer", + golden = "unconditional_send.ir", +) + +spin_ir_golden_test( + name = "xr_hints_ir_golden", + src = "xr_hints.x", + dslx_top = "RwProc", + golden = "xr_hints.ir", +) + +spin_ir_golden_test( + name = "channel_naming_ir_golden", + src = "channel_naming.x", + convert_tests = True, + dslx_top = "MultiplyBy4Test", + golden = "channel_naming.ir", +) + +spin_ir_golden_test( + name = "deadlock_ir_golden", + src = "deadlock.x", + convert_tests = True, + dslx_top = "Test", + golden = "deadlock.ir", +) diff --git a/xls/spin/testdata/assertion.ir b/xls/spin/testdata/assertion.ir new file mode 100644 index 0000000000..c75ca24daf --- /dev/null +++ b/xls/spin/testdata/assertion.ir @@ -0,0 +1,18 @@ +package assertion + +file_number 0 "xls/spin/testdata/assertion.x" + +top proc __assertion__Asserting_0_next<>(__state: bits[32], init={1}) { + literal.3: bits[1] = literal(value=1, id=3) + __state: bits[32] = state_read(state_element=__state, id=2) + literal.5: bits[32] = literal(value=0, id=5, pos=[(0,4,21)]) + not.8: bits[1] = not(literal.3, id=8) + ne.6: bits[1] = ne(__state, literal.5, id=6, pos=[(0,4,12)]) + __token: token = literal(value=token, id=1) + or.9: bits[1] = or(not.8, ne.6, id=9) + tuple.4: () = tuple(id=4, pos=[(0,1,13)]) + literal.7: bits[8][7] = literal(value=[110, 111, 110, 122, 101, 114, 111], id=7, pos=[(0,4,28)]) + assert.10: token = assert(__token, or.9, message="Assertion failure via assert! @ xls/spin/testdata/assertion.x:5:12-5:39", label="nonzero", id=10) + tuple.11: () = tuple(id=11) + next_value.12: () = next_value(param=__state, value=__state, id=12) +} diff --git a/xls/spin/testdata/assertion.x b/xls/spin/testdata/assertion.x new file mode 100644 index 0000000000..e808726e37 --- /dev/null +++ b/xls/spin/testdata/assertion.x @@ -0,0 +1,8 @@ +proc Asserting { + config() { () } + init { u32:1 } + next(state: u32) { + assert!(state != u32:0, "nonzero"); + state + } +} diff --git a/xls/spin/testdata/blocking_receive.ir b/xls/spin/testdata/blocking_receive.ir new file mode 100644 index 0000000000..df08aa365e --- /dev/null +++ b/xls/spin/testdata/blocking_receive.ir @@ -0,0 +1,17 @@ +package blocking_receive + +file_number 0 "xls/spin/testdata/blocking_receive.x" + +top proc __blocking_receive__Reader_0_next<_in_r: bits[32] in>(__state: bits[1], init={0}) { + chan_interface _in_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + after_all.5: token = after_all(id=5) + literal.3: bits[1] = literal(value=1, id=3) + receive.6: (token, bits[32]) = receive(after_all.5, predicate=literal.3, channel=_in_r, id=6) + __state: bits[1] = state_read(state_element=__state, id=2) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,2,31)]) + tuple_index.7: token = tuple_index(receive.6, index=0, id=7) + tuple_index.8: token = tuple_index(receive.6, index=0, id=8, pos=[(0,5,9)]) + tuple_index.9: bits[32] = tuple_index(receive.6, index=1, id=9, pos=[(0,5,12)]) + next_value.10: () = next_value(param=__state, value=__state, id=10) +} diff --git a/xls/spin/testdata/blocking_receive.x b/xls/spin/testdata/blocking_receive.x new file mode 100644 index 0000000000..31fadf947e --- /dev/null +++ b/xls/spin/testdata/blocking_receive.x @@ -0,0 +1,9 @@ +proc Reader { + in_r: chan in; + config(in_r: chan in) { (in_r,) } + init { u1:0 } + next(state: u1) { + let (_, _) = recv(join(), in_r); + state + } +} diff --git a/xls/spin/testdata/channel_naming.ir b/xls/spin/testdata/channel_naming.ir new file mode 100644 index 0000000000..38530588c9 --- /dev/null +++ b/xls/spin/testdata/channel_naming.ir @@ -0,0 +1,70 @@ +package channel_naming + +file_number 0 "xls/spin/testdata/channel_naming.x" + +proc __channel_naming__MultiplyBy2_0_next<_req_r: bits[32] in, _resp_s: bits[32] out>(__state: (), init={()}) { + chan_interface _req_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan_interface _resp_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + after_all.5: token = after_all(id=5) + literal.3: bits[1] = literal(value=1, id=3) + receive.6: (token, bits[32]) = receive(after_all.5, predicate=literal.3, channel=_req_r, id=6) + data: bits[32] = tuple_index(receive.6, index=1, id=9, pos=[(0,25,18)]) + literal.10: bits[32] = literal(value=2, id=10, pos=[(0,26,43)]) + tok: token = tuple_index(receive.6, index=0, id=8, pos=[(0,25,13)]) + umul.11: bits[32] = umul(data, literal.10, id=11, pos=[(0,26,36)]) + __state: () = state_read(state_element=__state, id=2) + tuple.13: () = tuple(id=13, pos=[(0,24,19)]) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,20,57)]) + tuple_index.7: token = tuple_index(receive.6, index=0, id=7) + tok__1: token = send(tok, umul.11, predicate=literal.3, channel=_resp_s, id=12) + next_value.14: () = next_value(param=__state, value=tuple.13, id=14) +} + +proc __channel_naming__MultiplyBy4_0_next<_req_r: bits[32] in, _resp_s: bits[32] out>(__state: (), init={()}) { + chan_interface _req_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan_interface _resp_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan _tmp(bits[32], id=0, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _tmp(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _tmp(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + proc_instantiation __channel_naming__MultiplyBy2_0_next_inst(_req_r, _tmp, proc=__channel_naming__MultiplyBy2_0_next) + proc_instantiation __channel_naming__MultiplyBy2_0_next_inst_1(_tmp, _resp_s, proc=__channel_naming__MultiplyBy2_0_next) + __state: () = state_read(state_element=__state, id=16) + tuple.19: () = tuple(id=19, pos=[(0,38,19)]) + __token: token = literal(value=token, id=15) + literal.17: bits[1] = literal(value=1, id=17) + tuple.18: () = tuple(id=18, pos=[(0,31,54)]) + next_value.20: () = next_value(param=__state, value=tuple.19, id=20) +} + +top proc __channel_naming__MultiplyBy4Test_0_next<_terminator: bits[1] out>(__state: (), init={()}) { + chan_interface _terminator(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan _req(bits[32], id=1, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _req(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _req(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan _resp(bits[32], id=2, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _resp(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _resp(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + proc_instantiation __channel_naming__MultiplyBy4_0_next_inst(_req, _resp, proc=__channel_naming__MultiplyBy4_0_next) + after_all.25: token = after_all(id=25) + literal.26: bits[32] = literal(value=1, id=26, pos=[(0,57,38)]) + literal.23: bits[1] = literal(value=1, id=23) + tok: token = send(after_all.25, literal.26, predicate=literal.23, channel=_req, id=27) + receive.28: (token, bits[32]) = receive(tok, predicate=literal.23, channel=_resp, id=28) + received_data: bits[32] = tuple_index(receive.28, index=1, id=31, pos=[(0,58,18)]) + literal.32: bits[32] = literal(value=4, id=32, pos=[(0,60,33)]) + not.34: bits[1] = not(literal.23, id=34) + eq.33: bits[1] = eq(received_data, literal.32, id=33) + __token: token = literal(value=token, id=21) + or.35: bits[1] = or(not.34, eq.33, id=35) + tok__1: token = tuple_index(receive.28, index=0, id=30, pos=[(0,58,13)]) + literal.38: bits[1] = literal(value=1, id=38, pos=[(0,61,30)]) + __state: () = state_read(state_element=__state, id=22) + tuple.40: () = tuple(id=40, pos=[(0,56,19)]) + tuple.24: () = tuple(id=24, pos=[(0,51,8)]) + tuple_index.29: token = tuple_index(receive.28, index=0, id=29) + assert.36: token = assert(__token, or.35, message="Assertion failure via assert_eq @ xls/spin/testdata/channel_naming.x:61:18-61:40", label="assert_eq(received_data, u32:4)", id=36) + tuple.37: () = tuple(id=37) + send.39: token = send(tok__1, literal.38, predicate=literal.23, channel=_terminator, id=39) + next_value.41: () = next_value(param=__state, value=tuple.40, id=41) +} diff --git a/xls/spin/testdata/channel_naming.x b/xls/spin/testdata/channel_naming.x new file mode 100644 index 0000000000..59721a1b2b --- /dev/null +++ b/xls/spin/testdata/channel_naming.x @@ -0,0 +1,64 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![feature(type_inference_v2)] + +proc MultiplyBy2 { + req_r: chan in; + resp_s: chan out; + + config(req_r: chan in, resp_s: chan out) { (req_r, resp_s) } + + init { () } + + next(state: ()) { + let (tok, data) = recv(join(), req_r); + let tok = send(tok, resp_s, data * 2); + } +} + +proc MultiplyBy4 { + config(req_r: chan in, resp_s: chan out) { + let (tmp_s, tmp_r) = chan("tmp"); + spawn MultiplyBy2(req_r, tmp_s); + spawn MultiplyBy2(tmp_r, resp_s); + } + + init { () } + next(state: ()) { } +} + +#[test_proc] +proc MultiplyBy4Test { + terminator: chan out; + req_s: chan out; + resp_r: chan in; + + config(terminator: chan out) { + let (req_s, req_r) = chan("req"); + let (resp_s, resp_r) = chan("resp"); + spawn MultiplyBy4(req_r, resp_s); + (terminator, req_s, resp_r) + } + + init { } + + next(state: ()) { + let tok = send(join(), req_s, u32:1); + let (tok, received_data) = recv(tok, resp_r); + + assert_eq(received_data, u32:4); + send(tok, terminator, true); + } +} diff --git a/xls/spin/testdata/conditional_io.ir b/xls/spin/testdata/conditional_io.ir new file mode 100644 index 0000000000..eed8a39506 --- /dev/null +++ b/xls/spin/testdata/conditional_io.ir @@ -0,0 +1,148 @@ +package conditional_io + +file_number 0 "xls/spin/testdata/conditional_io.x" + +proc __conditional_io__RecvIfTrue_0_next<_input_r: bits[32] in>(__state: (), init={()}) { + chan_interface _input_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + literal.3: bits[1] = literal(value=1, id=3) + literal.6: bits[1] = literal(value=1, id=6, pos=[(0,8,46)]) + after_all.5: token = after_all(id=5) + and.8: bits[1] = and(literal.3, literal.6, id=8) + receive.9: (token, bits[32]) = receive(after_all.5, predicate=and.8, channel=_input_r, id=9) + literal.7: bits[32] = literal(value=0, id=7, pos=[(0,8,52)]) + tuple_index.11: bits[32] = tuple_index(receive.9, index=1, id=11) + tuple_index.10: token = tuple_index(receive.9, index=0, id=10) + sel.12: bits[32] = sel(literal.6, cases=[literal.7, tuple_index.11], id=12) + tuple.13: (token, bits[32]) = tuple(tuple_index.10, sel.12, id=13) + __state: () = state_read(state_element=__state, id=2) + tuple.16: () = tuple(id=16, pos=[(0,7,19)]) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,5,36)]) + tuple_index.14: token = tuple_index(tuple.13, index=0, id=14, pos=[(0,8,13)]) + tuple_index.15: bits[32] = tuple_index(tuple.13, index=1, id=15, pos=[(0,8,16)]) + next_value.17: () = next_value(param=__state, value=tuple.16, id=17) +} + +proc __conditional_io__RecvIfFalse_0_next<_input_r: bits[32] in>(__state: (), init={()}) { + chan_interface _input_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + literal.20: bits[1] = literal(value=1, id=20) + literal.23: bits[1] = literal(value=0, id=23, pos=[(0,18,46)]) + after_all.22: token = after_all(id=22) + and.25: bits[1] = and(literal.20, literal.23, id=25) + receive.26: (token, bits[32]) = receive(after_all.22, predicate=and.25, channel=_input_r, id=26) + literal.24: bits[32] = literal(value=0, id=24, pos=[(0,18,53)]) + tuple_index.28: bits[32] = tuple_index(receive.26, index=1, id=28) + tuple_index.27: token = tuple_index(receive.26, index=0, id=27) + sel.29: bits[32] = sel(literal.23, cases=[literal.24, tuple_index.28], id=29) + tuple.30: (token, bits[32]) = tuple(tuple_index.27, sel.29, id=30) + __state: () = state_read(state_element=__state, id=19) + tuple.33: () = tuple(id=33, pos=[(0,17,19)]) + __token: token = literal(value=token, id=18) + tuple.21: () = tuple(id=21, pos=[(0,15,36)]) + tuple_index.31: token = tuple_index(tuple.30, index=0, id=31, pos=[(0,18,13)]) + tuple_index.32: bits[32] = tuple_index(tuple.30, index=1, id=32, pos=[(0,18,16)]) + next_value.34: () = next_value(param=__state, value=tuple.33, id=34) +} + +proc __conditional_io__SendIfFalse_0_next<_output_s: bits[32] out>(__state: (), init={()}) { + chan_interface _output_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + literal.37: bits[1] = literal(value=1, id=37) + literal.40: bits[1] = literal(value=0, id=40, pos=[(0,28,34)]) + after_all.39: token = after_all(id=39) + literal.41: bits[32] = literal(value=0, id=41, pos=[(0,28,41)]) + and.42: bits[1] = and(literal.37, literal.40, id=42) + __state: () = state_read(state_element=__state, id=36) + tuple.44: () = tuple(id=44, pos=[(0,27,19)]) + __token: token = literal(value=token, id=35) + tuple.38: () = tuple(id=38, pos=[(0,25,38)]) + send.43: token = send(after_all.39, literal.41, predicate=and.42, channel=_output_s, id=43) + next_value.45: () = next_value(param=__state, value=tuple.44, id=45) +} + +proc __conditional_io__RecvIfNonBlockingTrue_0_next<_input_r: bits[32] in>(__state: (), init={()}) { + chan_interface _input_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + literal.48: bits[1] = literal(value=1, id=48) + literal.51: bits[1] = literal(value=1, id=51, pos=[(0,38,62)]) + after_all.50: token = after_all(id=50) + and.53: bits[1] = and(literal.48, literal.51, id=53) + receive.54: (token, bits[32], bits[1]) = receive(after_all.50, predicate=and.53, channel=_input_r, blocking=false, id=54) + tuple_index.57: bits[1] = tuple_index(receive.54, index=2, id=57) + literal.52: bits[32] = literal(value=0, id=52, pos=[(0,38,68)]) + tuple_index.56: bits[32] = tuple_index(receive.54, index=1, id=56) + tuple_index.55: token = tuple_index(receive.54, index=0, id=55) + sel.58: bits[32] = sel(tuple_index.57, cases=[literal.52, tuple_index.56], id=58) + tuple.59: (token, bits[32], bits[1]) = tuple(tuple_index.55, sel.58, tuple_index.57, id=59) + __state: () = state_read(state_element=__state, id=47) + tuple.63: () = tuple(id=63, pos=[(0,37,19)]) + __token: token = literal(value=token, id=46) + tuple.49: () = tuple(id=49, pos=[(0,35,36)]) + tuple_index.60: token = tuple_index(tuple.59, index=0, id=60, pos=[(0,38,13)]) + tuple_index.61: bits[32] = tuple_index(tuple.59, index=1, id=61, pos=[(0,38,16)]) + tuple_index.62: bits[1] = tuple_index(tuple.59, index=2, id=62, pos=[(0,38,19)]) + next_value.64: () = next_value(param=__state, value=tuple.63, id=64) +} + +proc __conditional_io__RecvIfNonBlockingFalse_0_next<_input_r: bits[32] in>(__state: (), init={()}) { + chan_interface _input_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + literal.67: bits[1] = literal(value=1, id=67) + literal.70: bits[1] = literal(value=0, id=70, pos=[(0,48,62)]) + after_all.69: token = after_all(id=69) + and.72: bits[1] = and(literal.67, literal.70, id=72) + receive.73: (token, bits[32], bits[1]) = receive(after_all.69, predicate=and.72, channel=_input_r, blocking=false, id=73) + tuple_index.76: bits[1] = tuple_index(receive.73, index=2, id=76) + literal.71: bits[32] = literal(value=0, id=71, pos=[(0,48,69)]) + tuple_index.75: bits[32] = tuple_index(receive.73, index=1, id=75) + tuple_index.74: token = tuple_index(receive.73, index=0, id=74) + sel.77: bits[32] = sel(tuple_index.76, cases=[literal.71, tuple_index.75], id=77) + tuple.78: (token, bits[32], bits[1]) = tuple(tuple_index.74, sel.77, tuple_index.76, id=78) + __state: () = state_read(state_element=__state, id=66) + tuple.82: () = tuple(id=82, pos=[(0,47,19)]) + __token: token = literal(value=token, id=65) + tuple.68: () = tuple(id=68, pos=[(0,45,36)]) + tuple_index.79: token = tuple_index(tuple.78, index=0, id=79, pos=[(0,48,13)]) + tuple_index.80: bits[32] = tuple_index(tuple.78, index=1, id=80, pos=[(0,48,16)]) + tuple_index.81: bits[1] = tuple_index(tuple.78, index=2, id=81, pos=[(0,48,19)]) + next_value.83: () = next_value(param=__state, value=tuple.82, id=83) +} + +top proc __conditional_io__ConditionalIoTest_0_next<_terminator: bits[1] out>(__state: bits[32], init={0}) { + chan_interface _terminator(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan _recv_if_true(bits[32], id=0, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _recv_if_true(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _recv_if_true(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan _recv_if_false(bits[32], id=1, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _recv_if_false(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _recv_if_false(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan _send_if_false(bits[32], id=2, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _send_if_false(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _send_if_false(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan _recv_if_nb_true(bits[32], id=3, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _recv_if_nb_true(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _recv_if_nb_true(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan _recv_if_nb_false(bits[32], id=4, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _recv_if_nb_false(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _recv_if_nb_false(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + proc_instantiation __conditional_io__RecvIfTrue_0_next_inst(_recv_if_true, proc=__conditional_io__RecvIfTrue_0_next) + proc_instantiation __conditional_io__RecvIfFalse_0_next_inst(_recv_if_false, proc=__conditional_io__RecvIfFalse_0_next) + proc_instantiation __conditional_io__SendIfFalse_0_next_inst(_send_if_false, proc=__conditional_io__SendIfFalse_0_next) + proc_instantiation __conditional_io__RecvIfNonBlockingTrue_0_next_inst(_recv_if_nb_true, proc=__conditional_io__RecvIfNonBlockingTrue_0_next) + proc_instantiation __conditional_io__RecvIfNonBlockingFalse_0_next_inst(_recv_if_nb_false, proc=__conditional_io__RecvIfNonBlockingFalse_0_next) + __state: bits[32] = state_read(state_element=__state, id=85) + literal.89: bits[32] = literal(value=0, id=89, pos=[(0,76,59)]) + literal.86: bits[1] = literal(value=1, id=86) + eq.90: bits[1] = eq(__state, literal.89, id=90, pos=[(0,76,50)]) + literal.94: bits[32] = literal(value=1, id=94, pos=[(0,77,42)]) + after_all.88: token = after_all(id=88) + literal.91: bits[32] = literal(value=42, id=91, pos=[(0,76,66)]) + and.92: bits[1] = and(literal.86, eq.90, id=92) + eq.95: bits[1] = eq(__state, literal.94, id=95, pos=[(0,77,33)]) + literal.99: bits[32] = literal(value=1, id=99, pos=[(0,78,16)]) + tok: token = send(after_all.88, literal.91, predicate=and.92, channel=_recv_if_true, id=93) + literal.96: bits[1] = literal(value=1, id=96, pos=[(0,77,49)]) + and.97: bits[1] = and(literal.86, eq.95, id=97) + add.100: bits[32] = add(__state, literal.99, id=100, pos=[(0,78,8)]) + __token: token = literal(value=token, id=84) + tuple.87: () = tuple(id=87, pos=[(0,68,8)]) + send.98: token = send(tok, literal.96, predicate=and.97, channel=_terminator, id=98) + next_value.101: () = next_value(param=__state, value=add.100, id=101) +} diff --git a/xls/spin/testdata/conditional_io.x b/xls/spin/testdata/conditional_io.x new file mode 100644 index 0000000000..75fe17216f --- /dev/null +++ b/xls/spin/testdata/conditional_io.x @@ -0,0 +1,81 @@ +#![feature(type_inference_v2)] + +// recv_if with true predicate -- always receives when called +proc RecvIfTrue { + input_r: chan in; + config(input_r: chan in) { (input_r,) } + init { () } + next(state: ()) { + let (_, _) = recv_if(join(), input_r, true, u32:0); + } +} + +// recv_if with false predicate -- never receives +proc RecvIfFalse { + input_r: chan in; + config(input_r: chan in) { (input_r,) } + init { () } + next(state: ()) { + let (_, _) = recv_if(join(), input_r, false, u32:0); + } +} + +// send_if with false predicate -- never sends +proc SendIfFalse { + output_s: chan out; + config(output_s: chan out) { (output_s,) } + init { () } + next(state: ()) { + send_if(join(), output_s, false, u32:0); + } +} + +// recv_if_non_blocking with true predicate -- non-blocking poll, enabled +proc RecvIfNonBlockingTrue { + input_r: chan in; + config(input_r: chan in) { (input_r,) } + init { () } + next(state: ()) { + let (_, _, _) = recv_if_non_blocking(join(), input_r, true, u32:0); + } +} + +// recv_if_non_blocking with false predicate -- non-blocking poll, disabled +proc RecvIfNonBlockingFalse { + input_r: chan in; + config(input_r: chan in) { (input_r,) } + init { () } + next(state: ()) { + let (_, _, _) = recv_if_non_blocking(join(), input_r, false, u32:0); + } +} + +#[test_proc] +proc ConditionalIoTest { + terminator: chan out; + recv_if_true_s: chan out; + + config(terminator: chan out) { + let (recv_if_true_s, recv_if_true_r) = chan("recv_if_true"); + let (_recv_if_false_s, recv_if_false_r) = chan("recv_if_false"); + let (send_if_false_s, _send_if_false_r) = chan("send_if_false"); + let (_recv_if_nb_true_s, recv_if_nb_true_r) = chan("recv_if_nb_true"); + let (_recv_if_nb_false_s, recv_if_nb_false_r) = chan("recv_if_nb_false"); + spawn RecvIfTrue(recv_if_true_r); + spawn RecvIfFalse(recv_if_false_r); + spawn SendIfFalse(send_if_false_s); + spawn RecvIfNonBlockingTrue(recv_if_nb_true_r); + spawn RecvIfNonBlockingFalse(recv_if_nb_false_r); + (terminator, recv_if_true_s) + } + // state=0: send to recv_if_true; state=1: send to terminator. + // Two-cycle design ensures the DSLX trace records RecvIfTrue's RECV + // (cycle 1) before the terminator fires. SPIN interleaves the same events + // in the same relative order, so both traces match. + init { u32:0 } + next(state: u32) { + let tok = send_if(join(), recv_if_true_s, state == u32:0, u32:42); + send_if(tok, terminator, state == u32:1, true); + state + u32:1 + } +} diff --git a/xls/spin/testdata/counter.ir b/xls/spin/testdata/counter.ir new file mode 100644 index 0000000000..c3b9b0ddab --- /dev/null +++ b/xls/spin/testdata/counter.ir @@ -0,0 +1,55 @@ +package counter + +file_number 0 "xls/spin/testdata/counter.x" + +proc __counter__Counter_0_next<_data_s: bits[32] out>(__state: bits[32], init={0}) { + chan_interface _data_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __state: bits[32] = state_read(state_element=__state, id=2) + literal.6: bits[32] = literal(value=10, id=6, pos=[(0,22,40)]) + literal.3: bits[1] = literal(value=1, id=3) + ult.7: bits[1] = ult(__state, literal.6, id=7, pos=[(0,22,32)]) + literal.10: bits[32] = literal(value=1, id=10, pos=[(0,23,16)]) + after_all.5: token = after_all(id=5) + and.8: bits[1] = and(literal.3, ult.7, id=8) + add.11: bits[32] = add(__state, literal.10, id=11, pos=[(0,23,8)]) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,19,36)]) + send.9: token = send(after_all.5, __state, predicate=and.8, channel=_data_s, id=9) + next_value.12: () = next_value(param=__state, value=add.11, id=12) +} + +top proc __counter__CounterTest_0_next<_terminator: bits[1] out>(__state: bits[32], init={10}) { + chan_interface _terminator(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan _data(bits[32], id=0, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _data(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _data(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + proc_instantiation __counter__Counter_0_next_inst(_data, proc=__counter__Counter_0_next) + __state: bits[32] = state_read(state_element=__state, id=14) + literal.18: bits[32] = literal(value=0, id=18, pos=[(0,41,57)]) + literal.15: bits[1] = literal(value=1, id=15) + ugt.19: bits[1] = ugt(__state, literal.18, id=19, pos=[(0,41,49)]) + tok: token = after_all(id=17) + and.21: bits[1] = and(literal.15, ugt.19, id=21) + receive.22: (token, bits[32]) = receive(tok, predicate=and.21, channel=_data, id=22) + literal.20: bits[32] = literal(value=0, id=20, pos=[(0,41,64)]) + tuple_index.24: bits[32] = tuple_index(receive.22, index=1, id=24) + tuple_index.23: token = tuple_index(receive.22, index=0, id=23) + sel.25: bits[32] = sel(ugt.19, cases=[literal.20, tuple_index.24], id=25) + literal.29: bits[32] = literal(value=1, id=29, pos=[(0,42,42)]) + literal.34: bits[32] = literal(value=0, id=34, pos=[(0,44,19)]) + literal.36: bits[32] = literal(value=1, id=36, pos=[(0,44,35)]) + tuple.26: (token, bits[32]) = tuple(tuple_index.23, sel.25, id=26) + eq.30: bits[1] = eq(__state, literal.29, id=30, pos=[(0,42,33)]) + ugt.35: bits[1] = ugt(__state, literal.34, id=35, pos=[(0,44,11)]) + literal.38: bits[32] = literal(value=0, id=38, pos=[(0,44,50)]) + sub.37: bits[32] = sub(__state, literal.36, id=37, pos=[(0,44,27)]) + tok__1: token = tuple_index(tuple.26, index=0, id=27, pos=[(0,41,13)]) + literal.31: bits[1] = literal(value=1, id=31, pos=[(0,42,49)]) + and.32: bits[1] = and(literal.15, eq.30, id=32) + sel.39: bits[32] = sel(ugt.35, cases=[literal.38, sub.37], id=39, pos=[(0,44,8)]) + __token: token = literal(value=token, id=13) + tuple.16: () = tuple(id=16, pos=[(0,34,8)]) + _value: bits[32] = tuple_index(tuple.26, index=1, id=28, pos=[(0,41,18)]) + send.33: token = send(tok__1, literal.31, predicate=and.32, channel=_terminator, id=33) + next_value.40: () = next_value(param=__state, value=sel.39, id=40) +} diff --git a/xls/spin/testdata/counter.x b/xls/spin/testdata/counter.x new file mode 100644 index 0000000000..4964242f21 --- /dev/null +++ b/xls/spin/testdata/counter.x @@ -0,0 +1,47 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![feature(type_inference_v2)] + +// Counter sends values 0..9 then becomes a no-op; this bounds SPIN iterations. +proc Counter { + data_s: chan out; + config(data_s: chan out) { (data_s,) } + init { u32:0 } + next(state: u32) { + send_if(join(), data_s, state < u32:10, state); + state + u32:1 + } +} + +#[test_proc] +proc CounterTest { + terminator: chan out; + data_r: chan in; + config(terminator: chan out) { + let (data_s, data_r) = chan("data"); + spawn Counter(data_s); + (terminator, data_r) + } + + init { u32:10 } + + next(count: u32) { + let tok = join(); + let (tok, _value) = recv_if(tok, data_r, count > u32:0, u32:0); + send_if(tok, terminator, count == u32:1, true); + + if count > u32:0 { count - u32:1 } else { u32:0 } + } +} diff --git a/xls/spin/testdata/deadlock.ir b/xls/spin/testdata/deadlock.ir new file mode 100644 index 0000000000..2e46f5892f --- /dev/null +++ b/xls/spin/testdata/deadlock.ir @@ -0,0 +1,127 @@ +package deadlock + +file_number 0 "xls/spin/testdata/deadlock.x" + +proc __deadlock__A_0_next<_req0_r: bits[32] in, _resp0_s: bits[32] out, _resp1_s: bits[32] out>(__state: bits[32], init={0}) { + chan_interface _req0_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan_interface _resp0_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan_interface _resp1_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + after_all.5: token = after_all(id=5) + literal.3: bits[1] = literal(value=1, id=3) + __state: bits[32] = state_read(state_element=__state, id=2) + literal.14: bits[32] = literal(value=1, id=14, pos=[(0,25,16)]) + receive.6: (token, bits[32]) = receive(after_all.5, predicate=literal.3, channel=_req0_r, id=6) + after_all.10: token = after_all(id=10) + after_all.12: token = after_all(id=12) + add.15: bits[32] = add(__state, literal.14, id=15, pos=[(0,25,8)]) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,12,8)]) + tuple_index.7: token = tuple_index(receive.6, index=0, id=7) + tok: token = tuple_index(receive.6, index=0, id=8, pos=[(0,17,13)]) + tuple_index.9: bits[32] = tuple_index(receive.6, index=1, id=9, pos=[(0,17,18)]) + tok__1: token = send(after_all.10, __state, predicate=literal.3, channel=_resp0_s, id=11) + tok__2: token = send(after_all.12, __state, predicate=literal.3, channel=_resp1_s, id=13) + next_value.16: () = next_value(param=__state, value=add.15, id=16) +} + +proc __deadlock__B_0_next<_req0_r: bits[32] in, _resp0_s: bits[32] out>(__state: (), init={()}) { + chan_interface _req0_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan_interface _resp0_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + after_all.21: token = after_all(id=21) + literal.19: bits[1] = literal(value=1, id=19) + receive.22: (token, bits[32]) = receive(after_all.21, predicate=literal.19, channel=_req0_r, id=22) + after_all.26: token = after_all(id=26) + data: bits[32] = tuple_index(receive.22, index=1, id=25, pos=[(0,39,18)]) + __state: () = state_read(state_element=__state, id=18) + tuple.28: () = tuple(id=28, pos=[(0,38,19)]) + __token: token = literal(value=token, id=17) + tuple.20: () = tuple(id=20, pos=[(0,35,8)]) + tuple_index.23: token = tuple_index(receive.22, index=0, id=23) + tok: token = tuple_index(receive.22, index=0, id=24, pos=[(0,39,13)]) + tok__1: token = send(after_all.26, data, predicate=literal.19, channel=_resp0_s, id=27) + next_value.29: () = next_value(param=__state, value=tuple.28, id=29) +} + +top proc __deadlock__Test_0_next<_terminator: bits[1] out>(__state: (), init={()}) { + chan_interface _terminator(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan _req0(bits[32], id=0, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _req0(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _req0(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan _resp0(bits[32], id=1, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _resp0(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _resp0(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan _resp1(bits[32], id=2, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _resp1(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _resp1(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + proc_instantiation __deadlock__A_0_next_inst(_req0, _resp0, _resp1, proc=__deadlock__A_0_next) + proc_instantiation __deadlock__B_0_next_inst(_resp0, _req0, proc=__deadlock__B_0_next) + tok: token = after_all(id=34) + literal.32: bits[1] = literal(value=1, id=32) + receive.36: (token, bits[32]) = receive(tok, predicate=literal.32, channel=_resp1, id=36) + tok__2: token = tuple_index(receive.36, index=0, id=38, pos=[(0,64,17)]) + receive.42: (token, bits[32]) = receive(tok__2, predicate=literal.32, channel=_resp1, id=42) + tok__4: token = tuple_index(receive.42, index=0, id=44, pos=[(0,64,17)]) + receive.48: (token, bits[32]) = receive(tok__4, predicate=literal.32, channel=_resp1, id=48) + tok__6: token = tuple_index(receive.48, index=0, id=50, pos=[(0,64,17)]) + receive.54: (token, bits[32]) = receive(tok__6, predicate=literal.32, channel=_resp1, id=54) + tok__8: token = tuple_index(receive.54, index=0, id=56, pos=[(0,64,17)]) + receive.60: (token, bits[32]) = receive(tok__8, predicate=literal.32, channel=_resp1, id=60) + tok__10: token = tuple_index(receive.60, index=0, id=62, pos=[(0,64,17)]) + receive.66: (token, bits[32]) = receive(tok__10, predicate=literal.32, channel=_resp1, id=66) + tok__12: token = tuple_index(receive.66, index=0, id=68, pos=[(0,64,17)]) + receive.72: (token, bits[32]) = receive(tok__12, predicate=literal.32, channel=_resp1, id=72) + tok__14: token = tuple_index(receive.72, index=0, id=74, pos=[(0,64,17)]) + receive.78: (token, bits[32]) = receive(tok__14, predicate=literal.32, channel=_resp1, id=78) + tok__16: token = tuple_index(receive.78, index=0, id=80, pos=[(0,64,17)]) + receive.84: (token, bits[32]) = receive(tok__16, predicate=literal.32, channel=_resp1, id=84) + tok__18: token = tuple_index(receive.84, index=0, id=86, pos=[(0,64,17)]) + receive.90: (token, bits[32]) = receive(tok__18, predicate=literal.32, channel=_resp1, id=90) + __token: token = literal(value=token, id=30) + data: bits[32] = tuple_index(receive.36, index=1, id=39, pos=[(0,64,22)]) + data__1: bits[32] = tuple_index(receive.42, index=1, id=45, pos=[(0,64,22)]) + data__2: bits[32] = tuple_index(receive.48, index=1, id=51, pos=[(0,64,22)]) + data__3: bits[32] = tuple_index(receive.54, index=1, id=57, pos=[(0,64,22)]) + data__4: bits[32] = tuple_index(receive.60, index=1, id=63, pos=[(0,64,22)]) + data__5: bits[32] = tuple_index(receive.66, index=1, id=69, pos=[(0,64,22)]) + data__6: bits[32] = tuple_index(receive.72, index=1, id=75, pos=[(0,64,22)]) + data__7: bits[32] = tuple_index(receive.78, index=1, id=81, pos=[(0,64,22)]) + data__8: bits[32] = tuple_index(receive.84, index=1, id=87, pos=[(0,64,22)]) + data__9: bits[32] = tuple_index(receive.90, index=1, id=93, pos=[(0,64,22)]) + tok__20: token = tuple_index(receive.90, index=0, id=92, pos=[(0,64,17)]) + literal.95: bits[1] = literal(value=1, id=95, pos=[(0,69,30)]) + __state: () = state_read(state_element=__state, id=31) + tuple.97: () = tuple(id=97, pos=[(0,62,19)]) + tuple.33: () = tuple(id=33, pos=[(0,59,8)]) + i: bits[32] = literal(value=0, id=35, pos=[(0,63,40)]) + tuple_index.37: token = tuple_index(receive.36, index=0, id=37) + trace.40: token = trace(__token, literal.32, format="Data: {}", data_operands=[data], id=40) + i__1: bits[32] = literal(value=1, id=41, pos=[(0,63,40)]) + tuple_index.43: token = tuple_index(receive.42, index=0, id=43) + trace.46: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__1], id=46) + i__2: bits[32] = literal(value=2, id=47, pos=[(0,63,40)]) + tuple_index.49: token = tuple_index(receive.48, index=0, id=49) + trace.52: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__2], id=52) + i__3: bits[32] = literal(value=3, id=53, pos=[(0,63,40)]) + tuple_index.55: token = tuple_index(receive.54, index=0, id=55) + trace.58: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__3], id=58) + i__4: bits[32] = literal(value=4, id=59, pos=[(0,63,40)]) + tuple_index.61: token = tuple_index(receive.60, index=0, id=61) + trace.64: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__4], id=64) + i__5: bits[32] = literal(value=5, id=65, pos=[(0,63,40)]) + tuple_index.67: token = tuple_index(receive.66, index=0, id=67) + trace.70: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__5], id=70) + i__6: bits[32] = literal(value=6, id=71, pos=[(0,63,40)]) + tuple_index.73: token = tuple_index(receive.72, index=0, id=73) + trace.76: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__6], id=76) + i__7: bits[32] = literal(value=7, id=77, pos=[(0,63,40)]) + tuple_index.79: token = tuple_index(receive.78, index=0, id=79) + trace.82: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__7], id=82) + i__8: bits[32] = literal(value=8, id=83, pos=[(0,63,40)]) + tuple_index.85: token = tuple_index(receive.84, index=0, id=85) + trace.88: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__8], id=88) + i__9: bits[32] = literal(value=9, id=89, pos=[(0,63,40)]) + tuple_index.91: token = tuple_index(receive.90, index=0, id=91) + trace.94: token = trace(__token, literal.32, format="Data: {}", data_operands=[data__9], id=94) + send.96: token = send(tok__20, literal.95, predicate=literal.32, channel=_terminator, id=96) + next_value.98: () = next_value(param=__state, value=tuple.97, id=98) +} diff --git a/xls/spin/testdata/deadlock.x b/xls/spin/testdata/deadlock.x new file mode 100644 index 0000000000..551a1f3fc5 --- /dev/null +++ b/xls/spin/testdata/deadlock.x @@ -0,0 +1,72 @@ +proc A { + req0_r: chan in; + resp0_s: chan out; + resp1_s: chan out; + + init { u32:0 } + + config( + req0_r: chan in, + resp0_s: chan out, + resp1_s: chan out + ) { + (req0_r, resp0_s, resp1_s) + } + + next(state: u32) { + // This does not work in the DSLX interpreter + let (tok, _) = recv(join(), req0_r); + let tok = send(join(), resp0_s, state); + + // This does, but has exactly the same meaning as the code above + // let tok = send(join(), resp0_s, state); + // let (tok, _) = recv(join(), req0_r); + + let tok = send(join(), resp1_s, state); + state + u32:1 + } +} + +proc B { + req0_r: chan in; + resp0_s: chan out; + + init {} + config(req0_r: chan in, resp0_s: chan out) { + (req0_r, resp0_s) + } + + next(state: ()) { + let (tok, data) = recv(join(), req0_r); + // Data dependency enforces correct ordering of IO operations + let tok = send(join(), resp0_s, data); + } +} + +#[test_proc] +proc Test { + terminator: chan out; + resp1_r: chan in; + + init {} + config(terminator: chan out) { + let (req0_s, req0_r) = chan("req0"); + let (resp0_s, resp0_r) = chan("resp0"); + let (resp1_s, resp1_r) = chan("resp1"); + + spawn A(req0_r, resp0_s, resp1_s); + spawn B(resp0_r, req0_s); + + (terminator, resp1_r) + } + + next(state: ()) { + let tok = const for (i, tok) in u32:0..u32:10 { + let (tok, data) = recv(tok, resp1_r); + trace_fmt!("Data: {}", data); + tok + }(join()); + + send(tok, terminator, true); + } +} diff --git a/xls/spin/testdata/func.ir b/xls/spin/testdata/func.ir new file mode 100644 index 0000000000..23af1f245e --- /dev/null +++ b/xls/spin/testdata/func.ir @@ -0,0 +1,7 @@ +package func + +file_number 0 "xls/spin/testdata/func.x" + +top fn __func__myfunc(a: bits[32] id=1, b: bits[32] id=2) -> bits[32] { + ret add.3: bits[32] = add(a, b, id=3, pos=[(0,0,35)]) +} diff --git a/xls/spin/testdata/func.x b/xls/spin/testdata/func.x new file mode 100644 index 0000000000..b4bb50ef8e --- /dev/null +++ b/xls/spin/testdata/func.x @@ -0,0 +1 @@ +fn myfunc(a: u32, b: u32) -> u32 { a + b } diff --git a/xls/spin/testdata/multi_proc.ir b/xls/spin/testdata/multi_proc.ir new file mode 100644 index 0000000000..782b9982e6 --- /dev/null +++ b/xls/spin/testdata/multi_proc.ir @@ -0,0 +1,11 @@ +package multi_proc + +file_number 0 "xls/spin/testdata/multi_proc.x" + +top proc __multi_proc__ProcA_0_next<>(__state: bits[1], init={0}) { + __state: bits[1] = state_read(state_element=__state, id=2) + __token: token = literal(value=token, id=1) + literal.3: bits[1] = literal(value=1, id=3) + tuple.4: () = tuple(id=4, pos=[(0,1,13)]) + next_value.5: () = next_value(param=__state, value=__state, id=5) +} diff --git a/xls/spin/testdata/multi_proc.x b/xls/spin/testdata/multi_proc.x new file mode 100644 index 0000000000..eddb7b7f2e --- /dev/null +++ b/xls/spin/testdata/multi_proc.x @@ -0,0 +1,10 @@ +proc ProcA { + config() { () } + init { u1:0 } + next(state: u1) { state } +} +proc ProcB { + config() { () } + init { u1:0 } + next(state: u1) { state } +} diff --git a/xls/spin/testdata/non_blocking_receive.ir b/xls/spin/testdata/non_blocking_receive.ir new file mode 100644 index 0000000000..f4e7d7db3b --- /dev/null +++ b/xls/spin/testdata/non_blocking_receive.ir @@ -0,0 +1,23 @@ +package non_blocking_receive + +file_number 0 "xls/spin/testdata/non_blocking_receive.x" + +top proc __non_blocking_receive__NbReader_0_next<_in_r: bits[32] in>(__state: bits[1], init={0}) { + chan_interface _in_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + after_all.5: token = after_all(id=5) + literal.3: bits[1] = literal(value=1, id=3) + receive.7: (token, bits[32], bits[1]) = receive(after_all.5, predicate=literal.3, channel=_in_r, blocking=false, id=7) + tuple_index.10: bits[1] = tuple_index(receive.7, index=2, id=10) + literal.6: bits[32] = literal(value=0, id=6, pos=[(0,5,52)]) + tuple_index.9: bits[32] = tuple_index(receive.7, index=1, id=9) + tuple_index.8: token = tuple_index(receive.7, index=0, id=8) + sel.11: bits[32] = sel(tuple_index.10, cases=[literal.6, tuple_index.9], id=11) + tuple.12: (token, bits[32], bits[1]) = tuple(tuple_index.8, sel.11, tuple_index.10, id=12) + __state: bits[1] = state_read(state_element=__state, id=2) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,2,31)]) + tuple_index.13: token = tuple_index(tuple.12, index=0, id=13, pos=[(0,5,9)]) + tuple_index.14: bits[32] = tuple_index(tuple.12, index=1, id=14, pos=[(0,5,12)]) + tuple_index.15: bits[1] = tuple_index(tuple.12, index=2, id=15, pos=[(0,5,15)]) + next_value.16: () = next_value(param=__state, value=__state, id=16) +} diff --git a/xls/spin/testdata/non_blocking_receive.x b/xls/spin/testdata/non_blocking_receive.x new file mode 100644 index 0000000000..d460d72aaa --- /dev/null +++ b/xls/spin/testdata/non_blocking_receive.x @@ -0,0 +1,9 @@ +proc NbReader { + in_r: chan in; + config(in_r: chan in) { (in_r,) } + init { u1:0 } + next(state: u1) { + let (_, _, _) = recv_non_blocking(join(), in_r, u32:0); + state + } +} diff --git a/xls/spin/testdata/passthrough.ir b/xls/spin/testdata/passthrough.ir new file mode 100644 index 0000000000..ab439ef8fe --- /dev/null +++ b/xls/spin/testdata/passthrough.ir @@ -0,0 +1,68 @@ +package passthrough + +file_number 0 "xls/spin/testdata/passthrough.x" + +proc __passthrough__Passthrough_0_next<_req_r: bits[32] in, _resp_s: bits[32] out>(__state: (), init={()}) { + chan_interface _req_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan_interface _resp_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + after_all.5: token = after_all(id=5) + literal.3: bits[1] = literal(value=1, id=3) + receive.6: (token, bits[32]) = receive(after_all.5, predicate=literal.3, channel=_req_r, id=6) + tok: token = tuple_index(receive.6, index=0, id=8, pos=[(0,9,13)]) + data: bits[32] = tuple_index(receive.6, index=1, id=9, pos=[(0,9,18)]) + __state: () = state_read(state_element=__state, id=2) + tuple.11: () = tuple(id=11, pos=[(0,8,19)]) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,6,57)]) + tuple_index.7: token = tuple_index(receive.6, index=0, id=7) + tok__1: token = send(tok, data, predicate=literal.3, channel=_resp_s, id=10) + next_value.12: () = next_value(param=__state, value=tuple.11, id=12) +} + +top proc __passthrough__PassthroughTest_0_next<_terminator: bits[1] out>(__state: bits[32], init={10}) { + chan_interface _terminator(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan _req(bits[32], id=0, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _req(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _req(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan _resp(bits[32], id=1, kind=streaming, ops=send_receive, flow_control=ready_valid, strictness=proven_mutually_exclusive) + chan_interface _resp(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + chan_interface _resp(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=none, flop_kind=none) + proc_instantiation __passthrough__Passthrough_0_next_inst(_req, _resp, proc=__passthrough__Passthrough_0_next) + __state: bits[32] = state_read(state_element=__state, id=14) + literal.18: bits[32] = literal(value=0, id=18, pos=[(0,32,46)]) + literal.15: bits[1] = literal(value=1, id=15) + ugt.19: bits[1] = ugt(__state, literal.18, id=19, pos=[(0,32,38)]) + literal.22: bits[32] = literal(value=0, id=22, pos=[(0,33,64)]) + tok: token = after_all(id=17) + and.20: bits[1] = and(literal.15, ugt.19, id=20) + ugt.23: bits[1] = ugt(__state, literal.22, id=23, pos=[(0,33,56)]) + tok__1: token = send(tok, __state, predicate=and.20, channel=_req, id=21) + and.25: bits[1] = and(literal.15, ugt.23, id=25) + receive.26: (token, bits[32]) = receive(tok__1, predicate=and.25, channel=_resp, id=26) + literal.24: bits[32] = literal(value=0, id=24, pos=[(0,33,71)]) + tuple_index.28: bits[32] = tuple_index(receive.26, index=1, id=28) + tuple_index.27: token = tuple_index(receive.26, index=0, id=27) + sel.29: bits[32] = sel(ugt.23, cases=[literal.24, tuple_index.28], id=29) + tuple.30: (token, bits[32]) = tuple(tuple_index.27, sel.29, id=30) + received_data: bits[32] = tuple_index(tuple.30, index=1, id=32, pos=[(0,33,18)]) + literal.38: bits[32] = literal(value=1, id=38, pos=[(0,35,42)]) + literal.43: bits[32] = literal(value=0, id=43, pos=[(0,36,19)]) + literal.45: bits[32] = literal(value=1, id=45, pos=[(0,36,35)]) + not.34: bits[1] = not(literal.15, id=34) + eq.33: bits[1] = eq(__state, received_data, id=33) + eq.39: bits[1] = eq(__state, literal.38, id=39, pos=[(0,35,33)]) + ugt.44: bits[1] = ugt(__state, literal.43, id=44, pos=[(0,36,11)]) + literal.47: bits[32] = literal(value=0, id=47, pos=[(0,36,50)]) + sub.46: bits[32] = sub(__state, literal.45, id=46, pos=[(0,36,27)]) + __token: token = literal(value=token, id=13) + or.35: bits[1] = or(not.34, eq.33, id=35) + tok__2: token = tuple_index(tuple.30, index=0, id=31, pos=[(0,33,13)]) + literal.40: bits[1] = literal(value=1, id=40, pos=[(0,35,49)]) + and.41: bits[1] = and(literal.15, eq.39, id=41) + sel.48: bits[32] = sel(ugt.44, cases=[literal.47, sub.46], id=48, pos=[(0,36,8)]) + tuple.16: () = tuple(id=16, pos=[(0,24,8)]) + assert.36: token = assert(__token, or.35, message="Assertion failure via assert_eq @ xls/spin/testdata/passthrough.x:35:18-35:40", label="assert_eq(count, received_data)", id=36) + tuple.37: () = tuple(id=37) + send.42: token = send(tok__2, literal.40, predicate=and.41, channel=_terminator, id=42) + next_value.49: () = next_value(param=__state, value=sel.48, id=49) +} diff --git a/xls/spin/testdata/passthrough.x b/xls/spin/testdata/passthrough.x new file mode 100644 index 0000000000..55a85cc0db --- /dev/null +++ b/xls/spin/testdata/passthrough.x @@ -0,0 +1,39 @@ +#![feature(type_inference_v2)] + +proc Passthrough { + req_r: chan in; + resp_s: chan out; + + config(req_r: chan in, resp_s: chan out) { (req_r, resp_s) } + init { () } + next(state: ()) { + let (tok, data) = recv(join(), req_r); + let tok = send(tok, resp_s, data); + } +} + +#[test_proc] +proc PassthroughTest { + terminator: chan out; + req_s: chan out; + resp_r: chan in; + + config(terminator: chan out) { + let (req_s, req_r) = chan("req"); + let (resp_s, resp_r) = chan("resp"); + spawn Passthrough(req_r, resp_s); + (terminator, req_s, resp_r) + } + // count=10..1: forward one value per tick; fire terminator at count=1. + // count=0: no-op so SPIN extra iterations produce no channel events. + // Passthrough's blocking recv then stalls naturally until __terminated. + init { u32:10 } + next(count: u32) { + let tok = join(); + let tok = send_if(tok, req_s, count > u32:0, count); + let (tok, received_data) = recv_if(tok, resp_r, count > u32:0, u32:0); + assert_eq(count, received_data); + send_if(tok, terminator, count == u32:1, true); + if count > u32:0 { count - u32:1 } else { u32:0 } + } +} diff --git a/xls/spin/testdata/predicated_receive.ir b/xls/spin/testdata/predicated_receive.ir new file mode 100644 index 0000000000..364b898561 --- /dev/null +++ b/xls/spin/testdata/predicated_receive.ir @@ -0,0 +1,23 @@ +package predicated_receive + +file_number 0 "xls/spin/testdata/predicated_receive.x" + +top proc __predicated_receive__CondReader_0_next<_in_r: bits[32] in>(__state: bits[1], init={0}) { + chan_interface _in_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + literal.3: bits[1] = literal(value=1, id=3) + literal.6: bits[1] = literal(value=1, id=6, pos=[(0,5,39)]) + after_all.5: token = after_all(id=5) + and.8: bits[1] = and(literal.3, literal.6, id=8) + receive.9: (token, bits[32]) = receive(after_all.5, predicate=and.8, channel=_in_r, id=9) + literal.7: bits[32] = literal(value=0, id=7, pos=[(0,5,45)]) + tuple_index.11: bits[32] = tuple_index(receive.9, index=1, id=11) + tuple_index.10: token = tuple_index(receive.9, index=0, id=10) + sel.12: bits[32] = sel(literal.6, cases=[literal.7, tuple_index.11], id=12) + tuple.13: (token, bits[32]) = tuple(tuple_index.10, sel.12, id=13) + __state: bits[1] = state_read(state_element=__state, id=2) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,2,31)]) + tuple_index.14: token = tuple_index(tuple.13, index=0, id=14, pos=[(0,5,9)]) + tuple_index.15: bits[32] = tuple_index(tuple.13, index=1, id=15, pos=[(0,5,12)]) + next_value.16: () = next_value(param=__state, value=__state, id=16) +} diff --git a/xls/spin/testdata/predicated_receive.x b/xls/spin/testdata/predicated_receive.x new file mode 100644 index 0000000000..0e2f04419d --- /dev/null +++ b/xls/spin/testdata/predicated_receive.x @@ -0,0 +1,9 @@ +proc CondReader { + in_r: chan in; + config(in_r: chan in) { (in_r,) } + init { u1:0 } + next(state: u1) { + let (_, _) = recv_if(join(), in_r, true, u32:0); + state + } +} diff --git a/xls/spin/testdata/predicated_send.ir b/xls/spin/testdata/predicated_send.ir new file mode 100644 index 0000000000..c37ee7fc62 --- /dev/null +++ b/xls/spin/testdata/predicated_send.ir @@ -0,0 +1,16 @@ +package predicated_send + +file_number 0 "xls/spin/testdata/predicated_send.x" + +top proc __predicated_send__CondWriter_0_next<_out_s: bits[32] out>(__state: bits[32], init={0}) { + chan_interface _out_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + literal.3: bits[1] = literal(value=1, id=3) + literal.6: bits[1] = literal(value=1, id=6, pos=[(0,5,27)]) + after_all.5: token = after_all(id=5) + __state: bits[32] = state_read(state_element=__state, id=2) + and.7: bits[1] = and(literal.3, literal.6, id=7) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,2,33)]) + send.8: token = send(after_all.5, __state, predicate=and.7, channel=_out_s, id=8) + next_value.9: () = next_value(param=__state, value=__state, id=9) +} diff --git a/xls/spin/testdata/predicated_send.x b/xls/spin/testdata/predicated_send.x new file mode 100644 index 0000000000..9085d00b83 --- /dev/null +++ b/xls/spin/testdata/predicated_send.x @@ -0,0 +1,9 @@ +proc CondWriter { + out_s: chan out; + config(out_s: chan out) { (out_s,) } + init { u32:0 } + next(state: u32) { + send_if(join(), out_s, true, state); + state + } +} diff --git a/xls/spin/testdata/proc.ir b/xls/spin/testdata/proc.ir new file mode 100644 index 0000000000..6601a02e76 --- /dev/null +++ b/xls/spin/testdata/proc.ir @@ -0,0 +1,24 @@ +package proc + +file_number 0 "xls/spin/testdata/proc.x" + +top proc __proc__MyProc_0_next<_req_r: bits[8] in, _resp_s: bits[32] out>(__state: bits[16], init={0}) { + chan_interface _req_r(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan_interface _resp_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + after_all.5: token = after_all(id=5) + literal.3: bits[1] = literal(value=1, id=3) + receive.6: (token, bits[8]) = receive(after_all.5, predicate=literal.3, channel=_req_r, id=6) + data: bits[8] = tuple_index(receive.6, index=1, id=9, pos=[(0,14,18)]) + zero_ext.10: bits[16] = zero_ext(data, new_bit_count=16, id=10) + __state: bits[16] = state_read(state_element=__state, id=2) + add.11: bits[16] = add(zero_ext.10, __state, id=11, pos=[(0,15,37)]) + literal.14: bits[16] = literal(value=1, id=14, pos=[(0,16,16)]) + tok: token = tuple_index(receive.6, index=0, id=8, pos=[(0,14,13)]) + zero_ext.12: bits[32] = zero_ext(add.11, new_bit_count=32, id=12) + add.15: bits[16] = add(__state, literal.14, id=15, pos=[(0,16,8)]) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,9,8)]) + tuple_index.7: token = tuple_index(receive.6, index=0, id=7) + tok__1: token = send(tok, zero_ext.12, predicate=literal.3, channel=_resp_s, id=13) + next_value.16: () = next_value(param=__state, value=add.15, id=16) +} diff --git a/xls/spin/testdata/proc.x b/xls/spin/testdata/proc.x new file mode 100644 index 0000000000..dfe1823dd0 --- /dev/null +++ b/xls/spin/testdata/proc.x @@ -0,0 +1,19 @@ +#![feature(type_inference_v2)] + +proc MyProc { + req_r: chan in; + resp_s: chan out; + + config( + req_r: chan in, + resp_s: chan out + ) { (req_r, resp_s) } + + init { u16:0 } + + next(state: u16) { + let (tok, data) = recv(join(), req_r); + let tok = send(tok, resp_s, (data as u16 + state) as u32); + state + u16:1 + } +} diff --git a/xls/spin/testdata/state_counter.ir b/xls/spin/testdata/state_counter.ir new file mode 100644 index 0000000000..205d96431b --- /dev/null +++ b/xls/spin/testdata/state_counter.ir @@ -0,0 +1,13 @@ +package state_counter + +file_number 0 "xls/spin/testdata/state_counter.x" + +top proc __state_counter__Counter_0_next<>(__state: bits[32], init={0}) { + __state: bits[32] = state_read(state_element=__state, id=2) + literal.5: bits[32] = literal(value=1, id=5, pos=[(0,3,29)]) + add.6: bits[32] = add(__state, literal.5, id=6, pos=[(0,3,21)]) + __token: token = literal(value=token, id=1) + literal.3: bits[1] = literal(value=1, id=3) + tuple.4: () = tuple(id=4, pos=[(0,1,13)]) + next_value.7: () = next_value(param=__state, value=add.6, id=7) +} diff --git a/xls/spin/testdata/state_counter.x b/xls/spin/testdata/state_counter.x new file mode 100644 index 0000000000..7d49340c84 --- /dev/null +++ b/xls/spin/testdata/state_counter.x @@ -0,0 +1,5 @@ +proc Counter { + config() { () } + init { u32:0 } + next(state: u32) { state + u32:1 } +} diff --git a/xls/spin/testdata/unconditional_send.ir b/xls/spin/testdata/unconditional_send.ir new file mode 100644 index 0000000000..599a99b56e --- /dev/null +++ b/xls/spin/testdata/unconditional_send.ir @@ -0,0 +1,14 @@ +package unconditional_send + +file_number 0 "xls/spin/testdata/unconditional_send.x" + +top proc __unconditional_send__Writer_0_next<_out_s: bits[32] out>(__state: bits[32], init={0}) { + chan_interface _out_s(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + after_all.5: token = after_all(id=5) + __state: bits[32] = state_read(state_element=__state, id=2) + literal.3: bits[1] = literal(value=1, id=3) + __token: token = literal(value=token, id=1) + tuple.4: () = tuple(id=4, pos=[(0,2,33)]) + send.6: token = send(after_all.5, __state, predicate=literal.3, channel=_out_s, id=6) + next_value.7: () = next_value(param=__state, value=__state, id=7) +} diff --git a/xls/spin/testdata/unconditional_send.x b/xls/spin/testdata/unconditional_send.x new file mode 100644 index 0000000000..202cd17055 --- /dev/null +++ b/xls/spin/testdata/unconditional_send.x @@ -0,0 +1,9 @@ +proc Writer { + out_s: chan out; + config(out_s: chan out) { (out_s,) } + init { u32:0 } + next(state: u32) { + send(join(), out_s, state); + state + } +} diff --git a/xls/spin/testdata/xr_hints.ir b/xls/spin/testdata/xr_hints.ir new file mode 100644 index 0000000000..30d9702a8c --- /dev/null +++ b/xls/spin/testdata/xr_hints.ir @@ -0,0 +1,13 @@ +package xr_hints + +file_number 0 "xls/spin/testdata/xr_hints.x" + +top proc __xr_hints__RwProc_0_next<_in_ch: bits[32] in, _out_ch: bits[32] out>(__state: bits[1], init={0}) { + chan_interface _in_ch(direction=receive, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + chan_interface _out_ch(direction=send, kind=streaming, strictness=proven_mutually_exclusive, flow_control=ready_valid, flop_kind=none) + __state: bits[1] = state_read(state_element=__state, id=2) + __token: token = literal(value=token, id=1) + literal.3: bits[1] = literal(value=1, id=3) + tuple.4: () = tuple(id=4, pos=[(0,3,55)]) + next_value.5: () = next_value(param=__state, value=__state, id=5) +} diff --git a/xls/spin/testdata/xr_hints.x b/xls/spin/testdata/xr_hints.x new file mode 100644 index 0000000000..9ba9f38de9 --- /dev/null +++ b/xls/spin/testdata/xr_hints.x @@ -0,0 +1,7 @@ +proc RwProc { + in_ch: chan in; + out_ch: chan out; + config(in_ch: chan in, out_ch: chan out) { (in_ch, out_ch) } + init { u1:0 } + next(state: u1) { state } +} diff --git a/xls/spin/trace_compare.cc b/xls/spin/trace_compare.cc new file mode 100644 index 0000000000..bf73713ac7 --- /dev/null +++ b/xls/spin/trace_compare.cc @@ -0,0 +1,182 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "xls/spin/trace_compare.h" + +#include +#include +#include +#include +#include + +#include "absl/container/btree_map.h" +#include "absl/container/btree_set.h" +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_split.h" +#include "absl/strings/strip.h" +#include "google/protobuf/text_format.h" +#include "re2/re2.h" +#include "xls/ir/bits.h" +#include "xls/ir/evaluator_result.pb.h" + +namespace xls::spin { + +std::string NormalizeSpinChannel(std::string_view name) { + return std::string(absl::StripPrefix(name, "_")); +} + +std::string NormalizeDslxChannel(std::string_view name) { + auto pos = name.rfind("::"); + return std::string((pos == std::string_view::npos) ? name + : name.substr(pos + 2)); +} + +absl::Status ParseSpinTrace(std::string_view json, TraceMap& out, + std::string_view terminator_channel, + bool* saw_terminator) { + static const RE2 kRe( + "\\{\"channel_name\":\"([^\"]*)\"," + "\"direction\":\"([^\"]*)\"," + "\"value\":(-?[0-9]+)\\}"); + int64_t event_count = 0; + for (std::string_view line : absl::StrSplit(json, '\n')) { + if (line.empty()) { + continue; + } + std::string channel_name, direction; + int64_t value = 0; + if (!RE2::FullMatch(line, kRe, &channel_name, &direction, &value)) { + LOG(WARNING) << "SPIN trace: unrecognized line: " << line; + continue; + } + if (value < 0) { + value &= 0xFFFFFFFFULL; + } + std::string normalized_channel = NormalizeSpinChannel(channel_name); + if (!terminator_channel.empty() && + normalized_channel == terminator_channel && direction == "SEND") { + if (saw_terminator != nullptr) { + *saw_terminator = true; + } + break; + } + out[{normalized_channel, direction}].push_back(value); + ++event_count; + } + LOG(INFO) << "Parsed " << event_count << " SPIN trace event(s)"; + return absl::OkStatus(); +} + +absl::Status ParseDslxTrace(std::string_view textproto, TraceMap& out, + std::string_view terminator_channel) { + EvaluatorResultsProto proto; + if (!google::protobuf::TextFormat::ParseFromString(std::string(textproto), + &proto)) { + return absl::InvalidArgumentError("DSLX trace text proto parse error"); + } + bool terminated = false; + int64_t event_count = 0; + for (const EvaluatorResultProto& result : proto.results()) { + if (terminated) { + break; + } + for (const TraceMessageProto& trace_message : + result.events().trace_msgs()) { + if (!trace_message.has_channel()) { + continue; + } + const TraceChannelProto& trace_channel = trace_message.channel(); + std::string direction; + switch (trace_channel.direction()) { + case TraceChannelProto::SEND: + direction = "SEND"; + break; + case TraceChannelProto::RECV: + direction = "RECV"; + break; + default: + continue; + } + int64_t value = 0; + if (trace_channel.has_value() && trace_channel.value().has_bits()) { + value = Bits::FromBytes(trace_channel.value().bits().data()) + .UnsignedToInt64() + .value_or(0); + } + std::string normalized_channel = + NormalizeDslxChannel(trace_channel.channel_name()); + if (!terminator_channel.empty() && + normalized_channel == terminator_channel && direction == "SEND") { + terminated = true; + // Don't break: spawned procs share the round and their events must + // be included to match the SPIN trace. + continue; + } + out[{normalized_channel, direction}].push_back(value); + ++event_count; + } + } + LOG(INFO) << "Parsed " << event_count << " DSLX trace event(s)"; + return absl::OkStatus(); +} + +absl::Status CompareTraces(const TraceMap& spin, const TraceMap& dslx) { + // absl::btree_set gives deterministic (sorted) iteration for reproducible + // error messages. + absl::btree_set> all_keys; + for (const auto& [key, unused] : spin) { + all_keys.insert(key); + } + for (const auto& [key, unused] : dslx) { + all_keys.insert(key); + } + LOG(INFO) << "Comparing traces: " << all_keys.size() + << " channel/direction pair(s)"; + + std::string mismatches; + for (const auto& key : all_keys) { + const auto& [channel_name, direction] = key; + auto spin_it = spin.find(key); + auto dslx_it = dslx.find(key); + if (spin_it == spin.end()) { + absl::StrAppendFormat(&mismatches, + " channel=%s dir=%s: present in dslx only\n", + channel_name, direction); + continue; + } + if (dslx_it == dslx.end()) { + absl::StrAppendFormat(&mismatches, + " channel=%s dir=%s: present in spin only\n", + channel_name, direction); + continue; + } + if (spin_it->second != dslx_it->second) { + absl::StrAppendFormat( + &mismatches, " channel=%s dir=%s: spin=%zu dslx=%zu events differ\n", + channel_name, direction, spin_it->second.size(), + dslx_it->second.size()); + } + } + if (!mismatches.empty()) { + return absl::FailedPreconditionError( + absl::StrFormat("Trace mismatch:\n%s", mismatches)); + } + LOG(INFO) << "Traces match across all " << all_keys.size() + << " channel/direction pair(s)"; + return absl::OkStatus(); +} + +} // namespace xls::spin diff --git a/xls/spin/trace_compare.h b/xls/spin/trace_compare.h new file mode 100644 index 0000000000..527e1ba019 --- /dev/null +++ b/xls/spin/trace_compare.h @@ -0,0 +1,62 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// SPIN <-> DSLX channel-event trace parsing and comparison utilities. + +#ifndef XLS_SPIN_TRACE_COMPARE_H_ +#define XLS_SPIN_TRACE_COMPARE_H_ + +#include +#include +#include +#include +#include + +#include "absl/container/btree_map.h" +#include "absl/status/status.h" + +namespace xls::spin { + +// (channel_name, direction) -> ordered value sequence. +// btree_map for deterministic iteration in error messages. +using TraceMap = + absl::btree_map, std::vector>; + +// Strips the leading underscore that the Promela generator adds to names. +// e.g. "_req" -> "req", "req" -> "req". +std::string NormalizeSpinChannel(std::string_view name); + +// Strips the proc-hierarchy prefix from a DSLX channel name. +// e.g. "PassthroughTest->Sub#0::req" -> "req". +std::string NormalizeDslxChannel(std::string_view name); + +// Parses newline-delimited JSON from `spin -Q` into *out*; reinterprets signed +// values as uint32. Stops at and sets *saw_terminator on terminator_channel +// SEND. +absl::Status ParseSpinTrace(std::string_view json, TraceMap& out, + std::string_view terminator_channel = "", + bool* saw_terminator = nullptr); + +// Parses an EvaluatorResultsProto textproto into *out*. +// Stops after the first terminator_channel SEND if set. +absl::Status ParseDslxTrace(std::string_view textproto, TraceMap& out, + std::string_view terminator_channel = ""); + +// Compares two TraceMaps; returns FailedPrecondition with a per-channel diff on +// mismatch. +absl::Status CompareTraces(const TraceMap& spin, const TraceMap& dslx); + +} // namespace xls::spin + +#endif // XLS_SPIN_TRACE_COMPARE_H_ diff --git a/xls/spin/trace_compare_test.cc b/xls/spin/trace_compare_test.cc new file mode 100644 index 0000000000..0de31944ec --- /dev/null +++ b/xls/spin/trace_compare_test.cc @@ -0,0 +1,89 @@ +// Copyright 2026 The XLS Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "xls/spin/trace_compare.h" + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace xls::spin { +namespace { + +using ::testing::ElementsAre; + +testing::AssertionResult CompareMaps(const TraceMap& spin, + const TraceMap& dslx) { + auto status = CompareTraces(spin, dslx); + if (!status.ok()) return testing::AssertionFailure() << status.message(); + return testing::AssertionSuccess(); +} + +TEST(TraceCompareTest, NormalizeSpinChannel) { + EXPECT_EQ(NormalizeSpinChannel("_req"), "req"); + EXPECT_EQ(NormalizeSpinChannel("req"), "req"); + EXPECT_EQ(NormalizeSpinChannel("_terminator"), "terminator"); +} + +TEST(TraceCompareTest, NormalizeDslxChannel) { + EXPECT_EQ(NormalizeDslxChannel("PassthroughTest->Sub#0::req"), "req"); + EXPECT_EQ(NormalizeDslxChannel("PassthroughTest::req"), "req"); + EXPECT_EQ(NormalizeDslxChannel("req"), "req"); + EXPECT_EQ(NormalizeDslxChannel("PassthroughTest::result"), "result"); +} + +TEST(TraceCompareTest, ParseSpinTrace_Basic) { + constexpr std::string_view kJson = + "{\"channel_name\":\"_req\",\"direction\":\"SEND\",\"value\":1}\n" + "{\"channel_name\":\"_resp\",\"direction\":\"RECV\",\"value\":42}\n" + "{\"channel_name\":\"_req\",\"direction\":\"SEND\",\"value\":2}\n"; + TraceMap out; + EXPECT_TRUE(ParseSpinTrace(kJson, out).ok()); + EXPECT_THAT((out[{"req", "SEND"}]), ElementsAre(1, 2)); + EXPECT_THAT((out[{"resp", "RECV"}]), ElementsAre(42)); +} + +TEST(TraceCompareTest, ParseSpinTrace_NegativeValueReinterpret) { + // SPIN's 32-bit signed -1 must be reinterpreted as uint32 (4294967295). + constexpr std::string_view kJson = + "{\"channel_name\":\"_ch\",\"direction\":\"SEND\",\"value\":-1}\n"; + TraceMap out; + EXPECT_TRUE(ParseSpinTrace(kJson, out).ok()); + EXPECT_THAT((out[{"ch", "SEND"}]), ElementsAre(4294967295LL)); +} + +TEST(TraceCompareTest, CompareMaps_Match) { + TraceMap spin, dslx; + spin[{"req", "SEND"}] = {1, 2, 3}; + dslx[{"req", "SEND"}] = {1, 2, 3}; + EXPECT_TRUE(CompareMaps(spin, dslx)); +} + +TEST(TraceCompareTest, CompareMaps_Mismatch) { + TraceMap spin, dslx; + spin[{"req", "SEND"}] = {1, 2, 3}; + dslx[{"req", "SEND"}] = {1, 2, 99}; + EXPECT_FALSE(CompareMaps(spin, dslx)); +} + +TEST(TraceCompareTest, CompareMaps_AsymmetricKeys) { + TraceMap spin, dslx; + spin[{"req", "SEND"}] = {1}; + dslx[{"resp", "RECV"}] = {1}; + EXPECT_FALSE(CompareMaps(spin, dslx)); +} + +} // namespace +} // namespace xls::spin