diff --git a/projects/hipblaslt/CMakeLists.txt b/projects/hipblaslt/CMakeLists.txt index 499df8a2fcde..dcca6cd8b58e 100644 --- a/projects/hipblaslt/CMakeLists.txt +++ b/projects/hipblaslt/CMakeLists.txt @@ -306,6 +306,13 @@ if(_hipblaslt_mxdatagen_enabled) add_subdirectory(clients/common) endif() +# Enable stinkytofu backend only when building for gfx1250+. +string(REGEX MATCH "gfx1250" _has_gfx1250 "${GPU_TARGETS}") +if(_has_gfx1250) + set(ROCISA_ENABLE_STINKYTOFU ON CACHE BOOL + "Enable stinkytofu backend integration (gfx1250+)" FORCE) +endif() + add_subdirectory(tensilelite) if(HIPBLASLT_ENABLE_HOST) diff --git a/projects/hipblaslt/tensilelite/Tensile/KernelWriter.py b/projects/hipblaslt/tensilelite/Tensile/KernelWriter.py index c07dcaa8d61a..d2838c4ce9ca 100644 --- a/projects/hipblaslt/tensilelite/Tensile/KernelWriter.py +++ b/projects/hipblaslt/tensilelite/Tensile/KernelWriter.py @@ -6664,6 +6664,18 @@ def _restoreNtabState(): t1a_end = time.perf_counter() print2(f"StinkyTofu (1a) toStinkyTofuModule: {t1a_end - t1a_start:.4f}s") + # --- DEBUG: dump StinkyAsmModule BEFORE optimization pipeline --- + # Compares the two paths' logical→asm lowering output before shared passes run. + if os.environ.get("DUMP_STINKY_MODULE"): + _dump_dir = os.environ.get("DUMP_STINKY_MODULE", "/tmp/stinky_dump") + os.makedirs(_dump_dir, exist_ok=True) + _backend_tag = os.environ.get("ROCISA_BACKEND", "native") + _dump_asm = stModule.emitAssembly() + _dump_path = os.path.join(_dump_dir, f"stmodule_{_backend_tag}.s") + with open(_dump_path, "w") as _df: + _df.write(_dump_asm) + print2(f"StinkyTofu DEBUG: dumped pre-pipeline module ({len(_dump_asm)} chars) -> {_dump_path}") + # Run pipeline — builder handles O0 internally (skips optimization, # still runs required passes like InsertVgprMsb) t1b_start = time.perf_counter() diff --git a/projects/hipblaslt/tensilelite/Tensile/Source/memory_gfx.h b/projects/hipblaslt/tensilelite/Tensile/Source/memory_gfx.h index f0a8d12689f3..d7d92ae9ce49 100644 --- a/projects/hipblaslt/tensilelite/Tensile/Source/memory_gfx.h +++ b/projects/hipblaslt/tensilelite/Tensile/Source/memory_gfx.h @@ -161,35 +161,35 @@ __device__ char llvm_amdgcn_raw_buffer_load_i8(int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.i8"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.i8.v4i32"); // 2 bytes __device__ float16_t llvm_amdgcn_raw_buffer_load_f16(int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.f16"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.f16.v4i32"); // 4 bytes __device__ float32_t llvm_amdgcn_raw_buffer_load_f32(int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.f32"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.f32.v4i32"); // 8 bytes __device__ float32x2_t llvm_amdgcn_raw_buffer_load_f32x2(int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.v2f32"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.v2f32.v4i32"); // 16 bytes __device__ float32x4_t llvm_amdgcn_raw_buffer_load_f32x4(int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.v4f32"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.load.v4f32.v4i32"); // 4 bytes __device__ int32_t @@ -219,7 +219,7 @@ __device__ void int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.i8"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.i8.v4i32"); // 2 bytes __device__ void @@ -227,7 +227,7 @@ __device__ void int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.f16"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.f16.v4i32"); // 4 bytes __device__ void @@ -235,7 +235,7 @@ __device__ void int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.f32"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.f32.v4i32"); // 8 bytes __device__ void llvm_amdgcn_raw_buffer_store_f32x2( @@ -243,7 +243,7 @@ __device__ void llvm_amdgcn_raw_buffer_store_f32x2( int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.v2f32"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.v2f32.v4i32"); // 16 bytes __device__ void llvm_amdgcn_raw_buffer_store_f32x4( @@ -251,7 +251,7 @@ __device__ void llvm_amdgcn_raw_buffer_store_f32x4( int32x4_t buffer_resource, uint32_t voffset, uint32_t soffset, - int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.v4f32"); + int32_t cache_op) __asm("llvm.amdgcn.raw.buffer.store.v4f32.v4i32"); //////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/projects/hipblaslt/tensilelite/rocisa/CMakeLists.txt b/projects/hipblaslt/tensilelite/rocisa/CMakeLists.txt index 2b44eb7c293a..8a0cb3cf1b83 100644 --- a/projects/hipblaslt/tensilelite/rocisa/CMakeLists.txt +++ b/projects/hipblaslt/tensilelite/rocisa/CMakeLists.txt @@ -10,6 +10,9 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) project(rocisa LANGUAGES CXX) set(ROCISA_STANDALONE ON) + # Standalone (dev) builds always enable stinkytofu — the editable install + # is used exclusively for gfx1250 development. + set(ROCISA_ENABLE_STINKYTOFU ON CACHE BOOL "" FORCE) else() set(ROCISA_STANDALONE OFF) endif() @@ -129,48 +132,64 @@ if(HIPBLASLT_BUNDLE_PYTHON_DEPS OR ROCISA_STANDALONE) target_link_options(_rocisa PRIVATE -fprofile-instr-generate) endif() - # Locate stinkytofu: prefer installed package, fall back to monorepo sibling. - if(NOT TARGET stinkytofu::stinkytofu) - find_package(stinkytofu QUIET) - if(NOT stinkytofu_FOUND) - set(_stinkytofu_sibling "${CMAKE_CURRENT_SOURCE_DIR}/../../../../shared/stinkytofu") - if(IS_DIRECTORY "${_stinkytofu_sibling}") - set(STINKYTOFU_ENABLE_WERROR ON) - add_subdirectory("${_stinkytofu_sibling}" stinkytofu EXCLUDE_FROM_ALL) - set(_stinkytofu_from_subdirectory TRUE) - else() - message(FATAL_ERROR "stinkytofu not found via find_package or sibling directory") + option(ROCISA_ENABLE_STINKYTOFU "Enable stinkytofu backend integration (gfx1250+)" OFF) + + target_link_libraries(_rocisa PUBLIC roc::origami) + + if(ROCISA_ENABLE_STINKYTOFU) + # Locate stinkytofu: prefer installed package, fall back to monorepo sibling. + if(NOT TARGET stinkytofu::stinkytofu) + find_package(stinkytofu QUIET) + if(NOT stinkytofu_FOUND) + set(STINKYTOFU_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../shared/stinkytofu") + if(EXISTS "${STINKYTOFU_DIR}/CMakeLists.txt") + set(BUILD_SHARED_LIBS ON) + set(STINKYTOFU_ENABLE_WERROR ON) + + # Force StinkyTofu Python bindings ON because the rocisa->stinkytofu + # adapter relies on the _stinkytofu.so nanobind module. + set(STINKYTOFU_BUILD_PYTHON ON CACHE BOOL + "Build StinkyTofu Python bindings (required by ROCISA_BACKEND=stinkytofu adapter)" + FORCE) + + add_subdirectory("${STINKYTOFU_DIR}" stinkytofu EXCLUDE_FROM_ALL) + set(_stinkytofu_from_subdirectory TRUE) + else() + message(FATAL_ERROR "stinkytofu not found via find_package or sibling directory: ${STINKYTOFU_DIR}") + endif() endif() endif() - endif() - if(NOT DEFINED _stinkytofu_from_subdirectory) - get_target_property(_st_imported stinkytofu::stinkytofu IMPORTED) - if(_st_imported) - set(_stinkytofu_from_subdirectory FALSE) - else() - set(_stinkytofu_from_subdirectory TRUE) + if(NOT DEFINED _stinkytofu_from_subdirectory) + get_target_property(_st_imported stinkytofu::stinkytofu IMPORTED) + if(_st_imported) + set(_stinkytofu_from_subdirectory FALSE) + else() + set(_stinkytofu_from_subdirectory TRUE) + endif() endif() - endif() - target_link_libraries(_rocisa PUBLIC roc::origami) - target_link_libraries(_rocisa PRIVATE hip::host stinkytofu::stinkytofu) + target_link_libraries(_rocisa PRIVATE hip::host stinkytofu::stinkytofu) + target_compile_definitions(_rocisa PRIVATE ROCISA_HAS_STINKYTOFU=1) - # rocisa<->stinkytofu conversion glue. These sources live in the stinkytofu - # source tree but are rocisa-side bindings (they include rocisa headers and - # define init_stinkytofu), so they compile into _rocisa, not libstinkytofu. - # rocisa owns them directly here rather than relying on stinkytofu to inject - # them, so the de-bundled (find_package) build links them too. - set(_stinkytofu_src_dir "${CMAKE_CURRENT_SOURCE_DIR}/../../../../shared/stinkytofu") - target_sources(_rocisa PRIVATE - "${_stinkytofu_src_dir}/src/conversion/rocisa/AllHwMappings.cpp" - "${_stinkytofu_src_dir}/src/conversion/rocisa/ToStinkyTofuUtils.cpp" - ) + # rocisa<->stinkytofu conversion glue. These sources live in the stinkytofu + # source tree but are rocisa-side bindings (they include rocisa headers and + # define init_stinkytofu), so they compile into _rocisa, not libstinkytofu. + # rocisa owns them directly here rather than relying on stinkytofu to inject + # them, so the de-bundled (find_package) build links them too. + set(_stinkytofu_src_dir "${CMAKE_CURRENT_SOURCE_DIR}/../../../../shared/stinkytofu") + target_sources(_rocisa PRIVATE + "${_stinkytofu_src_dir}/src/conversion/rocisa/AllHwMappings.cpp" + "${_stinkytofu_src_dir}/src/conversion/rocisa/ToStinkyTofuUtils.cpp" + ) - if(_stinkytofu_from_subdirectory) - # Source-built stinkytofu via add_subdirectory: generated headers are in - # the stinkytofu/ subdirectory of the binary dir. - target_include_directories(_rocisa PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/stinkytofu") + if(_stinkytofu_from_subdirectory) + # Source-built stinkytofu via add_subdirectory: generated headers are in + # the stinkytofu/ subdirectory of the binary dir. + target_include_directories(_rocisa PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/stinkytofu") + endif() + else() + target_link_libraries(_rocisa PRIVATE hip::host) endif() # _build_info.py is intentionally NOT in install() rules for production # wheels/packages, acting as the source-build sentinel. Dev/editable installs @@ -179,67 +198,119 @@ if(HIPBLASLT_BUNDLE_PYTHON_DEPS OR ROCISA_STANDALONE) # loader's default search path. option(ROCISA_INCLUDE_BUILD_INFO "Install _build_info.py for staleness detection" OFF) - # Directory containing libstinkytofu, resolved by CMake for the active - # config. $ handles both the add_subdirectory target - # (build output dir) and the imported package target (IMPORTED_LOCATION_) - # without hand-matching configs. - set(_st_lib_dir "$") - set_target_properties(_rocisa PROPERTIES - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rocisa" - BUILD_RPATH "${_st_lib_dir}" - ) - if(_stinkytofu_from_subdirectory) - # Source-built sibling: no installed libstinkytofu exists, so the wheel carries - # its own copy next to _rocisa and loads it from there. - set_target_properties(_rocisa PROPERTIES INSTALL_RPATH "$ORIGIN") - # Windows has no RPATH; copy stinkytofu.dll next to _rocisa.pyd so the loader - # finds it at runtime. - if(WIN32) - add_custom_command(TARGET _rocisa POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - ) + if(ROCISA_ENABLE_STINKYTOFU) + # Directory containing libstinkytofu, resolved by CMake for the active + # config. $ handles both the add_subdirectory target + # (build output dir) and the imported package target (IMPORTED_LOCATION_) + # without hand-matching configs. + set(_st_lib_dir "$") + set_target_properties(_rocisa PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rocisa" + BUILD_RPATH "${_st_lib_dir}" + ) + if(_stinkytofu_from_subdirectory) + # Source-built sibling: no installed libstinkytofu exists, so the wheel carries + # its own copy next to _rocisa and loads it from there. + set_target_properties(_rocisa PROPERTIES INSTALL_RPATH "$ORIGIN") + # Windows has no RPATH; copy stinkytofu.dll next to _rocisa.pyd so the loader + # finds it at runtime. + if(WIN32) + add_custom_command(TARGET _rocisa POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + ) + endif() + elseif(ROCISA_INCLUDE_BUILD_INFO) + # Dev/editable find_package build: libstinkytofu is installed to a temp + # prefix off the loader's default search path, so bake its dir into the + # install RPATH from the link line. Gated on the dev-build signal so + # released ROCm builds stay RPATH-free and rely on libstinkytofu being + # resident in rocm/lib. + set_target_properties(_rocisa PROPERTIES INSTALL_RPATH_USE_LINK_PATH ON) endif() - elseif(ROCISA_INCLUDE_BUILD_INFO) - # Dev/editable find_package build: libstinkytofu is installed to a temp - # prefix off the loader's default search path, so bake its dir into the - # install RPATH from the link line. Gated on the dev-build signal so - # released ROCm builds stay RPATH-free and rely on libstinkytofu being - # resident in rocm/lib. - set_target_properties(_rocisa PROPERTIES INSTALL_RPATH_USE_LINK_PATH ON) + else() + set_target_properties(_rocisa PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/rocisa" + ) endif() - # In a released build, installed stinkytofu is a standard ROCm library on the - # loader's search path (like libyaml behind PyYAML); _rocisa's DT_NEEDED - # resolves it from there, so no wheel-local copy or RPATH is needed. configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/rocisa/__init__.py" "${CMAKE_CURRENT_BINARY_DIR}/rocisa/__init__.py" COPYONLY ) - # STINKYTOFU_ROOT_DIR feeds @STINKYTOFU_ROOT_DIR@ in _build_info.py (the - # staleness check scans stinkytofu sources too, since the conversion glue is - # compiled into _rocisa). It is set via PARENT_SCOPE only in the - # add_subdirectory build; in the de-bundled find_package build it is unset, - # which would leave the path empty and make the check scan the CWD. Point it - # at the real stinkytofu source tree. - if(NOT STINKYTOFU_ROOT_DIR) - cmake_path(ABSOLUTE_PATH _stinkytofu_src_dir NORMALIZE OUTPUT_VARIABLE STINKYTOFU_ROOT_DIR) + if(ROCISA_ENABLE_STINKYTOFU) + # STINKYTOFU_ROOT_DIR feeds @STINKYTOFU_ROOT_DIR@ in _build_info.py (the + # staleness check scans stinkytofu sources too, since the conversion glue is + # compiled into _rocisa). It is set via PARENT_SCOPE only in the + # add_subdirectory build; in the de-bundled find_package build it is unset, + # which would leave the path empty and make the check scan the CWD. Point it + # at the real stinkytofu source tree. + if(NOT STINKYTOFU_ROOT_DIR) + set(_stinkytofu_src_dir "${CMAKE_CURRENT_SOURCE_DIR}/../../../../shared/stinkytofu") + cmake_path(ABSOLUTE_PATH _stinkytofu_src_dir NORMALIZE OUTPUT_VARIABLE STINKYTOFU_ROOT_DIR) + endif() + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/rocisa/_build_info.py.in" + "${CMAKE_CURRENT_BINARY_DIR}/rocisa/_build_info.py" + @ONLY + ) + if(ROCISA_INCLUDE_BUILD_INFO) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/rocisa/_build_info.py" DESTINATION rocisa) + endif() endif() - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/rocisa/_build_info.py.in" - "${CMAKE_CURRENT_BINARY_DIR}/rocisa/_build_info.py" - @ONLY - ) - if(ROCISA_INCLUDE_BUILD_INFO) - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/rocisa/_build_info.py" DESTINATION rocisa) + + # --------------------------------------------------------------------- + # Sibling staging for the stinkytofu Python package. + # --------------------------------------------------------------------- + if(ROCISA_ENABLE_STINKYTOFU AND TARGET stinkytofu_python) + add_dependencies(_rocisa stinkytofu_python) + + set_target_properties(stinkytofu_python PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/stinkytofu" + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN" + ) + + set(STINKYTOFU_TOP_DIR "${STINKYTOFU_DIR}") + configure_file( + "${STINKYTOFU_DIR}/python_module/stinkytofu/__init__.py" + "${CMAKE_CURRENT_BINARY_DIR}/stinkytofu/__init__.py" + COPYONLY + ) + configure_file( + "${STINKYTOFU_DIR}/python_module/stinkytofu/_build_info.py.in" + "${CMAKE_CURRENT_BINARY_DIR}/stinkytofu/_build_info.py" + @ONLY + ) + + # ----------------------------------------------------------------- + # intrinsics.st.bc — runtime bitcode loaded by IntrinsicRegistry + # ----------------------------------------------------------------- + if(TARGET intrinsics_compiled) + add_dependencies(_rocisa intrinsics_compiled) + + set(_intrinsics_bc_src "${CMAKE_BINARY_DIR}/intrinsics.st.bc") + set(_intrinsics_bc_dst "${CMAKE_CURRENT_BINARY_DIR}/stinkytofu/intrinsics.st.bc") + add_custom_command( + OUTPUT "${_intrinsics_bc_dst}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${_intrinsics_bc_src}" "${_intrinsics_bc_dst}" + DEPENDS intrinsics_compiled "${_intrinsics_bc_src}" + COMMENT "Staging intrinsics.st.bc next to _stinkytofu.so" + VERBATIM + ) + add_custom_target(rocisa_stage_intrinsics ALL + DEPENDS "${_intrinsics_bc_dst}") + add_dependencies(_rocisa rocisa_stage_intrinsics) + endif() endif() if(ROCISA_STANDALONE) # scikit-build-core wheel install rules: place _rocisa and __init__.py into the # rocisa/ package directory in the wheel. install(TARGETS _rocisa DESTINATION rocisa) - if(_stinkytofu_from_subdirectory) + if(ROCISA_ENABLE_STINKYTOFU AND _stinkytofu_from_subdirectory) # Source-built: vendor libstinkytofu into the wheel — no installed copy exists. install(FILES $ DESTINATION rocisa) if(NOT WIN32) @@ -263,7 +334,7 @@ if(HIPBLASLT_BUNDLE_PYTHON_DEPS OR ROCISA_STANDALONE) # No INSTALL_RPATH is needed: on Linux the plugin has no DT_NEEDED on libstinkytofu (it # is headers-only) and the host is already resident when loadPlugin() dlopens it. # Only installable for shared builds (MODULE target); static builds use OBJECT. - if(TARGET stinkytofu-plugin-helloworld AND BUILD_SHARED_LIBS AND _stinkytofu_from_subdirectory) + if(ROCISA_ENABLE_STINKYTOFU AND TARGET stinkytofu-plugin-helloworld AND BUILD_SHARED_LIBS AND _stinkytofu_from_subdirectory) set_target_properties(stinkytofu-plugin-helloworld PROPERTIES EXCLUDE_FROM_ALL FALSE) add_dependencies(_rocisa stinkytofu-plugin-helloworld) install(TARGETS stinkytofu-plugin-helloworld diff --git a/projects/hipblaslt/tensilelite/rocisa/rocisa/__init__.py b/projects/hipblaslt/tensilelite/rocisa/rocisa/__init__.py index acb1e2d0e012..8ab68fe29437 100644 --- a/projects/hipblaslt/tensilelite/rocisa/rocisa/__init__.py +++ b/projects/hipblaslt/tensilelite/rocisa/rocisa/__init__.py @@ -1,6 +1,7 @@ # Copyright © Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT +import os import sys import types @@ -14,26 +15,83 @@ ) del _Path -from ._rocisa import * -from . import _rocisa - -# Register nanobind submodules under the rocisa.* namespace so that -# `from rocisa.enum import X` and `import rocisa.instruction as ri` work. -for _name, _obj in vars(_rocisa).items(): - if isinstance(_obj, types.ModuleType) and not _name.startswith("_"): - sys.modules.setdefault(f"rocisa.{_name}", _obj) - -# Staleness check: only active in source builds. -# Pre-built packages (wheels, apt) lack _build_info.py — the import is -# silently skipped. Catching ImportError (not just ModuleNotFoundError) -# because Python 3.10 raises ImportError for missing relative submodules. -# The intentional staleness ImportError is raised outside the try/except -# so it is never swallowed. -_bi = None -try: - from . import _build_info as _bi -except ImportError: - pass # Pre-built package — no source tree, skip check +# --------------------------------------------------------------------------- +# Backend dispatch +# --------------------------------------------------------------------------- +# ``ROCISA_BACKEND=stinkytofu`` redirects ``import rocisa`` to the +# ``rocisa_stinkytofu_adaptor`` shim (a rocisa-shaped facade backed by the +# stinkytofu Python binding ``_stinkytofu.so``). Anything else (or unset) +# keeps the original nanobind bindings in ``_rocisa``. + +_BACKEND = os.environ.get("ROCISA_BACKEND", "").strip().lower() + +_ADAPTER_PKG = "rocisa_stinkytofu_adaptor" + + +def _load_stinkytofu_adapter() -> bool: + """Try to install the rocisa_stinkytofu_adaptor as the ``rocisa`` module. + + Returns True iff we successfully rewired sys.modules; on any failure + we fall back to the nanobind bindings silently (the caller decides + whether that is acceptable). + """ + + # Locate ``/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor`` + # by walking up from this file until we find an ancestor whose basename + # is ``projects``; the adapter then lives at + # ``/projects/hipblaslt/tensilelite/<_ADAPTER_PKG>/``. + # + # The adapter is a sibling of ``tensilelite/rocisa/`` on purpose — it is + # a *consumer* of the stinkytofu Python binding (a tensilelite-internal + # alternative backend), not a piece of ``shared/stinkytofu/`` itself. + # Sibling-of-rocisa keeps ``ROCISA_BACKEND=stinkytofu`` purely a + # tensilelite concern. + # + # Works for both the source tree (``/projects/hipblaslt/ + # tensilelite/rocisa/rocisa/__init__.py``) and CMake-staged copies + # under ``/projects/hipblaslt/tensilelite//tensilelite/ + # rocisa/rocisa/__init__.py`` because in either case walking up from + # ``__file__`` eventually hits the ``projects`` directory. + repo_root = None + cur = os.path.dirname(os.path.abspath(__file__)) + adapter_rel = os.path.join("projects", "hipblaslt", "tensilelite", _ADAPTER_PKG) + while True: + parent = os.path.dirname(cur) + if parent == cur: # reached filesystem root + break + if os.path.basename(cur) == "projects": + candidate = parent + if os.path.isdir(os.path.join(candidate, adapter_rel)): + repo_root = candidate + break + cur = parent + if repo_root is None: + return False + adapter_parent = os.path.join(repo_root, adapter_rel) + + if not os.path.isdir(os.path.join(adapter_parent, _ADAPTER_PKG)): + return False + + if adapter_parent not in sys.path: + sys.path.insert(0, adapter_parent) + + try: + import rocisa_stinkytofu_adaptor as _adapter # noqa: F401 + except Exception: + return False + + # Install the adapter as ``rocisa`` and re-export each + # ``rocisa_stinkytofu_adaptor.*`` submodule under ``rocisa.*`` in + # ``sys.modules``. + sys.modules["rocisa"] = _adapter + _prefix = f"{_ADAPTER_PKG}." + for _name, _obj in vars(_adapter).items(): + if isinstance(_obj, types.ModuleType) and _obj.__name__.startswith(_prefix): + short = _obj.__name__[len(_prefix):] + sys.modules[f"rocisa.{short}"] = _obj + + return True + def _find_stale_sources(so_path, source_roots, build_dir): """Return source files newer than so_path, excluding files under build_dir. @@ -54,37 +112,76 @@ def _find_stale_sources(so_path, source_roots, build_dir): return stale -if _bi is not None: - from pathlib import Path +def _stinkytofu_available() -> bool: + """Return True only if the stinkytofu Python module was built and is importable.""" + import importlib.util + return importlib.util.find_spec("stinkytofu") is not None + + +if _BACKEND == "stinkytofu" and _stinkytofu_available() and _load_stinkytofu_adapter(): + # stinkytofu adapter active; wiring done inside _load_stinkytofu_adapter. + pass +else: + # Default path: original nanobind bindings. + from ._rocisa import * # noqa: F401,F403 + from . import _rocisa + + # Register nanobind submodules under the rocisa.* namespace so that + # `from rocisa.enum import X` and `import rocisa.instruction as ri` work. + for _name, _obj in vars(_rocisa).items(): + if isinstance(_obj, types.ModuleType) and not _name.startswith("_"): + sys.modules.setdefault(f"rocisa.{_name}", _obj) + + # Staleness check: only active in source builds. + # Pre-built packages (wheels, apt) lack _build_info.py — the import is + # silently skipped. Catching ImportError (not just ModuleNotFoundError) + # because Python 3.10 raises ImportError for missing relative submodules. + # The intentional staleness ImportError is raised outside the try/except + # so it is never swallowed. + _bi = None + try: + from . import _build_info as _bi + except ImportError: + pass # Pre-built package — no source tree, skip check + + if _bi is not None: + from pathlib import Path + + _so = Path(_rocisa.__file__) + # Scan rocisa sources and stinkytofu asm-IR sources (since _rocisa.so + # links libstinkytofu.so for the toStinkyTofuModule / emitAssembly path). + # Both roots are populated by CMake; an empty one signals a malformed + # _build_info.py. Warn (rather than scan Path("") == the CWD) and skip it, + # so a regression surfaces instead of silently disabling the check. + # Excluded from the stinkytofu scan: + # - tests/ — test code is never compiled into .so + # - python_module/ — only compiled into _stinkytofu.so (Python bindings) + # - src/ir/logical — logical IR is only used by _stinkytofu.so (left + # path); _rocisa.so never touches logical modules. + _roots = [] + for _name, _root in (("rocisa", _bi.SOURCE_ROOT), ("stinkytofu", _bi.STINKYTOFU_SOURCE_ROOT)): + if _root: + _roots.append(Path(_root)) + else: + import warnings - _so = Path(_rocisa.__file__) - # Scan rocisa sources and, while stinkytofu is compiled into _rocisa.so, - # stinkytofu sources too. STINKYTOFU_SOURCE_ROOT is removed once rocisa - # and stinkytofu are loaded independently. - # Both roots are populated by CMake; an empty one signals a malformed - # _build_info.py. Warn (rather than scan Path("") == the CWD) and skip it, - # so a regression surfaces instead of silently disabling the check. - _roots = [] - for _name, _root in (("rocisa", _bi.SOURCE_ROOT), ("stinkytofu", _bi.STINKYTOFU_SOURCE_ROOT)): - if _root: - _roots.append(Path(_root)) - else: - import warnings - - warnings.warn( - f"rocisa staleness check: {_name} source root is unset in " - f"_build_info.py; skipping it. Rebuild with: invoke rocisa", - stacklevel=2, + warnings.warn( + f"rocisa staleness check: {_name} source root is unset in " + f"_build_info.py; skipping it. Rebuild with: invoke rocisa", + stacklevel=2, + ) + _st_root = Path(_bi.STINKYTOFU_SOURCE_ROOT) if _bi.STINKYTOFU_SOURCE_ROOT else None + _st_skip = {_st_root / "tests", _st_root / "src" / "ir" / "logical", _st_root / "python_module"} if _st_root else set() + _all_stale = _find_stale_sources(_so, _roots, _bi.BUILD_DIR) + _stale = [s for s in _all_stale if not any(Path(s).is_relative_to(sk) for sk in _st_skip)] + if _stale: + _preview = _stale[:3] + (["..."] if len(_stale) > 3 else []) + raise ImportError( + "rocisa C++ sources are newer than the built _rocisa.so — bindings are stale.\n" + f" Modified: {', '.join(_preview)}\n" + " Rebuild: invoke rocisa" ) - _stale = _find_stale_sources(_so, _roots, _bi.BUILD_DIR) - if _stale: - _preview = _stale[:3] + (["..."] if len(_stale) > 3 else []) - raise ImportError( - "rocisa C++ sources are newer than the built _rocisa.so — bindings are stale.\n" - f" Modified: {', '.join(_preview)}\n" - " Rebuild: invoke rocisa" - ) - del _bi, _so, _stale, _roots, _name, _root, Path + del _bi, _so, _stale, _all_stale, _roots, _name, _root, _st_root, _st_skip, Path def hasStinkyTofuBackend() -> bool: diff --git a/projects/hipblaslt/tensilelite/rocisa/rocisa/src/main.cpp b/projects/hipblaslt/tensilelite/rocisa/rocisa/src/main.cpp index 13d8741aa06f..5812514a35e5 100644 --- a/projects/hipblaslt/tensilelite/rocisa/rocisa/src/main.cpp +++ b/projects/hipblaslt/tensilelite/rocisa/rocisa/src/main.cpp @@ -22,7 +22,9 @@ * ************************************************************************ */ #include +#ifdef ROCISA_HAS_STINKYTOFU #include "stinkytofu/pipeline/BackendRegistry.hpp" +#endif namespace nb = nanobind; @@ -37,11 +39,17 @@ void init_pass(nb::module_ m); void init_macro(nb::module_ m); void init_func(nb::module_ m); void init_register(nb::module_ m); +#ifdef ROCISA_HAS_STINKYTOFU void init_stinkytofu(nb::module_ m); +#endif NB_MODULE(_rocisa, m) { + nb::set_leak_warnings(false); + +#ifdef ROCISA_HAS_STINKYTOFU stinkytofu::BackendRegistry::registerAllBackends(); +#endif m.doc() = "Module rocisa."; init_base(m); init_enum(m); @@ -54,5 +62,7 @@ NB_MODULE(_rocisa, m) init_macro(m); init_func(m); init_register(m); +#ifdef ROCISA_HAS_STINKYTOFU init_stinkytofu(m); +#endif } diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/README.md b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/README.md new file mode 100644 index 000000000000..37c5e3e2f06a --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/README.md @@ -0,0 +1,133 @@ +# rocisa_stinkytofu_adaptor + +A transient Python shim that lets Tensilelite (`projects/hipblaslt/tensilelite/`) +talk to the [stinkytofu](../../../../shared/stinkytofu/) Python binding +(`_stinkytofu.so`) while pretending to be the older `rocisa` package. + +## What + +`rocisa_stinkytofu_adaptor` mirrors the public surface of +`projects/hipblaslt/tensilelite/rocisa/rocisa` (the nanobind C++ bindings) +so KernelWriter callers can keep using `from rocisa import ...` unchanged. + +The dispatcher lives at `projects/hipblaslt/tensilelite/rocisa/rocisa/__init__.py`. +Set `ROCISA_BACKEND=stinkytofu` to activate this adapter; anything else (or +unset) keeps the original native bindings. + +```bash +export ROCISA_BACKEND=stinkytofu +``` + +## Why this lives next to `tensilelite/rocisa/` (and not under `shared/stinkytofu/`) + +This package is a *consumer* of the stinkytofu Python binding, not part of +stinkytofu itself. Two consequences drive the placement: + +* **Dependency direction is one-way**: tensilelite → adapter → + (native rocisa | stinkytofu). `shared/stinkytofu/` must stay neutral + for any future consumer; it does not need to know that a rocisa-shaped + adapter exists. Putting the adapter under `shared/stinkytofu/` would + invert that direction. +* **Lifecycle is tensilelite-local**: when KernelWriter eventually calls + stinkytofu directly, this folder gets deleted with no impact on + `shared/stinkytofu/`. Sibling-of-rocisa makes it visually obvious it is + the "alternative backend" to `tensilelite/rocisa/`. + +## Scope + +* gfx1250 only today. Other gfx generations stay on the native `_rocisa.so`. +* Transient. Expected to be deleted / drastically shrunk once gfx1250 + KernelWriter is rewritten against a real stinkytofu service API. + +## Layout + +```text +projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/ +├── rocisa_stinkytofu_adaptor/ # Python package (mirrors rocisa submodules) +│ ├── __init__.py # rocIsa singleton shell + re-exports +│ ├── base.py # Item, rocIsa state sink, isaToGfx +│ ├── caps.py / enum.py # dynamic HW caps + IntEnum shims +│ ├── register.py # RegisterPool (real) +│ ├── container.py # RegisterContainer, vgpr/sgpr/… factories +│ ├── code.py # Module, Macro, Signature*, KernelBody, … +│ ├── label.py # LabelManager (real) +│ ├── instruction.py # Instruction bases, MacroInstruction, VMovB32 (partial) +│ ├── functions.py # ArgumentLoader offsets real; emit stubs +│ ├── macro.py # dummy macro builders only (Macro* lives in code.py) +│ ├── asmpass.py # rocIsaPass port (partial; some passes stub) +│ ├── stinky_interop.py # toStinkyTofuModule + signature emit wrapper +│ └── _dummy.py # make_dummy_* factories +├── tests/ +│ ├── test.sh # wrapper: PYTHONPATH + unittest discover +│ ├── test_base.py +│ ├── test_register.py +│ ├── test_container.py +│ ├── test_code.py +│ ├── test_label.py +│ ├── test_instruction.py +│ ├── test_functions.py +│ ├── test_macro.py +│ ├── test_asmpass.py +│ └── test_emission_consistency.py +└── README.md +``` + +## Tests — 1:1 shim ↔ test mapping + +| Shim module | Test file | +|-------------|-----------| +| `base.py` | `test_base.py` | +| `register.py` | `test_register.py` | +| `container.py` | `test_container.py` | +| `code.py` | `test_code.py` | +| `label.py` | `test_label.py` | +| `instruction.py` | `test_instruction.py` | +| `functions.py` | `test_functions.py` | +| `macro.py` | `test_macro.py` | +| `asmpass.py` | `test_asmpass.py` | + +`test_emission_consistency.py` is the cross-module integration suite (not tied +to a single shim file). + +## Running the tests + +Use `test.sh` only — it sets `PYTHONPATH` and discovers the binding build +so callers do not run `python3 test_*.py` or `pytest` directly. + +From the tests directory (or any cwd): + +```bash +cd projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests + +./test.sh # all test_*.py (unittest discover) +./test.sh -v # same, verbose +./test.sh test_register # one file only — basename of test_*.py, no .py suffix +./test.sh --help +``` + +The second argument to `./test.sh` must be a **test file** name (`test_register`, +`test_code`, …), not an individual test method +(`test_kernarg_address_at_index_0` will not work). + +`test.sh` handles the environment: it auto-discovers +`/*build*/tensilelite/rocisa`, sets `PYTHONPATH` to +`:`, then runs `python3 -m unittest discover`. +No manual `PYTHONPATH` or `STINKY_BUILD_DIR` is needed in the normal case. +See `./test.sh --help` for optional overrides. + +### Python version must match the build + +Extension modules are tagged by CPython ABI, e.g. +`_stinkytofu.cpython-312-x86_64-linux-gnu.so` only loads under **Python 3.12**. +If `python3` and the build disagree, integration tests are **skipped** (not +failed). After `./test.sh -v`, expect `OK (skipped=0)` when the binding matches. + +## Smoke check (stinkytofu backend wired in) + +```bash +ROCISA_BACKEND=stinkytofu PYTHONPATH=/tensilelite/rocisa: \ + python3 -c "import rocisa; print(rocisa)" +``` + +This exercises the dispatcher path: +`rocisa/__init__.py → rocisa_stinkytofu_adaptor`. diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/__init__.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/__init__.py new file mode 100644 index 000000000000..db17ddf8da90 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/__init__.py @@ -0,0 +1,446 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""rocisa-compatible facade backed by stinkytofu (gfx1250-only). + +Activated via ``ROCISA_BACKEND=stinkytofu``; re-exports all rocisa submodules +so KernelWriter can use ``from rocisa import ...`` unchanged. +Not yet done: module-level counters, ``insertDelayAlu``, ``getCycles``, +full ``toStinkyTofuModule`` options forwarding. +""" + +from __future__ import annotations + +from typing import Any, Dict, Tuple + +from . import base as _base +from . import caps as _caps +from ._dummy import make_dummy_class, make_dummy_func +from .base import IsaInfo, KernelInfo, OutputOptions + +# Make submodules importable as attributes (``rocisa.code`` etc.). The +# rocisa dispatcher in ``tensilelite/rocisa/rocisa/__init__.py`` is what +# ultimately installs them under the ``rocisa.*`` name in ``sys.modules``. +from . import asmpass as asmpass +from . import base as base +from . import code as code +from . import container as container +from . import enum as enum +from . import functions as functions +from . import instruction as instruction +from . import label as label +from . import macro as macro +from . import register as register +from .stinky_interop import toStinkyTofuModule + +_P = "rocisa" + +try: + import stinkytofu as _stinkytofu # type: ignore[import-not-found] + + StinkyAsmModule = _stinkytofu.StinkyAsmModule + CloneSpec = _stinkytofu.CloneSpec +except ImportError: + StinkyAsmModule = make_dummy_class(f"{_P}.StinkyAsmModule") + CloneSpec = make_dummy_class(f"{_P}.CloneSpec") + + +# --------------------------------------------------------------------------- +# ``rocisa.rocIsa`` forwarding shell. +# +# Every method below is a one-line forwarder to the matching accessor in +# ``base.py`` (where the actual state lives -- see ``base.py`` design +# note on state sinking). The class is kept (rather than collapsed into +# module functions) purely to preserve the ``rocIsa.getInstance().X()`` +# call shape that KernelWriter / Tensile / the C++ binding share. +# +# Backwards-compatible read/write of the historical private fields +# (``_vgpr_idx`` / ``_kernel_info``) is provided via ``@property`` +# descriptors. New code should call the public accessors directly. +# --------------------------------------------------------------------------- + + +class rocIsa: + """Singleton forwarding shell mirroring ``rocisa::rocIsa``. + + All state lives in ``base.py`` module-level globals. This class + only exists to keep the public API surface + (``rocIsa.getInstance().method(...)``) intact for Tensile / + KernelWriter callers; ``__init__`` deliberately stores nothing on + the instance. + + Implemented members (real, all forward to ``base.*``): + - ``getInstance`` + - ``init`` / ``isInit`` + - ``getIsaInfo`` (returns ``IsaInfo``) + - ``getAsmCaps`` / ``getArchCaps`` / ``getRegCaps`` / ``getAsmBugs`` + - ``setKernel`` / ``getKernel`` + - ``getOutputOptions`` / ``setOutputOptions`` + - ``getData`` / ``setData`` (used by ``ParallelMap2`` workers via + ``KernelWriter.setRocIsa(data, outOptions)``) + - ``getVgprIdx`` / ``setVgprIdx`` + - ``getVgprMsb`` / ``setVgprMsb`` (wired for Commit Y -- + Label.toString side effect; no consumer today) + + The C++ original keeps per-thread state (``m_threads`` / + ``m_outputOptions``); Tensile only ever reads it back via + parameter-less getters from the same thread that wrote it, and across + process boundaries goes through pickle, so a single per-process + value (held in ``base.py``) is sufficient here. + """ + + _instance: "rocIsa | None" = None + + def __init__(self) -> None: + # All state lives in ``base.py``; nothing to initialise here. + pass + + @staticmethod + def getInstance() -> "rocIsa": + if rocIsa._instance is None: + rocIsa._instance = rocIsa() + return rocIsa._instance + + # --- ISA init / active-ISA accessors ---------------------------------- + + def init(self, arch: Any, assemblerPath: str = "", debug: bool = False) -> None: + _base.init(arch, assemblerPath, debug) + + def isInit(self) -> bool: + return _base.isInit() + + def getIsaInfo(self, arch: Any) -> IsaInfo: + return _base.getIsaInfo(arch) + + def getAsmCaps(self): + return _base.getAsmCaps() + + def getArchCaps(self): + return _base.getArchCaps() + + def getRegCaps(self): + return _base.getRegCaps() + + def getAsmBugs(self): + return _base.getAsmBugs() + + # --- Per-thread kernel state (used by KernelWriter / Generators). ------ + + def setKernel(self, arch: Any, wavefrontSize: int) -> None: + _base.setKernel(arch, wavefrontSize) + + def getKernel(self) -> KernelInfo: + return _base.getKernel() + + # --- Output options (mutated in main, shipped to workers via pickle). -- + + def getOutputOptions(self) -> OutputOptions: + return _base.getOutputOptions() + + def setOutputOptions(self, options: OutputOptions) -> None: + _base.setOutputOptions(options) + + # --- Pickle-friendly snapshot of all initialised ISAs. ---------------- + + def getData(self) -> Dict[Tuple[int, int, int], IsaInfo]: + return _base.getData() + + def setData(self, data: Dict[Tuple[int, int, int], IsaInfo]) -> None: + _base.setData(data) + + # --- Symbol -> base-index map (consumed by ``RegName.getTotalIdx``). --- + + def getVgprIdx(self) -> Dict[str, int]: + return _base.getVgprIdx() + + def setVgprIdx(self, name: str, idx: int) -> None: + _base.setVgprIdx(name, idx) + + # --- VGPR-MSB (wired for Commit Y / Label.toString side effect). ------ + + def getVgprMsb(self) -> int: + return _base.getVgprMsb() + + def setVgprMsb(self, msb: int) -> None: + _base.setVgprMsb(msb) + + # --- Backwards-compatible private-field shims -------------------------- + # + # Test harnesses (and possibly external code) historically reached + # into ``rocIsa.getInstance()._vgpr_idx`` / ``._kernel_info`` to + # reset or restore state. After the state-sink refactor these are + # exposed as ``@property`` descriptors that delegate to ``base.*``, + # so ``._vgpr_idx.clear()`` and ``._kernel_info = info`` keep + # working without touching callers. New code should prefer the + # public accessors (``base.getVgprIdx()`` / ``base.setKernelInfo``). + + @property + def _vgpr_idx(self) -> Dict[str, int]: + return _base.getVgprIdx() + + @property + def _kernel_info(self) -> KernelInfo: + return _base.getKernel() + + @_kernel_info.setter + def _kernel_info(self, info: KernelInfo) -> None: + _base.setKernelInfo(info) + +isaToGfx = _base.isaToGfx + + +def getGlcBitName() -> str: + """Mirror ``rocisa::getGlcBitName()`` using the active ISA asm caps.""" + return _caps.glc_bit_name_from_caps(rocIsa.getInstance().getAsmCaps()) + + +def getSlcBitName() -> str: + """Mirror ``rocisa::getSlcBitName()`` using the active ISA asm caps.""" + return _caps.slc_bit_name_from_caps(rocIsa.getInstance().getAsmCaps()) + +# ========================================================================== +# Module-level counting / analysis functions +# ========================================================================== +# Mirrors ``rocisa/src/count.cpp``. The C++ uses dynamic_cast to traverse +# a Module tree and count instructions by type. Here we use isinstance() +# against tuples of concrete adaptor classes. + +def _count_recursive(item, type_tuple, weights=None): + """Recursively count instructions matching *type_tuple* in *item* tree.""" + from .code import Module as _Mod + if isinstance(item, _Mod): + total = 0 + for child in item.itemList: + total += _count_recursive(child, type_tuple, weights) + return total + if isinstance(item, type_tuple): + if weights: + w = weights.get(type(item)) + if w is not None: + return w + return 1 + return 0 + + +def _count_exact_type(item, exact_type): + """Count items whose type is exactly *exact_type* (no subclass match).""" + from .code import Module as _Mod + if isinstance(item, _Mod): + total = 0 + for child in item.itemList: + total += _count_exact_type(child, exact_type) + return total + return 1 if type(item) is exact_type else 0 + + +def _get_matching(item, type_tuple): + """Collect all items matching *type_tuple* in tree order.""" + from .code import Module as _Mod + result = [] + if isinstance(item, _Mod): + for child in item.itemList: + result.extend(_get_matching(child, type_tuple)) + elif isinstance(item, type_tuple): + result.append(item) + return result + + +def countType(item, cls): + """Generic isinstance-based count (mirrors ``rocisa::countType``).""" + from .code import Module as _Mod + if isinstance(item, _Mod): + return sum(countType(child, cls) for child in item.itemList) + return 1 if isinstance(item, cls) else 0 + + +def countInstruction(item): + """Count all Instruction nodes in tree (mirrors ``countX``).""" + from .instruction import Instruction as _Inst + from .code import Module as _Mod + if isinstance(item, _Mod): + return sum(countInstruction(child) for child in item.itemList) + return 1 if isinstance(item, _Inst) else 0 + + +def countGlobalRead(item): + """Count BufferLoad*/FlatLoad*/GlobalLoadTR* (mirrors ``countX``).""" + return _count_recursive(item, _global_read_types()) + + +def countSMemLoad(item): + """Count SLoadB* instructions (mirrors ``countX``).""" + return _count_recursive(item, _smem_load_types()) + + +def countLocalRead(item): + """Count DSLoad* instructions (mirrors ``countX``).""" + return _count_recursive(item, _local_read_types()) + + +def countLocalWrite(item): + """Count DSStore* instructions (mirrors ``countX``).""" + return _count_recursive(item, _local_write_types()) + + +def countWeightedLocalRead(item): + """Count local reads with weights: DSLoadB192 counts as 2.""" + from . import instruction as _inst + weights = {_inst.DSLoadB192: 2} + return _count_recursive(item, _local_read_types(), weights) + + +def countWeightedLocalWrite(item): + """Count local writes with weights: DSStoreB192/B256 count as 2.""" + from . import instruction as _inst + weights = {_inst.DSStoreB192: 2, _inst.DSStoreB256: 2} + return _count_recursive(item, _local_write_types(), weights) + + +def countDSStoreB128(item): + """Count exact DSStoreB128 instances.""" + from . import instruction as _inst + return _count_exact_type(item, _inst.DSStoreB128) + + +def countDSStoreB192(item): + """Count exact DSStoreB192 instances.""" + from . import instruction as _inst + return _count_exact_type(item, _inst.DSStoreB192) + + +def countDSStoreB256(item): + """Count exact DSStoreB256 instances.""" + from . import instruction as _inst + return _count_exact_type(item, _inst.DSStoreB256) + + +def countVMovB32(item): + """Count exact VMovB32 instances.""" + from . import instruction as _inst + return _count_exact_type(item, _inst.VMovB32) + + +def countMFMA(item): + """Count all MFMA-family instructions (MFMA, SMFMA, MXMFMA).""" + return _count_recursive(item, _mfma_types()) + + +def getMFMAs(item): + """Get all MFMA-family instruction objects from tree.""" + return _get_matching(item, _mfma_types()) + + +def findInstCount(module, targetItem, count=0): + """Find index of *targetItem* in tree, skipping TextBlocks. + + Returns (count, found) pair matching C++ semantics. + """ + from .code import Module as _Mod, TextBlock as _TB + if isinstance(module, _Mod): + for child in module.itemList: + if isinstance(child, _Mod): + count, found = findInstCount(child, targetItem, count) + if found: + return (count, True) + elif child is targetItem: + return (count, True) + elif not isinstance(child, _TB): + count += 1 + return (count, False) + + +# --------------------------------------------------------------------------- +# Lazy type-tuple builders (avoid circular imports at module level) +# --------------------------------------------------------------------------- + +def _global_read_types(): + from . import instruction as _inst + return ( + _inst.BufferLoadU8, _inst.BufferLoadI8, + _inst.BufferLoadD16HIU8, _inst.BufferLoadD16U8, + _inst.BufferLoadD16I8, _inst.BufferLoadD16HII8, + _inst.BufferLoadD16HIB16, _inst.BufferLoadD16B16, + _inst.BufferLoadB16, _inst.BufferLoadI16, _inst.BufferLoadU16, + _inst.BufferLoadB32, _inst.BufferLoadB64, + _inst.BufferLoadB96, _inst.BufferLoadB128, _inst.BufferLoadB192, + _inst.FlatLoadU8, _inst.FlatLoadI8, + _inst.FlatLoadD16HIU8, _inst.FlatLoadD16U8, + _inst.FlatLoadD16I8, _inst.FlatLoadD16HII8, + _inst.FlatLoadD16HIB16, _inst.FlatLoadD16B16, + _inst.FlatLoadU16, _inst.FlatLoadI16, + _inst.FlatLoadB32, _inst.FlatLoadB64, + _inst.FlatLoadB96, _inst.FlatLoadB128, _inst.FlatLoadB192, + _inst.GlobalLoadTR8B64, _inst.GlobalLoadTR16B128, + ) + + +def _smem_load_types(): + from . import instruction as _inst + return ( + _inst.SLoadB32, _inst.SLoadB64, _inst.SLoadB128, + _inst.SLoadB256, _inst.SLoadB512, + ) + + +def _local_read_types(): + from . import instruction as _inst + return ( + _inst.DSLoadU8, _inst.DSLoadI8, _inst.DSLoadD16HIU8, + _inst.DSLoadU16, _inst.DSLoadI16, _inst.DSLoadD16HIU16, + _inst.DSLoadB16, _inst.DSLoadB32, _inst.DSLoadB64, + _inst.DSLoadB96, _inst.DSLoadB96TrB6, + _inst.DSLoadB64TrB4, _inst.DSLoadB64TrB16, + _inst.DSLoadB128TrB16, _inst.DSLoadB64TrB8, + _inst.DSLoadB128, _inst.DSLoadB192, + _inst.DSLoad2B32, _inst.DSLoad2B64, + ) + + +def _local_write_types(): + from . import instruction as _inst + return ( + _inst.DSStoreB8, _inst.DSStoreB16, + _inst.DSStoreB32, _inst.DSStoreB64, + _inst.DSStoreB96, _inst.DSStoreB128, + _inst.DSStoreB192, _inst.DSStoreB256, + _inst.DSStore2B32, _inst.DSStore2B64, + ) + + +def _mfma_types(): + from . import instruction as _inst + return (_inst.MFMAInstruction, _inst.SMFMAInstruction, _inst.MXMFMAInstruction) + +# rocisa <-> stinkytofu interop +# --------------------------------------------------------------------------- +# Architecture probes live on the standalone ``stinkytofu`` binding +# (``shared/stinkytofu/python_module/src/python_bindings.cpp``), not on +# ``_rocisa.so``. Mirror native ``hasStinkyTofuBackend`` semantics by +# checking ``hasattr(stinkytofu, "isSupportedByStinkyTofu")`` instead of +# ``hasattr(_rocisa, ...)``. + + +def hasStinkyTofuBackend() -> bool: + """Return True if the standalone stinkytofu binding exposes arch probes.""" + try: + import stinkytofu + except ImportError: + return False + return hasattr(stinkytofu, "isSupportedByStinkyTofu") + + +def isSupportedByStinkyTofu(isa) -> bool: + """Return True if *isa* has a registered StinkyTofu backend pipeline.""" + import stinkytofu + + return stinkytofu.isSupportedByStinkyTofu(list(_caps.normalize_isa_key(isa))) + + +def getRegisteredArchKeys(): + """Return arch name strings for all registered StinkyTofu backends.""" + import stinkytofu + + return stinkytofu.getRegisteredArchKeys() + + +# Path-1 interop: ``toStinkyTofuModule`` / ``StinkyAsmModule`` (see +# ``stinky_interop.py`` and ``code.Module.to_stinky_asm``). diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/_dummy.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/_dummy.py new file mode 100644 index 000000000000..478e6cf1148d --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/_dummy.py @@ -0,0 +1,164 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Factories for building rocisa-shaped class/function/enum stand-ins. + +All helpers here are real implementations (IntEnum, metaclass shims). +""" + +from __future__ import annotations + +import enum as _stdenum +from typing import Any, Iterable, Mapping, Type + + +def make_dummy_class(full_name: str, *, base: Type[Any] = object) -> Type[Any]: + """Create a dummy class whose ``__init__`` prints ``full_name``. + + Calls such as ``BufferLoadB128(dst=...)`` will print + ``rocisa.instruction.BufferLoadB128`` and return a dummy instance. + Arbitrary attribute access on the instance returns another dummy + callable so that chained method calls (e.g. ``.add(...)``) never + raise AttributeError during the structural-only phase. + + ``base`` lets the dummy participate in the real class hierarchy + (rocisa C++ has every code-composition node inheriting from + ``Item``). When set, ``isinstance(dummy_instance, base)`` returns + ``True`` -- so + e.g. ``Module.findIndexByType(Item)`` would also match dummy + Label / ValueSet / Macro nodes that are still pending real + implementations. The dummy ``__init__`` calls ``super().__init__()`` + with no args, so ``base`` must have an ``__init__`` whose required + parameters are all defaultable; ``Item`` qualifies because + ``Item.__init__(self, name="")`` defaults name. + + Methods defined on ``base`` (``toString`` / ``prettyPrint`` / + ``countType`` / ``countExactType`` / capability proxies) take + precedence over the dummy ``__getattr__`` no-op, so a dummy Label + will inherit the real Item behaviour for those methods rather + than silently returning ``None``. + """ + + short = full_name.rsplit(".", 1)[-1] + + # The custom metaclass must be a subclass of ``base``'s metaclass so + # Python's "child metaclass derives from all parent metaclasses" + # rule is satisfied. ``type(base)`` covers both ``type`` (the + # default for regular classes like ``Item``) and any custom + # metaclass downstream might introduce. + _BaseMeta = type(base) + + class _DummyMeta(_BaseMeta): + def __getattr__(cls, name: str) -> Any: + def _classlevel_noop(*args: Any, **kwargs: Any) -> None: + return 1 + return _classlevel_noop + + class _DummyInstance(base, metaclass=_DummyMeta): # type: ignore[misc] + __slots__ = ("_full_name",) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + # Initialise the ``base`` portion (e.g. Item.name / parent) + # so downstream code that touches inherited slots through + # ``base``'s methods (Item.prettyPrint reads self.name) + # doesn't AttributeError on uninitialised slots. + super().__init__() + object.__setattr__(self, "_full_name", full_name) + print(full_name) + + def __getattr__(self, name: str) -> Any: + def _noop(*args: Any, **kwargs: Any) -> None: + return None + + return _noop + + def __setattr__(self, name: str, value: Any) -> None: + object.__setattr__(self, name, value) + + def __repr__(self) -> str: + return f"" + + _DummyInstance.__name__ = short + _DummyInstance.__qualname__ = short + _DummyInstance.__module__ = full_name.rsplit(".", 1)[0] + return _DummyInstance + + +def make_dummy_func(full_name: str): + """Create a dummy function that prints ``full_name`` when called.""" + + short = full_name.rsplit(".", 1)[-1] + + def _dummy(*args: Any, **kwargs: Any) -> None: + print(full_name) + return None + + _dummy.__name__ = short + _dummy.__qualname__ = short + _dummy.__module__ = full_name.rsplit(".", 1)[0] + return _dummy + + +def make_dummy_enum(full_name: str, values: Iterable[str]) -> Type[Any]: + """Create a real ``IntEnum`` mirroring ``nb::enum_`` + ``export_values``. + + Each member exposes ``.name`` and ``.value`` (matching nanobind), is an + ``int`` itself (so ``DataTypeEnum.Float == 0`` keeps working), and the + class is callable as ``DataTypeEnum(0)``. The numeric value of each + member is its 0-based index in ``values`` — this matches enums whose C++ + underlying values are sequential from zero (``RegisterType``, + ``DataTypeEnum``, ``CacheScope``, ...). + + Note (was originally a "structural-only" dummy): + Tensile's import-time machinery in ``Tensile/Common/DataType.py`` + reads ``e['enum'].value`` and ``e['enum'].name`` while building the + ``DataType`` lookup table, so a bare ``int`` placeholder is not + enough to pass ``import Tensile``. ``IntEnum`` gives us both the + attribute surface and the raw-int behaviour the rest of the code + treats it as. + + For enums with explicit non-sequential C++ values (``InstType``, + ``TemporalHint``, ...), use ``make_bound_enum`` instead. + """ + + short = full_name.rsplit(".", 1)[-1] + values = list(values) + module = full_name.rsplit(".", 1)[0] + + cls = _stdenum.IntEnum(short, [(v, i) for i, v in enumerate(values)]) + cls.__module__ = module + cls.__qualname__ = short + return cls + + +def make_bound_enum( + full_name: str, + members: Iterable[tuple[str, int]], +) -> Type[Any]: + """Create an ``IntEnum`` with explicit per-member C++ integral values. + + Mirrors ``nb::enum_::value("NAME", T::NAME)`` bindings in + ``rocisa::enum.cpp`` where the exposed ``.value`` is the C++ enumerator, + not a 0-based Python index. Supports alias members that share a value + (e.g. ``TH_WB`` and ``TH_LU`` both equal ``3``). + """ + + short = full_name.rsplit(".", 1)[-1] + module = full_name.rsplit(".", 1)[0] + member_list = list(members) + + cls = _stdenum.IntEnum(short, member_list) + cls.__module__ = module + cls.__qualname__ = short + return cls + + +def export_enum_values(target_namespace: Mapping[str, Any], enum_cls: Type[Any], + values: Iterable[str]) -> None: + """Replicate nanobind's ``.export_values()`` at Python module scope. + + Usage: + _my = make_dummy_enum("rocisa.enum.SelectBit", ["SEL_NONE", "DWORD", ...]) + export_enum_values(globals(), _my, ["SEL_NONE", "DWORD", ...]) + """ + for v in values: + target_namespace[v] = getattr(enum_cls, v) # type: ignore[index] diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/_pass_impl.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/_pass_impl.py new file mode 100644 index 000000000000..3755c2e5a70f --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/_pass_impl.py @@ -0,0 +1,949 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Python port of rocisa asm-pass pipeline (pass.cpp). + +Operates on the in-memory Module/Macro tree, not on stinkytofu LogicalModule. +""" + +from __future__ import annotations + +import copy +import re +from typing import Any, Dict, List, Tuple + +from . import code as _code +from . import instruction as _inst +from .container import RegisterContainer + +# --------------------------------------------------------------------------- +# getActFunc* — string helpers (remove.cpp:177-198) +# --------------------------------------------------------------------------- + + +def get_act_func_module_name(gwvw: int, sgpr: int, tmp_vgpr: int, tmp_sgpr: int) -> str: + return ( + f"ActFunc_VW{int(gwvw)}_Sgpr{int(sgpr)}_Tmp{int(tmp_vgpr)}_{int(tmp_sgpr)}" + ) + + +def get_act_func_branch_module_name() -> str: + return "InsertActFuncCallAddrCalc" + + +# --------------------------------------------------------------------------- +# removeDuplicatedFunction — activation de-dup (remove.cpp) +# --------------------------------------------------------------------------- + + +def remove_duplicated_function(module: _code.Module) -> None: + """Mirror ``rocisa::removeDuplicatedFunction`` (remove.cpp). + + Finds duplicate ``ActFunc_VW*`` modules, keeps only the first instance, + removes the rest, and redirects branch label references to the surviving + copy. The surviving functions are appended at the module tail followed + by an ``SEndpgm``. + """ + _remove_duplicated_activation_functions(module) + + +def _find_act_func( + module: _code.Module, +) -> Dict[str, List[_code.Module]]: + """Recursively find all modules whose name contains ``ActFunc_VW``.""" + mod_func: Dict[str, List[_code.Module]] = {} + for item in module.items(): + if isinstance(item, _code.Module): + if "ActFunc_VW" in item.name: + mod_func.setdefault(item.name, []).append(item) + else: + sub = _find_act_func(item) + for key, mlist in sub.items(): + mod_func.setdefault(key, []).extend(mlist) + return mod_func + + +def _replace_act_branch_label( + module: _code.Module, labels: List[str] +) -> None: + """Redirect ``InsertActFuncCallAddrCalc`` branch offsets to the kept label.""" + label_first = labels[0] + num_underscores = label_first.count("_") + part_first = label_first.rfind("_") + last_postfix = label_first[part_first + 1:] + label_rest = labels[1:] + + for item in module.items(): + if not isinstance(item, _code.Module): + continue + if "InsertActFuncCallAddrCalc" in item.name: + replace_label = False + for inst in item.items(): + if (isinstance(inst, _inst.CommonInstruction) + and getattr(inst, "comment", "") == "target branch offset"): + src0 = inst.srcs[0] if inst.srcs else None + if isinstance(src0, str) and src0 in label_rest: + replace_label = True + break + if replace_label: + for inst in item.items(): + if (isinstance(inst, _inst.CommonInstruction) + and getattr(inst, "comment", "") == "target branch offset"): + src0 = inst.srcs[0] if inst.srcs else None + if not isinstance(src0, str): + continue + num_us = src0.count("_") + if num_underscores == num_us: + part = src0.rfind("_") + inst.srcs[0] = src0[:part] + "_" + last_postfix + elif num_underscores == num_us - 1: + part = src0.rfind("_") + inst.srcs[0] = src0[:part] + else: + raise RuntimeError("Incorrect Activation Label") + else: + _replace_act_branch_label(item, labels) + + +def _remove_duplicated_activation_functions(module: _code.Module) -> None: + """Core logic of ``removeDuplicatedFunction`` (remove.cpp).""" + mod_func = _find_act_func(module) + module_last = _code.Module("AddToLast") + + for _key, mlist in mod_func.items(): + if len(mlist) <= 1: + continue + labels: List[str] = [] + for ml in mlist: + if isinstance(ml, _code.Module) and ml.items(): + mod2 = ml.items()[0] + if isinstance(mod2, _code.Module) and mod2.items(): + label = mod2.items()[0] + if isinstance(label, _code.Label): + labels.append(label.getLabelName()) + parent = getattr(ml, "parent", None) + if parent is not None and isinstance(parent, _code.Module): + parent.removeItem(ml) + + module_last.add(mlist[0]) + _replace_act_branch_label(module, labels) + + if module_last.items(): + module.add(module_last) + module.add(_inst.SEndpgm()) + + +# --------------------------------------------------------------------------- +# compositeToInstruction — composite.cpp +# --------------------------------------------------------------------------- + + +def composite_to_instruction(module: _code.Module) -> None: + """Mirror ``rocisa::compositeToInstruction`` (composite.cpp). + + Flattens ``CompositeInstruction`` children into plain instructions by + calling ``getInstructions()`` and splicing the results into the parent + itemList. Recurses into nested ``Module`` / ``Macro`` containers. + """ + _composite_to_instruction_impl(module) + + +def _composite_to_instruction_impl(container: Any) -> None: + """Mirrors ``compositeToInstructionTemplate`` in composite.cpp.""" + new_items: List[Any] = [] + for item in container.itemList: + geti = getattr(item, "getInstructions", None) + if callable(geti): + expanded = geti() + if isinstance(expanded, (list, tuple)): + new_items.extend(expanded) + continue + if isinstance(item, _code.Module): + _composite_to_instruction_impl(item) + elif isinstance(item, _code.Macro): + _composite_to_instruction_impl(item) + new_items.append(item) + container.itemList = new_items + + +# --------------------------------------------------------------------------- +# convertTextVariablesToRegisters — graph.cpp +# --------------------------------------------------------------------------- + + +def _set_name_to_reg_num( + gpr: RegisterContainer, assignment: Dict[str, int] +) -> None: + if gpr.regIdx != -1 or gpr.regName is None: + return + key = gpr.getRegNameWithType() + base = assignment.get(key, 0) + off = gpr.regName.getTotalOffsets() + gpr.regIdx = base + off + + +def _convert_text_variables_impl( + module: _code.Module, assignment: Dict[str, int] +) -> None: + for item in module.itemList: + if isinstance(item, _code.Module): + _convert_text_variables_impl(item, assignment) + elif isinstance(item, _inst.Instruction): + get_params = getattr(item, "getParams", None) + if not callable(get_params): + continue + try: + params = get_params() + except (NotImplementedError, RuntimeError): + continue + for p in params: + if isinstance(p, RegisterContainer): + _set_name_to_reg_num(p, assignment) + elif isinstance(item, _code.RegSet): + if item.ref is not None: + # C++ unordered_map::operator[] returns 0 for missing keys; + # match that lenient behaviour so forward-refs don't crash. + num = int(assignment.get(item.ref, 0)) + int(item.offset) + else: + assert item.value is not None + num = int(item.value) + int(item.offset) + assignment[item.name] = num + + +def convert_text_variables_to_registers(module: _code.Module) -> None: + """Mirror ``rocisa::convertTextVariablesToRegisters``.""" + _convert_text_variables_impl(module, {}) + + +# --------------------------------------------------------------------------- +# insertDelayAlu — insert_delay_alu.cpp +# --------------------------------------------------------------------------- + +# DelayALU type constants (mirrors rocisa::DelayALUType) +_DELAY_VALU = 0 +_DELAY_TRANS = 1 +_DELAY_SALU = 2 +_DELAY_OTHER = 3 + +_ALU_DEP_MAX = { + _DELAY_VALU: 4, + _DELAY_SALU: 1, + _DELAY_TRANS: 3, + _DELAY_OTHER: 0, +} + +_DELAY_TYPE_NAME = { + _DELAY_VALU: "VALU", + _DELAY_TRANS: "TRANS", + _DELAY_SALU: "SALU", +} + +_SKIP_MAX = 5 + + +def _delay_alu_type(inst: _inst.Instruction) -> int: + pre = inst.preStr() + if pre.startswith("v_s_"): + return _DELAY_TRANS + if pre.startswith("v_"): + return _DELAY_VALU + if pre.startswith("s_"): + return _DELAY_SALU + return _DELAY_OTHER + + +def _get_dst_src_regs(inst: _inst.Instruction): + """Return (set_of_dst_RegisterContainers, set_of_src_RegisterContainers). + + Mirrors native _getDstSrcRegs which only processes CommonInstruction + and CompositeInstruction. Memory ops (SMEM, Buffer, Flat, DS, Global), + MFMA, branches, etc. return empty sets. + """ + if not isinstance(inst, _inst.CommonInstruction): + return set(), set() + pre = inst.preStr() + if pre.startswith(("buffer_", "flat_", "ds_", "global_", "scratch_")): + return set(), set() + + dsts: set = set() + srcs: set = set() + try: + for p in inst.getDstParams(): + if isinstance(p, RegisterContainer): + dsts.add(p) + except (NotImplementedError, AttributeError): + pass + try: + for p in inst.getSrcParams(): + if isinstance(p, RegisterContainer): + srcs.add(p) + except (NotImplementedError, AttributeError): + pass + return dsts, srcs + + +def _format_dep_str(alu_type: int, cnt: int) -> str: + if cnt == 0: + return "NO_DEP" + name = _DELAY_TYPE_NAME.get(alu_type) + if name is None: + return "" + if alu_type == _DELAY_SALU: + return f"SALU_CYCLE_{cnt}" + return f"{name}_DEP_{cnt}" + + +def _make_delay_alu_inst(instid0type: int, instid0cnt: int) -> _inst.Instruction: + """Create an SDelayAlu instruction with the proper formatted immediate.""" + dep_str = _format_dep_str(instid0type, instid0cnt) + from .instruction import SDelayAlu as _SDelayAlu # noqa: WPS433 + + # The SDelayAlu class stores a raw integer. We need the instruction to + # render as "s_delay_alu instid0(VALU_DEP_N)" in toString(). + # Override: build a minimal Instruction that renders correctly. + inst = _SDelayAluFormatted(instid0type, instid0cnt) + return inst + + +class _SDelayAluFormatted(_inst.Instruction): + """SDelayAlu with human-readable encoding matching native rocisa output.""" + + __slots__ = ("instid0type", "instid0cnt", "instskipCnt", + "instid1type", "instid1cnt") + + def __init__(self, instid0type: int, instid0cnt: int, comment: str = ""): + super().__init__(_inst.InstType.INST_NOTYPE, comment) + self.instid0type = instid0type + self.instid0cnt = instid0cnt + self.instskipCnt = None + self.instid1type = None + self.instid1cnt = None + self.setInst("s_delay_alu") + + def hasInstID1(self) -> bool: + return (self.instskipCnt is not None or self.instid1type is not None + or self.instid1cnt is not None) + + def setInstID1(self, skip_cnt: int, instid1type: int, instid1cnt: int) -> bool: + if self.hasInstID1(): + return False + self.instskipCnt = skip_cnt + self.instid1type = instid1type + self.instid1cnt = instid1cnt + return True + + def getParams(self): + return [] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [] + + def toString(self) -> str: + result = " instid0(" + _format_dep_str(self.instid0type, self.instid0cnt) + ")" + if not self.hasInstID1(): + return self.formatWithComment(self.instStr + result) + _SKIP_NAMES = {0: "SAME", 1: "NEXT", 2: "SKIP_1", 3: "SKIP_2", + 4: "SKIP_3", 5: "SKIP_4"} + result += " | instskip(" + _SKIP_NAMES.get(self.instskipCnt, "") + ")" + result += " | instid1(" + _format_dep_str(self.instid1type, self.instid1cnt) + ")" + return self.formatWithComment(self.instStr + result) + + def to_stinky_logical(self): + import stinkytofu as _st # noqa: WPS433 + + # stinkytofu's Python SDelayAlu binding triggers an assertion failure + # (SDelayAluData not initialized) during emission. Work around by + # emitting an SNop(0) placeholder whose comment carries the original + # s_delay_alu text; post-processing restores it. + alu_text = "instid0(" + _format_dep_str(self.instid0type, self.instid0cnt) + ")" + if self.hasInstID1(): + _SKIP_NAMES = {0: "SAME", 1: "NEXT", 2: "SKIP_1", 3: "SKIP_2", + 4: "SKIP_3", 5: "SKIP_4"} + alu_text += " | instskip(" + _SKIP_NAMES.get(self.instskipCnt, "") + ")" + alu_text += " | instid1(" + _format_dep_str(self.instid1type, self.instid1cnt) + ")" + return _st.SNop(_st.Register(0), "DELAY_ALU:" + alu_text) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = _SDelayAluFormatted(self.instid0type, self.instid0cnt, self.comment) + if self.hasInstID1(): + dup.setInstID1(self.instskipCnt, self.instid1type, self.instid1cnt) + memo[id(self)] = dup + return dup + + +def _is_valu_writes_sgpr(inst: _inst.Instruction) -> bool: + """True if inst is a VALU that writes an SGPR (e.g. v_cmp_*, v_readfirstlane).""" + pre = inst.preStr() + if not pre.startswith("v_"): + return False + try: + for p in inst.getDstParams(): + if isinstance(p, RegisterContainer) and p.regType == "s": + return True + except (NotImplementedError, AttributeError): + pass + return False + + +def _insert_delay_alu_recursive(module: _code.Module) -> None: + """Walk Module tree, inserting s_delay_alu where register deps are close.""" + for item in module.items(): + if isinstance(item, _code.Module): + _insert_delay_alu_recursive(item) + + items = module.items() + if len(items) < 2: + return + + last_dst_inst_idx: Dict[Any, int] = {} + inst_idx_delay_info: Dict[int, Tuple[int, int, int]] = {} # idx -> (type, type_count, total) + delay_type_counts: Dict[int, int] = {_DELAY_VALU: 0, _DELAY_TRANS: 0, + _DELAY_SALU: 0, _DELAY_OTHER: 0} + dep_idxs: Dict[int, _SDelayAluFormatted] = {} + total_count = 0 + + for i, item in enumerate(items): + if not isinstance(item, _inst.Instruction): + continue + + alu_type = _delay_alu_type(item) + delay_type_counts[alu_type] = delay_type_counts.get(alu_type, 0) + 1 + total_count += 1 + inst_idx_delay_info[i] = (alu_type, delay_type_counts[alu_type], total_count) + + dsts, srcs = _get_dst_src_regs(item) + + # Find most-recently-written source register. + # Native C++ uses std::max_element with a comparator that returns an + # untracked register as the "maximum", then breaks when find()==end(). + # Net effect: if ANY src is not in last_dst_inst_idx, skip entirely. + # Native does NOT subtract dst from srcs — self-read instructions + # (e.g. s_lshr_b32 sX, sX, imm) still track the dep on sX. + best_src = None + best_idx = -1 + for src in srcs: + idx = last_dst_inst_idx.get(src) + if idx is None: + best_src = None + break + if idx > best_idx: + best_idx = idx + best_src = src + + if best_src is not None: + last_idx = best_idx + dep_alu_type, dep_type_count, _ = inst_idx_delay_info[last_idx] + inst_cnt = delay_type_counts[dep_alu_type] - dep_type_count + max_dep = _ALU_DEP_MAX.get(dep_alu_type, 0) + if inst_cnt <= max_dep: + dep_idxs[i] = _SDelayAluFormatted(dep_alu_type, inst_cnt) + + for dst in dsts: + last_dst_inst_idx[dst] = i + + # Insert in reverse order so indices stay valid + for idx in sorted(dep_idxs.keys(), reverse=True): + module.add(dep_idxs[idx], idx) + + +def insert_delay_alu(module: _code.Module) -> None: + """Mirror ``rocisa::insertDelayAlu`` (insert_delay_alu.cpp). + + Inserts ``_SDelayAluFormatted`` instructions into the Module tree. + These are later converted to SNop placeholders during stinkytofu lowering + (see ``_SDelayAluFormatted.to_stinky_logical``), then restored to real + ``s_delay_alu`` text by ``_postprocess_delay_alu_placeholder`` in code.py. + """ + _insert_delay_alu_recursive(module) + + +# --------------------------------------------------------------------------- +# getCycles — cycle.cpp (gfx1250 unsupported branch returns 0) +# --------------------------------------------------------------------------- + + +def get_cycles(module: _code.Module, num_waves: int) -> int: + """Mirror ``rocisa::getCycles`` well enough for KernelWriter metadata. + + Native ``cycle.cpp`` returns ``0`` for ISAs outside the origami + formocast table (includes gfx1250 today). We keep the same contract + without pulling the full C++ analyzer into Python. + """ + del module, num_waves + return 0 + + +# --------------------------------------------------------------------------- +# macroToInstruction — macro_inline.cpp +# --------------------------------------------------------------------------- + +_MACRO_ARG_RE = re.compile(r"([^=:]+)(?::req)?=?(\w+)?") + + +def _extract_macro( + table: Dict[str, Tuple[_code.Macro, List[Tuple[str, str]]]], macro: _code.Macro +) -> None: + params: List[Tuple[str, str]] = [] + for arg in macro.macro.args: + arg_str = arg if isinstance(arg, str) else str(arg) + m = _MACRO_ARG_RE.match(arg_str) + if not m: + params.append((arg_str, "")) + continue + name = m.group(1) + default = m.group(2) or "" + params.append((name, default)) + table[macro.name] = (macro, params) + + +def _collect_macros( + module: _code.Module, table: Dict[str, Tuple[_code.Macro, List[Tuple[str, str]]]] +) -> None: + for it in module.itemList: + if isinstance(it, _code.Module): + _collect_macros(it, table) + elif isinstance(it, _code.Macro): + if it.name not in table: + _extract_macro(table, it) + + +def _replace_whole_word(hay: str, needle: str, repl: str) -> str: + out: List[str] = [] + pos = 0 + nlen = len(needle) + while pos < len(hay): + idx = hay.find(needle, pos) + if idx == -1: + out.append(hay[pos:]) + break + end = idx + nlen + before_ok = idx == 0 or not (hay[idx - 1].isalnum() or hay[idx - 1] == "_") + after_ok = end >= len(hay) or not (hay[end].isalnum() or hay[end] == "_") + if before_ok and after_ok: + out.append(hay[pos:idx]) + out.append(repl) + pos = end + else: + out.append(hay[pos:end]) + pos = end + return "".join(out) + + +def _substitute_string_param(s: str, params: List[Tuple[str, str]]) -> str: + """Replace ``\\param`` tokens with the current actual argument strings.""" + result = s + for name, value in params: + needle = "\\" + name + result = _replace_whole_word(result, needle, value) + return result + + +def _eval_macro_condition(value: str, params: List[Tuple[str, str]]) -> bool: + """Port of ``evalMacroCondition`` in ``macro_inline.cpp`` (``.if`` / ``.elseif``).""" + token_re = re.compile(r"\\([^=\s]+)|\w+|==|!=|&&") + lhs = op = rhs = val = "" + token_idx = 0 + results: List[int] = [] + for m in token_re.finditer(value): + val = m.group(0) + if val.startswith("\\"): + var = m.group(1) + pval = "" + for pname, pv in params: + if pname == var: + pval = pv + break + assert any(pn == var for pn, _ in params), ( + "unknown macro argument in condition: " + repr(var) + ) + val = pval + mod4 = token_idx % 4 + if mod4 == 0: + lhs = val + elif mod4 == 1: + op = val + elif mod4 == 2: + rhs = val + if op == "==": + results.append(1 if lhs == rhs else 0) + elif op == "!=": + results.append(1 if lhs != rhs else 0) + else: + raise AssertionError( + "unknown macro condition operator: " + repr(op) + ) + elif mod4 == 3: + assert val == "&&", "unknown macro logical operator: " + repr(val) + results.append(2) + token_idx += 1 + if not results: + return True + result = bool(results[0]) + i = 1 + while i < len(results): + if results[i] == 2 and i + 1 < len(results): + result = result and bool(results[i + 1]) + i += 2 + else: + i += 1 + return result + + +def _substitute_register_param(reg: RegisterContainer, params: List[Tuple[str, str]]) -> None: + if not reg.isMacro or reg.regName is None: + return + name_str = reg.regName.name + for param_name, param_value in params: + stripped = ( + param_name[4:] + if len(param_name) > 4 + and param_name[:4] in ("vgpr", "sgpr", "mgpr") + else param_name + ) + name_str = _replace_whole_word(name_str, stripped, param_value) + reg.isMacro = False + full_prefix = reg.regType + "gpr" + if len(name_str) > len(full_prefix) and name_str.startswith(full_prefix): + name_str = name_str[len(full_prefix) :] + elif len(name_str) > len(reg.regType) and name_str.startswith(reg.regType): + name_str = name_str[len(reg.regType) :] + plus = name_str.find("+") + try: + if plus != -1: + reg.regIdx = int(name_str[:plus]) + int(name_str[plus + 1 :]) + reg.regName = None + else: + reg.regIdx = int(name_str) + reg.regName = None + except ValueError: + from .container import RegName # local import + + reg.regName = RegName(name_str) + reg.regIdx = -1 + + +def _substitute_input(arg: Any, params: List[Tuple[str, str]]) -> Any: + if isinstance(arg, str): + return _substitute_string_param(arg, params) + if isinstance(arg, RegisterContainer): + cloned = copy.deepcopy(arg, {}) + _substitute_register_param(cloned, params) + return cloned + return arg + + +def _substitute_common_inst(inst: _inst.CommonInstruction, params: List[Tuple[str, str]]) -> None: + if inst.dst is not None and isinstance(inst.dst, RegisterContainer): + _substitute_register_param(inst.dst, params) + if inst.dst1 is not None and isinstance(inst.dst1, RegisterContainer): + _substitute_register_param(inst.dst1, params) + inst.srcs = [_substitute_input(s, params) for s in inst.srcs] + inst.comment = _substitute_string_param(inst.comment, params) + + +def _clone_instruction(inst: _inst.Instruction) -> _inst.Instruction: + if isinstance(inst, _inst.MacroInstruction): + return inst.clone() + if isinstance(inst, _inst.CommonInstruction): + return copy.deepcopy(inst, {}) + raise TypeError( + f"macroToInstruction: cannot clone instruction type {type(inst).__name__!r}" + ) + + +def _clone_and_substitute( + inst: _inst.Instruction, params: List[Tuple[str, str]] +) -> _inst.Instruction: + cloned = _clone_instruction(inst) + if isinstance(cloned, _inst.CommonInstruction): + _substitute_common_inst(cloned, params) + return cloned + + +def _expand_macro_body( + output: List[Any], + macro_items: List[Any], + params: List[Tuple[str, str]], + branch: List[bool], +) -> None: + for item in macro_items: + if isinstance(item, _code.Module): + _expand_macro_body(output, item.itemList, params, branch) + elif isinstance(item, _code.ValueIf): + branch.append(branch[-1] and _eval_macro_condition(item.value, params)) + elif isinstance(item, _code.ValueElseIf): + if_taken = branch.pop() + branch.append( + (not if_taken) and _eval_macro_condition(item.value, params) + ) + elif isinstance(item, _code.ValueEndif): + branch.pop() + elif isinstance(item, _inst.Instruction): + if branch[-1]: + output.append(_clone_and_substitute(item, params)) + else: + raise AssertionError( + "macroToInstruction: unexpected item type in macro body: " + + type(item).__name__ + ) + + +def _macro_to_instruction_impl( + container: Any, table: Dict[str, Tuple[_code.Macro, List[Tuple[str, str]]]] +) -> None: + items = container.itemList + new_items: List[Any] = [] + for item in items: + if isinstance(item, _code.Module): + _macro_to_instruction_impl(item, table) + new_items.append(item) + elif isinstance(item, _code.Macro): + continue + elif isinstance(item, _inst.MacroInstruction): + ent = table.get(item.name) + if ent is None: + raise AssertionError( + "macroToInstruction: MacroInstruction references undefined macro " + + repr(item.name) + ) + macro_def, defaults = ent + param_list: List[Tuple[str, str]] = [(n, v) for n, v in defaults] + for i, arg in enumerate(item.args): + if i >= len(param_list): + break + name, _ = param_list[i] + param_list[i] = (name, _inst._input_to_str(arg)) + branch = [True] + _expand_macro_body(new_items, macro_def.itemList, param_list, branch) + else: + new_items.append(item) + container.itemList = new_items + + +def macro_to_instruction(module: _code.Module) -> None: + """Mirror ``rocisa::macroToInstruction``.""" + table: Dict[str, Tuple[_code.Macro, List[Tuple[str, str]]]] = {} + _collect_macros(module, table) + if not table: + return + _macro_to_instruction_impl(module, table) + + +# --------------------------------------------------------------------------- +# Graph optimisation — graph.cpp (buildGraph + removeDuplicateAssignment) +# --------------------------------------------------------------------------- + + +class _NoOptItem: + """Wrapper mirroring native ``NoOptItem`` — marks items inside NoOpt modules.""" + __slots__ = ("item",) + + def __init__(self, item: Any) -> None: + self.item = item + + +class _Graph: + """Per-register item lists mirroring native ``Graph`` struct.""" + __slots__ = ("vgpr", "sgpr", "mgpr", "max_vgpr_seen") + + def __init__(self, max_vgpr: int, max_sgpr: int) -> None: + self.vgpr: List[List[Any]] = [[] for _ in range(max_vgpr)] + self.sgpr: List[List[Any]] = [[] for _ in range(max_sgpr)] + self.mgpr: List[List[Any]] = [[]] + self.max_vgpr_seen: int = -1 + + def get_gpr_ref(self, reg_type: str) -> List[List[Any]]: + if reg_type == "v": + return self.vgpr + elif reg_type == "s": + return self.sgpr + elif reg_type == "m": + return self.mgpr + raise RuntimeError(f"Invalid gpr type: {reg_type!r}") + + +_BARRIER_TYPES = ( + _inst.BranchInstruction, + _code.Label, + _inst._SWaitCnt, + _inst._SWaitCntVscnt, + _inst.SEndpgm, + _inst.SBarrier, + _inst.SNop, +) + +# SSleep may not exist in adaptor; check at import time. +_SSleep = getattr(_inst, "SSleep", None) + + +def _is_barrier_item(item: Any) -> bool: + if isinstance(item, _BARRIER_TYPES): + return True + if _SSleep is not None and isinstance(item, _SSleep): + return True + return False + + +def _add_reg_to_graph(item: Any, params: List[Any], graph: _Graph, no_opt: bool) -> None: + """Add *item* to graph lists for every register it touches.""" + for p in params: + if not isinstance(p, RegisterContainer): + continue + if p.regType == "acc": + continue + gpr_vec = graph.get_gpr_ref(p.regType) + if p.regType == "v": + last_idx = p.regIdx + p.regNum - 1 + if last_idx > graph.max_vgpr_seen: + graph.max_vgpr_seen = last_idx + for i in range(p.regIdx, p.regIdx + p.regNum): + if i >= len(gpr_vec): + continue + if gpr_vec[i] and gpr_vec[i][-1] is item: + continue + if no_opt: + gpr_vec[i].append(_NoOptItem(item)) + else: + gpr_vec[i].append(item) + + +def _record_graph(module: _code.Module, graph: _Graph) -> None: + """Recursively populate graph from module tree (mirrors native ``_recordGraph``).""" + for item in module.items(): + if isinstance(item, _code.Module): + _record_graph(item, graph) + elif isinstance(item, (_inst.CommonInstruction, _inst.MacroInstruction)): + get_params = getattr(item, "getParams", None) + if callable(get_params): + try: + params = get_params() + except (NotImplementedError, RuntimeError): + continue + _add_reg_to_graph(item, params, graph, module.isNoOpt()) + elif isinstance(item, _inst.Instruction) and not isinstance(item, _inst.BranchInstruction): + # ReadWriteInstruction equivalent (SMemLoad, MUBUF, GLOBAL, FLAT, MFMA, etc.) + get_params = getattr(item, "getParams", None) + if callable(get_params): + try: + params = get_params() + except (NotImplementedError, RuntimeError): + pass + else: + _add_reg_to_graph(item, params, graph, module.isNoOpt()) + continue + if _is_barrier_item(item): + for v in graph.vgpr: + v.append(item) + for s in graph.sgpr: + s.append(item) + elif _is_barrier_item(item): + for v in graph.vgpr: + v.append(item) + for s in graph.sgpr: + s.append(item) + + +def _build_graph(module: _code.Module, max_vgpr: int, max_sgpr: int) -> _Graph: + """Mirror native ``buildGraph``.""" + graph = _Graph(max_vgpr, max_sgpr) + _record_graph(module, graph) + return graph + + +def _remove_duplicate_assignment_gpr(graph: _Graph, reg_type: str) -> None: + """Mirror native ``_removeDuplicateAssignmentGPR``. + + For each register index, track the last assigned value. If a subsequent + ``SMovB32`` writes the same value to the same register, remove it. + """ + from .container import EXEC as _EXEC # noqa: WPS433 + + gpr_ref = graph.get_gpr_ref(reg_type) + for idx in range(len(gpr_ref)): + s_list = gpr_ref[idx] + assign_value: Any = None # None means 'unknown/invalidated' + has_assign = False + new_list: List[Any] = [] + removed_any = False + + for item in s_list: + is_removed = False + + if isinstance(item, _NoOptItem): + assign_value = None + has_assign = False + elif isinstance(item, _inst.MacroInstruction): + assign_value = None + has_assign = False + elif _is_barrier_item(item): + assign_value = None + has_assign = False + elif isinstance(item, _inst.SMovB32): + dst = item.dst + if not isinstance(dst, _EXEC): + if isinstance(dst, RegisterContainer) and dst.regIdx == idx: + gpr_value = item.srcs[0] if item.srcs else None + if has_assign and gpr_value == assign_value: + # Duplicate assignment — remove or replace with comment + parent = getattr(item, "parent", None) + if parent is not None and isinstance(parent, _code.Module): + if item.comment: + comment = item.comment + " (dup assign opt.)" + new_item = _code.TextBlock( + " " * 50 + " // " + comment + "\n" + ) + parent.replaceItem(item, new_item) + else: + parent.removeItem(item) + is_removed = True + assign_value = gpr_value + has_assign = True + else: + # SMovB32 to a different register — just update if it writes this idx + pass + else: + # Writing to EXEC — doesn't affect sgpr tracking + pass + elif isinstance(item, _inst.Instruction): + # Other instruction writing to this register invalidates tracking + get_params = getattr(item, "getParams", None) + if callable(get_params): + try: + params = get_params() + except (NotImplementedError, RuntimeError): + params = [] + if params: + p0 = params[0] + if isinstance(p0, RegisterContainer) and p0.regType == reg_type: + for i in range(p0.regIdx, p0.regIdx + p0.regNum): + if i == idx: + assign_value = None + has_assign = False + break + + if not is_removed: + new_list.append(item) + else: + removed_any = True + + if removed_any: + gpr_ref[idx] = new_list + + +def build_graph_and_remove_dup_assign( + module: _code.Module, max_vgpr: int, max_sgpr: int +) -> int: + """Mirror native ``buildGraph`` + ``removeDuplicateAssignment`` (graph.cpp). + + Builds a per-register reference graph, then eliminates redundant + ``s_mov_b32`` instructions that re-assign the same value to the same SGPR. + + Returns the highest VGPR index seen (-1 if none). + """ + graph = _build_graph(module, max_vgpr, max_sgpr) + _remove_duplicate_assignment_gpr(graph, "s") + return graph.max_vgpr_seen diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/asmpass.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/asmpass.py new file mode 100644 index 000000000000..4375c0ee1f01 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/asmpass.py @@ -0,0 +1,96 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Python port of rocisa asm optimization passes. + +Implemented: macroToInstruction, compositeToInstruction, convertTextVariablesToRegisters. +Not yet done: removeDuplicatedFunction, buildGraph, insertDelayAlu, getCycles (returns 0). +""" + +from __future__ import annotations + +from typing import Any + +from . import code as _code +from ._pass_impl import ( + build_graph_and_remove_dup_assign, + composite_to_instruction, + convert_text_variables_to_registers, + get_act_func_branch_module_name, + get_act_func_module_name, + get_cycles, + insert_delay_alu, + macro_to_instruction, + remove_duplicated_function, +) + + +class rocIsaPassOption: + """Mirror ``rocisa::rocIsaPassOption`` (``pass.hpp``).""" + + __slots__ = ( + "insertDelayAlu", + "removeDupFunc", + "removeDupAssign", + "getCycles", + "numWaves", + ) + + def __init__(self) -> None: + self.insertDelayAlu: bool = False + self.removeDupFunc: bool = True + self.removeDupAssign: bool = True + self.getCycles: bool = True + self.numWaves: int = 0 + + def doOpt(self) -> bool: + return self.removeDupAssign + + +class rocIsaPassResult: + """Mirror ``rocisa::rocIsaPassResult`` (``pass.hpp``).""" + + __slots__ = ("cycles", "maxVgpr") + + def __init__(self) -> None: + self.cycles: int = -1 + self.maxVgpr: int = -1 + + +def getActFuncModuleName(gwvw: int, sgpr: int, tmpVgpr: int, tmpSgpr: int) -> str: + return get_act_func_module_name(gwvw, sgpr, tmpVgpr, tmpSgpr) + + +def getActFuncBranchModuleName() -> str: + return get_act_func_branch_module_name() + + +def rocIsaPass(kernel: Any, option: rocIsaPassOption) -> rocIsaPassResult: + """Mirror ``rocisa::rocIsaPass`` (``pass.cpp``).""" + body = kernel.body + if body is None: + raise RuntimeError("Kernel body is empty") + + result = rocIsaPassResult() + + if option.removeDupFunc: + remove_duplicated_function(body) + + macro_to_instruction(body) + composite_to_instruction(body) + convert_text_variables_to_registers(body) + + if option.doOpt(): + max_vgpr_seen = build_graph_and_remove_dup_assign( + body, int(kernel.totalVgprs), int(kernel.totalSgprs) + ) + result.maxVgpr = (max_vgpr_seen + 1) if max_vgpr_seen >= 0 else int(kernel.totalVgprs) + else: + result.maxVgpr = int(kernel.totalVgprs) + + if option.insertDelayAlu: + insert_delay_alu(body) + + if option.getCycles: + result.cycles = get_cycles(body, int(option.numWaves)) + + return result diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/base.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/base.py new file mode 100644 index 000000000000..1d1fdf438eb2 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/base.py @@ -0,0 +1,621 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Process-wide rocIsa singleton state and code-composition root types. + +Provides KernelInfo, IsaInfo, OutputOptions, Item, and all rocIsa accessors. +Not yet done: IsaVersion is a marker only. +""" + +from __future__ import annotations + +from typing import Any, Dict, Optional, Tuple + +from . import caps as _caps +from ._dummy import make_dummy_class + +_P = "rocisa.base" + + +IsaVersion = make_dummy_class(f"{_P}.IsaVersion") + +# ``Item`` and ``DummyItem`` are real classes; they live at the bottom of +# this file because their default ``toString`` / ``prettyPrint`` / cap- +# proxy methods reach into the module-level accessors defined further +# down. See the "Item base class" section near the end. + + +# Re-export the public ISA key alias from caps for nicer typing downstream. +IsaKey = Tuple[int, int, int] + + +class OutputOptions: + """Mirror of ``rocisa::OutputOptions`` (base.hpp:59-62) — mutable, picklable. + + The C++ struct only carries one bool today. We keep the Python shape + identical so ``rocIsa.getInstance().getOutputOptions().outputNoComment = + True`` and the subsequent ``setOutputOptions(opts)`` round-trip across + multiprocessing pickles unchanged. + """ + + __slots__ = ("outputNoComment",) + + def __init__(self, outputNoComment: bool = False) -> None: + self.outputNoComment = bool(outputNoComment) + + def __repr__(self) -> str: + return f"OutputOptions(outputNoComment={self.outputNoComment})" + + # Pickle support — needed because Tensile passes this object across the + # ParallelMap2 fork/spawn boundary. + def __getstate__(self) -> tuple: + return (self.outputNoComment,) + + def __setstate__(self, state: tuple) -> None: + (self.outputNoComment,) = state + + +class IsaInfo: + """Mirror of ``rocisa::IsaInfo`` (base.hpp:64-70) — asm/arch/reg/bug dicts. + + Moved here from ``__init__.py`` to honour the rocisa file layout + (the C++ struct is declared in ``base.hpp`` alongside ``KernelInfo`` + / ``OutputOptions``). ``from rocisa import IsaInfo`` continues to + work because ``__init__.py`` re-exports the symbol. + + Pickle support so workers spawned by ``ParallelMap2`` can rehydrate + the data dict via ``setData(getData())``. + """ + + __slots__ = ("asmCaps", "archCaps", "regCaps", "asmBugs") + + def __init__(self, asmCaps, archCaps, regCaps, asmBugs): + self.asmCaps = asmCaps + self.archCaps = archCaps + self.regCaps = regCaps + self.asmBugs = asmBugs + + def __repr__(self) -> str: + return ( + f"IsaInfo(asmCaps={self.asmCaps}, archCaps={self.archCaps}, " + f"regCaps={self.regCaps}, asmBugs={self.asmBugs})" + ) + + def __getstate__(self) -> tuple: + return (self.asmCaps, self.archCaps, self.regCaps, self.asmBugs) + + def __setstate__(self, state: tuple) -> None: + self.asmCaps, self.archCaps, self.regCaps, self.asmBugs = state + + +class KernelInfo: + """Mirror of ``rocisa::KernelInfo`` (base.hpp:47-57) — per-thread current + kernel state. + + Only the attributes Tensile actually reads back are typed: ``isa`` + (a 3-tuple) and ``wavefrontSize``. + """ + + __slots__ = ("isa", "wavefrontSize") + + def __init__(self, isa=None, wavefrontSize: int = 0) -> None: + self.isa = isa + self.wavefrontSize = int(wavefrontSize) + + def __repr__(self) -> str: + return f"KernelInfo(isa={self.isa}, wavefrontSize={self.wavefrontSize})" + + def __getstate__(self) -> tuple: + return (self.isa, self.wavefrontSize) + + def __setstate__(self, state: tuple) -> None: + self.isa, self.wavefrontSize = state + + +# --------------------------------------------------------------------------- +# Process-wide state mirror of ``rocisa::rocIsa`` (base.hpp:72-218). +# --------------------------------------------------------------------------- +# +# Module globals ARE the per-process singleton in Python — no Singleton +# class needed. The ``rocIsa`` class in ``__init__.py`` is a forwarding +# shell that preserves the rocisa API surface (KernelWriter / Tensile +# callers can keep doing ``rocIsa.getInstance().getXxx()``); the actual +# data lives down here. +# +# Each global below has a corresponding pair of accessor functions +# whose names track the C++ ``rocisa::rocIsa::*`` methods 1:1. Mutating +# the globals directly from outside this module is supported but +# discouraged — prefer the accessor functions so future refactors +# (e.g. switching to ``threading.local()`` if KernelWriter ever calls +# from worker threads) need only touch this file. +# +# Layout mirrors the order of declarations in rocisa C++ ``base.hpp`` +# (lines 210-217) for easy side-by-side comparison. + +_current_output_options: "OutputOptions" = OutputOptions() +"""Live ``OutputOptions`` for the codegen comment-suppression flag. +Mirror of ``rocisa::rocIsa::m_outputOptions`` (base.hpp:215).""" + +_current_isa: Optional[IsaKey] = None +"""Currently-selected ISA, or ``None`` before ``init()`` / ``setKernel()``. +No direct C++ analogue (C++ derives the current ISA from the per-thread +``m_threads[id].isaVersion``); we keep a separate flag because Tensile +calls ``init()`` before any ``setKernel()`` to populate caps.""" + +_is_init: bool = False +"""``True`` once ``init()`` has populated at least one ISA's caps. +Mirror of ``rocisa::rocIsa::isInit()`` (base.hpp:103-106), which the +C++ derives from ``m_isainfo.size() > 0``.""" + +_assembler_path: str = "" +"""Path to the external assembler (``hipcc`` / ``llvm-mc``). Recorded by +``init()`` for parity with C++; the logical adaptor never shells out.""" + +_kernel_info: "KernelInfo" = KernelInfo() +"""Live ``KernelInfo`` for the currently-selected kernel. Mirror of +``rocisa::rocIsa::m_threads[id]`` (base.hpp:213) collapsed to a single +process-wide slot — see thread-semantics note in the module docstring.""" + +_data: Dict[IsaKey, "IsaInfo"] = {} +"""ISA-keyed snapshot of capability dicts. Mirror of +``rocisa::rocIsa::m_isainfo`` (base.hpp:214). ``ParallelMap2`` workers +pickle / unpickle this via ``getData()`` / ``setData()``.""" + +_vgpr_idx: Dict[str, int] = {} +"""Symbol -> base-VGPR-index map shared by ``RegName`` instances. +Mirror of ``rocisa::rocIsa::m_vgpridx[id]`` (base.hpp:216).""" + +_vgpr_msb: int = 0 +"""Current value of ``s_set_vgpr_msb`` (gfx1250 register-bank prefix). +Mirror of ``rocisa::rocIsa::m_vgprmsb[id]`` (base.hpp:217). Default 0 +matches the C++ ``int()`` default-construct of a missing map entry. +``setVgprMsb`` is wired in for Commit Y (``Label.toString`` side +effect, code.hpp:122-125) — no consumer reads it today.""" + + +# --------------------------------------------------------------------------- +# OutputOptions accessors (unchanged from the prior commit). +# --------------------------------------------------------------------------- + +def getOutputOptions() -> OutputOptions: + """Return the process-wide ``OutputOptions`` instance. + + The returned object IS the source of truth: mutating + ``getOutputOptions().outputNoComment = True`` immediately affects + every subsequent ``outputNoComment()`` call and every TextBlock + rendered after this point. That mirrors rocisa C++ behaviour where + ``rocIsa::getOutputOptions()`` returns a mutable reference. + """ + return _current_output_options + + +def setOutputOptions(options: OutputOptions) -> None: + """Replace the process-wide ``OutputOptions`` instance. + + Used by ``rocIsa.getInstance().setOutputOptions(...)`` -- which is + what Tensile / ParallelMap2 workers call to ship a pickled + ``OutputOptions`` from the parent into the worker process. + """ + global _current_output_options + _current_output_options = options + + +def outputNoComment() -> bool: + """Return whether the codegen should suppress all comments / TextBlocks. + + Used by ``code.TextBlock.toString`` (gates the entire text payload, + mirroring rocisa ``code.hpp:154-159``) and ``instruction._fmt_str`` + (drops the per-instruction ``// comment`` tail). One-line direct + read of the module-level singleton: no lazy import, no try/except, + no defensive ``getattr`` -- the state object is guaranteed to exist + because we eagerly construct it at module load. + """ + return _current_output_options.outputNoComment + + +# --------------------------------------------------------------------------- +# ISA init & active-ISA accessors. +# --------------------------------------------------------------------------- +# +# These four collectively mirror ``rocisa::rocIsa::init`` / +# ``isInit`` / ``getIsaInfo`` and the four ``get{Asm,Arch,Reg}Caps`` / +# ``getAsmBugs`` proxies (base.hpp:88-155). + +def init(arch: Any, assemblerPath: str = "", debug: bool = False) -> None: + """Mirror of ``rocisa::rocIsa::init`` (base.hpp:88-101). + + Selects an ISA, lazily populates its capability snapshot from + ``caps.getCaps``, records the assembler path, and marks the + singleton as initialised. Idempotent for the same ISA — the C++ + version short-circuits on ``m_isainfo.find(isaVersion) != + m_isainfo.end()``, we mirror that with ``if key not in _data``. + + ``debug`` is accepted for signature parity but ignored. Capability + probing is delegated to ``stinkytofu.getHardwareCaps`` via + ``caps.getCaps`` (comgr inside stinkytofu, not ``llvm-mc`` here). + """ + global _current_isa, _assembler_path, _is_init + key = _caps.normalize_isa_key(arch) + _current_isa = key + _assembler_path = assemblerPath + if key not in _data: + asm, arch_c, reg, bugs = _caps.getCaps(key) + _data[key] = IsaInfo(asm, arch_c, reg, bugs) + _is_init = True + # ``debug`` is accepted for API parity only. + del debug + + +def isInit() -> bool: + """Mirror of ``rocisa::rocIsa::isInit`` (base.hpp:103-106). + + C++ derives this from ``m_isainfo.size() > 0``; we use the explicit + ``_is_init`` flag so ``init()`` and ``setData()`` can both flip it + deterministically. + """ + return _is_init + + +def getIsaInfo(arch: Any) -> "IsaInfo": + """Mirror of ``rocisa::rocIsa::getIsaInfo`` (base.hpp:125-135). + + Lazy-populates the cache so callers can ask for any supported ISA + without a prior ``init()`` call (Tensile's + ``Tensile.Common.Capabilities.makeIsaInfoMap`` relies on this). + """ + key = _caps.normalize_isa_key(arch) + info = _data.get(key) + if info is None: + asm, arch_c, reg, bugs = _caps.getCaps(key) + info = IsaInfo(asm, arch_c, reg, bugs) + _data[key] = info + return info + + +def _activeCaps() -> Tuple[Dict, Dict, Dict, Dict]: + """Internal: return ``(asmCaps, archCaps, regCaps, asmBugs)`` for the + currently-selected ISA. Used by the four public ``get*Caps`` helpers. + + Raises ``RuntimeError`` if no ISA has been selected yet so a missing + ``init()`` / ``setKernel()`` produces a loud error rather than a + confusing ``KeyError`` deep inside instruction emission. + """ + if _current_isa is None: + raise RuntimeError( + "rocisa.base: init(arch, ...) or setKernel(arch, ...) must " + "be called before getAsmCaps()/getArchCaps()/getRegCaps()/" + "getAsmBugs()." + ) + info = _data.get(_current_isa) + if info is None: + asm, arch_c, reg, bugs = _caps.getCaps(_current_isa) + info = IsaInfo(asm, arch_c, reg, bugs) + _data[_current_isa] = info + return (info.asmCaps, info.archCaps, info.regCaps, info.asmBugs) + + +def getAsmCaps() -> Dict[str, int]: + """Mirror of ``rocisa::rocIsa::getAsmCaps`` (base.hpp:137-140).""" + return _activeCaps()[0] + + +def getArchCaps() -> Dict[str, int]: + """Mirror of ``rocisa::rocIsa::getArchCaps`` (base.hpp:147-150).""" + return _activeCaps()[1] + + +def getRegCaps() -> Dict[str, int]: + """Mirror of ``rocisa::rocIsa::getRegCaps`` (base.hpp:142-145).""" + return _activeCaps()[2] + + +def getAsmBugs() -> Dict[str, bool]: + """Mirror of ``rocisa::rocIsa::getAsmBugs`` (base.hpp:152-155).""" + return _activeCaps()[3] + + +# --------------------------------------------------------------------------- +# KernelInfo accessors. +# --------------------------------------------------------------------------- + +def setKernel(arch: Any, wavefrontSize: int) -> None: + """Mirror of ``rocisa::rocIsa::setKernel`` (base.hpp:108-118). + + Pins the active ISA AND records the wavefront size in the live + ``KernelInfo``. NOTE: the C++ version ALSO resets ``m_vgpridx[id]`` + and ``m_vgprmsb[id]`` on every call (base.hpp:115-116). The Python + adaptor historically did NOT mirror that reset; preserving that + omission here to keep this commit a pure state-sink (no behaviour + change). Re-evaluate when wiring Label.toString in Commit Y. + """ + global _current_isa, _kernel_info + key = _caps.normalize_isa_key(arch) + _current_isa = key + _kernel_info = KernelInfo(isa=key, wavefrontSize=wavefrontSize) + + +def setKernelInfo(info: KernelInfo) -> None: + """Replace the live ``KernelInfo`` directly. No C++ equivalent. + + Used by test harnesses that need to restore a previously captured + ``KernelInfo`` -- in particular the initial ``KernelInfo()`` state + where ``info.isa is None``, which ``setKernel`` cannot represent + (it requires a concrete ISA tuple). + """ + global _kernel_info + _kernel_info = info + + +def getKernel() -> KernelInfo: + """Mirror of ``rocisa::rocIsa::getKernel`` (base.hpp:120-123).""" + return _kernel_info + + +def isaToGfx(arch: Any) -> str: + """Mirror of ``rocisa::isaToGfx`` / ``getGfxNameTuple`` (helper.hpp). + + Accepts a 3-tuple/list ``(major, minor, stepping)`` or any object + with ``major / minor / stepping`` attributes (rocisa ``IsaVersion``). + """ + if hasattr(arch, "major"): + major = int(arch.major) + minor = int(arch.minor) + stepping = int(arch.stepping) + else: + major = int(arch[0]) + minor = int(arch[1]) + stepping = int(arch[2]) + hex_digit = "0123456789abcdef"[stepping & 0xF] + return f"gfx{major}{minor}{hex_digit}" + + +# --------------------------------------------------------------------------- +# Data-dict accessors (pickled across ParallelMap2 worker boundary). +# --------------------------------------------------------------------------- + +def getData() -> Dict[IsaKey, "IsaInfo"]: + """Mirror of ``rocisa::rocIsa::getData`` (base.hpp:157-160). + + Returns the live dict (NOT a copy). Tensile pickles the result and + ships it to ``ParallelMap2`` workers, which call ``setData`` to + rehydrate. Mutating the returned dict mutates the process-wide + state -- usually what you want. + """ + return _data + + +def setData(data: Dict[IsaKey, "IsaInfo"]) -> None: + """Mirror of ``rocisa::rocIsa::setData`` (base.hpp:172-175). + + Replaces the cap snapshot table wholesale. Also flips ``_is_init`` + True/False based on emptiness so ``isInit()`` matches the C++ + behaviour (``m_isainfo.size() > 0``). + """ + global _data, _is_init + _data = dict(data) + _is_init = bool(_data) + + +# --------------------------------------------------------------------------- +# VGPR-index map accessors. +# --------------------------------------------------------------------------- + +def getVgprIdx() -> Dict[str, int]: + """Mirror of ``rocisa::rocIsa::getVgprIdx`` (base.hpp:162-165). + + Returns the live dict so callers can ``getVgprIdx()[name]`` for + a single lookup AND ``getVgprIdx().clear()`` for a process-wide + reset (used by test harnesses). The C++ version returns by value + so ``.clear()`` would not propagate there; Python dict semantics + make the mutation visible, which matches what callers actually + want. + """ + return _vgpr_idx + + +def setVgprIdx(name: str, idx: int) -> None: + """Mirror of ``rocisa::rocIsa::setVgprIdx`` (base.hpp:191-198).""" + _vgpr_idx[name] = idx + + +# --------------------------------------------------------------------------- +# VGPR-MSB accessors. +# --------------------------------------------------------------------------- + +def getVgprMsb() -> int: + """Mirror of ``rocisa::rocIsa::getVgprMsb`` (base.hpp:167-170). + + Default ``0`` matches the C++ ``int()`` default-construct of an + absent ``m_vgprmsb[id]`` entry. + """ + return _vgpr_msb + + +def setVgprMsb(msb: int) -> None: + """Mirror of ``rocisa::rocIsa::setVgprMsb`` (base.hpp:200-207). + + Used by ``Label::toString`` (code.hpp:122-125) as a side effect + when the active ISA has ``HasVgprMSB``. No Python consumer reads + it today; the accessor is wired in here so Commit Y (Label real + implementation) can pick it up without further base.py changes. + """ + global _vgpr_msb + _vgpr_msb = int(msb) + + +# --------------------------------------------------------------------------- +# Item base class -- polymorphic root of the code composition tree. +# --------------------------------------------------------------------------- +# +# Mirrors ``rocisa::Item`` (base.hpp:220-297). Lives at the bottom of +# this file so its capability-proxy methods can refer to the module- +# level accessors above by simple name (Python's LEGB lookup resolves +# ``getAsmCaps`` etc. to the module globals, not to ``self.getAsmCaps`` +# which would recurse -- method names are not in the method's local +# scope, only in the class dict). +# +# Design choices vs. C++: +# * ``Item`` is a regular (concrete) class, NOT an ``abc.ABC``. +# ``Item("foo")`` is valid (matches C++ where ``rocisa::Item it("foo")`` +# compiles and ``.toString()`` returns "foo"); only ``clone()`` raises +# by default. +# * ``__slots__ = ("name", "parent")`` -- subclasses (Module / TextBlock / +# ...) must NOT redeclare these. They declare only their own new +# slots; ``name`` / ``parent`` are inherited. +# * Capability proxies (``getAsmCaps`` / ``getArchCaps`` / ``getRegCaps`` / +# ``getAsmBugs`` / ``getVgprIdx`` / ``getVgprMsb`` / ``kernel``) all +# forward to the module-level accessors -- they reach module-level +# ``base.py`` state, NOT the ``rocIsa`` singleton class. That keeps +# the dependency direction one-way and means an Item subclass can +# read caps even if the package facade hasn't been imported yet. +# * ``countType`` accepts a Python ``type`` (not a nanobind ``nb::object``) +# and uses ``isinstance``; ``countExactType`` checks ``type(self) is +# target`` to match the C++ ``typeid(*this) == targetType`` semantics. + +class Item: + """Polymorphic root of the code composition tree; mirror of + ``rocisa::Item`` (base.hpp:220-297). + + Concrete (not abstract): ``Item("foo").toString()`` returns ``"foo"`` + and ``Item().prettyPrint()`` returns ``" "``; only + ``clone()`` raises by default, matching the C++ ``throw + std::runtime_error("clone() not implemented")``. + + Subclassing: + Subclasses declare only their own additional ``__slots__`` -- + ``name`` and ``parent`` come from this class. ``__init__`` + must call ``super().__init__(name)`` (or + ``super().__init__()`` for the default empty name) so the + inherited slots get populated. + + Capability proxies (``getAsmCaps`` etc.) forward to the module- + level accessors in this file; an Item subclass need never know + that the actual state lives in ``_data`` / ``_kernel_info`` / + ``_vgpr_idx`` globals. + + Item-inherited proxies NOT exposed on ``Module`` instances + (matches Phase-4 audit -- KernelWriter only reads these off + Instruction subclasses) are still present on the base class + itself, so explicit ``isinstance(x, Item)`` callers get the + full surface. + """ + + __slots__ = ("name", "parent") + + def __init__(self, name: str = "") -> None: + self.name: str = name + self.parent: "Item | None" = None + + # ---- Polymorphic operations with default impls ----------------------- + + def toString(self) -> str: + """Default: return ``name`` (mirror of ``Item::toString``, + base.hpp:282-285).""" + return self.name + + def __str__(self) -> str: + # Bound to toString() so subclass overrides automatically + # propagate to ``str(item)``, matching nanobind's + # ``.def("__str__", &Item::toString)`` pattern. + return self.toString() + + def prettyPrint(self, indent: str = "") -> str: + """Default: ``indent + className + " " + toString()`` + (mirror of ``Item::prettyPrint``, base.hpp:287-293). + + Note the trailing space + ``toString()`` with NO newline -- + callers (e.g. ``Module::prettyPrint``) concatenate child + ``prettyPrint`` results verbatim and the children that need + a newline (``Module``, ``Instruction``) emit it themselves + in their override. + """ + return f"{indent}{type(self).__name__} {self.toString()}" + + def countType(self, type_obj: type) -> int: + """Default: ``1`` if ``self`` is an instance of ``type_obj``, + else ``0`` (mirror of ``Item::countType``, base.hpp:271-275). + + C++ accepts an ``nb::object`` (i.e. a Python class object) and + uses ``nb::isinstance``; the Python adaptor accepts a regular + ``type`` directly and uses the builtin ``isinstance``. + Subclasses with children (Module, Macro, StructuredModule) + override this to recurse. + """ + return int(isinstance(self, type_obj)) + + def countExactType(self, type_obj: type) -> int: + """Default: ``1`` if ``type(self) is type_obj``, else ``0`` + (mirror of ``Item::countExactType``, base.hpp:277-280). + + The exact-type check uses ``is`` rather than ``isinstance`` + to match C++ ``typeid(*this) == targetType`` semantics -- a + ``StructuredModule`` does NOT count as a ``Module`` here even + though it inherits from it. Containers (Module, Macro, + StructuredModule) override this to recurse. + """ + return int(type(self) is type_obj) + + def clone(self) -> "Item": + """Mirror of ``Item::clone`` (base.hpp:230-234) -- raises by + default, subclasses override (the C++ original throws + ``std::runtime_error("clone() not implemented")``). Python + subclasses typically provide ``__deepcopy__`` instead; this + method is here for rocisa API parity.""" + raise NotImplementedError("clone() not implemented") + + # ---- Capability proxies (forward to module-level accessors) ---------- + # + # Mirror of the seven ``def_prop_ro`` / member-function proxies on + # ``rocisa::Item`` (base.hpp:236-269 + base.cpp:202-212). All of + # them defer to the active-ISA state in ``base.py`` module globals. + # + # NB: writing ``return getAsmCaps()`` resolves to the module-level + # ``getAsmCaps`` defined above (Python LEGB: local -> enclosing -> + # module-globals -> builtins; class-method names are NOT in scope + # inside the method body). No self-recursion. + + def getAsmCaps(self) -> Dict[str, int]: + """Mirror of ``Item::getAsmCaps`` (base.hpp:236-239).""" + return getAsmCaps() + + def getArchCaps(self) -> Dict[str, int]: + """Mirror of ``Item::getArchCaps`` (base.hpp:246-249).""" + return getArchCaps() + + def getRegCaps(self) -> Dict[str, int]: + """Mirror of ``Item::getRegCaps`` (base.hpp:241-244).""" + return getRegCaps() + + def getAsmBugs(self) -> Dict[str, bool]: + """Mirror of ``Item::getAsmBugs`` (base.hpp:251-254).""" + return getAsmBugs() + + def getVgprIdx(self) -> Dict[str, int]: + """Mirror of ``Item::getVgprIdx`` (base.hpp:256-259).""" + return getVgprIdx() + + def getVgprMsb(self) -> int: + """Mirror of ``Item::getVgprMsb`` (base.hpp:261-264).""" + return getVgprMsb() + + def kernel(self) -> "KernelInfo": + """Mirror of ``Item::kernel`` (base.hpp:266-269).""" + return getKernel() + + +class DummyItem(Item): + """Mirror of ``rocisa::DummyItem`` (base.hpp:299-310). + + A no-op Item subclass that exists purely so KernelWriter can stick + a marker into a Module tree without triggering ``countType`` / + instance accounting. The C++ override returns 0 regardless of the + queried type; we mirror that. + """ + + __slots__ = () + + def __init__(self) -> None: + super().__init__(name="") + + def countType(self, type_obj: type) -> int: + # rocisa C++ override (base.hpp:306-309) hardcodes 0. + return 0 diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/caps.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/caps.py new file mode 100644 index 000000000000..b91922b5ba18 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/caps.py @@ -0,0 +1,112 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""ISA capability bridge via stinkytofu.getHardwareCaps (dynamic comgr probing). + +Returns asmCaps/archCaps/regCaps/asmBugs dicts; no static snapshot table. +""" + +from __future__ import annotations + +from typing import Any, Dict, Tuple + +IsaKey = Tuple[int, int, int] + + +# Friendly-name aliases (``"gfx1250"`` etc.). Keep in lock-step with +# ``Tensile/Common/Architectures.isaToGfx`` when adding ISAs. +_GFX_ALIASES: Dict[str, IsaKey] = { + "gfx1250": (12, 5, 0), +} + + +def normalize_isa_key(arch: Any) -> IsaKey: + """Coerce assorted ISA spellings into a ``(major, minor, patch)`` tuple. + + Accepts: + - ``IsaVersion`` / ``SemanticVersion`` / any 3-element NamedTuple + - ``tuple`` / ``list`` of 3 ints + - ``"gfx1250"``-style strings (looked up in ``_GFX_ALIASES``) + + Raises ``TypeError`` for anything else so a wrong call site is loud + instead of silently producing the wrong caps. + """ + + if isinstance(arch, str): + try: + return _GFX_ALIASES[arch] + except KeyError: + raise KeyError( + f"caps.normalize_isa_key: unknown gfx alias {arch!r}; " + f"known: {sorted(_GFX_ALIASES)}" + ) from None + + if isinstance(arch, (tuple, list)) and len(arch) == 3: + return (int(arch[0]), int(arch[1]), int(arch[2])) + + # Last-ditch attempt for objects that quack like an IsaVersion + # (e.g. ``rocisa.base.IsaVersion`` once it has a real impl). + for triple in ("major", "minor", "patch"), ("Major", "Minor", "Step"): + if all(hasattr(arch, name) for name in triple): + return tuple(int(getattr(arch, name)) for name in triple) # type: ignore[return-value] + + raise TypeError( + f"caps.normalize_isa_key: cannot interpret {arch!r} (type " + f"{type(arch).__name__}) as an IsaVersion-like value" + ) + + +def getCaps(key: IsaKey) -> Tuple[Dict, Dict, Dict, Dict]: + """Return ``(asmCaps, archCaps, regCaps, asmBugs)`` for ``key``. + + Delegates to ``stinkytofu.getHardwareCaps`` (result cached inside C++). + Probing uses comgr against the target ISA name (e.g. + ``amdgcn-amd-amdhsa--gfx1250``); the host GPU identity is irrelevant. + + Returns *fresh shallow copies* so callers (and Tensile's pickle of + ``rocIsa.getData()``) cannot mutate shared tables in place. + """ + + import stinkytofu # noqa: WPS433 (runtime required dep; ImportError propagates) + + raw = stinkytofu.getHardwareCaps(list(key)) + asm_caps = raw.get("asmCaps") or {} + if not asm_caps: + raise KeyError( + f"caps.getCaps: stinkytofu has no hardware caps for ISA {key}. " + f"Registered backends: {stinkytofu.getRegisteredArchKeys()}" + ) + + arch_caps = raw.get("archCaps") or {} + reg_caps = raw.get("regCaps") or {} + asm_bugs = raw.get("asmBugs") or {} + + return ( + {str(k): int(v) for k, v in asm_caps.items()}, + {str(k): int(v) for k, v in arch_caps.items()}, + {str(k): int(v) for k, v in reg_caps.items()}, + {str(k): bool(v) for k, v in asm_bugs.items()}, + ) + + +def supportedIsas() -> Tuple[IsaKey, ...]: + """Return ISA keys that have a gfx alias mapping in this adaptor.""" + + return tuple(_GFX_ALIASES.values()) + + +def glc_bit_name_from_caps(asm_caps: Dict[str, int]) -> str: + """Mirror ``rocisa::getGlcBitName()`` (``base.cpp``).""" + if asm_caps.get("HasGLCModifier"): + return "glc" + if asm_caps.get("HasSC0Modifier"): + return "sc0" + return "" + + +def slc_bit_name_from_caps(asm_caps: Dict[str, int]) -> str: + """Mirror ``rocisa::getSlcBitName()`` (``base.cpp``).""" + if asm_caps.get("HasGLCModifier"): + return "slc" + if asm_caps.get("HasSC0Modifier"): + return "sc1" + return "" diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/code.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/code.py new file mode 100644 index 000000000000..f9166e9dac95 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/code.py @@ -0,0 +1,2375 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Code-composition IR nodes (Module, KernelBody, Label, TextBlock, etc.). + +Module.to_stinky_asm() is the left-path entry point: walks the item tree, +builds a LogicalModule, and routes through lower_logical_module for emission. +""" + +from __future__ import annotations + +import copy as _copy +import re as _re +from typing import Any, Iterable, List, Optional, Sequence + +from ._dummy import make_dummy_class +from .base import ( + Item, + isaToGfx as _isa_to_gfx, + outputNoComment as _outputNoComment, + setVgprIdx as _set_vgpr_idx, + setVgprMsb as _set_vgpr_msb, +) +from .enum import SignatureValueKind as _SVK +from .instruction import ( + Instruction as _Instruction, + MacroInstruction as _MacroInstruction, +) + +_P = "rocisa.code" + + +# --------------------------------------------------------------------------- +# Wait-instruction post-processing +# --------------------------------------------------------------------------- +# stinkytofu's Python LogicalModule path does not support SWaitCntData +# modifiers, so the gfx12+ legalization pass cannot split s_waitcnt into +# individual s_wait_loadcnt / s_wait_dscnt etc. As a workaround, we encode +# the target wait type in the comment of SWaitCnt logical instructions via +# NUL-prefixed markers and post-process the emitted assembly text here. + +from .instruction import ( + _WAIT_MARKER_LOADCNT, + _WAIT_MARKER_STORECNT, + _WAIT_MARKER_DSCNT, + _WAIT_MARKER_KMCNT, +) + +_WAIT_MARKER_MAP = { + _WAIT_MARKER_LOADCNT: "s_wait_loadcnt", + _WAIT_MARKER_STORECNT: "s_wait_storecnt", + _WAIT_MARKER_DSCNT: "s_wait_dscnt", + _WAIT_MARKER_KMCNT: "s_wait_kmcnt", +} + +_WAIT_MARKER_RE = _re.compile( + r"^(\s*)s_waitcnt\s+(\d+)\s*//\s*\x00@W([LSDK])@(.*)$" +) + +_MARKER_CODE_TO_OPCODE = { + "L": "s_wait_loadcnt", + "S": "s_wait_storecnt", + "D": "s_wait_dscnt", + "K": "s_wait_kmcnt", +} + + +def _postprocess_wait_markers(asm: str) -> str: + """Replace marker-annotated ``s_waitcnt N`` lines with gfx12+ waits.""" + lines = asm.split("\n") + out: List[str] = [] + for line in lines: + m = _WAIT_MARKER_RE.match(line) + if m: + indent = m.group(1) + count = m.group(2) + code = m.group(3) + comment = m.group(4) or "" + opcode = _MARKER_CODE_TO_OPCODE[code] + new_line = f"{indent}{opcode} {count}" + if comment: + pad = max(1, 51 - len(new_line)) + new_line += " " * pad + "// " + comment + out.append(new_line) + else: + out.append(line) + return "\n".join(out) + + +_VCMPX_RE = _re.compile( + r"^(\s*)(v_cmpx_\w+)\s+(.+?)(\s*(//.*?))?$" +) + +_SBARRIER_RE = _re.compile( + r"^(\s*)s_barrier\s*(//.+)?$" +) + +_CARRY_RE = _re.compile( + r"^(\s*)(v_(?:add|sub)_co(?:_ci)?_u32)\s+(.+?)(\s*(//.*?))?$" +) + + +def _postprocess_vcmpx(asm: str) -> str: + """Expand ``v_cmpx_*`` into ``v_cmp_* + s_mov_b32 exec_lo`` for gfx1250.""" + lines = asm.split("\n") + out: List[str] = [] + for line in lines: + m = _VCMPX_RE.match(line) + if not m: + out.append(line) + continue + indent = m.group(1) + mnemonic = m.group(2) + operands_str = m.group(3) + comment = m.group(5) or "" + + cmp_mnemonic = mnemonic.replace("_cmpx_", "_cmp_", 1) + operands = [op.strip() for op in operands_str.split(",")] + + if len(operands) >= 3: + dst = operands[0] + srcs = ", ".join(operands[1:]) + if dst in ("exec_lo", "exec_hi", "exec"): + dst = "vcc_lo" + else: + dst = "vcc_lo" + srcs = ", ".join(operands) + + cmp_line = f"{indent}{cmp_mnemonic} {dst}, {srcs}" + mov_line = f"{indent}s_mov_b32 exec_lo, {dst}" + if comment: + cmp_pad = max(1, 51 - len(cmp_line)) + cmp_line += " " * cmp_pad + comment + mov_pad = max(1, 51 - len(mov_line)) + mov_line += " " * mov_pad + comment + out.append(cmp_line) + out.append(mov_line) + return "\n".join(out) + + +def _postprocess_sbarrier(asm: str) -> str: + """Expand ``s_barrier`` into ``s_barrier_signal -1 + s_barrier_wait -1`` for gfx1250.""" + lines = asm.split("\n") + out: List[str] = [] + for line in lines: + m = _SBARRIER_RE.match(line) + if not m: + out.append(line) + continue + indent = m.group(1) + comment_part = m.group(2) or "" + sig_line = f"{indent}s_barrier_signal -1" + wait_line = f"{indent}s_barrier_wait -1" + if comment_part: + comment_text = comment_part + pad = max(1, 51 - len(wait_line)) + wait_line += " " * pad + comment_text + out.append(sig_line) + out.append(wait_line) + return "\n".join(out) + + +def _postprocess_carry(asm: str) -> str: + """Insert ``vcc_lo`` carry operand into ``v_add_co_u32``/``v_sub_co_u32``/``v_add_co_ci_u32``.""" + lines = asm.split("\n") + out: List[str] = [] + for line in lines: + m = _CARRY_RE.match(line) + if not m: + out.append(line) + continue + indent = m.group(1) + mnemonic = m.group(2) + operands_str = m.group(3) + comment = m.group(5) or "" + + operands = [op.strip() for op in operands_str.split(",")] + + is_ci = "_ci_" in mnemonic + if is_ci and len(operands) == 3: + # v_add_co_ci_u32 dst, src0, src1 → dst, vcc_lo, src0, src1, vcc_lo + new_ops = f"{operands[0]}, vcc_lo, {operands[1]}, {operands[2]}, vcc_lo" + elif not is_ci and len(operands) == 3: + # v_add_co_u32 / v_sub_co_u32 dst, src0, src1 → dst, vcc_lo, src0, src1 + new_ops = f"{operands[0]}, vcc_lo, {operands[1]}, {operands[2]}" + else: + out.append(line) + continue + + new_line = f"{indent}{mnemonic} {new_ops}" + if comment: + pad = max(1, 51 - len(new_line)) + new_line += " " * pad + comment + out.append(new_line) + return "\n".join(out) + + +# --------------------------------------------------------------------------- +# Post-process: restore s_delay_alu from SNop placeholders +# --------------------------------------------------------------------------- + +_DELAY_ALU_PLACEHOLDER_RE = _re.compile( + r"^(\s*)s_nop 0\s+//\s*DELAY_ALU:(.+?)(?:\s*)?$" +) + + +def _postprocess_delay_alu_placeholder(asm: str) -> str: + """Restore s_delay_alu instructions from SNop(0) placeholders. + + During stinkytofu lowering, SDelayAlu instructions are emitted as + ``s_nop 0 // DELAY_ALU:instid0(...)`` because the Python binding cannot + properly lower SDelayAlu (SDelayAluData assertion). This pass restores + the original ``s_delay_alu`` text from the placeholder comments. + """ + lines = asm.split("\n") + out: List[str] = [] + for line in lines: + m = _DELAY_ALU_PLACEHOLDER_RE.match(line) + if m: + indent = m.group(1) + alu_operands = m.group(2) + out.append(f"{indent}s_delay_alu {alu_operands}") + else: + out.append(line) + return "\n".join(out) + + +class _PostProcessModule: + """Wraps a StinkyAsmModule and post-processes emitAssembly() output. + + Applies transforms: + 1. Wait-marker expansion (s_waitcnt → s_wait_loadcnt/etc) + 2. VCmpX expansion (v_cmpx_* → v_cmp_* + s_mov_b32 exec_lo) + 3. s_delay_alu placeholder restoration (SNop → s_delay_alu) + """ + + __slots__ = ("_inner",) + + def __init__(self, inner: Any, insert_delay_alu: bool = False, + set_directives: str = "") -> None: + self._inner = inner + + def runOptimizationPipeline(self) -> None: + self._inner.runOptimizationPipeline() + + def emitAssembly(self) -> str: + asm = self._inner.emitAssembly() + asm = _postprocess_wait_markers(asm) + asm = _postprocess_vcmpx(asm) + asm = _postprocess_sbarrier(asm) + asm = _postprocess_carry(asm) + asm = asm.replace("+-", "-") + return asm + + def getSetDirectives(self) -> str: + """Legacy accessor — .set directives are now emitted inline.""" + return "" + + def getName(self) -> str: + return self._inner.getName() + + def setOutputName(self, name: str) -> None: + self._inner.setOutputName(name) + + def getOutputName(self) -> str: + return self._inner.getOutputName() + + def setOutputDir(self, directory: str) -> None: + self._inner.setOutputDir(directory) + + def getOutputDir(self) -> str: + return self._inner.getOutputDir() + + def getMetaDataU64(self, *args: Any, **kwargs: Any) -> Any: + return self._inner.getMetaDataU64(*args, **kwargs) + + +# --------------------------------------------------------------------------- +# TextBlock -- raw text leaf node. +# --------------------------------------------------------------------------- +# +# Mirrors ``rocisa::TextBlock`` (code.hpp:133-160 + code.cpp:140-155). The +# binding boils down to: +# - ctor ``TextBlock(text) : Item(text), text(text)`` -- name == text +# - ``toString()`` returns ``text`` unless ``outputNoComment`` is set +# - ``__getstate__`` / ``__setstate__`` round-trip ``(name, text)`` +# - ``prettyPrint`` inherits ``Item::prettyPrint`` -- ``indent + className +# + " " + toString()`` with NO trailing newline (we now actually +# inherit it via the Python ``Item`` base class rather than re- +# declaring it -- matches the C++ inheritance shape one-for-one) +# We mirror every one of these points so an adapter swap is byte-identical +# at both the asm-emit layer (``Module.toString``) and the debug-dump layer +# (``Module.prettyPrint``). +class TextBlock(Item): + """Raw text leaf; mirror of ``rocisa::TextBlock`` (code.hpp:133-160). + + Created internally by ``Module.addSpaceLine`` / ``Module.addComment*`` + and also constructed directly by KernelWriter for inline asm / macro + snippets. Has no logicalIR counterpart -- ``Module.to_stinky_asm`` + silently skips TextBlock items. + + Production-build comment suppression: ``rocIsa.getOutputOptions(). + outputNoComment = True`` causes ``toString()`` to return ``""`` for + every TextBlock (rocisa code.hpp:154-159 -- the flag is a blanket + suppressor, not just for ``// foo`` comments; inline-asm TextBlocks + are also stripped). We read the flag through ``base.outputNoComment()`` + so the adapter stays decoupled from the rocisa C++ singleton. + + Inheritance: subclass of ``Item`` -- ``name`` / ``parent`` come from + the Item base (its ``__slots__`` provides them; redeclaring them + here would TypeError). ``__str__`` / ``prettyPrint`` are inherited + too (Item's defaults match TextBlock's required format byte-for- + byte: ``"{indent}TextBlock {toString()}"`` via + ``type(self).__name__``). + """ + + __slots__ = ("text",) + + def __init__(self, text: str = ""): + # rocisa C++: ``TextBlock(text) : Item(text), text(text)`` -- the + # base ``Item(name)`` ctor stores ``name = text``. Match exactly so + # ``Module.findNamedItem`` / ``removeItemsByName`` see identical + # behaviour on both backends. (Default text="" still gives name="", + # so the common addComment / addSpaceLine path is unchanged.) + super().__init__(name=text) + self.text: str = text + + def toString(self) -> str: + # rocisa code.hpp:154-159 -- the ``outputNoComment`` flag blanket- + # suppresses TextBlock output regardless of whether the text is a + # comment or an inline-asm fragment. Production builds rely on + # this to strip every human-readable annotation in one pass. + if _outputNoComment(): + return "" + return self.text + + def __deepcopy__(self, memo): + # rocisa binding lambda (code.cpp:143-148) copies via the public + # ctor and then patches ``name`` separately. Since our ctor already + # sets ``name = text``, replicate the same patch path so callers + # that mutate ``name`` post-construction (rare but legal) survive + # the clone. ``__new__`` skips ``__init__`` so we set the Item- + # inherited slots (name / parent) explicitly here. + tb = TextBlock.__new__(TextBlock) + tb.name = self.name + tb.text = self.text + tb.parent = None # cloned subtree gets re-parented by Module.__deepcopy__ + memo[id(self)] = tb + return tb + + def __getstate__(self) -> tuple: + # rocisa code.cpp:149-150 -- ``make_tuple(name, text)``. + return (self.name, self.text) + + def __setstate__(self, state: tuple) -> None: + # rocisa code.cpp:151-154 -- rebuild via ``TextBlock(text)`` (which + # sets name=text), then patch ``name`` from the stored value. The + # two-step matters when a TextBlock was renamed after construction. + # ``name`` / ``parent`` are Item-inherited slots; they exist on + # the unpickled instance even though ``__init__`` wasn't called. + name, text = state + self.name = name + self.text = text + self.parent = None + + def __repr__(self) -> str: + return f"TextBlock({self.text!r})" + + +# --------------------------------------------------------------------------- +# Comment formatters -- low-stakes approximations of rocisa's format.hpp. +# --------------------------------------------------------------------------- +# +# rocisa C++ has slash() / slash50() / block() / blockNewLine() / block3Line() +# in format.hpp. They control human-readable comment layout, NOT instruction +# emit, so byte-parity is not on the vertical-slice critical path; KernelWriter +# only uses them for headers / dividers. We keep the formats simple and +# distinct (so a comment never silently merges with adjacent text) and document +# the byte-parity caveat. Lift these into a dedicated module + tighten formats +# once a KernelWriter diff exposes a mismatch. +def _slash(comment: str) -> str: + """``// COMMENT`` on its own line. Used by ``Module.addComment``.""" + return f"// {comment}\n" + + +def _slash50(comment: str) -> str: + """``// COMMENT`` aligned to col 50 -- mirrors instruction-line layout.""" + return f"{'':<50} // {comment}\n" + + +def _block(comment: str) -> str: + """Single-line block comment ``/* COMMENT */`` — matches native format.hpp::block().""" + return f"/* {comment} */\n" + + +def _block_newline(comment: str) -> str: + """Same as ``_block`` but with a leading blank line.""" + return "\n" + _block(comment) + + +def _block_3line(comment: str) -> str: + """Star-bar block comment — matches native format.hpp::block3Line(). + + Produces: newline + star-bar + per-line ``/* text (padded to 38) */`` + star-bar. + """ + bar = "/" + "*" * 42 + "/" + lines = comment.split("\n") + out = "\n" + bar + "\n" + for line in lines: + out += f"/* {line:<38} */\n" + out += bar + "\n" + return out + + +def _format_endif_str(instr: str, comment: str) -> str: + """Format an instruction line with an optional trailing comment. + + Used by ``ValueEndif.toString`` for the ``.endif [// ]`` + rendering. Layout rules: + + * ``comment`` empty OR ``_outputNoComment()`` returns True -> + ``"{instr}\\n"`` with no padding. + * Otherwise: ``instr`` is right-padded with spaces to width 50 + (``max(0, 50 - len(instr))`` spaces), then ``" // {comment}\\n"`` + is appended. Padding width 50 matches the column where rocisa + instruction lines align their trailing ``// ...`` notes. + + Currently used only by ``ValueEndif``; Phase 5 (assembly emit) + will need the full surface (including an ``outputInlineAsm`` + branch that wraps the instruction string in ``"...\\n\\t"`` for + inline-asm output). When that lands, lift this into a public + ``format.py`` module; for now keeping it private to ``code.py`` + keeps the surface area minimal. + """ + if not comment or _outputNoComment(): + return instr + "\n" + padding = " " * max(0, 50 - len(instr)) + return f"{instr}{padding} // {comment}\n" + + +def _to_hex_parity(num: int) -> str: + """Lowercase hex (no ``0x`` prefix) mirroring rocisa's ``std::hex`` + cast over an ``int64_t``. + + Used by ``ValueSet.toString`` for the ``format == 1`` branch. + Rocisa builds the payload as ``"0x" + std::hex(value + offset)`` + where the operand is ``int64_t`` and ``std::hex`` prints the raw + two's-complement bits with no minus sign. Python's ``f"{-1:x}"`` + would emit ``"-1"`` instead, diverging for negative inputs. Mask + to 64 bits to recover byte-parity. + + Non-negative values are unaffected (the mask is a no-op for any + int64_t-representable non-negative value). + """ + if num < 0: + num &= 0xFFFFFFFFFFFFFFFF + return f"{num:x}" + + +# --------------------------------------------------------------------------- +# Preprocessor conditional blocks -- ValueIf / ValueElseIf / ValueEndif. +# --------------------------------------------------------------------------- +# +# Mirror of rocisa's ``ValueIf`` / ``ValueElseIf`` / ``ValueEndif``. +# These produce the GNU assembler preprocessor directives ``.if`` / +# ``.elseif`` / ``.endif`` that KernelWriter uses to gate macro / +# kernel-text sections at assemble time (CustomSchedule.py:448-508 +# chains them; KernelWriterAssembly.py:1827 uses a single ValueEndif +# for the "overflowed resources" guard). +# +# Parity notes: +# * ``Item.name`` is set to the CLASS NAME ("ValueIf" / ... ) rather +# than to the condition expression -- matches rocisa's ctors which +# pass ``Item("ValueIf")``. This means +# ``findNamedItem("ValueIf")`` matches every ValueIf node in a +# Module; KernelWriter doesn't rely on that today but the parity +# keeps any future searcher behaviour identical. +# * Subclasses of ``Item`` -- ``__str__`` / ``prettyPrint`` / +# ``countType`` / ``countExactType`` / 7 cap-proxy methods all +# come from Item's defaults. We override only ``toString``, +# ``__deepcopy__``, ``__getstate__``, ``__setstate__`` -- the +# same four overrides rocisa's nanobind binding wires up +# explicitly. +# * ValueIf / ValueElseIf store a ``value`` (the condition +# expression); ValueEndif stores a ``comment`` and uses +# ``_format_endif_str`` to byte-match rocisa's ``formatStr`` +# padding semantics. + +class ValueIf(Item): + """``.if `` directive; mirror of ``rocisa::ValueIf``.""" + + __slots__ = ("value",) + + def __init__(self, value: str): + # ``Item.name`` is the literal "ValueIf" (class name), NOT the + # condition expression -- matches rocisa's ctor. + super().__init__(name="ValueIf") + self.value: str = value + + def toString(self) -> str: + # Raw ``.if`` + value + newline; no padding / comment support + # (the condition expression IS the trailing payload on this + # line). + return f".if {self.value}\n" + + def __deepcopy__(self, memo): + # Copy ctor -- a fresh ValueIf with the same value. + clone = ValueIf(self.value) + memo[id(self)] = clone + return clone + + def __getstate__(self) -> str: + # Pickle just the value string. + return self.value + + def __setstate__(self, state: str) -> None: + # Rebuild as ``ValueIf(value)``. Have to populate Item- + # inherited slots manually since ``__init__`` isn't called + # by the pickle machinery. + self.name = "ValueIf" + self.parent = None + self.value = state + + def __repr__(self) -> str: + return f"ValueIf({self.value!r})" + + +class ValueElseIf(Item): + """``.elseif `` directive; mirror of ``rocisa::ValueElseIf``.""" + + __slots__ = ("value",) + + def __init__(self, value: str): + # ``Item.name`` is the literal "ValueElseIf" (class name), NOT + # the condition expression -- matches rocisa's ctor. + super().__init__(name="ValueElseIf") + self.value: str = value + + def toString(self) -> str: + # Raw ``.elseif`` + value + newline. + return f".elseif {self.value}\n" + + def __deepcopy__(self, memo): + clone = ValueElseIf(self.value) + memo[id(self)] = clone + return clone + + def __getstate__(self) -> str: + return self.value + + def __setstate__(self, state: str) -> None: + self.name = "ValueElseIf" + self.parent = None + self.value = state + + def __repr__(self) -> str: + return f"ValueElseIf({self.value!r})" + + +class ValueEndif(Item): + """``.endif [// ]`` directive; mirror of + ``rocisa::ValueEndif``. + + The comment is padding-aligned to column 50 to match how rocisa + instruction lines align their trailing ``// ...`` notes, and is + suppressed entirely when ``outputNoComment`` is set (see + ``_format_endif_str`` for the exact rules). + """ + + __slots__ = ("comment",) + + def __init__(self, comment: str = ""): + # Default ``comment=""`` matches the most common KernelWriter + # call site (a bare ``ValueEndif()`` closing an .if block); + # ``Item.name`` is the literal "ValueEndif" -- matches rocisa's + # ctor. + super().__init__(name="ValueEndif") + self.comment: str = comment + + def toString(self) -> str: + # ``.endif`` + optional ``// `` padded to column 50; + # gated by ``outputNoComment``. See ``_format_endif_str`` for + # the byte-level rules. + return _format_endif_str(".endif", self.comment) + + def __deepcopy__(self, memo): + clone = ValueEndif(self.comment) + memo[id(self)] = clone + return clone + + def __getstate__(self) -> str: + # Pickle just the comment string (which is "" for the bare- + # ``ValueEndif()`` common case). + return self.comment + + def __setstate__(self, state: str) -> None: + self.name = "ValueEndif" + self.parent = None + self.comment = state + + def __repr__(self) -> str: + return f"ValueEndif({self.comment!r})" + + +# --------------------------------------------------------------------------- +# Label -- branch / loop target leaf. +# --------------------------------------------------------------------------- +# +# Mirror of rocisa's ``Label``. Emits an assembler label line of the +# form ``label_:``, optionally preceded by ``.align `` and +# followed by `` /// `` (the comment uses ``///`` not ``//`` +# so it survives later passes that strip ordinary ``//`` comments). +# +# KernelWriter creates Labels through ``LabelManager.getName(...)`` +# (or ``getUniqueName*``) to guarantee uniqueness, then pairs each +# Label with one or more branch instructions (``s_branch``, +# ``s_cbranch_*``) that target it. The Label's ``getLabelName()`` +# is the string those branches reference. +# +# Parity notes: +# * ``Item.name`` is the EMPTY STRING (matches rocisa's ``Item("")`` +# ctor argument), NOT the label payload. The actual label text +# comes from ``getLabelName()`` -> ``getFormatting(self.label)``. +# This means ``findNamedItem("Label")`` does NOT match Labels -- +# KernelWriter relies on ``getLabelName()`` for the textual +# identity instead. +# * The ``label`` field carries an ``int | str`` payload (``rocisa:: +# Label`` uses ``std::variant``). Python's union +# types make this transparent -- we just store the value as-is +# and dispatch in ``getFormatting`` via ``isinstance(label, int)``. +# * ``toString`` is intentionally side-effecting: when ``HasVgprMSB`` +# is set, the active VGPR-MSB tracking is reset to ``-1``. The +# reasoning is that emitting a label means "we're entering a new +# basic block whose entry MSB state is not knowable at this point +# in the rocisa-side analysis" -- the next instruction must +# re-establish MSB explicitly. Consumers must NOT rely on the +# pre-toString value of ``getVgprMsb()`` surviving across a +# ``Label.toString()`` call when the cap is set. +# * Comment formatting: ``" /// "`` (three slashes, two +# leading spaces). Suppressed entirely when ``outputNoComment`` +# is set, matching rocisa. +# * ``alignment <= 1`` means "no .align prefix"; ``alignment > 1`` +# emits a ``.align \\n`` line BEFORE the label. KernelWriter +# uses ``alignment > 1`` for loop-entry labels (cache-line +# alignment) and the default ``alignment=1`` for ordinary +# branch targets. + +class Label(Item): + """``label_:`` assembler label; mirror of ``rocisa::Label``. + + Carries a ``label`` payload (``int | str``) plus an optional + ``comment`` and an ``alignment`` (default 1; ``> 1`` prepends + ``.align \\n`` to the emission). See the section header above + for parity rules and the ``setVgprMsb(-1)`` side effect. + """ + + __slots__ = ("label", "comment", "alignment") + + def __init__(self, label, comment: str, alignment: int = 1): + # ``Item.name`` is the EMPTY STRING (matches rocisa's + # ``Item("")``) -- the label's textual identity comes from + # ``getLabelName()``, not from Item.name. + super().__init__(name="") + self.label = label + self.comment: str = comment + self.alignment: int = alignment + + @staticmethod + def getFormatting(label) -> str: # noqa: N802 (matches rocisa public API) + """Format an ``int | str`` payload into ``label_``. + + rocisa ``Label::getFormatting`` is a static method visiting + the ``std::variant`` and returning ``"label_"`` + prefixed by the numeric or string value. Python's f-string + formatting handles both branches uniformly, so a single + format expression is enough -- the variant dispatch is + implicit in ``__format__``. + """ + return f"label_{label}" + + def getLabelName(self) -> str: # noqa: N802 (matches rocisa public API) + """Return the full ``label_<...>`` text for this Label. + + Equivalent to ``Label.getFormatting(self.label)``. Branch + instructions reference the result of this call (NOT + ``self.name`` or ``self.label`` directly). + """ + return Label.getFormatting(self.label) + + def toString(self) -> str: + body = self.getLabelName() + ":" + if self.alignment > 1: + body = f".align {self.alignment}\n" + body + if self.comment and not _outputNoComment(): + body += " /// " + self.comment + body += "\n" + # Side effect: emitting a label resets the in-process + # VGPR-MSB tracker. See section header note for the + # justification (entering a new basic block). + if self.getAsmCaps().get("HasVgprMSB", 0): + _set_vgpr_msb(-1) + return body + + def __deepcopy__(self, memo): + clone = Label(self.label, self.comment, self.alignment) + # Item.name is "" by default but rocisa's copy ctor preserves + # whatever the original Item had -- patch it explicitly here + # so any caller-set ``label.name = ...`` round-trips. + clone.name = self.name + memo[id(self)] = clone + return clone + + def __getstate__(self): + # 4-tuple matches rocisa pickle order: + # ``(name, label, comment, alignment)``. + return (self.name, self.label, self.comment, self.alignment) + + def __setstate__(self, state) -> None: + # Rebuild via the public ctor (so any ctor-level invariants + # apply) then patch ``name`` -- mirrors rocisa's + # ``new(&self) Label(label, comment, alignment); self.name = name`` + # placement-new pattern. + name, label, comment, alignment = state + self.__init__(label, comment, alignment) + self.name = name + + def __repr__(self) -> str: + return ( + f"Label({self.label!r}, comment={self.comment!r}, " + f"alignment={self.alignment})" + ) + + +# --------------------------------------------------------------------------- +# Symbol-emission nodes -- ValueSet / RegSet. +# --------------------------------------------------------------------------- +# +# Mirror of rocisa's ``ValueSet`` (assembler ``.set`` directive) and +# ``RegSet`` (a ``ValueSet`` subclass that also tracks VGPR +# allocation in the rocIsa singleton for MSB-aware archs). +# +# These produce GNU assembler ``.set , `` lines that +# KernelWriter sprinkles throughout a kernel to give names to integer +# constants and register aliases. RegSet adds the convention that +# ``.set vgprX, N`` ALSO informs the in-process index map of the +# binding so subsequent ``getVgprIdx()`` lookups see the latest +# allocation. +# +# Parity notes: +# * C++ has 3 ValueSet ctors (int / uint32 / string ref) and 2 +# RegSet ctors (int / string value). Python collapses each into +# a single ``__init__`` that dispatches on +# ``isinstance(value, str)`` -- this matches what nanobind's +# overload resolution does at runtime based on Python type. +# * The ``uint32`` overload was a C++-side type-system concern only. +# Python ints are arbitrary precision so both routes share the +# int path; ``toString`` produces identical output either way. +# * ``format`` is a raw int sentinel: ``-1`` = literal, ``0`` = +# decimal-with-offset (default), ``1`` = hex-with-offset. Kept +# as a plain int (not an IntEnum) to match rocisa's API surface +# -- KernelWriter passes 0/1/-1 directly. +# * Negative ``value + offset`` with ``format == 1`` mirrors +# rocisa's ``std::hex`` over int64_t two's-complement -- see +# ``_to_hex_parity``. Non-negative payloads are unaffected. +# * ``RegSet.__init__`` AND ``RegSet.toString`` both call +# ``setVgprIdx`` when ``regType == "v"`` on a HasVgprMSB arch. +# ``toString`` is intentionally NOT pure here: KernelWriter +# relies on each emitted ``.set vgprX, N`` line ALSO refreshing +# the in-memory index map as the asm text is built. +# * ``self.name[4:]`` strips the conventional ``"vgpr"`` prefix +# before registering the index. rocisa does not validate that +# the prefix is present; neither do we. + +class ValueSet(Item): + """``.set , `` directive; mirror of + ``rocisa::ValueSet``. + + Holds either an integer ``value`` OR a string ``ref`` (mutually + exclusive); the unused field is ``None``. ``offset`` is added to + the payload, and ``format`` controls the rendering: + + ``format == -1`` -> literal ref / ``str(value)`` (no offset + arithmetic on the value path) + ``format == 0`` -> decimal ``str(value + offset)`` (default) + ``format == 1`` -> ``"0x" + hex(value + offset)`` + + See ``_to_hex_parity`` for the negative-value byte-parity rules. + """ + + __slots__ = ("ref", "value", "offset", "format") + + def __init__( + self, + name: str, + value, + offset: int = 0, + format: int = 0, # noqa: A002 (matches rocisa's public arg name) + ): + # Single Python ctor dispatching on the runtime type of + # ``value`` -- matches what nanobind's 3-overload resolution + # does at runtime. ``isinstance(value, str)`` discriminates + # the ref-payload path from the int-payload path; the C++ + # ``int`` vs ``uint32_t`` overloads collapse into a single + # Python int route (arbitrary precision -- no truncation). + super().__init__(name=name) + if isinstance(value, str): + self.ref: Optional[str] = value + self.value: Optional[int] = None + else: + self.ref = None + self.value = int(value) + self.offset: int = offset + self.format: int = format + + def toString(self) -> str: + body = f".set {self.name}, " + if self.ref is not None: + # ref path: ``format == -1`` emits the ref alone; any other + # format suffixes ``+``. (rocisa does NOT short- + # circuit ``offset == 0`` to ``ref`` alone -- the literal + # ``+0`` is preserved for byte parity with the assembler.) + if self.format == -1: + body += self.ref + else: + body += f"{self.ref}+{self.offset}" + elif self.value is not None: + # value path: ``format == -1`` emits ``str(value)`` with + # NO offset arithmetic (mirrors C++ which calls + # ``std::to_string(value)`` directly in that branch); + # ``format == 0`` adds the offset; ``format == 1`` adds + # the offset and emits hex. + v = self.value if self.format == -1 else self.value + self.offset + if self.format == 1: + body += f"0x{_to_hex_parity(v)}" + else: + body += str(v) + # ``ref is None and value is None`` is unreachable under the + # ctor invariant (one of the two is always set); rocisa would + # dereference an empty optional and crash here. + return body + "\n" + + def __deepcopy__(self, memo): + if self.ref is not None: + clone = ValueSet(self.name, self.ref, self.offset, self.format) + else: + clone = ValueSet(self.name, self.value, self.offset, self.format) + memo[id(self)] = clone + return clone + + def __getstate__(self): + # 5-tuple: ``(name, ref, value, offset, format)``. + return (self.name, self.ref, self.value, self.offset, self.format) + + def __setstate__(self, state) -> None: + # Restore slots directly (no side effect to mirror, matching + # how ValueIf / ValueElseIf / ValueEndif round-trip). + name, ref, value, offset, fmt = state + self.name = name + self.parent = None + self.ref = ref + self.value = value + self.offset = offset + self.format = fmt + + def __repr__(self) -> str: + payload = self.ref if self.ref is not None else self.value + return ( + f"ValueSet({self.name!r}, {payload!r}, " + f"offset={self.offset}, format={self.format})" + ) + + +class RegSet(ValueSet): + """``.set , `` plus VGPR-index tracking; + mirror of ``rocisa::RegSet``. + + Adds a ``regType`` field (``"v"`` for VGPR, ``"s"`` for SGPR, + etc.) on top of ``ValueSet``. When ``regType == "v"`` AND the + active arch has ``HasVgprMSB`` set, BOTH ``__init__`` and every + call to ``toString`` invoke ``setVgprIdx`` on the rocIsa + singleton so later ``getVgprIdx()`` lookups observe the latest + allocation. + + The conventional ``"vgpr"`` prefix on ``name`` (e.g. + ``"vgprFoo"``) is stripped before registering -- the key in the + index map is ``name[4:]``. + + ``toString`` is intentionally side-effecting: KernelWriter relies + on each emitted ``.set vgprX, N`` line refreshing the in-memory + index map as the asm text is built, not just at construction + time. + """ + + __slots__ = ("regType",) + + def __init__( + self, + regType: str, + name: str, + value, + offset: int = 0, + ): + # No ``format`` parameter at the RegSet ctor surface (matches + # rocisa -- the two C++ RegSet ctors only forward + # ``(name, value, offset)`` to ValueSet, leaving ``format`` + # at its default 0). Callers can patch ``self.format`` + # post-construction; pickle round-trip preserves it. + super().__init__(name=name, value=value, offset=offset) + self.regType: str = regType + if self._vgpr_msb_active(): + self._set_idx(value, offset) + + def toString(self) -> str: + # Re-trigger the index update every time. Mirrors rocisa's + # ``RegSet::toString`` which first re-runs ``setIdx`` then + # delegates the actual string formatting to ``ValueSet``. + if self._vgpr_msb_active(): + if self.ref is not None: + self._set_idx(self.ref, self.offset) + elif self.value is not None: + self._set_idx(self.value, self.offset) + return super().toString() + + def _vgpr_msb_active(self) -> bool: + """Cap-gated guard for the ``setVgprIdx`` side effect. + + ``getAsmCaps()`` returns a dict; missing keys default to 0 + (matches ``std::map::operator[]`` which value- + initialises on access). + """ + return ( + self.regType == "v" + and bool(self.getAsmCaps().get("HasVgprMSB", 0)) + ) + + def _set_idx(self, value, offset: int) -> None: + # Two cases mirror the ``setIdx(int, int)`` and + # ``setIdx(string, int)`` C++ overloads: + # * int value -> store ``value + offset`` directly + # * str value -> look ``value[4:]`` up in the current + # vgprIdx map then add offset (the lookup + # key has the ``"vgpr"`` prefix stripped) + # + # IMPORTANT: rocisa uses ``std::map::operator[]`` + # for the lookup, which value-initialises missing keys to 0 + # rather than throwing. KernelWriter's ``macroAndSet`` pattern + # ``RegSet("v", "vgprX", "vgprX_BASE", 0)`` deliberately + # establishes an alias at offset 0 of a not-yet-registered + # base name, so we MUST treat missing keys as 0 to match. + if isinstance(value, str): + current = self.getVgprIdx() + idx = current.get(value[4:], 0) + offset + else: + idx = int(value) + offset + _set_vgpr_idx(self.name[4:], idx) + + def __deepcopy__(self, memo): + # Use the int- or string-payload ctor depending on which is + # set, then patch ``format`` (RegSet's ctor does not take it). + # Mirrors the C++ copy ctor's effect. + if self.ref is not None: + clone = RegSet(self.regType, self.name, self.ref, self.offset) + else: + clone = RegSet(self.regType, self.name, self.value, self.offset) + clone.format = self.format + memo[id(self)] = clone + return clone + + def __getstate__(self): + # 6-tuple: ``(regType, name, ref, value, offset, format)``. + return ( + self.regType, + self.name, + self.ref, + self.value, + self.offset, + self.format, + ) + + def __setstate__(self, state) -> None: + # Restore via ``__init__`` (so the ``setVgprIdx`` side effect + # fires for ``regType == "v"`` on HasVgprMSB archs, matching + # the C++ ``new(&self) RegSet(...)`` placement-new), then + # patch ``format`` -- RegSet's ctor does not accept it. + regType, name, ref, value, offset, fmt = state + payload = ref if ref is not None else value + self.__init__(regType, name, payload, offset) + self.format = fmt + + def __repr__(self) -> str: + payload = self.ref if self.ref is not None else self.value + return ( + f"RegSet({self.regType!r}, {self.name!r}, {payload!r}, " + f"offset={self.offset}, format={self.format})" + ) + + +# --------------------------------------------------------------------------- +# Module -- real tree-shaped IR container. +# --------------------------------------------------------------------------- +# +# Methods are 1:1 with the nanobind binding in rocisa/src/code.cpp:157-212. +# Item-handling semantics (parent rebind on add/setItem, None tolerance on +# add, ``count`` recurses into sub-Modules but stops at leaves) match +# rocisa::Module behaviour so byte-for-byte downstream emission is preserved. +# +# Items inserted here can be *anything that quacks like a rocisa Item* -- +# we do not type-check on ``Item`` so the dummy instruction shims still +# work during the bring-up phase. The only hard requirement at lowering +# time is that logical-IR leaves expose ``to_stinky_logical()`` (Step 3). +class Module(Item): + """Tree-shaped IR container mirroring ``rocisa::Module``. + + The container is identity-based for ``replaceItem`` / ``removeItem`` + (matches rocisa, which compares ``shared_ptr`` equality). + Children with a ``.parent`` attribute are reparented to this Module + on insertion so KernelWriter's tree-walks ascend correctly. + + Item inheritance: ``Module(Item)`` -- ``name`` / ``parent`` come + from Item's ``__slots__``; the seven capability proxies + (``getAsmCaps`` / ``getArchCaps`` / ``getRegCaps`` / ``getAsmBugs`` + / ``getVgprIdx`` / ``getVgprMsb`` / ``kernel``) are inherited as + methods that forward to module-level ``base.py`` accessors. + + Property vs method note: rocisa C++ binds these as ``def_prop_ro`` + (so ``module.asmCaps`` works in C++/Python); the Python adapter + exposes them as methods (``module.getAsmCaps()``). KernelWriter + never reads them off Module instances today (workspace-wide grep + for ``\\.asmCaps`` / ``\\.kernel`` on Module comes back empty -- it + only does so on Instruction subclasses), so this method-vs-property + asymmetry is invisible in practice. Promote to ``@property`` here + only if KernelWriter ever starts reading them off a Module. + + Pickling: ``__reduce__`` raises ``RuntimeError("Module is not + picklable")``, matching rocisa code.cpp -- ``ParallelMap2`` workers + are expected to round-trip the kernel **arguments** (and let each + worker rebuild its own Module tree from scratch), not the IR. + + Left-path entry point:: + + asm_module = code_module.to_stinky_asm([12, 5, 0]) + # Or with Tensile-style kernel label: + # asm_module = code_module.to_stinky_asm([12, 5, 0], logical_name="my_kernel") + print(asm_module.emitAssembly()) + + See module-level docstring for the architectural picture. + """ + + __slots__ = ("itemList", "tempVgpr", "_isNoOpt") + + def __init__(self, name: str = ""): + super().__init__(name=name) + self.itemList: List[Any] = [] + self.tempVgpr: Any = None + self._isNoOpt: bool = False + + # ------------------------------------------------------------------ add + def add(self, item: Any, pos: int = -1) -> Any: + """Add ``item`` to ``itemList`` and return it for chaining. + + ``None`` is silently dropped, matching rocisa's ``if(item)`` + guard (KernelWriter frequently passes optional values directly). + """ + if item is None: + return item + self._reparent(item) + if pos == -1: + self.itemList.append(item) + else: + self.itemList.insert(pos, item) + return item + + def addItems(self, items: Iterable[Any]) -> None: + for it in items: + self.add(it) + + def addSpaceLine(self) -> None: + self.add(TextBlock("\n")) + + def addComment(self, comment: str) -> None: + self.add(TextBlock(_slash(comment))) + + def addCommentAlign(self, comment: str) -> None: + self.add(TextBlock(_slash50(comment))) + + def addComment0(self, comment: str) -> None: + self.add(TextBlock(_block(comment))) + + def addComment1(self, comment: str) -> None: + self.add(TextBlock(_block_newline(comment))) + + def addComment2(self, comment: str) -> None: + self.add(TextBlock(_block_3line(comment))) + + # ---------------------------------------------------------- accessors + def items(self) -> List[Any]: + return self.itemList + + def itemsSize(self) -> int: + return len(self.itemList) + + def count(self) -> int: + """Recursive leaf count (sub-Modules expand, everything else +=1).""" + n = 0 + for it in self.itemList: + if isinstance(it, Module): + n += it.count() + else: + n += 1 + return n + + def countType(self, type_obj: type) -> int: + """Recursive ``isinstance`` count -- mirror of + ``rocisa::Module::countType`` (code.hpp:441-449). + + Sums ``isinstance(self, type_obj)`` (this Module) plus the + ``countType`` of every child. Non-Item children that happen to + live in ``itemList`` (raw dummies during bring-up) fall back + to a manual ``isinstance`` check so they still contribute. + """ + n = 1 if isinstance(self, type_obj) else 0 + for it in self.itemList: + inner_count = getattr(it, "countType", None) + if callable(inner_count): + res = inner_count(type_obj) + if isinstance(res, int): + n += res + continue + # Fallback for items that don't expose countType (e.g. raw + # Python objects KernelWriter might stash). Still respects + # isinstance so type checks against ``object`` work. + if isinstance(it, type_obj): + n += 1 + return n + + def countExactType(self, type_obj: type) -> int: + """Recursive ``type(...) is`` count -- mirror of + ``rocisa::Module::countExactType`` (code.hpp:451-459). + + Uses identity comparison (``type(self) is type_obj``) rather + than ``isinstance`` so subclasses do NOT count -- a + ``StructuredModule`` is not counted as a ``Module`` here even + though it inherits from it. Matches ``typeid(*this) == + targetType`` in C++. + """ + n = 1 if type(self) is type_obj else 0 + for it in self.itemList: + inner_count = getattr(it, "countExactType", None) + if callable(inner_count): + res = inner_count(type_obj) + if isinstance(res, int): + n += res + continue + if type(it) is type_obj: + n += 1 + return n + + def getItem(self, index: int) -> Any: + # rocisa throws ``std::runtime_error("index out of range")``; we + # match the message exactly so callers comparing exception text + # see the same string regardless of backend. + if index >= len(self.itemList) or index < -len(self.itemList): + raise RuntimeError("index out of range") + return self.itemList[index] + + def setItem(self, index: int, item: Any) -> None: + if index >= len(self.itemList) or index < -len(self.itemList): + raise RuntimeError("index out of range") + self._reparent(item) + self.itemList[index] = item + + def setItems(self, items: Sequence[Any]) -> None: + self.itemList = list(items) + for it in self.itemList: + self._reparent(it) + + # ------------------------------------------------------------ find / index + def findNamedItem(self, name: str) -> Any: + for it in self.itemList: + if getattr(it, "name", None) == name: + return it + return None + + def findIndex(self, target: Any) -> int: + for i, it in enumerate(self.itemList): + if it is target: + return i + return -1 + + def findIndexByType(self, type_obj: type) -> int: + for i, it in enumerate(self.itemList): + if isinstance(it, type_obj): + return i + return -1 + + # ------------------------------------------------------------- mutations + def replaceItem(self, src: Any, dst: Any) -> None: + for i, it in enumerate(self.itemList): + if it is src: + self._reparent(dst) + self.itemList[i] = dst + return # rocisa replaces only the first match + + def replaceItemByIndex(self, index: int, item: Any) -> None: + # rocisa silently no-ops when ``index >= itemList.size()``. + if index >= len(self.itemList) or index < -len(self.itemList): + return + self._reparent(item) + self.itemList[index] = item + + def removeItem(self, item: Any) -> None: + self.itemList = [it for it in self.itemList if it is not item] + + def removeItemByIndex(self, index: int) -> None: + if not self.itemList: + return + # rocisa clamps over-range indices to the last element (not an error). + if index >= len(self.itemList): + index = len(self.itemList) - 1 + del self.itemList[index] + + def removeItemsByName(self, name: str) -> None: + self.itemList = [ + it for it in self.itemList if getattr(it, "name", None) != name + ] + + def popFirstItem(self) -> Any: + if not self.itemList: + return None + return self.itemList.pop(0) + + def popFirstNItems(self, n: int) -> List[Any]: + if n >= len(self.itemList): + popped, self.itemList = self.itemList, [] + return popped + popped = self.itemList[:n] + self.itemList = self.itemList[n:] + return popped + + # ---------------------------------------------------------- tree ops + def appendModule(self, module: "Module") -> "Module": + for it in module.items(): + self.add(it) + return module + + def addModuleAsFlatItems(self, module: "Module") -> "Module": + if module is None: + return module + for it in module.flatitems(): + self.add(it) + return module + + def flatitems(self) -> List[Any]: + out: List[Any] = [] + for it in self.itemList: + if isinstance(it, Module): + out.extend(it.flatitems()) + else: + out.append(it) + return out + + def setParent(self) -> None: + for it in self.itemList: + self._reparent(it) + if isinstance(it, Module): + it.setParent() + + def setNoOpt(self, b: bool) -> None: + self._isNoOpt = bool(b) + + def isNoOpt(self) -> bool: + return self._isNoOpt + + def setInlineAsmPrintMode(self, mode: bool) -> None: + for it in self.itemList: + if isinstance(it, Module): + it.setInlineAsmPrintMode(mode) + elif hasattr(it, "setInlineAsm"): + it.setInlineAsm(mode) + + def addTempVgpr(self, vgpr: Any) -> None: + self.tempVgpr = vgpr + + # --------------------------------------------------------------- render + def toString(self) -> str: + return "".join(str(it) for it in self.itemList) + + def __str__(self) -> str: + return self.toString() + + def prettyPrint(self, indent: str = "") -> str: + """Tree dump mirroring ``rocisa::Module::prettyPrint`` (code.hpp:418-427). + + Format (byte-for-byte): + + {indent}{ClassName} "{name}"\\n <- this Module's header + {indent}|--child.prettyPrint(indent+"|--") + ... + + Children are concatenated **verbatim** (no separator) -- each child + is expected to emit its own trailing ``\\n`` (Module's header line + does, ``Instruction.toString`` does; ``Item::prettyPrint`` does + NOT, matching rocisa C++). Dummy shims whose ``__getattr__`` makes + ``prettyPrint`` return ``None`` fall back to a class-name line with + an explicit ``\\n`` so the dump stays well-formed during bring-up. + """ + out = f'{indent}{type(self).__name__} "{self.name}"\n' + for it in self.itemList: + pp = getattr(it, "prettyPrint", None) + if callable(pp): + res = pp(indent + "|--") + if isinstance(res, str): + out += res + continue + # dummy ``_noop`` returned None -- fall through to fallback + out += f"{indent}|--{type(it).__name__}\n" + return out + + # ------------------------------------------------------------- pickle + def __deepcopy__(self, memo): + clone = Module(self.name) + memo[id(self)] = clone + clone._isNoOpt = self._isNoOpt + # rocisa clones tempVgpr via Container::clone(); here we deepcopy so + # value-typed wrappers stay independent. Non-deepcopyable objects + # (rare) fall through to a shallow copy to match nanobind tolerance. + if self.tempVgpr is not None: + try: + clone.tempVgpr = _copy.deepcopy(self.tempVgpr, memo) + except Exception: # noqa: BLE001 (intentionally permissive) + clone.tempVgpr = self.tempVgpr + for it in self.itemList: + new_it = _copy.deepcopy(it, memo) + clone._reparent(new_it) + clone.itemList.append(new_it) + return clone + + def __reduce__(self): + # rocisa raises "Module is not picklable"; mirror that exactly so the + # ParallelMap2 worker harness sees the same failure mode. + raise RuntimeError("Module is not picklable") + + # ----------------------------------------------------- logicalIR handoff + def to_stinky_asm( + self, + arch: Sequence[int], + *, + logical_name: Optional[str] = None, + options: Any = None, + ): + """Lower this Module tree to a ``stinkytofu.StinkyAsmModule``. + + Walks ``itemList`` in tree order and forwards every leaf that + exposes ``to_stinky_logical()`` (the Step-3 instruction shims) + into a fresh ``_stinkytofu.LogicalModule``. The logical module + is then run through ``_stinkytofu.lower_logical_module(...)`` + which wires composite expansion + ToStinkyAsmPass and produces + a ``StinkyAsmModule`` ready for ``emitAssembly()``. + + Non-logical items (``TextBlock``, ``Label``, raw asm fragments) + are silently skipped because they have no logical-IR counterpart. + Once Step 3+ lands more instruction families, this also serves + as the natural place to surface "leaf X has no + ``to_stinky_logical``" diagnostics. + + Args: + arch: target ISA ``[major, minor, stepping]``, e.g. + ``[12, 5, 0]`` for gfx1250. + logical_name: optional override for the ``LogicalModule`` / + asm module name. When omitted, uses ``self.name`` or + ``"kernel"`` (matches prior behaviour and pairs with + ``rocisa.toStinkyTofuModule(..., moduleName, ...)``). + + Returns: + ``stinkytofu.StinkyAsmModule``. + + Raises: + ImportError: when the ``stinkytofu`` Python binding + (``_stinkytofu.so``) is not built / on PYTHONPATH. + """ + import stinkytofu as _st # noqa: WPS433 (optional runtime dep) + + lm_label = logical_name if logical_name is not None else (self.name or "kernel") + lm = _st.LogicalModule(lm_label) + self._populate_logical_module(lm) + + # --- DEBUG: dump LogicalModule IR before lowering --- + import os as _os + _dump_dir = _os.environ.get("DUMP_STINKY_MODULE") + if _dump_dir: + _os.makedirs(_dump_dir, exist_ok=True) + _lm_dump = lm.dump() + _lm_path = _os.path.join(_dump_dir, "logical_module_adaptor.txt") + with open(_lm_path, "w") as _f: + _f.write(_lm_dump) + + return _PostProcessModule(_st.lower_logical_module(lm, list(arch), options)) + + def _populate_logical_module(self, lm: Any) -> None: + """In-order walk adding instructions and .set directives to *lm*. + + Preserves source ordering: when a ``ValueSet`` appears between two + instructions in the Module tree, ``lm.add_set_directive(symbol, value)`` + is called at that position so the lowered BasicBlock will contain the + ``AsmDirective(SET)`` node interleaved with instructions — matching the + native ``toStinkyTofuModule`` behaviour. + + Named sub-Modules emit begin_group/end_group markers so the C++ + lowering pipeline can reconstruct instruction-group ranges (used by + ScopeAdaptor passes like ESM2, RegionClone, DAG scheduler). + """ + for it in self.itemList: + if isinstance(it, Module): + if it.name: + lm.begin_group(it.name) + it._populate_logical_module(lm) + if it.name: + lm.end_group(it.name) + continue + if isinstance(it, ValueSet): + text = it.toString().strip() # ".set , " + prefix = ".set " + if text.startswith(prefix): + rest = text[len(prefix):] + comma = rest.find(", ") + if comma != -1: + sym = rest[:comma] + val = rest[comma + 2:] + lm.add_set_directive(sym, val) + continue + if isinstance(it, Label): + lm.add_label(it.getLabelName(), it.alignment, it.comment or "") + continue + if isinstance(it, TextBlock): + lm.add_textblock(it.text) + continue + # Skip SDelayAlu instructions — the optimization pipeline handles + # all hazard insertion (InsertWaitAluPass for ESM2, InsertDelayAluPass + # for non-ESM2 regions). The adaptor previously emitted these as + # s_nop 0 placeholders that survived the pipeline unrecognized. + if getattr(it, "instStr", "") == "s_delay_alu": + continue + handle = getattr(it, "to_stinky_logical", None) + if not callable(handle): + continue + logical = handle() + if logical is None: + continue + comment = getattr(it, "comment", None) or "" + if isinstance(logical, list): + for inst in logical: + if comment and not inst.comment: + inst.comment = comment + lm.add(inst) + else: + if comment and not logical.comment: + logical.comment = comment + lm.add(logical) + + def _collect_logical_insts(self) -> List[Any]: + """Legacy helper for tests: collect logical instructions in-order.""" + out: List[Any] = [] + for it in self.itemList: + if isinstance(it, Module): + out.extend(it._collect_logical_insts()) + continue + handle = getattr(it, "to_stinky_logical", None) + if not callable(handle): + continue + logical = handle() + if logical is None: + continue + if isinstance(logical, list): + out.extend(logical) + else: + out.append(logical) + return out + + # ---------------------------------------------------------- internal + def _reparent(self, item: Any) -> None: + """Best-effort ``item.parent = self`` (frozen / __slots__ tolerant).""" + if not hasattr(item, "parent"): + return + try: + item.parent = self + except (AttributeError, TypeError): + # Some dummy shims forbid attribute writes; not fatal for + # rocisa-shape parity here. KernelWriter's tree-walks only + # rely on ``parent`` for known item types. + pass + + +# --------------------------------------------------------------------------- +# StructuredModule -- 3-bucket Module subclass for SIA scheduling. +# --------------------------------------------------------------------------- +# +# Mirror of rocisa's ``StructuredModule``. A Module subclass that +# carves its body into three named sub-modules -- ``header``, +# ``middle``, ``footer`` -- and auto-adds them to its ``itemList`` +# at construction. The result is an Item tree where: +# +# sm = StructuredModule("globalReadA") # itemList = [header, middle, footer] +# sm.middle.add(SomeLoadInstruction(...)) +# str(sm) # emits header + middle + footer (in that order) +# +# Why this exists: +# The Tensile SIA (Scheduling Iterations Ahead) pass needs to +# distinguish "load issue" (header), "decomposable middle body" +# (the chunk it can reorder for latency hiding), and "guard / +# bookkeeping tail" (footer). By giving each chunk its own named +# sub-module, the scheduler can splice ``sm.middle.items()`` into +# the pipeline at any cycle without touching ``sm.header`` / +# ``sm.footer``. See KernelWriter.scheduleLatencyHiding callers. +# +# The future logical-IR lowering (``to_stinky_asm``) flattens +# StructuredModule transparently -- the structural distinction is +# only needed on the Python side while SIA still runs there. +# +# Parity contract: +# * Public surface matches rocisa's nanobind binding (code.cpp: +# 233-244): ctor ``(name="")``, read-write attrs ``header / +# middle / footer``, ``__deepcopy__``, AND a ``__reduce__`` that +# raises ``RuntimeError("StructuredModule is not picklable")`` +# -- note the message differs from Module's "Module is not +# picklable" (the C++ binding has its own message). +# * The 3 sub-modules are aliased into ``itemList`` AT +# CONSTRUCTION: ``sm.header is sm.itemList[0]`` is True so +# mutations to ``sm.header.add(...)`` show up in ``str(sm)``. +# * Conscious divergence from rocisa C++ on deepcopy: rocisa's +# C++ copy ctor (code.hpp:781-790) accidentally BREAKS the +# aliasing post-clone (clones header/middle/footer a second +# time, leaving them out of itemList). We deliberately PRESERVE +# the aliasing using Python's deepcopy ``memo`` mechanism -- +# see ``__deepcopy__``'s long comment for full rationale. +# Net effect: ``clone.header is clone.itemList[0]`` after +# deepcopy in this adapter (False in rocisa). Original / clone +# remain fully isolated either way. +# * Names of the sub-modules are the literals ``"header" / +# "middle" / "footer"``, matching the C++ ``make_shared +# ("header")`` calls. ``findNamedItem("header")`` works. + + +class StructuredModule(Module): + """``Module`` subclass with auto-added ``header / middle / footer`` + sub-modules. Mirror of ``rocisa::StructuredModule``. + + See the section header above for the SIA-scheduling rationale, + the parity contract, and the deepcopy-aliasing quirk. + """ + + __slots__ = ("header", "middle", "footer") + + def __init__(self, name: str = ""): + # ``Module.__init__`` populates ``itemList`` as empty plus + # all the Item-inherited slots. THEN we mint the three + # sub-modules and add them in order so itemList = [header, + # middle, footer]. + super().__init__(name) + self.header: Module = Module("header") + self.middle: Module = Module("middle") + self.footer: Module = Module("footer") + # ``Module.add`` patches the child's ``.parent`` to ``self``, + # so the 3 sub-modules end up reparented here. + self.add(self.header) + self.add(self.middle) + self.add(self.footer) + + def __deepcopy__(self, memo): + # ------------------------------------------------------------------ + # Conscious divergence from rocisa C++ copy ctor (code.hpp:781-790). + # ------------------------------------------------------------------ + # The C++ copy ctor breaks the construction-time aliasing + # between ``header / middle / footer`` attrs and + # ``itemList[0..2]``: it first calls ``Module(other)`` (which + # clones the entire itemList, including the 3 sub-modules), + # then independently calls ``other.header->clone()`` AGAIN + # in the field initializer -- producing a SECOND clone that + # is NOT in itemList. After the C++ copy ctor: + # + # clone.header is not clone.itemList[0] # True (!) + # + # That is almost certainly an unintentional bug. The + # construction-time aliasing is THE central invariant of + # this class -- it's what makes ``sm.middle.add(x)`` + # propagate into ``str(sm)``. Breaking that invariant in the + # copy ctor means a freshly-constructed instance and a + # deepcopy'd instance behave DIFFERENTLY for the same API + # call -- a violation of the "principle of least surprise" + # that ``deepcopy`` typically obeys. + # + # We deliberately diverge from rocisa here for two reasons: + # + # 1. KernelWriter's PGR=2 path indirectly triggers this + # via ``deepcopy(perIterGlobalRead[0])`` -- when + # Components/SIA.py:564 has inserted a StructuredModule + # as a transitive child, Python's recursive deepcopy + # will reach ``__deepcopy__`` here. If we mirror the + # C++ bug bit-for-bit, any subsequent code that touches + # ``cloned_sm.middle.add(...)`` on those nested clones + # silently produces nothing -- a sleeper bug nobody + # would notice until output diffs appear. + # + # 2. Test suite isolation: ``copy.deepcopy(sm)`` should + # produce a clone that behaves like a fresh ctor result. + # Mirroring the C++ aliasing-break would force every + # consumer to add "did this come from deepcopy?" + # branches around any sub-module mutation -- not + # feasible. + # + # Implementation: we let Python's standard ``memo`` mechanism + # preserve aliasing for free. Step 1 ``deepcopy(it, memo)`` + # on each itemList entry populates ``memo[id(it)] -> + # cloned_it``. Step 2 then asks for ``deepcopy(self.header, + # memo)`` with the SAME memo -- which short-circuits to the + # already-cloned ``itemList[0]``. Result: the post-deepcopy + # invariant ``clone.header is clone.itemList[0]`` holds, AND + # original / clone remain fully isolated (mutating + # ``original.header`` after the copy does NOT bleed into + # ``clone.header`` -- ``memo`` only caches within a SINGLE + # deepcopy operation, not across them). + # + # If rocisa upstream eventually fixes this (the natural fix + # would be ``header = dynamic_pointer_cast(itemList[0])`` + # in the C++ copy ctor body), the two sides will converge + # and this comment can be deleted. + # ------------------------------------------------------------------ + clone = StructuredModule.__new__(StructuredModule) + memo[id(self)] = clone + # --- Step 1: Module(other) equivalent (clone itemList). ---------- + Module.__init__(clone, self.name) + clone._isNoOpt = self._isNoOpt + if self.tempVgpr is not None: + try: + clone.tempVgpr = _copy.deepcopy(self.tempVgpr, memo) + except Exception: # noqa: BLE001 + clone.tempVgpr = self.tempVgpr + for it in self.itemList: + new_it = _copy.deepcopy(it, memo) + clone._reparent(new_it) + clone.itemList.append(new_it) + # --- Step 2: rebind attrs to the already-cloned itemList --------- + # entries via the SAME memo -- this is the aliasing-preserving + # step. See the long comment above for the rationale. + clone.header = _copy.deepcopy(self.header, memo) + clone.middle = _copy.deepcopy(self.middle, memo) + clone.footer = _copy.deepcopy(self.footer, memo) + return clone + + def __reduce__(self): + # rocisa's StructuredModule binding (code.cpp:242-244) has its + # OWN ``__reduce__`` that raises with a class-specific + # message -- distinct from Module's "Module is not + # picklable". We mirror the message text exactly so any + # consumer that string-matches on the exception keeps + # working. + raise RuntimeError("StructuredModule is not picklable") + + +# --------------------------------------------------------------------------- +# Macro -- ``.macro args ... .endm`` block. +# --------------------------------------------------------------------------- +# +# Mirror of ``rocisa::Macro``. KernelWriter constructs 4 macros (KWA: +# ``GLOBAL_OFFSET_*``, ``MAC_*``, ``MAC_*_OneIUI``; +# Components/CustomSchedule.py: ``MAINLOOP``) and adds CommonInstruction +# / Module / TextBlock children into each. +# +# Notable divergences from generic ``Module``: +# * Macro is NOT a subclass of Module -- it holds its own ``itemList`` +# directly (rocisa C++ does the same). Different add() contract, +# different toString format, no addComment1 / addComment2, no +# findNamedItem etc. +# * ``add()`` rejects unknown item types with RuntimeError -- only +# Instruction / Module / TextBlock / ValueIf* are allowed (matches +# the rocisa C++ dynamic_cast whitelist). +# * ``addComment0`` emits a single-line ``/* ... */\n`` -- distinct +# from Module.addComment0's 3-line banner (rocisa C++ explicitly +# chose simpler formatting for macro bodies). +# * ``toString()`` wraps children with ``.macro
`` / ``.endm`` +# and indents each child line 4 spaces. The ``s_set_vgpr_msb`` +# line gets a SECOND 4-space indent on the body line right after +# the first newline (rocisa C++ hack to keep auto-inserted MSB +# toggles visually aligned). +# +# Implementation note: the ``.macro NAME arg0, arg1, ...`` header line +# is rendered by an internal ``MacroInstruction`` (stored on +# ``self.macro``) -- same trick as rocisa C++. + + +class Macro(Item): + """Mirror of ``rocisa::Macro``. + + Layout: ``.macro \\n \\n \\n.endm\\n``. + Children are typed (Instruction / Module / TextBlock / ValueIf*); + other item types raise RuntimeError at ``add()`` time. + """ + + __slots__ = ("itemList", "macro") + + # Whitelist mirrors the rocisa C++ dynamic_cast chain. + # ``_Instruction`` covers CommonInstruction / MacroInstruction / + # CompositeInstruction subclasses transitively. + _ALLOWED_TYPES: tuple = () # populated lazily in __init__ + + def __init__(self, name: str, args: List[Any]): + super().__init__(name=name) + self.itemList: List[Any] = [] + # The header MacroInstruction is what renders ``.macro NAME args`` + # via its own toString(). We never expose it via items() -- it's + # an internal rendering helper, not a child. + self.macro = _MacroInstruction(name, args) + + @classmethod + def _allowed(cls) -> tuple: + # Built once on first call; defers import-order dependencies. + if not cls._ALLOWED_TYPES: + cls._ALLOWED_TYPES = ( + _Instruction, Module, TextBlock, + ValueIf, ValueEndif, ValueElseIf, + ) + return cls._ALLOWED_TYPES + + def add(self, item: Any) -> Any: + if not isinstance(item, self._allowed()): + # Match rocisa's exception message verbatim so any string- + # matching consumer still works. + raise RuntimeError( + "unknown item type for Macro.add: " + str(item) + ) + item.parent = self + self.itemList.append(item) + return item + + def addT(self, cls, *args, **kwargs) -> Any: + # rocisa template helper: construct + add. + return self.add(cls(*args, **kwargs)) + + def addComment0(self, comment: str) -> None: + # NB: simpler than Module.addComment0 (single line vs 3-line + # banner) -- matches rocisa C++ exactly. + self.add(TextBlock("/* " + comment + " */\n")) + + def setItems(self, items: Sequence[Any]) -> None: + # rocisa C++ does plain ``itemList = items`` -- no type check, + # no reparent. Mirror exactly. + self.itemList = list(items) + + def items(self) -> List[Any]: + return self.itemList + + def prettyPrint(self, indent: str = "") -> str: + out = f'{indent}{type(self).__name__} "{self.name}"\n' + for it in self.itemList: + pp = getattr(it, "prettyPrint", None) + if callable(pp): + res = pp(indent + "|--") + if isinstance(res, str): + out += res + continue + out += f"{indent}|--{type(it).__name__}\n" + return out + + def toString(self) -> str: + # Layout: + # .macro
(header includes the trailing newline) + # + # (continuation lines NOT re-indented) + # ... + # .endm + # When a child's text contains ``s_set_vgpr_msb``, insert an extra + # 4-space indent right after the FIRST newline of that child + # (rocisa quirk to keep auto-inserted MSB toggles aligned). + s = ".macro " + self.macro.toString() + for it in self.itemList: + tmp = it.toString() if hasattr(it, "toString") else str(it) + if "s_set_vgpr_msb" in tmp: + pos = tmp.find("\n") + if pos != -1: + tmp = tmp[:pos + 1] + " " + tmp[pos + 1:] + s += " " + tmp + s += ".endm\n" + return s + + def __str__(self) -> str: + return self.toString() + + def __deepcopy__(self, memo): + clone = Macro.__new__(Macro) + memo[id(self)] = clone + # Item-inherited slots first. + clone.name = self.name + clone.parent = None + # Header MacroInstruction is cloned via its own __deepcopy__. + clone.macro = _copy.deepcopy(self.macro, memo) + clone.itemList = [] + for it in self.itemList: + new_it = _copy.deepcopy(it, memo) + new_it.parent = clone + clone.itemList.append(new_it) + return clone + + def __reduce__(self): + # rocisa Macro binding has no pickle support; mirror by raising. + raise RuntimeError("Macro is not picklable") + + +# --------------------------------------------------------------------------- +# Signature helpers (module-private) + public SignatureCodeMeta / SignatureBase +# --------------------------------------------------------------------------- +# +# ``_SignatureArgument`` and ``_SignatureKernelDescriptor`` mirror rocisa +# C++ internal structs; only ``SignatureCodeMeta`` and ``SignatureBase`` +# are exported (nanobind binds those two, not the helpers). + +_VALUE_TYPE_SIZE: dict = { + "i8": 1, "i16": 2, "i32": 4, "i64": 8, + "u8": 1, "u16": 2, "u32": 4, "u64": 8, + "bf16": 2, "f16": 2, "f32": 4, "f32c": 8, + "f64": 8, "f64c": 16, "pkf16": 4, "struct": 8, +} + + +def _sig_block(comment: str) -> str: + """Single-line block comment -- mirror of rocisa ``block()``.""" + return f"/* {comment} */\n" + + +def _sig_block3line(comment: str) -> str: + """Multi-line banner comment -- mirror of rocisa ``block3Line()``.""" + out = "\n/******************************************/\n" + for line in comment.splitlines(): + out += f"/* {line:<38} */\n" + out += "/******************************************/\n" + return out + + +def _sig_kind_is_global_buffer(kind: Any) -> bool: + return int(kind) == int(_SVK.SIG_GLOBALBUFFER) + + +def _sig_kind_is_value(kind: Any) -> bool: + return int(kind) == int(_SVK.SIG_VALUE) + + +class _SignatureArgument(Item): + """Internal kernarg descriptor leaf -- mirror of ``SignatureArgument``.""" + + __slots__ = ("valueKind", "valueType", "offset", "size", "addrSpaceQual") + + def __init__( + self, + offset: int, + name: str, + valueKind: Any, + valueType: str, + addrSpaceQual: str = "", + ) -> None: + super().__init__(name=name) + self.valueKind = valueKind + self.valueType = valueType + self.offset = int(offset) + self.size = self._value_to_size(valueKind, valueType) + self.addrSpaceQual = addrSpaceQual + + @staticmethod + def _value_to_size(valueKind: Any, valueType: str) -> int: + if _sig_kind_is_global_buffer(valueKind): + return 8 + try: + return _VALUE_TYPE_SIZE[valueType] + except KeyError as exc: + raise RuntimeError(f"Unknown value type: {valueType}") from exc + + def _value_kind_to_str(self) -> str: + if _sig_kind_is_global_buffer(self.valueKind): + return "global_buffer" + if _sig_kind_is_value(self.valueKind): + return "by_value" + raise RuntimeError("Unknown value kind") + + def toString(self) -> str: + indent = " " + out = f"{indent[2:]}- .name: {self.name}\n" + out += f"{indent}.size: {self.size}\n" + out += f"{indent}.offset: {self.offset}\n" + out += f"{indent}.value_kind: {self._value_kind_to_str()}\n" + out += f"{indent}.value_type: {self.valueType}\n" + if self.addrSpaceQual: + out += f"{indent}.address_space: {self.addrSpaceQual}\n" + return out + + def __str__(self) -> str: + return self.toString() + + +class _SignatureKernelDescriptor(Item): + """Internal ``.amdhsa_kernel`` header -- mirror of ``SignatureKernelDescriptor``.""" + + __slots__ = ( + "totalVgprs", "totalAgprs", "totalSgprs", "originalTotalVgprs", + "accumOffset", "groupSegSize", "sgprWorkGroup", "vgprWorkItem", + "numSgprPreload", + ) + + def __init__( + self, + name: str, + groupSegSize: int, + sgprWorkGroup: Sequence[int], + vgprWorkItem: int, + totalVgprs: int = 0, + totalAgprs: int = 0, + totalSgprs: int = 0, + numSgprPreload: int = 0, + ) -> None: + super().__init__(name=name) + self.groupSegSize = int(groupSegSize) + self.sgprWorkGroup = tuple(int(x) for x in sgprWorkGroup) + self.vgprWorkItem = int(vgprWorkItem) + self.totalAgprs = int(totalAgprs) + self.totalSgprs = int(totalSgprs) + self.originalTotalVgprs = int(totalVgprs) + self.numSgprPreload = int(numSgprPreload) + self._apply_gpr_layout(int(totalVgprs), int(totalAgprs)) + + def _apply_gpr_layout(self, total_vgprs: int, total_agprs: int) -> None: + if self.getArchCaps()["ArchAccUnifiedRegs"]: + self.accumOffset = ((total_vgprs + 7) // 8) * 8 + self.totalVgprs = self.accumOffset + total_agprs + else: + self.accumOffset = -1 + self.totalVgprs = total_vgprs + + def setGprs(self, totalVgprs: int, totalAgprs: int, totalSgprs: int) -> None: + if self.getArchCaps()["ArchAccUnifiedRegs"]: + self.accumOffset = ((totalVgprs + 7) // 8) * 8 + self.totalVgprs = self.accumOffset + totalAgprs + else: + self.accumOffset = -1 + self.totalVgprs = max(totalAgprs, totalVgprs) + self.originalTotalVgprs = int(totalVgprs) + self.totalAgprs = int(totalAgprs) + self.totalSgprs = int(totalSgprs) + + def getNextFreeVgpr(self) -> int: + return self.totalVgprs + + def getNextFreeSgpr(self) -> int: + return self.totalSgprs + + def toString(self) -> str: + kd_indent = " " + isa = self.kernel().isa + if isa is None: + raise RuntimeError("kernel ISA is not set") + out = _sig_block3line("Begin Kernel") + out += f'.amdgcn_target "amdgcn-amd-amdhsa--{_isa_to_gfx(isa)}"\n' + out += ".text\n" + out += f".protected {self.name}\n" + out += f".globl {self.name}\n" + out += ".p2align 8\n" + out += f".type {self.name},@function\n" + out += ".section .rodata,#alloc\n" + out += ".p2align 6\n" + out += f".amdhsa_kernel {self.name}\n" + out += f"{kd_indent}.amdhsa_user_sgpr_kernarg_segment_ptr 1\n" + if self.accumOffset != -1: + out += ( + f"{kd_indent}.amdhsa_accum_offset {self.accumOffset}" + " // accvgpr offset\n" + ) + out += ( + f"{kd_indent}.amdhsa_next_free_vgpr {self.totalVgprs}" + " // vgprs\n" + ) + out += ( + f"{kd_indent}.amdhsa_next_free_sgpr {self.totalSgprs}" + " // sgprs\n" + ) + out += ( + f"{kd_indent}.amdhsa_group_segment_fixed_size {self.groupSegSize}" + " // lds bytes\n" + ) + if self.getArchCaps()["HasWave32"]: + wavefront = self.kernel().wavefrontSize + if wavefront == 32: + out += ( + f"{kd_indent}.amdhsa_wavefront_size32 1" + " // 32-thread wavefronts\n" + ) + else: + out += ( + f"{kd_indent}.amdhsa_wavefront_size32 0" + " // 64-thread wavefronts\n" + ) + out += f"{kd_indent}.amdhsa_private_segment_fixed_size 0\n" + out += ( + f"{kd_indent}.amdhsa_system_sgpr_workgroup_id_x " + f"{self.sgprWorkGroup[0]}\n" + ) + out += ( + f"{kd_indent}.amdhsa_system_sgpr_workgroup_id_y " + f"{self.sgprWorkGroup[1]}\n" + ) + out += ( + f"{kd_indent}.amdhsa_system_sgpr_workgroup_id_z " + f"{self.sgprWorkGroup[2]}\n" + ) + out += ( + f"{kd_indent}.amdhsa_system_vgpr_workitem_id " + f"{self.vgprWorkItem}\n" + ) + out += f"{kd_indent}.amdhsa_float_denorm_mode_32 3\n" + out += f"{kd_indent}.amdhsa_float_denorm_mode_16_64 3\n" + if self.numSgprPreload: + # kernArg ptr (2 sgprs) is preloaded in user sgpr, but not counted + # in preload_length (rocisa code.hpp SignatureKernelDescriptor). + out += ( + f"{kd_indent}.amdhsa_user_sgpr_count " + f"{self.numSgprPreload + 2}\n" + ) + out += ( + f"{kd_indent}.amdhsa_user_sgpr_kernarg_preload_length " + f"{self.numSgprPreload}\n" + ) + out += ( + f"{kd_indent}.amdhsa_user_sgpr_kernarg_preload_offset 0\n" + ) + out += ".end_amdhsa_kernel\n" + out += ".text\n" + out += _sig_block(f"Num VGPR ={self.originalTotalVgprs}") + out += _sig_block(f"Num AccVGPR={self.totalAgprs}") + out += _sig_block(f"Num SGPR ={self.totalSgprs}") + return out + + def prettyPrint(self, indent: str = "") -> str: + return f"{indent}{type(self).__name__} " + + def __str__(self) -> str: + return self.toString() + + +class SignatureCodeMeta(Item): + """YAML ``.amdgpu_metadata`` trailer -- mirror of ``SignatureCodeMeta``.""" + + __slots__ = ( + "kernArgsVersion", "groupSegSize", "flatWgSize", "codeObjectVersion", + "totalVgprs", "totalSgprs", "offset", "argList", + ) + + def __init__( + self, + name: str, + kernArgsVersion: int, + groupSegSize: int, + flatWgSize: int, + codeObjectVersion: str, + totalVgprs: int = 0, + totalSgprs: int = 0, + ) -> None: + super().__init__(name=name) + self.kernArgsVersion = int(kernArgsVersion) + self.groupSegSize = int(groupSegSize) + self.flatWgSize = int(flatWgSize) + self.codeObjectVersion = str(codeObjectVersion) + self.totalVgprs = int(totalVgprs) + self.totalSgprs = int(totalSgprs) + self.offset = 0 + self.argList: List[_SignatureArgument] = [] + + def setGprs(self, totalVgprs: int, totalSgprs: int) -> None: + self.totalVgprs = int(totalVgprs) + self.totalSgprs = int(totalSgprs) + + def addArg( + self, + name: str, + kind: Any, + type: str, + addrSpaceQual: Optional[str] = None, + ) -> None: + qual = addrSpaceQual or "" + sa = _SignatureArgument(self.offset, name, kind, type, qual) + self.argList.append(sa) + self.offset += sa.size + + def toString(self) -> str: + out = ".amdgpu_metadata\n" + out += "---\n" + out += "custom.config:\n" + out += " InternalSupportParams:\n" + out += f" KernArgsVersion: {self.kernArgsVersion}\n" + out += "amdhsa.version:\n" + out += " - 1\n" + if self.codeObjectVersion in ("4", "default"): + out += " - 1\n" + elif self.codeObjectVersion == "5": + out += " - 2\n" + out += "amdhsa.kernels:\n" + out += f" - .name: {self.name}\n" + out += f" .symbol: '{self.name}.kd'\n" + out += " .language: OpenCL C\n" + out += " .language_version:\n" + out += " - 2\n" + out += " - 0\n" + out += " .args:\n" + for arg in self.argList: + out += arg.toString() + out += f" .group_segment_fixed_size: {self.groupSegSize}\n" + out += " .kernarg_segment_align: 8\n" + kernarg_size = ((self.offset + 7) // 8) * 8 + out += f" .kernarg_segment_size: {kernarg_size}\n" + out += f" .max_flat_workgroup_size: {self.flatWgSize}\n" + out += " .private_segment_fixed_size: 0\n" + out += f" .sgpr_count: {self.totalSgprs}\n" + out += " .sgpr_spill_count: 0\n" + out += f" .vgpr_count: {self.totalVgprs}\n" + out += " .vgpr_spill_count: 0\n" + out += ( + f" .wavefront_size: {self.kernel().wavefrontSize}\n" + ) + out += "...\n" + out += ".end_amdgpu_metadata\n" + out += f"{self.name}:\n" + return out + + def prettyPrint(self, indent: str = "") -> str: + return f"{indent}{type(self).__name__} " + + def __str__(self) -> str: + return self.toString() + + def __deepcopy__(self, memo): + raise RuntimeError("SignatureCodeMeta is not deepcopyable") + + def __reduce__(self): + raise RuntimeError("SignatureCodeMeta is not picklable") + + +class SignatureBase(Item): + """Full kernel signature -- mirror of ``SignatureBase``.""" + + __slots__ = ("kernelDescriptor", "codeMeta", "descriptionTopic", "descriptionList") + + def __init__( + self, + kernelName: str, + kernArgsVersion: int, + codeObjectVersion: str, + groupSegmentSize: int, + sgprWorkGroup: Sequence[int], + vgprWorkItem: int, + flatWorkGroupSize: int, + totalVgprs: int = 0, + totalAgprs: int = 0, + totalSgprs: int = 0, + numSgprPreload: int = 0, + ) -> None: + super().__init__(name=kernelName) + self.kernelDescriptor = _SignatureKernelDescriptor( + kernelName, + groupSegmentSize, + sgprWorkGroup, + vgprWorkItem, + totalVgprs, + totalAgprs, + totalSgprs, + numSgprPreload, + ) + self.codeMeta = SignatureCodeMeta( + kernelName, + kernArgsVersion, + groupSegmentSize, + flatWorkGroupSize, + codeObjectVersion, + totalVgprs, + totalSgprs, + ) + self.descriptionTopic = TextBlock("") + self.descriptionList: List[TextBlock] = [] + + def setGprs(self, totalVgprs: int, totalAgprs: int, totalSgprs: int) -> None: + self.kernelDescriptor.setGprs(totalVgprs, totalAgprs, totalSgprs) + self.codeMeta.setGprs(totalVgprs, totalSgprs) + + def addArg( + self, + name: str, + kind: Any, + type: str, + addrSpaceQual: Optional[str] = None, + ) -> None: + self.codeMeta.addArg(name, kind, type, addrSpaceQual) + + def addDescriptionTopic(self, text: str) -> None: + self.descriptionTopic = TextBlock(_sig_block3line(text)) + + def addDescriptionBlock(self, text: str) -> None: + self.descriptionList.append(TextBlock(_sig_block(text))) + + def addDescription(self, text: str) -> None: + self.descriptionList.append(TextBlock(_slash(text))) + + def getNextFreeVgpr(self) -> int: + return self.kernelDescriptor.getNextFreeVgpr() + + def getNextFreeSgpr(self) -> int: + return self.kernelDescriptor.getNextFreeSgpr() + + def clearDescription(self) -> None: + self.descriptionList.clear() + + def toString(self) -> str: + out = self.kernelDescriptor.toString() + topic = self.descriptionTopic.toString() + if topic: + out += topic + for block in self.descriptionList: + out += block.toString() + out += self.codeMeta.toString() + return out + + def prettyPrint(self, indent: str = "") -> str: + return f"{indent}{type(self).__name__} " + + def __str__(self) -> str: + return self.toString() + + def __deepcopy__(self, memo): + raise RuntimeError("SignatureBase is not deepcopyable") + + def __reduce__(self): + raise RuntimeError("SignatureBase is not picklable") + + +class KernelBody(Item): + """Top-level kernel wrapper -- mirror of ``rocisa::KernelBody``. + + Holds an optional ``SignatureBase`` (via ``addSignature``) and a + ``Module`` body (via ``addBody`` / the rw ``body`` attribute). + ``toString()`` emits the ``Begin Kernel`` banner, then signature, + then body; missing body raises ``RuntimeError`` (rocisa parity). + """ + + __slots__ = ( + "signature", "body", "totalVgprs", "totalAgprs", "totalSgprs", + ) + + def __init__(self, name: str) -> None: + super().__init__(name=name) + self.signature: Optional[SignatureBase] = None + self.body: Optional[Module] = None + self.totalVgprs: int = 0 + self.totalAgprs: int = 0 + self.totalSgprs: int = 0 + + def addSignature(self, signature: SignatureBase) -> None: + self.signature = signature + + def addBody(self, body: Module) -> None: + self.body = body + + def setGprs(self, totalVgprs: int, totalAgprs: int, totalSgprs: int) -> None: + self.totalVgprs = int(totalVgprs) + self.totalAgprs = int(totalAgprs) + self.totalSgprs = int(totalSgprs) + if self.signature is not None: + self.signature.setGprs(totalVgprs, totalAgprs, totalSgprs) + + def getNextFreeVgpr(self) -> int: + if self.signature is not None: + return self.signature.getNextFreeVgpr() + return 0 + + def getNextFreeSgpr(self) -> int: + if self.signature is not None: + return self.signature.getNextFreeSgpr() + return 0 + + def toString(self) -> str: + out = TextBlock(_sig_block3line("Begin Kernel")).toString() + if self.signature is not None: + out += self.signature.toString() + if self.body is not None: + out += self.body.toString() + else: + raise RuntimeError("Kernel body is empty") + return out + + def prettyPrint(self, indent: str = "") -> str: + return f"{indent}{type(self).__name__} " + + def __str__(self) -> str: + return self.toString() + + def __deepcopy__(self, memo): + raise RuntimeError("KernelBody is not deepcopyable") + + def __reduce__(self): + raise RuntimeError("KernelBody is not picklable") + + +# --------------------------------------------------------------------------- +# Dummy code-composition components (pending real implementation). +# --------------------------------------------------------------------------- +# +# Every dummy below inherits from the real ``Item`` base so: +# * ``isinstance(dummy_instance, Item)`` is True -- ``Module.findIndex +# ByType(Item)`` / KernelWriter type-walks see them as IR nodes. +# * ``dummy.toString()`` / ``dummy.prettyPrint()`` / +# ``dummy.countType()`` / ``dummy.countExactType()`` resolve to the +# real Item methods (not the no-op ``__getattr__`` shim), so a +# dummy dropped into a Module tree still produces sensible +# prettyPrint / count output during bring-up. +# +# Inheritance map (mirror of code.hpp): +# BitfieldUnion -- standalone polymorphic root in C++ +# (code.hpp:928, NOT a subclass of Item). Kept ``base=object`` +# so ``isinstance(bf, Item)`` correctly stays False; the C++ class +# is the polymorphic root for the ``SrdUpperValue*`` family. +# +# Already real (above this block): TextBlock, Module, ValueIf, +# ValueElseIf, ValueEndif, ValueSet, RegSet, Label, StructuredModule, +# Macro, SignatureCodeMeta, SignatureBase, KernelBody. + +BitfieldUnion = make_dummy_class(f"{_P}.BitfieldUnion") + + +# --------------------------------------------------------------------------- +# logicalIR-backed: gfx1250 SRD upper accessor. +# +# Soft-import so this package itself stays importable even when +# ``_stinkytofu.so`` hasn't been built yet (the rocisa dispatcher silently +# falls back to native bindings on any adapter import failure; we don't +# want that fallback triggered just because logicalIR is missing). +# ``SrdUpperValue`` re-raises a clear actionable error on first use. +# --------------------------------------------------------------------------- +try: + from stinkytofu import SrdUpperValue as _stinky_srd_upper_value # type: ignore[import-not-found] + + _STINKYTOFU_IMPORT_ERR: "ImportError | None" = None +except ImportError as _e: # pragma: no cover - exercised only without a build + _stinky_srd_upper_value = None + _STINKYTOFU_IMPORT_ERR = _e + + +def SrdUpperValue(isa): # noqa: N802 (matches rocisa public API) + """Forwarder matching ``rocisa::SrdUpperValue(IsaVersion)`` for gfx1250. + + Accepts either a 3-tuple/list (``kernel["ISA"]``-style) or a struct + with ``.major / .minor / .stepping`` (rocisa's ``IsaVersion``). + Only ``(12, 5, *)`` is supported today; ISA dispatch inside stinkytofu + is handled by ``createSrdUpperValue``. + """ + if _stinky_srd_upper_value is None: + raise ImportError( + "rocisa_stinkytofu_adaptor.code.SrdUpperValue requires the " + "stinkytofu Python binding (_stinkytofu.so). Build it via:\n" + " cmake --build --target stinkytofu_python\n" + "and ensure /tensilelite/rocisa/stinkytofu is on PYTHONPATH.\n" + f" Underlying error: {_STINKYTOFU_IMPORT_ERR}" + ) + if hasattr(isa, "major"): + major = int(isa.major) + minor = int(isa.minor) + stepping = int(isa.stepping) + else: + major = int(isa[0]) + minor = int(isa[1]) + stepping = int(isa[2]) if len(isa) > 2 else 0 + if (major, minor) != (12, 5): + raise NotImplementedError( + f"rocisa_stinkytofu_adaptor.code.SrdUpperValue is gfx1250-only; " + f"got ISA major={major}, minor={minor}." + ) + return _stinky_srd_upper_value((major, minor, stepping)) diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/container.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/container.py new file mode 100644 index 000000000000..c11f200b4266 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/container.py @@ -0,0 +1,1966 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Register references, factory helpers (sgpr/vgpr/accvgpr/mgpr), and asm modifiers. + +All register containers, holders, HW aliases (VCC/EXEC), and modifier +descriptors (DS/FLAT/GLOBAL/SMEM/SDWA/DPP/VOP3P/True16) are real. +""" + +from __future__ import annotations + +import math +from abc import ABC, abstractmethod +from copy import deepcopy +from typing import List, Optional, Tuple, Union + +from .caps import glc_bit_name_from_caps, slc_bit_name_from_caps +from .enum import CacheScope, HighBitSel, NonVolatile, SelectBit, TemporalHint, UnusedBit + +_P = "rocisa.container" + + +# --------------------------------------------------------------------------- +# Container ABC -- polymorphic base (rocisa::Container). +# --------------------------------------------------------------------------- +# +# C++ ``rocisa::Container`` exposes pure ``clone()`` / ``toString()``; nanobind +# subclasses inherit through it (``RegisterContainer``, modifiers, EXEC/VCC, …). +# Python mirrors that hierarchy via this ABC. +# +# Note: native rocisa can ``Container()`` via a nanobind trampoline +# (``PyContainer``); this ABC is not constructible, which matches the C++ +# abstract type more closely than the binding quirk. +class Container(ABC): + """Abstract base matching ``rocisa::Container`` (clone + toString).""" + + @abstractmethod + def toString(self) -> str: + """Return the asm operand / modifier token string.""" + + def clone(self) -> "Container": + """Deep copy (C++ ``clone()`` returns ``shared_ptr``).""" + return deepcopy(self) + + def __str__(self) -> str: + return self.toString() + + +# --------------------------------------------------------------------------- +# RegName -- symbolic register name + per-offset chain. +# --------------------------------------------------------------------------- +# +# Single source of truth for the (name, offsets) struct view that +# KernelWriter touches. Stinkytofu encodes the same idea as a +# "name+off1+off2+..." string on StinkyRegister.literalValue; that +# string is rehydrated only at to_stinky() time. +class RegName: + """Symbolic register name with optional offset chain. + + Encodings: + * ``str(rn)`` -> ``"name+off1+off2+..."``. + * ``rn.getTotalIdx()`` -> ``rocIsa.getVgprIdx()[name] + + sum(offsets)``. + """ + + __slots__ = ("name", "offsets", "nameIdx") + + def __init__(self, name: str = "", offsets: Optional[List[int]] = None) -> None: + self.name: str = name + # Copy on assign to avoid sharing the caller's list. + self.offsets: List[int] = list(offsets) if offsets else [] + self.nameIdx: int = 0 + + # --- Offset accessors. ------------------------------------------------ + + def getOffsets(self) -> List[int]: + return self.offsets + + def setOffset(self, i: int, offset: int) -> None: + if i >= len(self.offsets): + raise IndexError("Index out of range") + self.offsets[i] = offset + + def addOffset(self, offset: int) -> None: + self.offsets.append(offset) + + def getTotalOffsets(self) -> int: + return sum(self.offsets) + + # --- Symbol -> base-index resolution. --------------------------------- + + def setNameIdx(self) -> None: + # Lazy import: avoids pulling the rocIsa singleton at module + # import time (otherwise hits an adapter -> caps -> adapter loop). + from . import rocIsa # noqa: WPS433 (runtime import is intentional) + + self.nameIdx = rocIsa.getInstance().getVgprIdx().get(self.name, 0) + + def getTotalIdx(self) -> int: + self.setNameIdx() + return self.getTotalOffsets() + self.nameIdx + + # --- Dunder surface. -------------------------------------------------- + + def __eq__(self, other: object) -> bool: + if not isinstance(other, RegName): + return NotImplemented + return self.name == other.name and self.offsets == other.offsets + + def __ne__(self, other: object) -> bool: + eq = self.__eq__(other) + return NotImplemented if eq is NotImplemented else not eq + + def __hash__(self) -> int: + return hash((self.name, tuple(self.offsets))) + + def __str__(self) -> str: + if not self.offsets: + return self.name + return self.name + "".join("+" + str(o) for o in self.offsets) + + def __repr__(self) -> str: + return f"RegName(name={self.name!r}, offsets={self.offsets!r})" + + def __deepcopy__(self, memo: dict) -> "RegName": + return RegName(self.name, list(self.offsets)) + + def __copy__(self) -> "RegName": + return RegName(self.name, list(self.offsets)) + + # Pickle support. Used by KernelWriter when shipping kernels across + # worker processes. + def __getstate__(self) -> Tuple[str, List[int]]: + return (self.name, list(self.offsets)) + + def __setstate__(self, state: Tuple[str, List[int]]) -> None: + name, offsets = state + self.name = name + self.offsets = list(offsets) + self.nameIdx = 0 + + +# --------------------------------------------------------------------------- +# RegisterContainer -- register reference with rocisa-specific decorations. +# --------------------------------------------------------------------------- +# +# Stores the full attribute set (regType, regName, regIdx, regNum plus +# the isInlineAsm/isMacro/isOff/msb decoration flags) as plain Python +# fields and rebuilds a stinky.Register on demand via to_stinky(). +# +# Why not subclass / hold an eager stinky.Register? +# * Nanobind-exposed classes are not natural Python bases. +# * Most attributes (isInlineAsm/isMacro/isOff/msb, regName as a +# struct) have no stinkytofu equivalent; this wrapper has to be +# the single source of truth. +# * Lazy to_stinky avoids the build cost (and the unknown-regtype +# validation) for wrappers that never reach emit. +class RegisterContainer(Container): + """Reference to one or more architectural registers. + + Optional kwargs ``isAbs`` / ``isMacro`` / ``isOff`` mirror the + extended ctor variant. + + Attributes (all mutable): + regType (str) -- "v" / "s" / "a" / "m". + regName (RegName | None) + regIdx (int) + regNum (int) -- stored as ``int(ceil(regNum))``. + msb (int) -- transient, populated by ``setMsb``. + isInlineAsm, isMinus, isAbs, isMacro, isOff (bool) + """ + + __slots__ = ( + "regType", + "regName", + "regIdx", + "regNum", + "msb", + "isInlineAsm", + "isMinus", + "isAbs", + "isMacro", + "isOff", + ) + + def __init__( + self, + regType: str, + regName: Optional[RegName], + regIdx: int = 0, + regNum: float = 1, + *, + isAbs: bool = False, + isMacro: bool = False, + isOff: bool = False, + ) -> None: + self.regType: str = regType + self.regName: Optional[RegName] = regName + self.regIdx: int = regIdx + # Round up so regNum=1.5 -> 2. + self.regNum: int = int(math.ceil(regNum)) + self.msb: int = 0 + self.isInlineAsm: bool = False + self.isMinus: bool = False + self.isAbs: bool = isAbs + self.isMacro: bool = isMacro + self.isOff: bool = isOff + + # --- Setters. --------------------------------------------------------- + + def setInlineAsm(self, setting: bool) -> None: + self.isInlineAsm = setting + + def setMinus(self, isMinus: bool) -> None: + self.isMinus = isMinus + + def setAbs(self, isAbs: bool) -> None: + self.isAbs = isAbs + + def getMinus(self) -> "RegisterContainer": + """Return a copy with ``isMinus=True`` (for negation operands).""" + c = self._shallow_clone() + c.setMinus(True) + return c + + # --- regName mutation. ------------------------------------------------ + + def replaceRegName(self, srcName: str, dst: Union[int, str]) -> None: + """In-place substitute ``srcName`` in ``regName.name``. + + Two overloads: + * ``dst: int`` -> exact match collapses the symbolic name + into a literal index (``regIdx``); partial match substring- + replaces ``str(dst)``. + * ``dst: str`` -> substring-replace ``srcName`` with ``dst``. + """ + if self.regName is None: + return + if isinstance(dst, bool): + # bool is a subclass of int; reject so callers don't hit the + # int overload accidentally. + raise TypeError("replaceRegName dst must be int or str, not bool") + if isinstance(dst, int): + if self.regName.name == srcName: + self.regIdx = dst + self.regName.getTotalOffsets() + self.regName = None + return + pos = self.regName.name.find(srcName) + if pos != -1: + self.regName.name = ( + self.regName.name[:pos] + + str(dst) + + self.regName.name[pos + len(srcName) :] + ) + return + if isinstance(dst, str): + pos = self.regName.name.find(srcName) + if pos != -1: + self.regName.name = ( + self.regName.name[:pos] + dst + self.regName.name[pos + len(srcName) :] + ) + return + raise TypeError(f"replaceRegName dst must be int or str, not {type(dst).__name__}") + + # --- Composite name accessors. ---------------------------------------- + + def getRegNameWithType(self) -> str: + # Crashes if regName is None (caller's responsibility). + return self.regType + "gpr" + self.regName.name # type: ignore[union-attr] + + def getCompleteRegNameWithType(self) -> str: + return self.regType + "gpr" + str(self.regName) # type: ignore[arg-type] + + def getCompleteRegName(self) -> str: + return str(self.regName) # type: ignore[arg-type] + + # --- splitRegContainer. ----------------------------------------------- + + def splitRegContainer(self) -> Tuple["RegisterContainer", "RegisterContainer"]: + """Split into two halves; second half is offset by 1 reg. + + Returns two independent instances; caller can mutate either side. + """ + r1 = self._deep_clone() + r2 = self._deep_clone() + new_reg_num = math.ceil(self.regNum / 2) + if self.regName is not None: + r2.regName.addOffset(1) # type: ignore[union-attr] + else: + r2.regIdx += 1 + r1.regNum = new_reg_num + r2.regNum = self.regNum - new_reg_num + return (r1, r2) + + # --- msb (HasVgprMSB byte-pair encoding). ----------------------------- + + def setMsb(self) -> None: + if self.regName is not None: + self.msb = self.regName.getTotalIdx() // 256 + else: + self.msb = self.regIdx // 256 + + # --- Hash / equality. ------------------------------------------------- + + def __hash__(self) -> int: + return hash((self.regType, self.regIdx, self.regNum, self.regName)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, RegisterContainer): + return False + return ( + self.regType == other.regType + and self.regIdx == other.regIdx + and self.regNum == other.regNum + and self.regName == other.regName + ) + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + # --- Aliasing predicates. --------------------------------------------- + + def sameRegBaseAddr(self, b: "RegisterContainer") -> bool: + if self.regName is not None and b.regName is not None: + return self.regName.name == b.regName.name + if self.regName is None and b.regName is None: + return self.regIdx == b.regIdx + return False + + def __and__(self, b: "RegisterContainer") -> bool: + # Overlap check on the (offset, offset+regNum) interval. + if not self.sameRegBaseAddr(b): + return False + len_a = self.regNum + off_a = sum(self.regName.offsets) if self.regName is not None else 0 + len_b = b.regNum + off_b = sum(b.regName.offsets) if b.regName is not None else 0 + range_a = (off_a, off_a + len_a) + range_b = (off_b, off_b + len_b) + if range_a[0] > range_b[0]: + range_a, range_b = range_b, range_a + return range_a[1] > range_b[0] + + # --- Stringification. ------------------------------------------------- + + def toString(self) -> str: + if self.isOff: + return "off" + + minus_str = "-" if self.isMinus else "" + if self.isAbs: + minus_str = "abs(" + minus_str + abs_str = ")" if self.isAbs else "" + + # Empty when HasVgprMSB cap isn't set or rocIsa wasn't initialised, + # so __str__ stays side-effect-free for unit tests. + msb_str = self._msb_suffix() + + if self.isInlineAsm: + return minus_str + "%" + str(self.regIdx) + abs_str + + if self.regName is not None: + macro_slash = "\\" if self.isMacro else "" + if self.regNum == 1: + return ( + minus_str + + self.regType + + "[" + + macro_slash + + self.regType + + "gpr" + + str(self.regName) + + msb_str + + "]" + + abs_str + ) + return ( + minus_str + + self.regType + + "[" + + macro_slash + + self.regType + + "gpr" + + str(self.regName) + + msb_str + + ":" + + self.regType + + "gpr" + + str(self.regName) + + msb_str + + "+" + + str(self.regNum - 1) + + "]" + + abs_str + ) + + if self.regNum == 1: + if self.msb > 0: + return ( + minus_str + + self.regType + + "[" + + str(self.regIdx) + + msb_str + + "]" + + abs_str + ) + return minus_str + self.regType + str(self.regIdx) + abs_str + + return ( + minus_str + + self.regType + + "[" + + str(self.regIdx) + + msb_str + + ":" + + str(self.regIdx + self.regNum - 1) + + msb_str + + "]" + + abs_str + ) + + def _msb_suffix(self) -> str: + """Compute the ``-256*msb`` suffix appended to ``toString``.""" + try: + from . import rocIsa # lazy import; see RegName.setNameIdx + except ImportError: + return "" + inst = rocIsa.getInstance() + if not inst.isInit(): + return "" + try: + asm_caps = inst.getAsmCaps() + except RuntimeError: + return "" + if not asm_caps.get("HasVgprMSB", 0) or self.regType != "v": + return "" + self.setMsb() + if self.msb > 0: + return str(-256 * self.msb) + return "" + + def __str__(self) -> str: + return self.toString() + + def __repr__(self) -> str: + return ( + f"RegisterContainer(regType={self.regType!r}, regName={self.regName!r}, " + f"regIdx={self.regIdx}, regNum={self.regNum})" + ) + + # --- Copy semantics. -------------------------------------------------- + + def _shallow_clone(self) -> "RegisterContainer": + # regName is rebuilt as a new RegName so downstream mutation + # (e.g. getMinus -> setMinus) does not bleed back. + c = RegisterContainer.__new__(RegisterContainer) + c.regType = self.regType + c.regName = ( + RegName(self.regName.name, list(self.regName.offsets)) + if self.regName is not None + else None + ) + c.regIdx = self.regIdx + c.regNum = self.regNum + c.msb = self.msb + c.isInlineAsm = self.isInlineAsm + c.isMinus = self.isMinus + c.isAbs = self.isAbs + c.isMacro = self.isMacro + c.isOff = self.isOff + return c + + def _deep_clone(self) -> "RegisterContainer": + # Independent regName so splitRegContainer.addOffset(1) on the + # right half does not contaminate the left. + return self._shallow_clone() + + def __copy__(self) -> "RegisterContainer": + return self._shallow_clone() + + def __deepcopy__(self, memo: dict) -> "RegisterContainer": + return self._shallow_clone() + + def __getstate__(self) -> Tuple[str, Optional[RegName], int, int, bool, bool, bool, bool, bool, int]: + return ( + self.regType, + deepcopy(self.regName), + self.regIdx, + self.regNum, + self.isInlineAsm, + self.isMinus, + self.isAbs, + self.isMacro, + self.isOff, + self.msb, + ) + + def __setstate__( + self, + state: Tuple[str, Optional[RegName], int, int, bool, bool, bool, bool, bool, int], + ) -> None: + ( + self.regType, + self.regName, + self.regIdx, + self.regNum, + self.isInlineAsm, + self.isMinus, + self.isAbs, + self.isMacro, + self.isOff, + self.msb, + ) = state + + # --- logicalIR handoff ------------------------------------------------- + + def to_stinky(self): + """Build a fresh ``stinky.Register`` from this wrapper's state. + + ``RegisterContainer`` is the source of truth; the stinky register + is built fresh on each call so wrapper mutations + (``replaceRegName``, ``setMinus``, ``setAbs``) become visible + without an explicit sync step. rocisa-only emit decorations + (``isMacro`` / ``isInlineAsm`` / ``msb`` / ``isOff``-as-flag) are + translated here into stinky's vocabulary. + + Returns: + ``stinky.Register`` -- register-typed for normal wrappers, + ``LiteralString("off")`` for ``isOff=True``. + + Raises: + NotImplementedError: when ``isInlineAsm=True``. + + TODO: + - isMacro currently injects a backslash into the symbolic + name as a stop-gap; T6 should route macros through proper + TEXTBLOCK handling at the Module layer. + - isInlineAsm has no stinkytofu path; T6 Module layer must + gate inline-asm modules and route them back to rocisa + native via ``$ROCISA_BACKEND``. + """ + import stinkytofu as _stinky # noqa: WPS433 (runtime: optional dep) + + if self.isOff: + return _stinky.Register("off") + + if self.isInlineAsm: + raise NotImplementedError( + "RegisterContainer.to_stinky: isInlineAsm=True has no " + "stinkytofu emit path (rocisa-only %-operand format). " + "Module layer (T6) must route inline-asm modules back " + "to rocisa native." + ) + + # Resolve physical index. + if self.regName is not None and self.regType == "v": + physical_idx = self.regName.getTotalIdx() + else: + physical_idx = self.regIdx + + # Route through the rocisa-style helper that python_bindings.cpp + # exposes (vgpr/sgpr/accvgpr/mgpr). Keeps the emit boundary + # symmetric with the rocisa factory surface and avoids encoding + # the regType string twice. + helper = { + "v": _stinky.vgpr, + "s": _stinky.sgpr, + "acc": _stinky.accvgpr, + "m": _stinky.mgpr, + }.get(self.regType) + if helper is None: + raise NotImplementedError( + f"RegisterContainer.to_stinky: regType={self.regType!r} has " + "no stinkytofu helper. Add one in python_bindings.cpp first." + ) + reg = helper(physical_idx, self.regNum) + + # VGPR MSB offset: for idx >= 256, the asm emitter needs + # reg.offset = -(idx//256)*256 to produce "v[516-512]" format. + # Mirrors native C++ ToStinkyTofuUtils.cpp getMsbOffsetFromStinkyVgpr(). + if self.regType == "v" and physical_idx >= 256: + reg.set_offset(-(physical_idx // 256) * 256) + + if self.isMinus: + reg.set_minus(True) + if self.isAbs: + reg.set_abs(True) + + if self.regName is not None: + name = self.getRegNameWithType() + offsets = list(self.regName.offsets) + if self.isMacro: + name = "\\" + name + reg.set_reg_name(name, offsets) + + return reg + + +# --------------------------------------------------------------------------- +# generateRegName -- "Foo+1+2" -> RegName(name="Foo", offsets=[1, 2]). +# --------------------------------------------------------------------------- +# +# Parses a string-form symbolic ref back into a structured RegName. +# Inverse of RegName.__str__. Used by Holder(name=...) ctor. +def _generateRegName(rawText: str) -> RegName: + parts = rawText.split("+") + name = parts[0] + offsets = [int(p) for p in parts[1:]] if len(parts) > 1 else [] + return RegName(name, offsets) + + +# --------------------------------------------------------------------------- +# Holder -- deferred register reference resolved at replaceHolder time. +# --------------------------------------------------------------------------- +# +# An unresolved register reference: KernelWriter creates a Holder before +# knowing the final register index, hands it to an instruction inside a +# Module template, and later calls replaceHolder(module, dst) to resolve +# all Holders in the tree to concrete RegisterContainer instances. +# +# Two flavours: +# * Holder(idx) -- numeric base; resolved idx = holder_idx + dst +# * Holder(name) -- symbolic base ("Foo" or "Foo+1+2"); resolved +# regName = RegName(name, [dst, *parsed_offsets]) +class Holder: + """Deferred register reference (resolved later by ``replaceHolder``). + + Constructors: + * ``Holder(idx)`` -- int. Stores ``idx``, leaves ``name=None``. + * ``Holder(name)`` -- str. Stores ``idx=-1``, ``name= + generateRegName(name)``. + + Attributes (both mutable): + idx (int): numeric base, or ``-1`` when ``name`` set. + name (RegName | None): symbolic base, or ``None`` when ``idx`` set. + """ + + __slots__ = ("idx", "name") + + def __init__(self, *args, **kwargs) -> None: + # Dispatch on keyword first, then positional arg type. bool is + # rejected so callers don't accidentally hit the int overload. + if "idx" in kwargs and "name" not in kwargs: + arg = kwargs["idx"] + if isinstance(arg, bool): + raise TypeError("Holder(idx=...): idx must be int, not bool") + if not isinstance(arg, int): + raise TypeError( + f"Holder(idx=...): idx must be int, got {type(arg).__name__}" + ) + self.idx = arg + self.name = None + return + if "name" in kwargs and "idx" not in kwargs: + arg = kwargs["name"] + if not isinstance(arg, str): + raise TypeError( + f"Holder(name=...): name must be str, got {type(arg).__name__}" + ) + self.idx = -1 + self.name = _generateRegName(arg) + return + if kwargs: + raise TypeError( + "Holder() takes exactly one of (idx, name); got: " + ", ".join(kwargs) + ) + if len(args) != 1: + raise TypeError("Holder() takes exactly one positional arg (idx or name)") + arg = args[0] + if isinstance(arg, bool): + raise TypeError("Holder(): arg must be int or str, not bool") + if isinstance(arg, int): + self.idx = arg + self.name = None + elif isinstance(arg, str): + self.idx = -1 + self.name = _generateRegName(arg) + else: + raise TypeError( + f"Holder(): arg must be int or str, got {type(arg).__name__}" + ) + + def __repr__(self) -> str: + return f"Holder(idx={self.idx}, name={self.name!r})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Holder): + return NotImplemented + return self.idx == other.idx and self.name == other.name + + def __ne__(self, other: object) -> bool: + eq = self.__eq__(other) + return NotImplemented if eq is NotImplemented else not eq + + def __hash__(self) -> int: + return hash((self.idx, self.name)) + + def __copy__(self) -> "Holder": + c = Holder.__new__(Holder) + c.idx = self.idx + c.name = ( + RegName(self.name.name, list(self.name.offsets)) + if self.name is not None + else None + ) + return c + + def __deepcopy__(self, memo: dict) -> "Holder": + return self.__copy__() + + # Pickle support. + def __getstate__(self) -> Tuple[int, Optional[RegName]]: + return (self.idx, self.name) + + def __setstate__(self, state: Tuple[int, Optional[RegName]]) -> None: + self.idx, self.name = state + + +# --------------------------------------------------------------------------- +# HolderContainer -- RegisterContainer subclass with deferred resolution. +# --------------------------------------------------------------------------- +# +# Three ctors: +# * (regType, holderName: str, regNum) -- type 1, named via string +# * (regType, regName: RegName, regNum) -- type 1, named via RegName +# (preserves RegName's +# own offsets in holderOffsets) +# * (regType, holderIdx: int, regNum) -- type 0, numeric +# +# setRegNum(dst) is the deferred-resolution mutator: it mutates the +# parent RegisterContainer state in place using dst as the resolved +# offset/idx. replaceHolder() calls it before swapping the +# HolderContainer out for a plain RegisterContainer snapshot via +# getCopiedRC(). +class HolderContainer(RegisterContainer): + """Deferred-resolution ``RegisterContainer`` subclass. + + Two ``holderType`` modes: + * ``holderType == 0``: numeric -- ``setRegNum(num)`` resolves + ``regIdx = holderIdx + num``. + * ``holderType == 1``: named -- ``setRegNum(num)`` resolves + ``regName = RegName(holderName, [num, *holderOffsets])``. + + Attributes (all mutable): + holderName (str) + holderIdx (int) + holderType (int) -- 0 (numeric) or 1 (named) + holderOffsets (list[int]) + """ + + __slots__ = ("holderName", "holderIdx", "holderType", "holderOffsets") + + def __init__(self, regType: str, holderArg, regNum: float) -> None: + # bool is a subclass of int; reject so True/False doesn't fall + # into the numeric branch silently. + if isinstance(holderArg, bool): + raise TypeError( + "HolderContainer: 2nd arg must be str/RegName/int, not bool" + ) + if isinstance(holderArg, str): + # Named via string: parent gets bare RegName(holderName, []) + # so toString/emit still works before setRegNum lands. + super().__init__(regType, RegName(holderArg), 0, regNum) + self.holderName = holderArg + self.holderIdx = 0 + self.holderType = 1 + self.holderOffsets: List[int] = [] + elif isinstance(holderArg, RegName): + # Named via RegName: RegName.offsets become holderOffsets so + # setRegNum can later prepend dst and re-append them. + super().__init__(regType, holderArg, 0, regNum) + self.holderName = holderArg.name + self.holderIdx = 0 + self.holderType = 1 + self.holderOffsets = list(holderArg.offsets) + elif isinstance(holderArg, int): + # Numeric: parent regName=None, regIdx=holderIdx so toString + # prints a placeholder numeric register pre-resolution. + super().__init__(regType, None, holderArg, regNum) + self.holderName = "" + self.holderIdx = holderArg + self.holderType = 0 + self.holderOffsets = [] + else: + raise TypeError( + f"HolderContainer: 2nd arg must be str/RegName/int, " + f"got {type(holderArg).__name__}" + ) + + # --- Deferred resolution. --------------------------------------------- + + def setRegNum(self, num: int) -> None: + """Resolve the holder using ``num`` as the produced offset/idx. + + Mutates the parent RegisterContainer state in place. After this + call ``getCopiedRC()`` returns a fully-resolved snapshot. + """ + if self.holderType == 0: + self.regIdx = self.holderIdx + num + elif self.holderType == 1: + # Re-seed regName from holderName (drop stale offsets), then + # prepend num and append the saved holderOffsets. + self.regName = RegName(self.holderName) + self.regName.offsets.insert(0, num) + for off in self.holderOffsets: + self.regName.offsets.append(off) + + # --- Snapshot to plain RegisterContainer. ----------------------------- + + def getCopiedRC(self) -> "RegisterContainer": + """Snapshot current state as a plain ``RegisterContainer``. + + Called by ``replaceHolder`` after ``setRegNum`` to substitute the + HolderContainer with a fully-resolved RC. The snapshot is + independent: subsequent mutation of either side does not bleed. + """ + if self.holderType == 0: + return RegisterContainer(self.regType, None, self.regIdx, self.regNum) + return RegisterContainer( + self.regType, + RegName(self.regName.name, list(self.regName.offsets)) + if self.regName is not None + else None, + self.regIdx, + self.regNum, + ) + + # --- splitRegContainer override. -------------------------------------- + + def splitRegContainer(self) -> Tuple["HolderContainer", "HolderContainer"]: + """Split into two HolderContainer halves. + + Differs from the parent in how the right half is shifted: + + * type 1 (named): push ``1`` onto ``r2.regName.offsets``. + * type 0 (numeric): bump ``r2.holderIdx`` by 1 (the holder index + shifts, not the resolved ``regIdx`` -- so subsequent + ``setRegNum`` on r2 produces a shifted result). + """ + r1 = self._shallow_clone() + r2 = self._shallow_clone() + new_reg_num = math.ceil(self.regNum / 2) + if self.holderName: + # Named: parent regName is already set so r2.regName is safe + # to mutate. + r2.regName.addOffset(1) # type: ignore[union-attr] + else: + r2.holderIdx += 1 + r1.regNum = new_reg_num + r2.regNum = self.regNum - new_reg_num + return (r1, r2) + + # --- Copy semantics. -------------------------------------------------- + + def _shallow_clone(self) -> "HolderContainer": # type: ignore[override] + c = HolderContainer.__new__(HolderContainer) + # Parent fields. + c.regType = self.regType + c.regName = ( + RegName(self.regName.name, list(self.regName.offsets)) + if self.regName is not None + else None + ) + c.regIdx = self.regIdx + c.regNum = self.regNum + c.msb = self.msb + c.isInlineAsm = self.isInlineAsm + c.isMinus = self.isMinus + c.isAbs = self.isAbs + c.isMacro = self.isMacro + c.isOff = self.isOff + # Subclass fields. + c.holderName = self.holderName + c.holderIdx = self.holderIdx + c.holderType = self.holderType + c.holderOffsets = list(self.holderOffsets) + return c + + def __copy__(self) -> "HolderContainer": # type: ignore[override] + return self._shallow_clone() + + def __deepcopy__(self, memo: dict) -> "HolderContainer": # type: ignore[override] + return self._shallow_clone() + + # --- Pickle. ---------------------------------------------------------- + + def __getstate__( # type: ignore[override] + self, + ) -> Tuple[str, int, int, str, Optional[RegName], int, int]: + return ( + self.holderName, + self.holderIdx, + self.holderType, + self.regType, + deepcopy(self.regName), + self.regIdx, + self.regNum, + ) + + def __setstate__( # type: ignore[override] + self, + state: Tuple[str, int, int, str, Optional[RegName], int, int], + ) -> None: + ( + holder_name, + holder_idx, + holder_type, + reg_type, + reg_name, + reg_idx, + reg_num, + ) = state + # Restore the snapshot verbatim, skipping ctor's holderType-derived + # state setup. RC flags default to False (not preserved in state). + self.regType = reg_type + self.regName = reg_name + self.regIdx = reg_idx + self.regNum = reg_num + self.msb = 0 + self.isInlineAsm = False + self.isMinus = False + self.isAbs = False + self.isMacro = False + self.isOff = False + self.holderName = holder_name + self.holderIdx = holder_idx + self.holderType = holder_type + # holderOffsets is not in the state tuple; re-derive from regName. + self.holderOffsets = ( + list(reg_name.offsets) if reg_name is not None and reg_name.offsets else [] + ) + + +# --------------------------------------------------------------------------- +# replaceHolder -- in-place holder resolution walker. +# --------------------------------------------------------------------------- +# +# Walks an instruction / module tree; when a HolderContainer is found in +# an Instruction's parameter list, calls its setRegNum(dst) and replaces +# the slot with the resolved RegisterContainer snapshot. +# +# Tree shape follows real ``Module.items()`` / ``Instruction.getParams()`` +# from this adaptor (historical T6 note removed). +def replaceHolder(inst, dst: int): + """Resolve all ``HolderContainer``s in ``inst`` using ``dst``. + + Walks Module-like (``.items()``) and Instruction-like + (``.getParams()``) objects. For each ``HolderContainer`` found in an + Instruction's parameter list, calls ``setRegNum(dst)`` then replaces + the slot with the holder's ``getCopiedRC()`` (a plain + ``RegisterContainer`` snapshot). Returns ``inst`` unchanged + structurally; the resolution is in-place on the param lists. + + Args: + inst: A Module-like, Instruction-like, or arbitrary object. + Unknown types are passed through unchanged. + dst (int): The base value to feed into each holder's + ``setRegNum``. + + Returns: + The same ``inst`` (mutated in place for Module/Instruction; left + untouched otherwise). + + Raises: + RuntimeError: if ``inst`` is an ``SWaitCnt`` (intentional gap). + """ + # Detect by class name to avoid importing the dummy SWaitCnt class. + if type(inst).__name__ == "SWaitCnt": + raise RuntimeError("SWaitCnt is not supported yet") + + items_method = getattr(inst, "items", None) + if callable(items_method): + # Module-like: recurse into each child. + for item in items_method(): + replaceHolder(item, dst) + return inst + + get_params = getattr(inst, "getParams", None) + if callable(get_params): + # Instruction-like: walk parameter list, mutate any + # HolderContainer slot in place. + params = get_params() + try: + indices = range(len(params)) + except TypeError: + # Non-indexable iterable (e.g. generator): can't mutate in place. + return inst + for i in indices: + param = params[i] + if isinstance(param, HolderContainer): + param.setRegNum(dst) + params[i] = param.getCopiedRC() + return inst + + # Unknown / leaf object: return as-is. + return inst + + +# --------------------------------------------------------------------------- +# GPR factory functions -- vgpr / sgpr / accvgpr / mgpr. +# --------------------------------------------------------------------------- +# +# Mirror of rocisa createGPR + the four type-specific factory wrappers. +# Three dispatch forms per factory: Holder / int / str. Each form maps +# to either a RegisterContainer (int / str) or a HolderContainer (Holder). +def _create_gpr( + gpr_type: str, + arg, + reg_num: float, + *, + isMacro: bool = False, + isAbs: bool = False, + isOff: bool = False, +) -> RegisterContainer: + """Internal dispatcher (rocisa::createGPR analogue). + + Dispatches on ``arg`` type: ``Holder`` → ``HolderContainer``, + ``int`` → numeric ``RegisterContainer``, ``str`` → symbolic + ``RegisterContainer`` whose ``regIdx`` defaults to ``-1``. + """ + if isinstance(arg, Holder): + if arg.idx == -1: + return HolderContainer(gpr_type, arg.name, reg_num) + return HolderContainer(gpr_type, arg.idx, reg_num) + if isinstance(arg, bool): + # bool is a subclass of int; reject so callers don't drift into + # the int overload by accident. + raise TypeError( + f"{gpr_type}gpr factory: positional arg must be Holder/int/str, not bool" + ) + if isinstance(arg, int): + return RegisterContainer(gpr_type, None, arg, reg_num) + if isinstance(arg, str): + return RegisterContainer( + gpr_type, + _generateRegName(arg), + -1, + reg_num, + isAbs=isAbs, + isMacro=isMacro, + isOff=isOff, + ) + raise TypeError( + f"{gpr_type}gpr factory: arg must be Holder/int/str, got {type(arg).__name__}" + ) + + +def vgpr( + arg, + regNum: float = 1.0, + isMacro: bool = False, + isAbs: bool = False, + isOff: bool = False, +) -> RegisterContainer: + """Build a VGPR ``RegisterContainer`` (or ``HolderContainer``). + + Three forms: ``vgpr(holder, regNum)``, ``vgpr(idx, regNum)``, + ``vgpr(name, regNum, isMacro, isAbs, isOff)``. + """ + return _create_gpr("v", arg, regNum, isMacro=isMacro, isAbs=isAbs, isOff=isOff) + + +def sgpr(arg, regNum: float = 1.0, isMacro: bool = False) -> RegisterContainer: + """Build an SGPR ``RegisterContainer`` (or ``HolderContainer``). + + String form accepts ``isMacro`` only (rocisa signature parity: + ``sgpr(name, regNum, isMacro)`` -- no ``isAbs`` / ``isOff``). + """ + return _create_gpr("s", arg, regNum, isMacro=isMacro) + + +def accvgpr(arg, regNum: float = 1.0) -> RegisterContainer: + """Build an accumulator VGPR ``RegisterContainer`` (rocisa type ``"acc"``). + + No modifier kwargs in rocisa signature. + """ + return _create_gpr("acc", arg, regNum) + + +def mgpr(arg, regNum: float = 1.0) -> RegisterContainer: + """Build an MGPR (memory descriptor) ``RegisterContainer``. + + No modifier kwargs in rocisa signature. + """ + return _create_gpr("m", arg, regNum) + + +# --------------------------------------------------------------------------- +# ContinuousRegister -- POD {idx, size} value object. +# --------------------------------------------------------------------------- +# +# RegisterPool yields one of these from each allocTmpGpr / allocTmpVgpr +# context; KernelWriter then reads .idx / .size off it. rocisa exposes +# the fields as ``def_ro`` so the instance is effectively immutable; +# we mirror that semantics in Python. +class ContinuousRegister: + """Immutable ``{idx, size}`` register-range descriptor. + + Constructor: ``ContinuousRegister(idx, size)`` (positional or + keyword). Both fields are read-only after construction; mutate by + building a fresh instance. + """ + + __slots__ = ("_idx", "_size", "_frozen") + + def __init__(self, idx: int, size: int) -> None: + # bool is an int subclass; reject so True/False don't slip in. + if isinstance(idx, bool) or isinstance(size, bool): + raise TypeError("ContinuousRegister: idx/size must be int, not bool") + if not isinstance(idx, int) or not isinstance(size, int): + raise TypeError( + f"ContinuousRegister: idx/size must be int, got " + f"{type(idx).__name__}/{type(size).__name__}" + ) + object.__setattr__(self, "_idx", idx) + object.__setattr__(self, "_size", size) + object.__setattr__(self, "_frozen", True) + + @property + def idx(self) -> int: + return self._idx + + @property + def size(self) -> int: + return self._size + + def __setattr__(self, name: str, value) -> None: + if getattr(self, "_frozen", False): + raise AttributeError( + f"ContinuousRegister is read-only; cannot set {name!r}" + ) + object.__setattr__(self, name, value) + + def __repr__(self) -> str: + return f"ContinuousRegister(idx={self._idx}, size={self._size})" + + def __copy__(self) -> "ContinuousRegister": + return ContinuousRegister(self._idx, self._size) + + def __deepcopy__(self, memo: dict) -> "ContinuousRegister": + return ContinuousRegister(self._idx, self._size) + + def __getstate__(self) -> Tuple[int, int]: + return (self._idx, self._size) + + def __setstate__(self, state: Tuple[int, int]) -> None: + idx, size = state + object.__setattr__(self, "_idx", idx) + object.__setattr__(self, "_size", size) + object.__setattr__(self, "_frozen", True) + + +def _kernel_wavefront_size() -> int: + """Current kernel wavefront from ``rocIsa.getInstance().getKernel()``.""" + from . import rocIsa # lazy: avoid import cycle at module load + + return int(rocIsa.getInstance().getKernel().wavefrontSize) + + +# --------------------------------------------------------------------------- +# Hardware register tokens (T4) -- EXEC / VCC / HWRegContainer family. +# --------------------------------------------------------------------------- + + +class EXEC(Container): + """Execution mask token; ``toString()`` depends on wavefront size.""" + + __slots__ = ("setHi",) + + def __init__(self, setHi: bool = False) -> None: + self.setHi = bool(setHi) + + def toString(self) -> str: + if _kernel_wavefront_size() == 64: + return "exec" + return "exec_lo" + + def __repr__(self) -> str: + return f"EXEC(setHi={self.setHi!r})" + + def __copy__(self) -> "EXEC": + return EXEC(self.setHi) + + def __deepcopy__(self, memo: dict) -> "EXEC": + return EXEC(self.setHi) + + def __getstate__(self) -> Tuple[bool]: + return (self.setHi,) + + def __setstate__(self, state: Tuple[bool]) -> None: + self.setHi = state[0] + + +class EXECLO(Container): + """Low half of EXEC (32-lane wavefronts).""" + + __slots__ = () + + def toString(self) -> str: + return "exec_lo" + + def __repr__(self) -> str: + return "EXECLO()" + + def __copy__(self) -> "EXECLO": + return EXECLO() + + def __deepcopy__(self, memo: dict) -> "EXECLO": + return EXECLO() + + def __getstate__(self) -> Tuple[()]: + return () + + def __setstate__(self, state: Tuple[()]) -> None: + pass + + +class EXECHI(Container): + """High half of EXEC (32-lane wavefronts).""" + + __slots__ = () + + def toString(self) -> str: + return "exec_hi" + + def __repr__(self) -> str: + return "EXECHI()" + + def __copy__(self) -> "EXECHI": + return EXECHI() + + def __deepcopy__(self, memo: dict) -> "EXECHI": + return EXECHI() + + def __getstate__(self) -> Tuple[()]: + return () + + def __setstate__(self, state: Tuple[()]) -> None: + pass + + +class VCC(Container): + """Vector condition code token; ``toString()`` depends on wavefront / setHi.""" + + __slots__ = ("setHi",) + + def __init__(self, setHi: bool = False) -> None: + self.setHi = bool(setHi) + + def toString(self) -> str: + if _kernel_wavefront_size() == 64: + return "vcc" + return "vcc_hi" if self.setHi else "vcc_lo" + + def __repr__(self) -> str: + return f"VCC(setHi={self.setHi!r})" + + def __copy__(self) -> "VCC": + return VCC(self.setHi) + + def __deepcopy__(self, memo: dict) -> "VCC": + return VCC(self.setHi) + + def __getstate__(self) -> Tuple[bool]: + return (self.setHi,) + + def __setstate__(self, state: Tuple[bool]) -> None: + self.setHi = state[0] + + +class HWRegContainer(Container): + """Hardware register immediate bundle for ``SSetRegIMM32B32`` operands.""" + + __slots__ = ("reg", "value") + + def __init__(self, reg: str, value: List[int]) -> None: + self.reg = reg + self.value = list(value) + + def toString(self) -> str: + parts = ",".join(str(v) for v in self.value) + return f"hwreg({self.reg},{parts})" + + def __repr__(self) -> str: + return f"HWRegContainer(reg={self.reg!r}, value={self.value!r})" + + def __copy__(self) -> "HWRegContainer": + return HWRegContainer(self.reg, self.value) + + def __deepcopy__(self, memo: dict) -> "HWRegContainer": + return HWRegContainer(self.reg, list(self.value)) + + def __getstate__(self) -> Tuple[str, List[int]]: + return (self.reg, list(self.value)) + + def __setstate__(self, state: Tuple[str, List[int]]) -> None: + self.reg, self.value = state[0], list(state[1]) + + +# --------------------------------------------------------------------------- +# MemTokenData -- virtual scheduling fence metadata (T5). +# --------------------------------------------------------------------------- +# +# Attached to instructions via setMemToken(); emits no asm by itself but +# carries token ids for the dependency graph. C++ binding exposes a +# mutable ``tokens`` vector (def_rw). +class MemTokenData(Container): + """Memory-token list carried on fence/barrier instructions. + + Constructor: ``MemTokenData(tokens=[])``. ``tokens`` is a mutable + list of ints (mirrors C++ ``std::vector``). + """ + + __slots__ = ("tokens",) + + def __init__(self, tokens: Optional[List[int]] = None) -> None: + self.tokens: List[int] = list(tokens) if tokens is not None else [] + + def toString(self) -> str: + result = "mem_token:" + for i, tok in enumerate(self.tokens): + if i > 0: + result += "," + result += f" {tok}" + return result + + def __repr__(self) -> str: + return f"MemTokenData(tokens={self.tokens!r})" + + def __copy__(self) -> "MemTokenData": + return MemTokenData(self.tokens) + + def __deepcopy__(self, memo: dict) -> "MemTokenData": + return MemTokenData(list(self.tokens)) + + def __getstate__(self) -> List[int]: + return list(self.tokens) + + def __setstate__(self, state: List[int]) -> None: + self.tokens = list(state) + + +# --------------------------------------------------------------------------- +# Instruction modifier descriptors (T5.2) — rocisa::Container subclasses. +# --------------------------------------------------------------------------- + + +def _asm_caps() -> dict: + """Active ISA asm caps (requires ``rocIsa.init`` or ``setKernel``).""" + from . import rocIsa # noqa: WPS433 + + return rocIsa.getInstance().getAsmCaps() + + +def _cache_scope_to_string(scope: CacheScope) -> str: + """Mirror ``rocisa::toString(CacheScope)`` (``enum.hpp``).""" + if scope == CacheScope.SCOPE_NONE: + return "" + return scope.name + + +def _select_bit_to_string(bit: SelectBit) -> str: + if bit == SelectBit.SEL_NONE: + return "" + return bit.name + + +def _unused_bit_to_string(bit: UnusedBit) -> str: + if bit == UnusedBit.UNUSED_NONE: + return "" + return bit.name + + +def _int_vector_to_string(vec: List[int]) -> str: + inner = ",".join(str(v) for v in vec) + return f"[{inner}]" + + +def _has_temporal_hint(th: TemporalHint) -> bool: + """Mirror ``rocisa::hasTemporalHint`` (``enum.hpp``).""" + return th != TemporalHint.TH_NONE + + +def _temporal_hint_to_string(th: TemporalHint, is_store: bool) -> str: + """Mirror ``rocisa::toString(TemporalHint, bool isStore)`` (``enum.hpp``).""" + prefix = "TH_STORE_" if is_store else "TH_LOAD_" + if th == TemporalHint.TH_RT: + return prefix + "RT" + if th == TemporalHint.TH_NT: + return prefix + "NT" + if th == TemporalHint.TH_HT: + return prefix + "HT" + if th in (TemporalHint.TH_LU, TemporalHint.TH_WB): + return prefix + ("WB" if is_store else "LU") + if th == TemporalHint.TH_NT_RT: + return prefix + "NT_RT" + if th == TemporalHint.TH_RT_NT: + return prefix + "RT_NT" + if th == TemporalHint.TH_NT_HT: + return prefix + "NT_HT" + if th in (TemporalHint.TH_RESERVED, TemporalHint.TH_NT_WB): + return prefix + ("NT_WB" if is_store else "RESERVED") + return "" + + +def _non_volatile_to_string(nv: NonVolatile) -> str: + """Mirror ``rocisa::toString(NonVolatile)`` (``enum.hpp``).""" + return "nv" if nv == NonVolatile.NV else "" + + +class DSModifiers(Container): + """LDS/GDS modifier bundle (``rocisa::DSModifiers``).""" + + __slots__ = ("na", "offset", "offset0", "offset1", "gds") + + def __init__( + self, + na: int = 1, + offset: int = 0, + offset0: int = 0, + offset1: int = 0, + gds: bool = False, + ) -> None: + self.na = na + self.offset = offset + self.offset0 = offset0 + self.offset1 = offset1 + self.gds = gds + + def toString(self) -> str: + k_str = "" + if self.na == 1: + k_str += f" offset:{self.offset}" + elif self.na == 2: + k_str += f" offset0:{self.offset0} offset1:{self.offset1}" + if self.gds: + k_str += " gds" + return k_str + + def __repr__(self) -> str: + return ( + f"DSModifiers(na={self.na!r}, offset={self.offset!r}, " + f"offset0={self.offset0!r}, offset1={self.offset1!r}, gds={self.gds!r})" + ) + + def __copy__(self) -> "DSModifiers": + return DSModifiers(self.na, self.offset, self.offset0, self.offset1, self.gds) + + def __deepcopy__(self, memo: dict) -> "DSModifiers": + return DSModifiers(self.na, self.offset, self.offset0, self.offset1, self.gds) + + def __getstate__(self) -> Tuple[int, int, int, int, bool]: + return (self.na, self.offset, self.offset0, self.offset1, self.gds) + + def __setstate__(self, state: Tuple[int, int, int, int, bool]) -> None: + self.na, self.offset, self.offset0, self.offset1, self.gds = state + + +class FLATModifiers(Container): + """FLAT memory modifier bundle (``rocisa::FLATModifiers``).""" + + __slots__ = ( + "offset12", "glc", "slc", "dlc", "lds", "isStore", "scope", "th", "nv", + ) + + def __init__( + self, + offset12: int = 0, + glc: bool = False, + slc: bool = False, + dlc: bool = False, + lds: bool = False, + isStore: bool = False, + scope: CacheScope = CacheScope.SCOPE_NONE, + th: TemporalHint = TemporalHint.TH_NONE, + nv: NonVolatile = NonVolatile.NV_NONE, + ) -> None: + self.offset12 = offset12 + self.glc = glc + self.slc = slc + self.dlc = dlc + self.lds = lds + self.isStore = isStore + self.scope = scope + self.th = th + self.nv = nv + + def toString(self) -> str: + caps = _asm_caps() + has_dlc = bool(caps.get("HasDLCModifier")) + has_scope = bool(caps.get("HasSCOPEModifier")) + has_th = bool(caps.get("HasTHModifier")) + has_nv = bool(caps.get("HasNVModifier")) + k_str = "" + if self.offset12 != 0: + k_str += f" offset:{self.offset12}" + if self.glc: + k_str += f" {glc_bit_name_from_caps(caps)}" + if self.slc: + k_str += f" {slc_bit_name_from_caps(caps)}" + if has_dlc and self.dlc: + k_str += " dlc" + if has_scope and self.scope != CacheScope.SCOPE_NONE: + k_str += f" scope:{_cache_scope_to_string(self.scope)}" + if has_th and _has_temporal_hint(self.th): + k_str += " th:" + _temporal_hint_to_string(self.th, self.isStore) + if has_nv and self.nv != NonVolatile.NV_NONE: + k_str += " " + _non_volatile_to_string(self.nv) + if self.lds: + k_str += " lds" + return k_str + + def __repr__(self) -> str: + return ( + f"FLATModifiers(offset12={self.offset12!r}, glc={self.glc!r}, " + f"slc={self.slc!r}, dlc={self.dlc!r}, lds={self.lds!r}, " + f"isStore={self.isStore!r}, scope={self.scope!r}, " + f"th={self.th!r}, nv={self.nv!r})" + ) + + def __copy__(self) -> "FLATModifiers": + return FLATModifiers( + self.offset12, self.glc, self.slc, self.dlc, + self.lds, self.isStore, self.scope, self.th, self.nv, + ) + + def __deepcopy__(self, memo: dict) -> "FLATModifiers": + return self.__copy__() + + def __getstate__( + self, + ) -> Tuple[int, bool, bool, bool, bool, bool, int, int, int]: + return ( + self.offset12, self.glc, self.slc, self.dlc, + self.lds, self.isStore, int(self.scope), int(self.th), int(self.nv), + ) + + def __setstate__( + self, state: Tuple[int, bool, bool, bool, bool, bool, int, int, int], + ) -> None: + ( + self.offset12, self.glc, self.slc, self.dlc, + self.lds, self.isStore, scope_val, th_val, nv_val, + ) = state + self.scope = CacheScope(scope_val) + self.th = TemporalHint(th_val) + self.nv = NonVolatile(nv_val) + + +class GLOBALModifiers(Container): + """GLOBAL memory modifier bundle (``rocisa::GLOBALModifiers``).""" + + __slots__ = ("offset", "th", "scope") + + def __init__( + self, + offset: int = 0, + th: TemporalHint = TemporalHint.TH_NONE, + scope: CacheScope = CacheScope.SCOPE_NONE, + ) -> None: + self.offset = offset + self.th = th + self.scope = scope + + def toString(self) -> str: + k_str = "" + if self.offset != 0: + k_str += f" offset:{self.offset}" + if _has_temporal_hint(self.th): + k_str += " th:" + _temporal_hint_to_string(self.th, False) + if self.scope != CacheScope.SCOPE_NONE: + k_str += f" scope:{_cache_scope_to_string(self.scope)}" + return k_str + + def __repr__(self) -> str: + return ( + f"GLOBALModifiers(offset={self.offset!r}, th={self.th!r}, " + f"scope={self.scope!r})" + ) + + def __copy__(self) -> "GLOBALModifiers": + return GLOBALModifiers(self.offset, self.th, self.scope) + + def __deepcopy__(self, memo: dict) -> "GLOBALModifiers": + return GLOBALModifiers(self.offset, self.th, self.scope) + + def __getstate__(self) -> Tuple[int, int, int]: + return (self.offset, int(self.th), int(self.scope)) + + def __setstate__(self, state: Tuple[int, int, int]) -> None: + self.offset, th_val, scope_val = state + self.th = TemporalHint(th_val) + self.scope = CacheScope(scope_val) + + +class MUBUFModifiers(Container): + """MUBUF memory modifier bundle (``rocisa::MUBUFModifiers``).""" + + __slots__ = ( + "offen", "offset12", "glc", "slc", "dlc", "nt", "lds", "isStore", + "scope", "th", "nv", + ) + + def __init__( + self, + offen: bool = False, + offset12: int = 0, + glc: bool = False, + slc: bool = False, + dlc: bool = False, + nt: bool = False, + lds: bool = False, + isStore: bool = False, + scope: CacheScope = CacheScope.SCOPE_NONE, + th: TemporalHint = TemporalHint.TH_NONE, + nv: NonVolatile = NonVolatile.NV_NONE, + ) -> None: + self.offen = offen + self.offset12 = offset12 + self.glc = glc + self.slc = slc + self.dlc = dlc + self.nt = nt + self.lds = lds + self.isStore = isStore + self.scope = scope + self.th = th + self.nv = nv + + def toString(self) -> str: + caps = _asm_caps() + has_dlc = bool(caps.get("HasDLCModifier")) + has_scope = bool(caps.get("HasSCOPEModifier")) + has_nt = bool(caps.get("HasNTModifier")) + has_th = bool(caps.get("HasTHModifier")) + has_nv = bool(caps.get("HasNVModifier")) + k_str = "" + if self.offen: + k_str += f" offen offset:{self.offset12}" + if self.glc: + k_str += f" {glc_bit_name_from_caps(caps)}" + if self.slc: + k_str += f" {slc_bit_name_from_caps(caps)}" + if has_dlc and self.dlc: + k_str += " dlc" + if has_scope and self.scope != CacheScope.SCOPE_NONE: + k_str += f" scope:{_cache_scope_to_string(self.scope)}" + if has_th and _has_temporal_hint(self.th): + k_str += " th:" + _temporal_hint_to_string(self.th, self.isStore) + elif has_nt and self.nt: + k_str += " nt" + if has_nv and self.nv != NonVolatile.NV_NONE: + k_str += " " + _non_volatile_to_string(self.nv) + if self.lds: + k_str += " lds" + return k_str + + def __repr__(self) -> str: + return ( + f"MUBUFModifiers(offen={self.offen!r}, offset12={self.offset12!r}, " + f"glc={self.glc!r}, slc={self.slc!r}, dlc={self.dlc!r}, " + f"nt={self.nt!r}, lds={self.lds!r}, isStore={self.isStore!r}, " + f"scope={self.scope!r}, th={self.th!r}, nv={self.nv!r})" + ) + + def __copy__(self) -> "MUBUFModifiers": + return MUBUFModifiers( + self.offen, self.offset12, self.glc, self.slc, self.dlc, + self.nt, self.lds, self.isStore, self.scope, self.th, self.nv, + ) + + def __deepcopy__(self, memo: dict) -> "MUBUFModifiers": + return self.__copy__() + + def __getstate__( + self, + ) -> Tuple[bool, int, bool, bool, bool, bool, bool, bool, int, int, int]: + return ( + self.offen, self.offset12, self.glc, self.slc, self.dlc, + self.nt, self.lds, self.isStore, int(self.scope), + int(self.th), int(self.nv), + ) + + def __setstate__( + self, + state: Tuple[bool, int, bool, bool, bool, bool, bool, bool, int, int, int], + ) -> None: + ( + self.offen, self.offset12, self.glc, self.slc, self.dlc, + self.nt, self.lds, self.isStore, scope_val, th_val, nv_val, + ) = state + self.scope = CacheScope(scope_val) + self.th = TemporalHint(th_val) + self.nv = NonVolatile(nv_val) + + +class SMEMModifiers(Container): + """SMEM modifier bundle (``rocisa::SMEMModifiers``).""" + + __slots__ = ("glc", "dlc", "offset", "isStore", "scope", "th", "nv") + + def __init__( + self, + glc: bool = False, + dlc: bool = False, + offset: int = 0, + isStore: bool = False, + scope: CacheScope = CacheScope.SCOPE_NONE, + th: TemporalHint = TemporalHint.TH_NONE, + nv: NonVolatile = NonVolatile.NV_NONE, + ) -> None: + self.glc = glc + self.dlc = dlc + self.offset = offset + self.isStore = isStore + self.scope = scope + self.th = th + self.nv = nv + + def toString(self) -> str: + caps = _asm_caps() + has_dlc = bool(caps.get("HasDLCModifier")) + has_scope = bool(caps.get("HasSCOPEModifier")) + has_th = bool(caps.get("HasTHModifier")) + has_nv = bool(caps.get("HasNVModifier")) + k_str = "" + if self.offset != 0: + k_str += f" offset:{self.offset}" + if not has_scope and self.glc: + k_str += " glc" + if has_dlc and self.dlc: + k_str += " dlc" + if has_scope and self.scope != CacheScope.SCOPE_NONE: + k_str += f" scope:{_cache_scope_to_string(self.scope)}" + if has_th and _has_temporal_hint(self.th): + k_str += " th:" + _temporal_hint_to_string(self.th, self.isStore) + if has_nv and self.nv != NonVolatile.NV_NONE: + k_str += " " + _non_volatile_to_string(self.nv) + return k_str + + def __repr__(self) -> str: + return ( + f"SMEMModifiers(glc={self.glc!r}, dlc={self.dlc!r}, " + f"offset={self.offset!r}, isStore={self.isStore!r}, " + f"scope={self.scope!r}, th={self.th!r}, nv={self.nv!r})" + ) + + def __copy__(self) -> "SMEMModifiers": + return SMEMModifiers( + self.glc, self.dlc, self.offset, self.isStore, + self.scope, self.th, self.nv, + ) + + def __deepcopy__(self, memo: dict) -> "SMEMModifiers": + return SMEMModifiers( + self.glc, self.dlc, self.offset, self.isStore, + self.scope, self.th, self.nv, + ) + + def __getstate__(self) -> Tuple[bool, bool, int, bool, int, int, int]: + return ( + self.glc, self.dlc, self.offset, self.isStore, + int(self.scope), int(self.th), int(self.nv), + ) + + def __setstate__(self, state: Tuple[bool, bool, int, bool, int, int, int]) -> None: + ( + self.glc, self.dlc, self.offset, self.isStore, + scope_val, th_val, nv_val, + ) = state + self.scope = CacheScope(scope_val) + self.th = TemporalHint(th_val) + self.nv = NonVolatile(nv_val) + + +class SDWAModifiers(Container): + """SDWA modifier bundle (``rocisa::SDWAModifiers``).""" + + __slots__ = ("dst_sel", "dst_unused", "src0_sel", "src1_sel") + + def __init__( + self, + dst_sel: SelectBit = SelectBit.SEL_NONE, + dst_unused: UnusedBit = UnusedBit.UNUSED_NONE, + src0_sel: SelectBit = SelectBit.SEL_NONE, + src1_sel: SelectBit = SelectBit.SEL_NONE, + ) -> None: + self.dst_sel = dst_sel + self.dst_unused = dst_unused + self.src0_sel = src0_sel + self.src1_sel = src1_sel + + def toString(self) -> str: + k_str = "" + if self.dst_sel != SelectBit.SEL_NONE: + k_str += f" dst_sel:{_select_bit_to_string(self.dst_sel)}" + if self.dst_unused != UnusedBit.UNUSED_NONE: + k_str += f" dst_unused:{_unused_bit_to_string(self.dst_unused)}" + if self.src0_sel != SelectBit.SEL_NONE: + k_str += f" src0_sel:{_select_bit_to_string(self.src0_sel)}" + if self.src1_sel != SelectBit.SEL_NONE: + k_str += f" src1_sel:{_select_bit_to_string(self.src1_sel)}" + return k_str + + def __repr__(self) -> str: + return ( + f"SDWAModifiers(dst_sel={self.dst_sel!r}, " + f"dst_unused={self.dst_unused!r}, src0_sel={self.src0_sel!r}, " + f"src1_sel={self.src1_sel!r})" + ) + + def __copy__(self) -> "SDWAModifiers": + return SDWAModifiers( + self.dst_sel, self.dst_unused, self.src0_sel, self.src1_sel, + ) + + def __deepcopy__(self, memo: dict) -> "SDWAModifiers": + return SDWAModifiers( + self.dst_sel, self.dst_unused, self.src0_sel, self.src1_sel, + ) + + def __getstate__(self) -> Tuple[int, int, int, int]: + return ( + int(self.dst_sel), int(self.dst_unused), + int(self.src0_sel), int(self.src1_sel), + ) + + def __setstate__(self, state: Tuple[int, int, int, int]) -> None: + self.dst_sel = SelectBit(state[0]) + self.dst_unused = UnusedBit(state[1]) + self.src0_sel = SelectBit(state[2]) + self.src1_sel = SelectBit(state[3]) + + +class DPPModifiers(Container): + """DPP modifier bundle (``rocisa::DPPModifiers``).""" + + __slots__ = ("row_shr", "row_bcast", "bound_ctrl", "quad_perm") + + def __init__( + self, + row_shr: int = -1, + row_bcast: int = -1, + bound_ctrl: int = -1, + quad_perm: Optional[List[int]] = None, + ) -> None: + self.row_shr = row_shr + self.row_bcast = row_bcast + self.bound_ctrl = bound_ctrl + self.quad_perm: List[int] = list(quad_perm) if quad_perm else [] + + def toString(self) -> str: + k_str = "" + if self.row_shr != -1: + k_str += f" row_shr:{self.row_shr}" + if self.row_bcast != -1: + k_str += f" row_bcast:{self.row_bcast}" + if self.bound_ctrl != -1: + k_str += f" bound_ctrl:{self.bound_ctrl}" + if self.quad_perm: + k_str += f" quad_perm:{_int_vector_to_string(self.quad_perm)}" + return k_str + + def __repr__(self) -> str: + return ( + f"DPPModifiers(row_shr={self.row_shr!r}, row_bcast={self.row_bcast!r}, " + f"bound_ctrl={self.bound_ctrl!r}, quad_perm={self.quad_perm!r})" + ) + + def __copy__(self) -> "DPPModifiers": + return DPPModifiers( + self.row_shr, self.row_bcast, self.bound_ctrl, self.quad_perm, + ) + + def __deepcopy__(self, memo: dict) -> "DPPModifiers": + return DPPModifiers( + self.row_shr, self.row_bcast, self.bound_ctrl, list(self.quad_perm), + ) + + def __getstate__(self) -> Tuple[int, int, int, List[int]]: + return (self.row_shr, self.row_bcast, self.bound_ctrl, list(self.quad_perm)) + + def __setstate__(self, state: Tuple[int, int, int, List[int]]) -> None: + self.row_shr, self.row_bcast, self.bound_ctrl, self.quad_perm = state + self.quad_perm = list(self.quad_perm) + + +class VOP3PModifiers(Container): + """VOP3P modifier bundle (``rocisa::VOP3PModifiers``).""" + + __slots__ = ("op_sel", "op_sel_hi", "byte_sel") + + def __init__( + self, + op_sel: Optional[List[int]] = None, + op_sel_hi: Optional[List[int]] = None, + byte_sel: Optional[List[int]] = None, + ) -> None: + self.op_sel: List[int] = list(op_sel) if op_sel else [] + self.op_sel_hi: List[int] = list(op_sel_hi) if op_sel_hi else [] + self.byte_sel: List[int] = list(byte_sel) if byte_sel else [] + + def toString(self) -> str: + k_str = "" + if self.op_sel: + k_str += f" op_sel:{_int_vector_to_string(self.op_sel)}" + if self.op_sel_hi: + k_str += f" op_sel_hi:{_int_vector_to_string(self.op_sel_hi)}" + if self.byte_sel: + k_str += f" byte_sel:{_int_vector_to_string(self.byte_sel)}" + return k_str + + def __repr__(self) -> str: + return ( + f"VOP3PModifiers(op_sel={self.op_sel!r}, " + f"op_sel_hi={self.op_sel_hi!r}, byte_sel={self.byte_sel!r})" + ) + + def __copy__(self) -> "VOP3PModifiers": + return VOP3PModifiers(self.op_sel, self.op_sel_hi, self.byte_sel) + + def __deepcopy__(self, memo: dict) -> "VOP3PModifiers": + return VOP3PModifiers( + list(self.op_sel), list(self.op_sel_hi), list(self.byte_sel), + ) + + def __getstate__(self) -> Tuple[List[int], List[int], List[int]]: + return (list(self.op_sel), list(self.op_sel_hi), list(self.byte_sel)) + + def __setstate__(self, state: Tuple[List[int], List[int], List[int]]) -> None: + self.op_sel, self.op_sel_hi, self.byte_sel = state + self.op_sel = list(self.op_sel) + self.op_sel_hi = list(self.op_sel_hi) + self.byte_sel = list(self.byte_sel) + + +class True16Modifiers(Container): + """True16 high/low selector (``rocisa::True16Modifiers``).""" + + __slots__ = ("high_bit",) + + def __init__(self, high_bit: Union[HighBitSel, int] = HighBitSel.NONE) -> None: + if isinstance(high_bit, int): + self.high_bit = HighBitSel(high_bit) + else: + self.high_bit = high_bit + + def toString(self) -> str: + if self.high_bit == HighBitSel.NONE: + return "" + return ".h" if self.high_bit == HighBitSel.HIGH else ".l" + + def __repr__(self) -> str: + return f"True16Modifiers(high_bit={self.high_bit!r})" + + def __copy__(self) -> "True16Modifiers": + return True16Modifiers(self.high_bit) + + def __deepcopy__(self, memo: dict) -> "True16Modifiers": + return True16Modifiers(self.high_bit) + + def __getstate__(self) -> Tuple[int]: + return (int(self.high_bit),) + + def __setstate__(self, state: Tuple[int]) -> None: + self.high_bit = HighBitSel(state[0]) diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/enum.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/enum.py new file mode 100644 index 000000000000..f7d0c45758ec --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/enum.py @@ -0,0 +1,138 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Real IntEnum definitions mirroring rocisa/src/enum.cpp 1:1. + +All enums (RegisterType, DataTypeEnum, InstType, SelectBit, etc.) are fully implemented. +""" + +from __future__ import annotations + +from ._dummy import export_enum_values, make_bound_enum, make_dummy_enum + +_P = "rocisa.enum" + + +_RegisterType_values = ["Vgpr", "Sgpr", "Accvgpr", "mgpr"] +RegisterType = make_dummy_enum(f"{_P}.RegisterType", _RegisterType_values) +export_enum_values(globals(), RegisterType, _RegisterType_values) + + +_DataTypeEnum_values = [ + "Float", "Double", "ComplexFloat", "ComplexDouble", "Half", + "Int8x4", "Int32", "BFloat16", "Int8", "Int64", "XFloat32", + "Float8_fnuz", "BFloat8_fnuz", "Float8BFloat8_fnuz", "BFloat8Float8_fnuz", + "Float8", "BFloat8", "Float8BFloat8", "BFloat8Float8", + "Float6", "BFloat6", "Float4", "E8", "E5M3", +] +DataTypeEnum = make_dummy_enum(f"{_P}.DataTypeEnum", _DataTypeEnum_values) +export_enum_values(globals(), DataTypeEnum, _DataTypeEnum_values) + + +_SignatureValueKind_values = ["SIG_VALUE", "SIG_GLOBALBUFFER"] +SignatureValueKind = make_dummy_enum(f"{_P}.SignatureValueKind", _SignatureValueKind_values) +export_enum_values(globals(), SignatureValueKind, _SignatureValueKind_values) + + +# Member order follows ``rocisa::enum.cpp`` ``InstType`` binding; values +# follow ``rocisa::InstType`` in ``enum.hpp``. +_InstType_members = [ + ("INST_E5M3", 65), ("INST_E8", 64), ("INST_F4", 44), ("INST_F6", 42), + ("INST_BF6", 43), ("INST_F6_B6", 61), ("INST_B6_F6", 62), ("INST_F8", 1), + ("INST_F16", 2), ("INST_F32", 3), ("INST_F64", 4), ("INST_I8", 5), + ("INST_I16", 6), ("INST_I32", 7), ("INST_U8", 8), ("INST_U16", 9), + ("INST_U32", 10), ("INST_U64", 11), ("INST_LO_I32", 12), ("INST_HI_I32", 13), + ("INST_LO_U32", 14), ("INST_HI_U32", 15), ("INST_BF16", 16), ("INST_B8", 17), + ("INST_B16", 18), ("INST_B32", 19), ("INST_B64", 20), ("INST_B128", 21), + ("INST_B256", 23), ("INST_B512", 24), ("INST_B8_HI_D16", 25), + ("INST_D16_U8", 26), ("INST_D16_HI_U8", 27), ("INST_D16_U16", 28), + ("INST_D16_HI_U16", 29), ("INST_D16_B8", 30), ("INST_D16_HI_B8", 31), + ("INST_D16_B16", 32), ("INST_D16_HI_B16", 33), ("INST_XF32", 34), + ("INST_BF8", 35), ("INST_F8_BF8", 36), ("INST_BF8_F8", 37), + ("INST_TR8_B64", 38), ("INST_TR16_B128", 39), ("INST_F8_F4", 45), + ("INST_F4_F8", 46), ("INST_F6_F4", 47), ("INST_F4_F6", 48), + ("INST_F8_F6", 49), ("INST_F6_F8", 50), ("INST_F8_B6", 51), + ("INST_B6_F8", 52), ("INST_B8_F4", 53), ("INST_F4_B8", 54), + ("INST_B6_F4", 55), ("INST_F4_B6", 56), ("INST_B8_F6", 57), + ("INST_F6_B8", 58), ("INST_B8_B6", 59), ("INST_B6_B8", 60), + ("INST_CVT", 40), ("INST_MACRO", 41), ("INST_B192", 22), + ("INST_NOTYPE", 68), +] +_InstType_values = [name for name, _ in _InstType_members] +InstType = make_bound_enum(f"{_P}.InstType", _InstType_members) +export_enum_values(globals(), InstType, _InstType_values) + + +_SelectBit_values = [ + "SEL_NONE", "DWORD", "BYTE_0", "BYTE_1", "BYTE_2", "BYTE_3", + "WORD_0", "WORD_1", +] +SelectBit = make_dummy_enum(f"{_P}.SelectBit", _SelectBit_values) +export_enum_values(globals(), SelectBit, _SelectBit_values) + + +_UnusedBit_values = ["UNUSED_NONE", "UNUSED_PAD", "UNUSED_SEXT", "UNUSED_PRESERVE"] +UnusedBit = make_dummy_enum(f"{_P}.UnusedBit", _UnusedBit_values) +export_enum_values(globals(), UnusedBit, _UnusedBit_values) + + +_CacheScope_values = ["SCOPE_NONE", "SCOPE_CU", "SCOPE_SE", "SCOPE_DEV", "SCOPE_SYS"] +CacheScope = make_dummy_enum(f"{_P}.CacheScope", _CacheScope_values) +export_enum_values(globals(), CacheScope, _CacheScope_values) + + +_TemporalHint_members = [ + ("TH_NONE", -1), + ("TH_RT", 0), + ("TH_NT", 1), + ("TH_HT", 2), + ("TH_LU", 3), + ("TH_NT_RT", 4), + ("TH_RT_NT", 5), + ("TH_NT_HT", 6), + ("TH_RESERVED", 7), + ("TH_WB", 3), + ("TH_NT_WB", 7), +] +_TemporalHint_values = [name for name, _ in _TemporalHint_members] +TemporalHint = make_bound_enum(f"{_P}.TemporalHint", _TemporalHint_members) +export_enum_values(globals(), TemporalHint, _TemporalHint_values) + + +_NonVolatile_members = [ + ("NV_NONE", 0), + ("NV", 1), +] +_NonVolatile_values = [name for name, _ in _NonVolatile_members] +NonVolatile = make_bound_enum(f"{_P}.NonVolatile", _NonVolatile_members) +export_enum_values(globals(), NonVolatile, _NonVolatile_values) + + +_CvtType_values = [ + "CVT_F16_to_F32", "CVT_F32_to_F16", "CVT_U32_to_F32", "CVT_F32_to_U32", + "CVT_U32_to_F64", "CVT_F64_to_U32", "CVT_I32_to_F32", "CVT_F32_to_I32", + "CVT_FP8_to_F32", "CVT_BF8_to_F32", "CVT_PK_FP8_to_F32", "CVT_PK_BF8_to_F32", + "CVT_PK_F32_to_FP8", "CVT_PK_F32_to_BF8", "CVT_SR_F32_to_FP8", + "CVT_SR_F32_to_BF8", +] +CvtType = make_dummy_enum(f"{_P}.CvtType", _CvtType_values) +export_enum_values(globals(), CvtType, _CvtType_values) + + +_ArgType_values = ["DST", "DST1", "SRC0"] +ArgType = make_dummy_enum(f"{_P}.ArgType", _ArgType_values) +export_enum_values(globals(), ArgType, _ArgType_values) + + +_HighBitSel_values = ["NONE", "LOW", "HIGH"] +HighBitSel = make_dummy_enum(f"{_P}.HighBitSel", _HighBitSel_values) +export_enum_values(globals(), HighBitSel, _HighBitSel_values) + + +_RoundType_values = ["ROUND_UP", "ROUND_TO_NEAREST_EVEN"] +RoundType = make_dummy_enum(f"{_P}.RoundType", _RoundType_values) +export_enum_values(globals(), RoundType, _RoundType_values) + + +_SaturateCastType_values = ["NORMAL", "DO_NOTHING", "UPPER", "LOWER"] +SaturateCastType = make_dummy_enum(f"{_P}.SaturateCastType", _SaturateCastType_values) +export_enum_values(globals(), SaturateCastType, _SaturateCastType_values) diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/functions.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/functions.py new file mode 100644 index 000000000000..95133790abb5 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/functions.py @@ -0,0 +1,1193 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Composite KernelWriter helpers (ArgumentLoader, math, branch). + +Real: ArgumentLoader, vector/scalar divide, magic division, branch helpers. +Not yet done: VSaturateCastInt, DSInit. +""" + +from __future__ import annotations + +import math +from typing import Any + +from ._dummy import make_dummy_func +from .code import Module, TextBlock +from .container import ContinuousRegister, EXEC, VCC, sgpr, vgpr +from .enum import DataTypeEnum +from .instruction import ( + SAddCU32, SAddU32, SAndB32, SAndB64, SCBranchSCC0, SCBranchSCC1, + SCBranchVCCNZ, SCBranchVCCZ, SCmpEQU32, SCmpEQU64, SCmpLgU32, + SLShiftLeftB32, SLShiftLeftB64, SLShiftRightB32, SLShiftRightB64, + SLoadB32, SLoadB64, SLoadB128, SLoadB256, SLoadB512, + SMulHIU32, SMulI32, SMovB32, SMovB64, SNop, SSubU32, + VAddCCOU32, VAddLShiftLeftU32, VAddU32, VAndB32, VCmpEQF32, + VCmpEQF64, VCmpNeU32, VCmpXEqU32, VCmpXGeU32, VCmpXGtU32, + VCvtF32toU32, VCvtF64toU32, VCvtU32toF32, VCvtU32toF64, + VLShiftLeftAddU32, VLShiftLeftB32, VLShiftLeftB64, + VLShiftRightB32, VLShiftRightB64, VMadU32U24, + VMovB32, VMulF32, VMulF64, VMulHIU32, VMulLOU32, VMulU32U24, + VRcpF64, VRcpIFlagF32, VReadfirstlaneB32, VSubU32, +) + +_P = "rocisa.functions" + +_SLOAD_BY_BITS = { + 32: SLoadB32, + 64: SLoadB64, + 128: SLoadB128, + 256: SLoadB256, + 512: SLoadB512, +} + + +class ArgumentLoader: + """Port of ``rocisa::ArgumentLoader`` (``functions/argument.hpp``). + + Manages kernel-argument offset bookkeeping and emits ``SLoadB*`` + instructions matching the C++ logic exactly. + """ + + __slots__ = ("_kernArgOffset",) + + def __init__(self) -> None: + self._kernArgOffset: int = 0 + + def resetOffset(self) -> None: + self._kernArgOffset = 0 + + def setOffset(self, offset: int) -> None: + self._kernArgOffset = int(offset) + + def getOffset(self) -> int: + return self._kernArgOffset + + def loadKernArg(self, dst, srcAddr, sgprOffset=None, dword: int = 1, + writeSgpr: bool = True) -> Any: + """Emit an ``SLoadB*`` and advance ``kernArgOffset``. + + Mirrors ``functions/argument.hpp:60-121``. + """ + size = int(dword) * 4 + + if writeSgpr: + if sgprOffset is not None: + comment = "" + else: + comment = str(self._kernArgOffset) + + dst_sgpr = sgpr(dst, dword) + src_sgpr = sgpr(srcAddr, 2) + offset = sgprOffset if sgprOffset is not None else self._kernArgOffset + + bits = int(dword) * 32 + cls = _SLOAD_BY_BITS.get(bits) + if cls is None: + raise ValueError(f"Invalid dword size {dword}") + item = cls(dst=dst_sgpr, base=src_sgpr, soffset=offset, comment=comment) + else: + item = TextBlock("Move offset by " + str(size) + "\n") + + if sgprOffset is None: + self._kernArgOffset += size + return item + + def loadAllKernArg(self, sgprStartIndex: int, srcAddr, numSgprToLoad: int, + numSgprPreload: int = 0) -> Module: + """Emit a ``Module`` of greedy-packed ``SLoadB*`` instructions. + + Mirrors ``functions/argument.hpp:126-199``. + """ + module = Module("LoadAllKernArg") + actual_load = int(numSgprToLoad) - int(numSgprPreload) + sgpr_idx = int(sgprStartIndex) + int(numSgprPreload) + self._kernArgOffset += int(numSgprPreload) * 4 + + while actual_load > 0: + i = 16 + while i >= 1: + is_aligned = False + if i >= 4 and sgpr_idx % 4 == 0: + is_aligned = True + elif i == 2 and sgpr_idx % 2 == 0: + is_aligned = True + elif i == 1: + is_aligned = True + + if is_aligned and actual_load >= i: + actual_load -= i + dst_sgpr = sgpr(sgpr_idx, i) + src_sgpr = sgpr(srcAddr, 2) + comment = str(self._kernArgOffset) + + bits = i * 32 + cls = _SLOAD_BY_BITS.get(bits) + if cls is None: + raise ValueError(f"Invalid SGPR size {i}") + module.add(cls(dst=dst_sgpr, base=src_sgpr, + soffset=self._kernArgOffset, comment=comment)) + sgpr_idx += i + self._kernArgOffset += i * 4 + break + i //= 2 + + return module + + +def _sgpr_with_offset(sgprName: str, offset: int) -> str: + try: + base = int(sgprName) + return str(base + offset) + except ValueError: + return sgprName + "+" + str(offset) + + +def BranchIfZero(sgprName: str, computeDataType, tmpSgprIdx: int, + laneSC: int, label, waveFrontSize: int) -> Module: + module = Module("BranchIfZero") + sgprStr = "s[" + sgprName + "]" + pVCC = VCC() + + if computeDataType == DataTypeEnum.ComplexDouble: + tmpSgpr = sgpr(tmpSgprIdx, laneSC) + module.add(VCmpEQF64(dst=tmpSgpr, src0=sgpr(sgprName, 2), src1=0.0, + comment=sgprStr + ".real == 0.0 ?")) + sgprVar = _sgpr_with_offset(sgprName, 2) + module.add(VCmpEQF64(dst=pVCC, src0=sgpr(sgprVar, 2), src1=0.0, + comment=sgprStr + ".imag == 0.0 ?")) + if waveFrontSize == 32: + module.add(SAndB32(dst=tmpSgpr, src0=pVCC, src1=tmpSgpr, + comment=sgprStr + " == 0 ?")) + module.add(SCmpEQU32(src0=tmpSgpr, src1=0, + comment="branch if " + sgprStr + " == 0")) + else: + module.add(SAndB64(dst=tmpSgpr, src0=pVCC, src1=tmpSgpr, + comment=sgprStr + " == 0 ?")) + module.add(SCmpEQU64(src0=tmpSgpr, src1=0, + comment="branch if " + sgprStr + " == 0")) + module.add(SCBranchSCC0(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " == 0")) + + elif computeDataType == DataTypeEnum.Double: + module.add(VCmpEQF64(dst=pVCC, src0=sgpr(sgprName, 2), src1=0.0, + comment=sgprStr + " == 0.0 ?")) + module.add(SCBranchVCCNZ(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " == 0")) + + elif computeDataType == DataTypeEnum.ComplexFloat: + tmpSgpr = sgpr(tmpSgprIdx, laneSC) + module.add(VCmpEQF32(dst=tmpSgpr, src0=sgpr(sgprName), src1=0.0, + comment=sgprStr + ".real == 0.0f ?")) + sgprVar = _sgpr_with_offset(sgprName, 1) + module.add(VCmpEQF32(dst=pVCC, src0=sgpr(sgprVar), src1=0.0, + comment=sgprStr + ".imag == 0.0f ?")) + if waveFrontSize == 32: + module.add(SAndB32(dst=tmpSgpr, src0=pVCC, src1=tmpSgpr, + comment=sgprStr + " == 0 ?")) + module.add(SCmpEQU32(src0=tmpSgpr, src1=0, + comment="branch if " + sgprStr + " == 0")) + else: + module.add(SAndB64(dst=tmpSgpr, src0=pVCC, src1=tmpSgpr, + comment=sgprStr + " == 0 ?")) + module.add(SCmpEQU64(src0=tmpSgpr, src1=0, + comment="branch if " + sgprStr + " == 0")) + module.add(SCBranchSCC0(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " == 0")) + + elif computeDataType in (DataTypeEnum.Float, DataTypeEnum.Half, + DataTypeEnum.BFloat16): + module.add(VCmpEQF32(dst=pVCC, src0=sgpr(sgprName), src1=0.0, + comment=sgprStr + " == 0.0f ?")) + module.add(SCBranchVCCNZ(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " == 0")) + + elif computeDataType == DataTypeEnum.Int32: + module.add(SCmpEQU32(src0=sgpr(sgprName), src1=0, + comment=sgprStr + " == 0 ?")) + module.add(SCBranchSCC1(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " == 0")) + + elif computeDataType == DataTypeEnum.Int64: + module.add(SCmpEQU64(src0=sgpr(sgprName, 2), src1=0, + comment=sgprStr + " == 0 ?")) + module.add(SCBranchSCC1(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " == 0")) + else: + raise RuntimeError("Unsupported compute data type") + + return module + + +def BranchIfNotZero(sgprName: str, computeDataType, label) -> Module: + module = Module("BranchIfNotZero") + sgprStr = "s[" + sgprName + "]" + pVCC = VCC() + + if computeDataType == DataTypeEnum.ComplexDouble: + module.add(VCmpEQF64(dst=pVCC, src0=sgpr(sgprName, 2), src1=0.0, + comment=sgprStr + ".real == 0.0 ?")) + module.add(SCBranchVCCZ(labelName=label.getLabelName(), + comment="branch if " + sgprStr + ".real != 0")) + sgprVar = _sgpr_with_offset(sgprName, 2) + module.add(VCmpEQF64(dst=pVCC, src0=sgpr(sgprVar, 2), src1=0.0, + comment=sgprStr + ".imag == 0.0 ?")) + module.add(SCBranchVCCZ(labelName=label.getLabelName(), + comment="branch if " + sgprStr + ".imag != 0")) + + elif computeDataType == DataTypeEnum.Double: + module.add(VCmpEQF64(dst=pVCC, src0=sgpr(sgprName, 2), src1=0.0, + comment=sgprStr + " == 0.0 ?")) + module.add(SCBranchVCCZ(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " != 0")) + + elif computeDataType == DataTypeEnum.ComplexFloat: + module.add(VCmpEQF32(dst=pVCC, src0=sgpr(sgprName), src1=0.0, + comment=sgprStr + ".real == 0.0f ?")) + module.add(SCBranchVCCZ(labelName=label.getLabelName(), + comment="branch if " + sgprStr + ".real != 0")) + sgprVar = _sgpr_with_offset(sgprName, 1) + module.add(VCmpEQF32(dst=pVCC, src0=sgpr(sgprVar), src1=0.0, + comment=sgprStr + ".imag == 0.0f ?")) + module.add(SCBranchVCCZ(labelName=label.getLabelName(), + comment="branch if " + sgprStr + ".imag != 0")) + + elif computeDataType in (DataTypeEnum.Float, DataTypeEnum.Half, + DataTypeEnum.BFloat16): + module.add(VCmpEQF32(dst=pVCC, src0=sgpr(sgprName), src1=0.0, + comment=sgprStr + " == 0.0f ?")) + module.add(SCBranchVCCZ(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " != 0")) + + elif computeDataType == DataTypeEnum.Int64: + module.add(SCmpEQU64(src0=sgpr(sgprName, 2), src1=0, + comment=sgprStr + " == 0 ?")) + module.add(SCBranchSCC0(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " != 0")) + else: + module.add(SCmpEQU32(src0=sgpr(sgprName), src1=0, + comment=sgprStr + " == 0 ?")) + module.add(SCBranchSCC0(labelName=label.getLabelName(), + comment="branch if " + sgprStr + " != 0")) + + return module + +def VSaturateCastInt(SumIdxVgpr: Any, tmpVgprIdx: int, tmpSgprIdx: int, + lowerBound: int, upperBound: int, + type: Any = None, initGpr: bool = True) -> Module: + """Saturate-cast integer (f_cast.cpp).""" + from .enum import SaturateCastType # noqa: WPS433 + from .instruction import SMovkI32, VMed3I32, VMinI32, VMaxI32 # noqa: WPS433 + + if type is None: + type = SaturateCastType.NORMAL + + initGprStr = "with init gpr" if initGpr else "without init gpr" + module = Module("SaturateCastInt " + initGprStr) + + if type == SaturateCastType.NORMAL: + tmpLowerBoundSgpr = sgpr(tmpSgprIdx) + tmpUpperBoundVgpr = vgpr(tmpVgprIdx) + + if initGpr: + module.add(SMovkI32(dst=tmpLowerBoundSgpr, src=lowerBound, + comment=str(lowerBound))) + module.add(VMovB32(dst=tmpUpperBoundVgpr, src=upperBound, + comment=str(upperBound))) + + module.add(VMed3I32( + dst=SumIdxVgpr, src0=SumIdxVgpr, src1=tmpLowerBoundSgpr, + src2=tmpUpperBoundVgpr, + comment=f"x= min({upperBound}, max({lowerBound}, x))", + )) + elif type == SaturateCastType.DO_NOTHING: + pass + elif type == SaturateCastType.UPPER: + module.add(VMinI32( + dst=SumIdxVgpr, src0=upperBound, src1=SumIdxVgpr, + comment=f"x = min({upperBound}, x)", + )) + elif type == SaturateCastType.LOWER: + module.add(VMaxI32( + dst=SumIdxVgpr, src0=lowerBound, src1=SumIdxVgpr, + comment=f"x = max({lowerBound}, x)", + )) + + return module + +# --------------------------------------------------------------------------- +# Math helper utilities +# --------------------------------------------------------------------------- + + +def _to_sgpr(arg): + if isinstance(arg, (int, str)): + return sgpr(arg) + return arg + + +def _to_vgpr(arg): + if isinstance(arg, (int, str)): + return vgpr(arg) + return arg + + +def _get_vgpr(arg, idx): + if isinstance(arg, int): + return vgpr(arg + idx) + elif isinstance(arg, str): + return vgpr(arg + "+" + str(idx)) + return vgpr(-1) + + +def _get_sgpr(arg, idx): + if isinstance(arg, int): + return sgpr(arg + idx) + elif isinstance(arg, str): + return sgpr(arg + "+" + str(idx)) + return sgpr(-1) + + +def _is_power_of_2(n): + return n > 0 and (n & (n - 1)) == 0 + + +def _to_str(val): + if isinstance(val, str): + return val + return str(val) + + +# --------------------------------------------------------------------------- +# SMulInt64to32 — internal helper (mirrors extension.hpp) +# --------------------------------------------------------------------------- + + +def _SMulInt64to32(dst0, dst1, src0, src1, tmpVgprRes, hasSMulHi, + sign, comment=""): + module = Module("SMulInt64to32") + if tmpVgprRes.size < 2: + raise RuntimeError("ContinuousRegister size must be at least 2.") + if hasSMulHi: + module.add(SMulHIU32(dst=dst1, src0=src0, src1=src1, comment=comment)) + module.add(SMulI32(dst=dst0, src0=src0, src1=src1, comment=comment)) + else: + vgprTmp = vgpr(tmpVgprRes.idx) + vgprTmp1 = vgpr(tmpVgprRes.idx + 1) + module.add(VMovB32(dst=vgprTmp, src=src0, comment=comment)) + module.add(VMulHIU32(dst=vgprTmp1, src0=vgprTmp, src1=src1, comment=comment)) + module.add(VReadfirstlaneB32(dst=dst1, src=vgprTmp1, comment=comment)) + module.add(VMulLOU32(dst=vgprTmp1, src0=vgprTmp, src1=src1, comment=comment)) + module.add(VReadfirstlaneB32(dst=dst0, src=vgprTmp1, comment=comment)) + return module + + +# --------------------------------------------------------------------------- +# Vector divide & remainder +# --------------------------------------------------------------------------- + + +def vectorStaticDivideAndRemainder(qReg, rReg, dReg, divisor, + tmpVgprRes=None, doRemainder=True, + comment=""): + qRegStr = _to_str(qReg) + dRegStr = _to_str(dReg) + dComment = comment if comment else f"{qRegStr} = {dRegStr} / {divisor}" + rComment = comment if comment else f"{rReg} = {dRegStr} % {divisor}" + + module = Module("vectorStaticDivideAndRemainder") + qRegVgpr = vgpr(qReg) + rRegVgpr = vgpr(rReg) + dRegVgpr = vgpr(dReg) + + if _is_power_of_2(divisor): + divisor_log2 = int(math.log2(divisor)) + module.add(VLShiftRightB32(dst=qRegVgpr, shiftHex=divisor_log2, + src=dRegVgpr, comment=dComment)) + if doRemainder: + module.add(VAndB32(dst=rRegVgpr, src0=divisor - 1, + src1=dRegVgpr, comment=rComment)) + else: + if not tmpVgprRes or tmpVgprRes.size < 2: + raise RuntimeError("Invalid tmpVgprRes, must be at least 2") + tmpVgprIdx = tmpVgprRes.idx + tmpVgpr = vgpr(tmpVgprIdx) + tmpVgpr1 = vgpr(tmpVgprIdx + 1) + + shift = 33 + magic = ((1 << shift) // divisor) + 1 + + if -16 <= magic <= 64: + module.add(VMulHIU32(dst=tmpVgpr1, src0=dRegVgpr, + src1=magic, comment=dComment)) + module.add(VMulLOU32(dst=tmpVgpr, src0=dRegVgpr, + src1=magic, comment=dComment)) + else: + module.add(VMovB32(dst=tmpVgpr, src=magic)) + module.add(VMulHIU32(dst=tmpVgpr1, src0=dRegVgpr, + src1=tmpVgpr, comment=dComment)) + module.add(VMulLOU32(dst=tmpVgpr, src0=dRegVgpr, + src1=tmpVgpr, comment=dComment)) + + module.add(VLShiftRightB64(dst=vgpr(tmpVgprIdx, 2), shiftHex=shift, + src=vgpr(tmpVgprIdx, 2), comment=dComment)) + module.add(VMovB32(dst=qRegVgpr, src=tmpVgpr, comment=dComment)) + + if doRemainder: + if -16 <= divisor <= 64: + module.add(VMulLOU32(dst=tmpVgpr, src0=qRegVgpr, + src1=divisor, comment=rComment)) + else: + module.add(VMovB32(dst=tmpVgpr1, src=divisor)) + module.add(VMulLOU32(dst=tmpVgpr, src0=qRegVgpr, + src1=tmpVgpr1, comment=rComment)) + module.add(VSubU32(dst=rRegVgpr, src0=dRegVgpr, + src1=tmpVgpr, comment=rComment)) + + return module + + +def vectorStaticDivide(qReg, dReg, divisor, tmpVgprRes=None, comment=""): + module = vectorStaticDivideAndRemainder( + qReg, -1, dReg, divisor, tmpVgprRes, False, comment) + module.name = "vectorStaticDivide (reg=-1)" + return module + + +def vectorUInt32DivideAndRemainder(qReg, dReg, divReg, rReg, + doRemainder=True, comment=""): + qRegVgpr = vgpr(qReg) + rRegVgpr = vgpr(rReg) + dRegVgpr = vgpr(dReg) + divRegVgpr = vgpr(divReg) + + dComment = (comment if comment else + f"{qRegVgpr.toString()} = {dRegVgpr.toString()}" + f" / {divRegVgpr.toString()}") + rComment = (comment if comment else + f"{rRegVgpr.toString()} = {dRegVgpr.toString()}" + f" % {divRegVgpr.toString()}") + + pEXEC = EXEC() + module = Module("vectorUInt32DivideAndRemainder") + module.add(VCvtU32toF32(dst=qRegVgpr, src=divRegVgpr, comment=dComment)) + module.add(VRcpIFlagF32(dst=qRegVgpr, src=qRegVgpr, comment=dComment)) + module.add(VCvtU32toF32(dst=rRegVgpr, src=dRegVgpr, comment=dComment)) + module.add(VMulF32(dst=qRegVgpr, src0=qRegVgpr, src1=rRegVgpr, + comment=dComment)) + module.add(VCvtF32toU32(dst=qRegVgpr, src=qRegVgpr, comment=dComment)) + module.add(VMulU32U24(dst=rRegVgpr, src0=qRegVgpr, src1=divRegVgpr, + comment=dComment)) + module.add(VSubU32(dst=rRegVgpr, src0=dRegVgpr, src1=rRegVgpr, + comment=dComment)) + module.add(VCmpXEqU32(dst=pEXEC, src0=rRegVgpr, src1=divRegVgpr, + comment=dComment)) + module.add(VAddU32(dst=qRegVgpr, src0=1, src1=qRegVgpr, comment=dComment)) + if doRemainder: + module.add(VMovB32(dst=rRegVgpr, src=0, comment=rComment)) + module.add(SMovB64(dst=pEXEC, src=-1, comment=dComment)) + return module + + +def vectorUInt32CeilDivideAndRemainder(qReg, dReg, divReg, rReg, + doRemainder=True, comment=""): + qRegVgpr = vgpr(qReg) + rRegVgpr = vgpr(rReg) + dRegVgpr = vgpr(dReg) + divRegVgpr = vgpr(divReg) + + dComment = (comment if comment else + f"{qRegVgpr.toString()} = ceil({dRegVgpr.toString()}" + f" / {divRegVgpr.toString()})") + rComment = (comment if comment else + f"{rRegVgpr.toString()} = {dRegVgpr.toString()}" + f" % {divRegVgpr.toString()}") + + pVCC = VCC() + pEXEC = EXEC() + module = Module("vectorUInt32CeilDivideAndRemainder") + module.add(VCvtU32toF32(dst=qRegVgpr, src=divRegVgpr, comment=dComment)) + module.add(VRcpIFlagF32(dst=qRegVgpr, src=qRegVgpr, comment=dComment)) + module.add(VCvtU32toF32(dst=rRegVgpr, src=dRegVgpr, comment=dComment)) + module.add(VMulF32(dst=qRegVgpr, src0=qRegVgpr, src1=rRegVgpr, + comment=dComment)) + module.add(VCvtF32toU32(dst=qRegVgpr, src=qRegVgpr, comment=dComment)) + module.add(VMulU32U24(dst=rRegVgpr, src0=qRegVgpr, src1=divRegVgpr, + comment=dComment)) + module.add(VSubU32(dst=rRegVgpr, src0=dRegVgpr, src1=rRegVgpr, + comment=dComment)) + module.add(VCmpNeU32(dst=pVCC, src0=rRegVgpr, src1=0, comment=dComment)) + module.add(VAddCCOU32(dst=qRegVgpr, src0=qRegVgpr, src1=0, + comment="ceil")) + if doRemainder: + module.add(VCmpXEqU32(dst=pEXEC, src0=rRegVgpr, src1=divRegVgpr, + comment=rComment)) + module.add(VMovB32(dst=rRegVgpr, src=0, comment=rComment)) + module.add(SMovB64(dst=pEXEC, src=-1, comment=dComment)) + return module + + +def vectorStaticRemainder(qReg, rReg, dReg, divisor, tmpVgprRes=None, + tmpSgprRes=None, comment=""): + qRegVgpr = vgpr(qReg) + rRegVgpr = vgpr(rReg) + dRegVgpr = vgpr(dReg) + + dComment = (comment if comment else + f"{rRegVgpr.toString()} = {dRegVgpr.toString()} % {divisor}") + + module = Module("vectorStaticRemainder") + + if _is_power_of_2(divisor): + module.add(VAndB32(dst=rRegVgpr, src0=divisor - 1, src1=dRegVgpr, + comment=dComment)) + else: + if not tmpVgprRes or tmpVgprRes.size < 2: + raise RuntimeError("Invalid tmpVgprRes, must be at least 2") + tmpVgprIdx = tmpVgprRes.idx + tmpVgpr = vgpr(tmpVgprIdx) + tmpVgpr1 = vgpr(tmpVgprIdx + 1) + + if not tmpSgprRes or tmpSgprRes.size < 1: + raise RuntimeError("Invalid tmpSgprRes, must be at least 1") + tmpSgprIdx = tmpSgprRes.idx + tmoSgpr = sgpr(tmpSgprIdx) + + shift = 33 + magic = ((1 << shift) // divisor) + 1 + + if -16 <= magic <= 64: + module.add(VMulHIU32(dst=tmpVgpr1, src0=dRegVgpr, src1=magic, + comment=dComment)) + module.add(VMulLOU32(dst=tmpVgpr, src0=dRegVgpr, src1=magic, + comment=dComment)) + else: + module.add(SMovB32(dst=tmoSgpr, src=magic, comment=dComment)) + module.add(VMulHIU32(dst=tmpVgpr1, src0=dRegVgpr, src1=tmoSgpr, + comment=dComment)) + module.add(VMulLOU32(dst=tmpVgpr, src0=dRegVgpr, src1=tmoSgpr, + comment=dComment)) + + module.add(VLShiftRightB64(dst=vgpr(tmpVgprIdx, 2), shiftHex=shift, + src=vgpr(tmpVgprIdx, 2), comment=dComment)) + module.add(VMovB32(dst=qRegVgpr, src=tmpVgpr, comment=dComment)) + + if -16 <= divisor <= 64: + module.add(VMulLOU32(dst=tmpVgpr, src0=qRegVgpr, src1=divisor, + comment=dComment)) + else: + module.add(SMovB32(dst=tmoSgpr, src=divisor, comment=dComment)) + module.add(VMulLOU32(dst=tmpVgpr, src0=qRegVgpr, src1=tmoSgpr, + comment=dComment)) + + module.add(VSubU32(dst=rRegVgpr, src0=dRegVgpr, src1=tmpVgpr, + comment=dComment)) + + return module + + +# --------------------------------------------------------------------------- +# Scalar divide & remainder +# --------------------------------------------------------------------------- + + +def scalarStaticDivideAndRemainder(qReg, rReg, dReg, divisor, + tmpSgprRes=None, doRemainder=1): + qRegSgpr = _to_sgpr(qReg) + rRegSgpr = _to_sgpr(rReg) + dRegSgpr = _to_sgpr(dReg) + + module = Module("scalarStaticDivideAndRemainder") + + if _is_power_of_2(divisor): + divisor_log2 = int(math.log2(divisor)) + if doRemainder != 2: + module.add(SLShiftRightB32( + dst=qRegSgpr, shiftHex=divisor_log2, src=dRegSgpr, + comment=f"{qRegSgpr.toString()} = {dRegSgpr.toString()}" + f" / {divisor}")) + if doRemainder: + module.add(SAndB32( + dst=rRegSgpr, src0=divisor - 1, src1=dRegSgpr, + comment=f"{rRegSgpr.toString()} = {dRegSgpr.toString()}" + f" % {divisor}")) + else: + if not tmpSgprRes or tmpSgprRes.size < 2: + raise RuntimeError("Invalid tmpSgprRes, must be at least 2") + tmpSgprIdx = tmpSgprRes.idx + tmpSgpr = sgpr(tmpSgprIdx) + tmpSgpr1 = sgpr(tmpSgprIdx + 1) + tmp2Sgpr = sgpr(tmpSgprIdx, 2) + + shift = 33 + magic = ((1 << shift) // divisor) + 1 + magicHi = magic >> 16 + magicLo = magic & 0xFFFF + + module.add(SMovB32(dst=tmpSgpr1, src=0, + comment=f"STATIC_DIV: divisor={divisor}")) + module.add(SMulI32(dst=tmpSgpr, src0=magicHi, src1=dRegSgpr, + comment="tmp1 = dividend * magic hi")) + module.add(SLShiftLeftB64(dst=tmp2Sgpr, shiftHex=16, src=tmp2Sgpr, + comment="left shift 16 bits")) + module.add(SMulI32(dst=qRegSgpr, src0=dRegSgpr, src1=magicLo, + comment="tmp0 = dividend * magic lo")) + module.add(SAddU32(dst=tmpSgpr, src0=qRegSgpr, src1=tmpSgpr, + comment="add lo")) + module.add(SAddCU32(dst=tmpSgpr1, src0=tmpSgpr1, src1=0, + comment="add hi")) + module.add(SLShiftRightB64( + dst=tmp2Sgpr, shiftHex=shift, src=tmp2Sgpr, + comment="tmp1 = (dividend * magic) << shift")) + module.add(SMovB32(dst=qRegSgpr, src=tmpSgpr, comment="quotient")) + + if doRemainder: + module.add(SMulI32(dst=tmpSgpr, src0=qRegSgpr, src1=divisor, + comment="quotient*divisor")) + module.add(SSubU32( + dst=rRegSgpr, src0=dRegSgpr, src1=tmpSgpr, + comment="rReg = dividend - quotient*divisor")) + + return module + + +def scalarStaticCeilDivide(qReg, dReg, divisor, tmpSgprRes=None): + qRegSgpr = _to_sgpr(qReg) + dRegSgpr = _to_sgpr(dReg) + + module = Module("scalarStaticCeilDivide") + + if _is_power_of_2(divisor): + divisor_log2 = int(math.log2(divisor)) + module.add(SLShiftRightB32( + dst=qRegSgpr, shiftHex=divisor_log2, src=dRegSgpr, + comment=f"{qRegSgpr.toString()} = {dRegSgpr.toString()}" + f" / {divisor}")) + tmpSgpr = sgpr(tmpSgprRes.idx) + module.add(SAndB32( + dst=tmpSgpr, src0=divisor - 1, src1=dRegSgpr, + comment=f"{tmpSgpr.toString()} = {dRegSgpr.toString()}" + f" % {divisor}")) + module.add(SAddCU32(dst=qRegSgpr, src0=qRegSgpr, src1=0)) + else: + if not tmpSgprRes or tmpSgprRes.size < 2: + raise RuntimeError("Invalid tmpSgprRes, must be at least 2") + tmpSgprIdx = tmpSgprRes.idx + tmpSgpr = sgpr(tmpSgprIdx) + tmpSgpr1 = sgpr(tmpSgprIdx + 1) + tmp2Sgpr = sgpr(tmpSgprIdx, 2) + + shift = 33 + magic = ((1 << shift) // divisor) + 1 + magicHi = magic >> 16 + magicLo = magic & 0xFFFF + + module.add(SMovB32(dst=tmpSgpr1, src=0, + comment=f"STATIC_DIV: divisor={divisor}")) + module.add(SMulI32(dst=tmpSgpr, src0=magicHi, src1=dRegSgpr, + comment="tmp1 = dividend * magic hi")) + module.add(SLShiftLeftB64(dst=tmp2Sgpr, shiftHex=16, src=tmp2Sgpr, + comment="left shift 16 bits")) + module.add(SMulI32(dst=qRegSgpr, src0=dRegSgpr, src1=magicLo, + comment="tmp0 = dividend * magic lo")) + module.add(SAddU32(dst=tmpSgpr, src0=qRegSgpr, src1=tmpSgpr, + comment="add lo")) + module.add(SAddCU32(dst=tmpSgpr1, src0=tmpSgpr1, src1=0, + comment="add hi")) + module.add(SLShiftRightB64(dst=tmp2Sgpr, shiftHex=shift, + src=tmp2Sgpr, comment="tmp0 = quotient")) + module.add(SMulI32(dst=tmpSgpr1, src0=tmpSgpr, src1=divisor, + comment="tmp1 = quotient * divisor")) + module.add(SCmpLgU32( + src0=tmpSgpr1, src1=dRegSgpr, + comment="if (quotient * divisor != dividend), result+=1")) + module.add(SAddCU32( + dst=qRegSgpr, src0=tmpSgpr, src1=0, + comment="if (quotient * divisor != dividend), result+=1")) + + return module + + +def scalarStaticRemainder(qReg, rReg, dReg, divisor, tmpSgprRes=None, + comment=""): + module = Module("scalarStaticRemainder") + + rRegSgpr = sgpr(rReg) + dRegSgpr = sgpr(dReg) + + dComment = (comment if comment else + f"{rRegSgpr.toString()} = {dRegSgpr.toString()} % {divisor}") + + if _is_power_of_2(divisor): + module.add(SAndB32(dst=rRegSgpr, src0=divisor - 1, src1=dRegSgpr, + comment=dComment)) + else: + qRegSgpr = sgpr(qReg) + + if not tmpSgprRes or tmpSgprRes.size < 3: + raise RuntimeError("Invalid tmpSgprRes, must be at least 3") + tmpSgprIdx = tmpSgprRes.idx + tmpSgpr = sgpr(tmpSgprIdx) + tmpSgpr1 = sgpr(tmpSgprIdx + 1) + tmpSgpr2 = sgpr(tmpSgprIdx + 2) + tmp2Sgpr = sgpr(tmpSgprIdx, 2) + + shift = 33 + magic = ((1 << shift) // divisor) + 1 + + if -16 <= magic <= 64: + module.add(SMulHIU32(dst=tmpSgpr1, src0=dRegSgpr, src1=magic, + comment=dComment)) + module.add(SMulI32(dst=tmpSgpr, src0=dRegSgpr, src1=magic, + comment=dComment)) + else: + module.add(SMovB32(dst=tmpSgpr2, src=magic, comment=dComment)) + module.add(SMulHIU32(dst=tmpSgpr1, src0=dRegSgpr, src1=tmpSgpr2, + comment=dComment)) + module.add(SMulI32(dst=tmpSgpr, src0=dRegSgpr, src1=tmpSgpr2, + comment=dComment)) + + module.add(SLShiftRightB64(dst=tmp2Sgpr, shiftHex=shift, + src=tmp2Sgpr, comment=dComment)) + module.add(SMovB32(dst=qRegSgpr, src=tmpSgpr, comment=dComment)) + + if -16 <= divisor <= 64: + module.add(SMulI32(dst=tmpSgpr, src0=qRegSgpr, src1=divisor, + comment=dComment)) + else: + module.add(SMovB32(dst=tmpSgpr2, src=divisor, comment=dComment)) + module.add(SMulI32(dst=tmpSgpr, src0=qRegSgpr, src1=tmpSgpr2, + comment=dComment)) + + module.add(SSubU32(dst=rRegSgpr, src0=dRegSgpr, src1=tmpSgpr, + comment=dComment)) + + return module + + +def scalarUInt24DivideAndRemainder(qReg, dReg, divReg, rReg, tmpVgprRes, + wavewidth, doRemainder=True, + doQuotient=True, comment=""): + module = Module("scalarUInt24DivideAndRemainder") + + qRegSgpr = sgpr(qReg) + rRegSgpr = sgpr(rReg) + dRegSgpr = sgpr(dReg) + divRegSgpr = sgpr(divReg) + + dComment = (comment if comment else + f"{qRegSgpr.toString()} = {dRegSgpr.toString()}" + f" / {divRegSgpr.toString()}") + rComment = "" + if doRemainder: + rComment = (comment if comment else + f"{sgpr(rReg).toString()} = {dRegSgpr.toString()}" + f" % {divRegSgpr.toString()}") + + if tmpVgprRes.size < 4: + raise RuntimeError("Invalid tmpVgprRes, must be at least 4") + tmpVgpr = tmpVgprRes.idx + tmpVgpr1 = tmpVgprRes.idx + 2 + + pEXEC = EXEC() + + module.add(VCvtU32toF64(dst=vgpr(tmpVgpr, 2), src=divRegSgpr, + comment=dComment)) + module.add(VRcpF64(dst=vgpr(tmpVgpr, 2), src=vgpr(tmpVgpr, 2), + comment=dComment)) + module.add(VCvtU32toF64(dst=vgpr(tmpVgpr1, 2), src=dRegSgpr, + comment=dComment)) + module.add(VMulF64(dst=vgpr(tmpVgpr, 2), src0=vgpr(tmpVgpr, 2), + src1=vgpr(tmpVgpr1, 2), comment=dComment)) + module.add(VCvtF64toU32(dst=vgpr(tmpVgpr), src=vgpr(tmpVgpr, 2), + comment=dComment)) + + module.add(VMulLOU32(dst=vgpr(tmpVgpr + 1), src0=vgpr(tmpVgpr), + src1=divRegSgpr, comment=dComment)) + module.add(VSubU32(dst=vgpr(tmpVgpr1), src0=dRegSgpr, + src1=vgpr(tmpVgpr + 1), comment=dComment)) + module.add(VCmpXGeU32(dst=pEXEC, src0=vgpr(tmpVgpr1), src1=divRegSgpr, + comment=dComment)) + module.add(VAddU32(dst=vgpr(tmpVgpr), src0=vgpr(tmpVgpr), src1=1, + comment=dComment)) + + if wavewidth == 64: + module.add(SMovB64(dst=pEXEC, src=-1, comment="Reset exec")) + else: + module.add(SMovB32(dst=pEXEC, src=-1, comment="Reset exec")) + + if doRemainder: + module.add(VMulLOU32(dst=vgpr(tmpVgpr + 1), src0=vgpr(tmpVgpr), + src1=divRegSgpr, comment=dComment)) + module.add(VSubU32(dst=vgpr(tmpVgpr1), src0=dRegSgpr, + src1=vgpr(tmpVgpr + 1), comment=dComment)) + + if doQuotient: + module.add(VReadfirstlaneB32(dst=qRegSgpr, src=vgpr(tmpVgpr), + comment="quotient")) + else: + module.add(SNop(0)) + + if doRemainder: + module.add(VReadfirstlaneB32(dst=sgpr(rReg), src=vgpr(tmpVgpr1), + comment="remainder")) + + return module + + +def scalarUInt32DivideAndRemainder(qReg, dReg, divReg, rReg, tmpVgprRes, + wavewidth, doRemainder=True, comment=""): + module = Module("scalarUInt32DivideAndRemainder") + + qRegSgpr = sgpr(qReg) + rRegSgpr = sgpr(rReg) + dRegSgpr = sgpr(dReg) + divRegSgpr = sgpr(divReg) + + dComment = (comment if comment else + f"{qRegSgpr.toString()} = {dRegSgpr.toString()}" + f" / {divRegSgpr.toString()}") + rComment = "" + if doRemainder: + rComment = (comment if comment else + f"{sgpr(rReg).toString()} = {dRegSgpr.toString()}" + f" % {divRegSgpr.toString()}") + + if tmpVgprRes.size < 2: + raise RuntimeError("Invalid tmpVgprRes, must be at least 2") + tmpVgpr = vgpr(tmpVgprRes.idx) + tmpVgpr1 = vgpr(tmpVgprRes.idx + 1) + + pEXEC = EXEC() + + module.add(VCvtU32toF32(dst=tmpVgpr, src=divRegSgpr, comment=dComment)) + module.add(VRcpIFlagF32(dst=tmpVgpr, src=tmpVgpr, comment=dComment)) + module.add(VCvtU32toF32(dst=tmpVgpr1, src=dRegSgpr, comment=dComment)) + module.add(VMulF32(dst=tmpVgpr, src0=tmpVgpr, src1=tmpVgpr1, + comment=dComment)) + module.add(VCvtF32toU32(dst=tmpVgpr, src=tmpVgpr, comment=dComment)) + module.add(VMulU32U24(dst=tmpVgpr1, src0=tmpVgpr, src1=divRegSgpr, + comment=dComment)) + module.add(VSubU32(dst=tmpVgpr1, src0=dRegSgpr, src1=tmpVgpr1, + comment=dComment)) + module.add(VCmpXEqU32(dst=pEXEC, src0=tmpVgpr1, src1=divRegSgpr, + comment=dComment)) + module.add(VAddU32(dst=tmpVgpr, src0=1, src1=tmpVgpr, comment=dComment)) + + if doRemainder: + module.add(VMovB32(dst=tmpVgpr1, src=0, comment=rComment)) + + def _resetExec(): + if wavewidth == 64: + module.add(SMovB64(dst=pEXEC, src=-1, comment="Reset exec")) + else: + module.add(SMovB32(dst=pEXEC, src=-1, comment="Reset exec")) + + _resetExec() + module.add(VCmpXGtU32(dst=pEXEC, src0=tmpVgpr1, src1=divRegSgpr, + comment="overflow happened in remainder")) + module.add(VSubU32(dst=tmpVgpr, src0=tmpVgpr, src1=1, + comment="quotient - 1")) + + if doRemainder: + module.add(VMulU32U24(dst=tmpVgpr1, src0=tmpVgpr, src1=divRegSgpr, + comment="re-calculate remainder")) + module.add(VSubU32(dst=tmpVgpr1, src0=dRegSgpr, src1=tmpVgpr1, + comment="re-calculate remainder")) + + _resetExec() + module.add(VReadfirstlaneB32(dst=qRegSgpr, src=tmpVgpr, + comment="quotient")) + + if doRemainder: + module.add(VReadfirstlaneB32(dst=sgpr(rReg), src=tmpVgpr1, + comment="remainder")) + + return module + + +# --------------------------------------------------------------------------- +# Magic division +# --------------------------------------------------------------------------- + + +def sMagicDiv(dest, hasSMulHi, dividend, magicNumber, magicShift, tmpVgpr): + module = Module("sMagicDiv") + + destSgpr = sgpr(dest, 2) + destSgpr0 = sgpr(dest) + destSgpr1 = _get_sgpr(dest, 1) + continuousReg = ContinuousRegister(tmpVgpr.idx, 2) + + module.addModuleAsFlatItems(_SMulInt64to32( + destSgpr0, destSgpr1, dividend, magicNumber, + continuousReg, hasSMulHi, False, "s_magic mul")) + module.add(SLShiftRightB64(dst=destSgpr, shiftHex=magicShift, + src=destSgpr, comment="sMagicDiv")) + return module + + +def sMagicDiv2(dst, dst2, dividend, magicNumber, magicShiftAbit, tmpSgpr): + module = Module("sMagicDiv2") + + module.add(SMulHIU32(dst=dst2, src0=dividend, src1=magicNumber, + comment="s_magic mul, div alg 2")) + module.add(SLShiftRightB32(dst=tmpSgpr, shiftHex=31, src=magicShiftAbit, + comment="tmpS = extract abit")) + module.add(SMulI32(dst=dst, src0=dividend, src1=tmpSgpr, + comment="s_magic mul, div alg 2")) + module.add(SAddU32(dst=dst, src0=dst, src1=dst2)) + + module.add(SAndB32(dst=tmpSgpr, src0=magicShiftAbit, src1=0x7fffffff, + comment="tmpS = remove abit to final shift")) + module.add(SLShiftRightB32(dst=dst, shiftHex=tmpSgpr, src=dst, + comment="sMagicDiv Alg 2")) + + return module + + +# --------------------------------------------------------------------------- +# Multiply helpers +# --------------------------------------------------------------------------- + + +def vectorStaticMultiply(product, operand, multiplier, tmpSgprRes=None, + comment=""): + dComment = (comment if comment else + f"{product.toString()} = {operand.toString()} * {multiplier}") + module = Module("vectorStaticMultiply") + if multiplier == 0: + module.add(VMovB32(dst=product, src=multiplier, comment=dComment)) + elif _is_power_of_2(multiplier): + multiplier_log2 = int(math.log2(multiplier)) + if multiplier_log2 == 0 and product == operand: + module.addCommentAlign(dComment + " (multiplier is 1, do nothing)") + else: + module.add(VLShiftLeftB32(dst=product, shiftHex=multiplier_log2, + src=operand, comment=dComment)) + else: + if -16 <= multiplier <= 64: + module.add(VMulLOU32(dst=product, src0=multiplier, src1=operand, + comment=dComment)) + else: + if not tmpSgprRes or tmpSgprRes.size < 1: + raise RuntimeError("Invalid tmpSgprRes, must be at least 1") + tmpSgpr = sgpr(tmpSgprRes.idx) + module.add(SMovB32(dst=tmpSgpr, src=multiplier, comment=dComment)) + module.add(VMulLOU32(dst=product, src0=tmpSgpr, src1=operand, + comment=dComment)) + return module + + +def vectorStaticMultiplyAdd(product, operand, multiplier, accumulator, + tmpSgprRes=None, comment=""): + dComment = (comment if comment else + f"{product.toString()} = {operand.toString()} * {multiplier}") + module = Module("vectorStaticMultiplyAdd") + if multiplier == 0: + module.add(VMovB32(dst=product, src=accumulator, comment=dComment)) + elif _is_power_of_2(multiplier): + multiplier_log2 = int(math.log2(multiplier)) + if multiplier_log2 == 0: + module.add(VAddU32(dst=product, src0=operand, src1=accumulator, + comment=dComment)) + else: + module.add(VLShiftLeftAddU32( + dst=product, shiftHex=multiplier_log2, src0=operand, + src1=accumulator, comment=dComment)) + else: + if -16 <= multiplier <= 64: + module.add(VMadU32U24(dst=product, src0=multiplier, src1=operand, + src2=accumulator, comment=dComment)) + else: + if not tmpSgprRes or tmpSgprRes.size < 1: + raise RuntimeError("Invalid tmpSgprRes, must be at least 1") + tmpSgpr = sgpr(tmpSgprRes.idx) + module.add(SMovB32(dst=tmpSgpr, src=multiplier, comment=dComment)) + module.add(VMadU32U24(dst=product, src0=tmpSgpr, src1=operand, + src2=accumulator, comment=dComment)) + return module + + +def scalarStaticMultiply64(product, operand, multiplier, tmpSgprRes=None, + comment=""): + commentStr = (comment if comment else + f"{product.toString()} = {operand.toString()}" + f" * {multiplier}") + module = Module("scalarStaticMultiply64") + if multiplier == 0: + module.add(SMovB64(dst=product, src=0, comment=commentStr)) + return module + + if not _is_power_of_2(multiplier): + raise RuntimeError("Multiplier must be a power of 2") + + multiplier_log2 = int(math.log2(multiplier)) + if multiplier_log2 == 0 and product == operand: + module.addCommentAlign(comment + " (multiplier is 1, do nothing)") + else: + module.add(SLShiftLeftB64(dst=product, shiftHex=multiplier_log2, + src=operand, comment=commentStr)) + return module + + +# --------------------------------------------------------------------------- +# Bpe multiply helpers +# --------------------------------------------------------------------------- + + +def vectorAddMultiplyBpe(dst, src0, src1, bpe, comment=""): + module = Module("vectorAddMultiplyBpe") + mcomment = comment + " (multiple bpe)" + dstVgpr = vgpr(dst) + src0Vgpr = vgpr(src0) + src1Vgpr = vgpr(src1) + if bpe == 0.5: + module.add(VAddU32(dst=dstVgpr, src0=src0Vgpr, src1=src1Vgpr, + comment=mcomment)) + module.add(VLShiftRightB32(dst=dstVgpr, shiftHex=1, src=dstVgpr, + comment=mcomment)) + elif bpe == 0.75: + module.add(VAddU32(dst=dstVgpr, src0=src0Vgpr, src1=src1Vgpr, + comment=mcomment)) + module.add(VMulLOU32(dst=dstVgpr, src0=6, src1=dstVgpr, + comment=mcomment)) + module.add(VLShiftRightB32(dst=dstVgpr, shiftHex=3, src=dstVgpr, + comment=mcomment)) + else: + bpe_log2 = int(math.log2(bpe)) + if bpe_log2 == 0: + module.add(VAddU32(dst=dstVgpr, src0=src0Vgpr, src1=src1Vgpr, + comment=mcomment)) + module.addCommentAlign(comment + " (bpe is 1, no mul)") + else: + module.add(VAddLShiftLeftU32( + dst=dstVgpr, shiftHex=bpe_log2, src0=src0Vgpr, src1=src1Vgpr, + comment=mcomment)) + return module + + +def vectorMultiplyBpe(dst, src, bpe, comment=""): + module = Module("vectorMultiplyBpe") + mcomment = comment + " (multiple bpe)" + dstVgpr = vgpr(dst) + srcVgpr = vgpr(src) + if bpe == 0.5: + module.add(VLShiftRightB32(dst=dstVgpr, shiftHex=1, src=srcVgpr, + comment=mcomment)) + elif bpe == 0.75: + module.add(VMulLOU32(dst=dstVgpr, src0=6, src1=srcVgpr, + comment=mcomment)) + module.add(VLShiftRightB32(dst=dstVgpr, shiftHex=3, src=dstVgpr, + comment=mcomment)) + else: + bpe_log2 = int(math.log2(bpe)) + dst_str = _to_str(dst) + src_str = _to_str(src) + if bpe_log2 == 0 and dst_str == src_str: + module.addCommentAlign(comment + " (bpe is 1, do nothing)") + else: + module.add(VLShiftLeftB32(dst=dstVgpr, shiftHex=bpe_log2, + src=srcVgpr, comment=mcomment)) + return module + + +def vectorMultiply64Bpe(dst, src, bpe, tmp, comment=""): + module = Module("vectorMultiply64Bpe") + mcomment = comment + " (multiple bpe)" + dstVgpr = vgpr(dst, 2) + srcVgpr = vgpr(src, 2) + tmpVgpr = vgpr(tmp) + if bpe == 0.5: + module.add(VLShiftRightB64(dst=dstVgpr, shiftHex=1, src=srcVgpr, + comment=mcomment)) + elif bpe == 0.75: + dstVgpr0 = vgpr(dst) + dstVgpr1 = _get_vgpr(dst, 1) + srcVgpr0 = vgpr(src) + srcVgpr1 = _get_vgpr(src, 1) + module.add(VMovB32(dst=tmpVgpr, src=srcVgpr1, comment=mcomment)) + module.add(VMulHIU32(dst=dstVgpr1, src0=6, src1=srcVgpr0, + comment=mcomment)) + module.add(VMulLOU32(dst=dstVgpr0, src0=6, src1=srcVgpr0, + comment=mcomment)) + module.add(VMulLOU32(dst=tmpVgpr, src0=6, src1=tmpVgpr, + comment=mcomment)) + module.add(VAddU32(dst=dstVgpr1, src0=dstVgpr1, src1=tmpVgpr, + comment=mcomment)) + module.add(VLShiftRightB64(dst=dstVgpr, shiftHex=3, src=dstVgpr, + comment=mcomment)) + else: + bpe_log2 = int(math.log2(bpe)) + dst_str = _to_str(dst) + src_str = _to_str(src) + if bpe_log2 == 0 and dst_str == src_str: + module.addCommentAlign(comment + " (bpe is 1, do nothing)") + else: + module.add(VLShiftLeftB64(dst=dstVgpr, shiftHex=bpe_log2, + src=srcVgpr, comment=mcomment)) + return module + + +def scalarMultiplyBpe(dst, src, bpe, comment=""): + module = Module("scalarMultiplyBpe") + mcomment = comment + " (multiple bpe)" + dstSgpr = sgpr(dst) + srcSgpr = sgpr(src) + if bpe == 0.5: + module.add(SLShiftRightB32(dst=dstSgpr, shiftHex=1, src=srcSgpr, + comment=mcomment)) + elif bpe == 0.75: + module.add(SMulI32(dst=dstSgpr, src0=6, src1=srcSgpr, + comment=mcomment)) + module.add(SLShiftRightB32(dst=dstSgpr, shiftHex=3, src=dstSgpr, + comment=mcomment)) + else: + bpe_log2 = int(math.log2(bpe)) + dst_str = _to_str(dst) + src_str = _to_str(src) + if bpe_log2 == 0 and dst_str == src_str: + module.addCommentAlign(comment + " (bpe is 1, do nothing)") + else: + module.add(SLShiftLeftB32(dst=dstSgpr, shiftHex=bpe_log2, + src=srcSgpr, comment=mcomment)) + return module + + +def scalarMultiply64Bpe(dst, src, bpe, tmp, comment=""): + module = Module("scalarMultiply64Bpe") + mcomment = comment + " (multiple bpe)" + dstSgpr = sgpr(dst, 2) + srcSgpr = sgpr(src, 2) + tmpSgpr = sgpr(tmp) + if bpe == 0.5: + module.add(SLShiftRightB64(dst=dstSgpr, shiftHex=1, src=srcSgpr, + comment=mcomment)) + elif bpe == 0.75: + dstSgpr0 = sgpr(dst) + dstSgpr1 = _get_sgpr(dst, 1) + srcSgpr0 = sgpr(src) + srcSgpr1 = _get_sgpr(src, 1) + module.add(SMovB32(dst=tmpSgpr, src=srcSgpr1, comment=mcomment)) + module.add(SMulHIU32(dst=dstSgpr1, src0=6, src1=srcSgpr0, + comment=mcomment)) + module.add(SMulI32(dst=dstSgpr0, src0=6, src1=srcSgpr0, + comment=mcomment)) + module.add(SMulI32(dst=tmpSgpr, src0=6, src1=tmpSgpr, + comment=mcomment)) + module.add(SAddU32(dst=dstSgpr1, src0=dstSgpr1, src1=tmpSgpr, + comment=mcomment)) + module.add(SLShiftRightB64(dst=dstSgpr, shiftHex=3, src=dstSgpr, + comment=mcomment)) + else: + bpe_log2 = int(math.log2(bpe)) + dst_str = _to_str(dst) + src_str = _to_str(src) + if bpe_log2 == 0 and dst_str == src_str: + module.addCommentAlign(comment + " (bpe is 1, do nothing)") + else: + module.add(SLShiftLeftB64(dst=dstSgpr, shiftHex=bpe_log2, + src=srcSgpr, comment=mcomment)) + return module + + +DSInit = make_dummy_func(f"{_P}.DSInit") diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/instruction.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/instruction.py new file mode 100644 index 000000000000..ad04dbb71cab --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/instruction.py @@ -0,0 +1,4628 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Instruction shims mirroring rocisa/src/instruction/. + +Real: VMovB32, SMovB32/B64, SLoadB32-B512, SNop, and base Instruction/CommonInstruction. +Each exposes to_stinky_logical() for logical IR lowering. +""" + +from __future__ import annotations + +from copy import deepcopy as _deepcopy +from typing import Any, Dict, List, Optional + +from ._dummy import make_dummy_class, make_dummy_func +from .enum import InstType + +_P = "rocisa.instruction" + + +# ========================================================================== +# Base instruction types +# source: rocisa/rocisa/src/instruction/instruction.{hpp,cpp} +# ========================================================================== +# +# The full rocisa hierarchy is: +# +# Item -> Instruction -> CommonInstruction (VMovB32, VAddF32, ...) +# -> CompositeInstruction (VMovB64, SLongBranch*, ...) +# -> MacroInstruction (V_MAGIC_DIV, MAINLOOP, ...) +# +# ``Instruction`` / ``CommonInstruction`` / ``MacroInstruction`` are +# real; ``CompositeInstruction`` is still a dummy until a concrete +# subclass needs it. +# +# KernelWriter exercises: +# - ``isinstance(x, Instruction)`` (KernelWriter.py:1105, 1790, Activation.py) +# - ``isinstance(x, CommonInstruction) and ... and x.dst.regType == 'm'`` +# (Components/SubtileBasedInstructionScheduler.py:151) +# - ``inst.getParams()`` (Components/SIA.py, Activation.py) +# - ``inst.dst = vgpr(...)`` post-construction rebinding +# (Components/GSU.py:1144 etc.) +# - ``inst.comment = ...`` post-construction mutation (KernelWriter:3422 etc.) +# - ``MacroInstruction(name="V_MAGIC_DIV", args=[...])`` macro-call emit +# (KernelWriterAssembly:2784, 3291, 9452 + Components/StreamK). + + +def _format_str(output_inline_asm: bool, inst_str: str, comment: str, + no_comment: bool) -> str: + """Byte-parity port of ``rocisa::formatStr`` (format.hpp:54-75). + + Reproduces the exact padding-to-50 + ``" // "`` + comment + ``"\\n"`` + layout so emitted asm diffs byte-for-byte against rocisa native. + """ + formatted = inst_str + if output_inline_asm: + # Inline-asm path wraps the instruction in quotes + ``\n\t``. + formatted = '"' + formatted + '\\n\\t"' + if comment and not no_comment: + pad = max(0, 50 - len(formatted)) + return formatted + (" " * pad) + " // " + comment + "\n" + return formatted + "\n" + + +# ``outputNoComment`` lives in ``base.py`` so ``code.TextBlock.toString`` +# and the instruction formatter share one reader. See ``base.outputNoComment`` +# for the full decoupling rationale (Python-side mirror, lazy-import, etc.). +from .base import outputNoComment as _output_no_comment # noqa: E402 + + +class Instruction: + """Minimal port of ``rocisa::Instruction`` (instruction.hpp:119-286). + + Carries the per-instance state KernelWriter sets / reads but leaves + ``toString`` / ``getParams`` family to subclasses. The MSB workaround + (``setMsb``) is intentionally omitted -- the stinkytofu left-path + runs ``InsertVgprMsbPass`` to inject ``s_set_vgpr_msb`` instructions + closer to the truth source. If a future test diffs rocisa-emitted + text against the right-path this gap will need to be revisited. + """ + + __slots__ = ( + "name", "parent", + "instType", "comment", "instStr", "outputInlineAsm", + "m_memToken", + ) + + def __init__(self, instType: Any, comment: str = ""): + self.name: str = "" # rocisa Item ctor uses "" (not the class name) + self.parent: Any = None + self.instType: Any = instType + self.comment: str = comment + self.instStr: str = "" + self.outputInlineAsm: bool = False + self.m_memToken: Any = None + + # ---------------------------------------------------- memToken / inline + def setMemToken(self, token: Any) -> None: + self.m_memToken = token + + def getMemToken(self) -> Any: + return self.m_memToken + + def setInlineAsm(self, is_true: bool) -> None: + self.outputInlineAsm = bool(is_true) + + # ---------------------------------------------------------- inst label + def setInst(self, inst_str: str) -> None: + self.instStr = inst_str + + def preStr(self) -> str: + # rocisa default; CompositeInstruction overrides. + return self.instStr + + # ----------------------------------------------------- comment formats + def formatOnly(self, inst_str: str, comment: str) -> str: + return _format_str(self.outputInlineAsm, inst_str, comment, + _output_no_comment()) + + def formatWithComment(self, inst_str: str) -> str: + return _format_str(self.outputInlineAsm, inst_str, self.comment, + _output_no_comment()) + + def formatWithExtraComment(self, inst_str: str, extra: str) -> str: + return _format_str(self.outputInlineAsm, inst_str, self.comment + extra, + _output_no_comment()) + + # ----------------------------------------------------- to be overridden + def toString(self) -> str: + raise NotImplementedError( + "Subclass must override toString() (rocisa::Instruction is abstract)." + ) + + def __str__(self) -> str: + return self.toString() + + def getParams(self): + raise NotImplementedError("Subclass must override getParams()") + + def getDstParams(self): + raise NotImplementedError("Subclass must override getDstParams()") + + def getSrcParams(self): + raise NotImplementedError("Subclass must override getSrcParams()") + + def getIssueLatency(self) -> int: + return 1 # rocisa default; override in subclasses with real values. + + def getIssueCycles(self) -> int: + return 1 + + # ----------------------------------------------------- copy / pickle + def __deepcopy__(self, memo): + # rocisa raises std::runtime_error("Deepcopy not supported for + # Instruction"); KernelWriter doesn't deepcopy bare Instructions + # (it deepcopies concrete subclasses which override this). We + # mirror the rocisa intent. + raise RuntimeError("Deepcopy not supported for Instruction") + + def __reduce__(self): + raise RuntimeError("Pickling not supported for Instruction") + + +def _input_to_str(arg: Any) -> str: + """Port of ``rocisa::InstructionInputToString`` (instruction.hpp:41-71). + + Handles the std::variant by Python + duck-typing: anything with ``toString`` (Container, RegisterContainer, + Holders) gets ``.toString()``; everything else uses ``str()`` with + the C++ ``double`` quirk (append ``.0`` when neither ``.`` nor ``e`` + is present so a literal ``1.0`` round-trips byte-for-byte). + """ + if hasattr(arg, "toString"): + return arg.toString() + if isinstance(arg, bool): + # bool is an int subclass; rocisa stores it as int -> "0"/"1". + return str(int(arg)) + if isinstance(arg, int): + return str(arg) + if isinstance(arg, float): + # ``%.17g`` matches C++ snprintf; trailing ``.0`` mirrors the + # std::visit branch in instruction.hpp:59-62. + s = format(arg, ".17g") + if "." not in s and "e" not in s and "E" not in s: + s += ".0" + return s + return str(arg) + + +class CommonInstruction(Instruction): + """Port of ``rocisa::CommonInstruction`` (instruction.hpp:382-526). + + Holds ``dst`` (+ optional ``dst1``), a list of ``srcs``, and modifier + bundles. ``toString`` produces ``" , , ... // + comment\\n"`` to match rocisa's ``getArgStr`` + ``formatWithComment``. + """ + + __slots__ = ("dst", "dst1", "srcs", "dpp", "sdwa", "vop3") + + def __init__(self, instType: Any, dst: Any, srcs: List[Any], + dpp: Any = None, sdwa: Any = None, vop3: Any = None, + comment: str = ""): + super().__init__(instType, comment) + self.dst: Any = dst + self.dst1: Any = None + self.srcs: List[Any] = list(srcs) + self.dpp: Any = dpp + self.sdwa: Any = sdwa + self.vop3: Any = vop3 + + # ----------------------------------------------------- accessors + def setSrc(self, idx: int, src: Any) -> None: + self.srcs[idx] = src + + def getParams(self): + l = [] + if self.dst is not None: + l.append(self.dst) + if self.dst1 is not None: + l.append(self.dst1) + l.extend(self.srcs) + return l + + def getDstParams(self): + dsts = [] + if self.dst is not None: + dsts.append(self.dst) + if self.dst1 is not None: + dsts.append(self.dst1) + return dsts + + def getSrcParams(self): + return list(self.srcs) + + # ----------------------------------------------------- rendering + def getArgStr(self) -> str: + parts = [] + if self.dst is not None: + ds = self.dst.toString() if hasattr(self.dst, "toString") else str(self.dst) + if ds: + parts.append(ds) + if self.dst1 is not None: + ds1 = self.dst1.toString() if hasattr(self.dst1, "toString") else str(self.dst1) + if ds1: + parts.append(ds1) + for s in self.srcs: + parts.append(_input_to_str(s)) + return ", ".join(parts) + + def toString(self) -> str: + # NB: gfx1250 ``s_set_vgpr_msb`` auto-prepend is deliberately + # omitted -- the stinkytofu left-path handles MSB via + # ``InsertVgprMsbPass``. See module docstring for the gap. + kstr = self.preStr() + " " + self.getArgStr() + if self.dpp is not None and hasattr(self.dpp, "toString"): + kstr += self.dpp.toString() + if self.sdwa is not None and hasattr(self.sdwa, "toString"): + kstr += self.sdwa.toString() + if self.vop3 is not None and hasattr(self.vop3, "toString"): + kstr += self.vop3.toString() + return self.formatWithComment(kstr) + + # ----------------------------------------------------- copy + def __deepcopy__(self, memo): + # rocisa CommonInstruction copy ctor clones dst / dst1 and + # deep-copies each src (Container ones via ``->clone()``, scalars + # by value). deepcopy gives us that for free. + clone = self.__class__.__new__(self.__class__) + memo[id(self)] = clone + Instruction.__init__(clone, self.instType, self.comment) + clone.outputInlineAsm = self.outputInlineAsm + clone.instStr = self.instStr + clone.m_memToken = _deepcopy(self.m_memToken, memo) if self.m_memToken else None + clone.dst = _deepcopy(self.dst, memo) if self.dst is not None else None + clone.dst1 = _deepcopy(self.dst1, memo) if self.dst1 is not None else None + clone.srcs = [_deepcopy(s, memo) for s in self.srcs] + clone.dpp = _deepcopy(self.dpp, memo) if self.dpp is not None else None + clone.sdwa = _deepcopy(self.sdwa, memo) if self.sdwa is not None else None + clone.vop3 = _deepcopy(self.vop3, memo) if self.vop3 is not None else None + return clone + + +# ``CompositeInstruction`` is still on the deferred list (no concrete +# subclass needs it yet). ``MacroInstruction`` is real -- see below. +CompositeInstruction = make_dummy_class(f"{_P}.CompositeInstruction") + + +class MacroInstruction(Instruction): + """Macro-call leaf -- mirror of ``rocisa::MacroInstruction``. + + Emits `` , , ...`` followed by the standard + ``Instruction`` comment formatting. ``args`` is read-write (matches + rocisa's ``def_rw`` binding); ``setSrc`` is the indexed-write helper. + + ``getDstParams`` / ``getSrcParams`` raise ``RuntimeError`` by design -- + macro args have no dst/src split, and rocisa actively throws here so + callers crash loudly instead of treating raw args as instruction + operands. + """ + + __slots__ = ("args",) + + def __init__(self, name: str, args: List[Any], comment: str = ""): + super().__init__(InstType.INST_MACRO, comment) + # C++ shadows Item::name with its own ``name`` field (Item::name + # stays ""); Python __slots__ can't shadow, so we overwrite the + # inherited slot. KernelWriter never findNamedItems by macro name + # so the divergence is unobservable. + self.name = name + self.args: List[Any] = list(args) + + def setSrc(self, idx: int, arg: Any) -> None: + self.args[idx] = arg + + def getParams(self): + return self.args + + def getDstParams(self): + raise RuntimeError( + "MacroInstruction does not have destination parameters" + ) + + def getSrcParams(self): + raise RuntimeError( + "MacroInstruction does not have source parameters" + ) + + def getArgStr(self) -> str: + # rocisa: " arg0, arg1, ..." (leading space when non-empty); + # empty args -> "". + if not self.args: + return "" + return " " + ", ".join(_input_to_str(a) for a in self.args) + + def toString(self) -> str: + return self.formatWithComment(self.name + self.getArgStr()) + + def __deepcopy__(self, memo): + clone = MacroInstruction.__new__(MacroInstruction) + memo[id(self)] = clone + Instruction.__init__(clone, self.instType, self.comment) + clone.outputInlineAsm = self.outputInlineAsm + clone.instStr = self.instStr + clone.m_memToken = _deepcopy(self.m_memToken, memo) if self.m_memToken else None + clone.name = self.name + # Containers deep-clone via their own __deepcopy__; primitives + # (int/float/str) round-trip identity-equal, matching C++ visit. + clone.args = [_deepcopy(a, memo) for a in self.args] + return clone + + def clone(self): + # rocisa Instruction::clone() returns a new shared_ptr; we mirror + # by deepcopy with a fresh memo so it behaves like a standalone + # copy (no memo sharing with outer deepcopy contexts). + return _deepcopy(self, {}) + + +# ========================================================================== +# logicalIR coercion -- rocisa operand -> stinkytofu Register. +# ========================================================================== +# +# rocisa stores instruction operands as a std::variant. Stinkytofu's ``_stinkytofu.Register`` has matching +# constructors for the literal cases. RegisterContainer already knows +# how to build a stinky.Register via ``to_stinky()`` (see +# ``rocisa_stinkytofu_adaptor.container.RegisterContainer.to_stinky``). +# +# String operands deserve a note: KernelWriter sometimes passes raw asm +# text like ``"0x0"`` or ``"s[\\sgprOffset0]"`` as a src. We round-trip +# both through ``Register(literal_string)`` -- the stinkytofu emit path +# prints literal strings verbatim, so ``Register("0x0")`` produces the +# same byte sequence on output as the rocisa right-path would. The +# ``"s[\\..." form is a TODO: stinkytofu has no analogous shorthand and +# the right behaviour is to lower the macro register reference into a +# concrete sgpr index ahead of the stinky call; for Step 3 we punt on +# this case and let the literal-string fallthrough emit it untouched, +# which is byte-correct for inspection but not semantically meaningful +# until the macro-expansion pass lands. +def _to_stinky_register(arg: Any) -> Any: + """Convert a rocisa-side instruction operand to a stinkytofu Register. + + Accepted inputs (matches ``InstructionInput`` variants): + * RegisterContainer (or subclass) -- delegate to ``.to_stinky()``. + * int / float -- wraps as a numeric literal Register. + * str -- wraps as a literal-string Register (verbatim emit). + + Raises: + ImportError: when the ``_stinkytofu.so`` binding is missing. + TypeError: on an unsupported operand type. + """ + import stinkytofu as _st # noqa: WPS433 (runtime: optional dep) + from .container import Container as _Container # noqa: WPS433 + + if hasattr(arg, "to_stinky"): + return arg.to_stinky() + if isinstance(arg, bool): + return _st.Register(int(arg)) + if isinstance(arg, int): + return _st.Register(arg) + if isinstance(arg, float): + return _st.Register(arg) + if isinstance(arg, str): + return _st.Register(arg) + if isinstance(arg, _Container): + return _st.Register(arg.toString()) + raise TypeError( + f"rocisa_stinkytofu_adaptor.instruction: cannot coerce operand of " + f"type {type(arg).__name__!r} into a stinkytofu Register. Supported " + f"types are RegisterContainer (.to_stinky()), int, float, str, Container." + ) + + +# gfx12+ style suffix on ``s_load_*`` (matches rocisa ``ReadWriteInstruction`` +# ``typeConvert`` for ``RW_TYPE0`` when ISA major >= 11). +_SMEM_LOAD_TYPE_SUFFIX = { + InstType.INST_B32: "b32", + InstType.INST_B64: "b64", + InstType.INST_B128: "b128", + InstType.INST_B256: "b256", + InstType.INST_B512: "b512", +} + + +def _smem_load_type_suffix(inst_type: Any) -> str: + try: + return _SMEM_LOAD_TYPE_SUFFIX[inst_type] + except KeyError as e: + raise ValueError(f"unsupported InstType for s_load: {inst_type!r}") from e + + +_ST_SLOAD_LOGICAL_BY_INST_TYPE = { + InstType.INST_B32: "SLoadB32", + InstType.INST_B64: "SLoadB64", + InstType.INST_B128: "SLoadB128", + InstType.INST_B256: "SLoadB256", + InstType.INST_B512: "SLoadB512", +} + + +# ========================================================================== +# VMovB32 -- first real instruction shim (Step-3 vertical slice). +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/common.hpp:5054-5076 (struct) +# rocisa/rocisa/src/instruction/common.cpp:1930-1942 (binding) +# logicalIR counterpart: +# shared/stinkytofu/src/ir/logical/LogicalInstructionDefs.inc +# (entry: VMovB32; auto-generated _stinkytofu.VMovB32 takes +# (dest, src0, comment="") -> LogicalInstruction shared_ptr). +class VMovB32(CommonInstruction): + """``v_mov_b32 dst, src`` shim with stinkytofu left-path bridge. + + Matches ``rocisa::VMovB32``'s ctor signature byte-for-byte: + ``VMovB32(dst, src, sdwa=None, comment="", dpp=None)``. Constructed + instances are slotted into a ``rocisa.code.Module``; when the module + is lowered via ``Module.to_stinky_asm(arch)`` the + ``to_stinky_logical()`` method here fabricates a fresh + ``_stinkytofu.VMovB32`` shared_ptr that the C++ pipeline consumes. + + Notes: + - dst can be any object exposing ``toString()`` (typically a + ``RegisterContainer`` from ``rocisa_stinkytofu_adaptor.container``). + - src is an ``InstructionInput``-shaped value: Container | int | + float | str. See ``_to_stinky_register`` for the routing table. + - ``sdwa`` / ``dpp`` are accepted for rocisa-API parity but are + NOT forwarded to the stinkytofu binding (the auto-generated + binding doesn't expose them). A non-None modifier today will + render correctly in rocisa-side ``__str__`` but disappear from + the lowered asm; document & raise once a KernelWriter caller + actually needs them. + """ + + def __init__(self, dst: Any, src: Any, sdwa: Any = None, + comment: str = "", dpp: Any = None): + super().__init__( + instType=InstType.INST_B32, + dst=dst, + srcs=[src], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst("v_mov_b32") + + def to_stinky_logical(self) -> Any: + """Build a fresh ``_stinkytofu.VMovB32(dst_reg, src_reg, comment)``. + + Called by ``rocisa.code.Module._collect_logical_insts`` during + ``Module.to_stinky_asm(arch)``. The returned shared_ptr is added + to a logical IR module and lowered to asm by the C++ pipeline. + + Raises: + ImportError: when the stinkytofu binding is missing. + TypeError: when ``dst`` or ``src`` is not a supported type + (see ``_to_stinky_register``). + """ + import stinkytofu as _st # noqa: WPS433 (runtime: optional dep) + + dst_reg = _to_stinky_register(self.dst) + src_reg = _to_stinky_register(self.srcs[0]) + return _st.VMovB32(dst_reg, src_reg, self.comment) + + def __deepcopy__(self, memo): + # Inherit CommonInstruction's clone behaviour but ensure + # ``__init__`` isn't re-invoked (which would re-run setInst etc.). + clone = CommonInstruction.__deepcopy__(self, memo) + # ``setInst`` was already called in __init__; the clone's + # instStr was copied from self in the base __deepcopy__, so we + # don't need to redo it. Returning ``clone`` directly preserves + # subclass identity because we used ``self.__class__.__new__``. + return clone + + +# ========================================================================== +# VMovRelsD2B32 -- indirect VGPR write via M0 offset (CompactLoopStore). +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/common.hpp:5584-5604 +# ISA mnemonic: v_movrelsd_2_b32 +# Note: stinkytofu LogicalIR does not define this instruction, so +# to_stinky_logical() returns None and _populate_logical_module will +# fall back to textblock emission via toString(). +class VMovRelsD2B32(CommonInstruction): + """``v_movrelsd_2_b32 dst, src`` -- indirect VGPR write offset by M0.""" + + def __init__(self, dst: Any, src: Any, sdwa: Any = None, + comment: str = "", dpp: Any = None): + super().__init__( + instType=InstType.INST_B32, + dst=dst, + srcs=[src], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst("v_movrelsd_2_b32") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + dst_reg = _to_stinky_register(self.dst) + src_reg = _to_stinky_register(self.srcs[0]) + return _st.VMovRelsD2B32(dst_reg, src_reg, self.comment) + + def __deepcopy__(self, memo): + clone = CommonInstruction.__deepcopy__(self, memo) + return clone + + +# ========================================================================== +# VPrngB32 -- pseudo-random number generator (VOP1, 1 src). +# ========================================================================== +class VPrngB32(CommonInstruction): + """``v_prng_b32 dst, src`` -- PRNG instruction.""" + + def __init__(self, dst: Any, src: Any, sdwa: Any = None, + comment: str = "", dpp: Any = None): + super().__init__( + instType=InstType.INST_B32, + dst=dst, + srcs=[src], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst("v_prng_b32") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + dst_reg = _to_stinky_register(self.dst) + src_reg = _to_stinky_register(self.srcs[0]) + return _st.VPrngB32(dst_reg, src_reg, comment=self.comment) + + def __deepcopy__(self, memo): + clone = CommonInstruction.__deepcopy__(self, memo) + return clone + + +# ========================================================================== +# SMovB32 / SMovB64 -- scalar moves (Phase A; same pattern as VMovB32). +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/common.hpp:1076-1117 +# rocisa/rocisa/src/instruction/common.cpp:506-524 +# logicalIR: ``SMovB32`` / ``SMovB64`` in LogicalInstructionDefs.inc +# (generated ``_stinkytofu.SMovB32(dest, src0, comment)``). +class SMovB32(CommonInstruction): + """``s_mov_b32 dst, src`` shim with stinkytofu left-path bridge.""" + + def __init__(self, dst: Any, src: Any, sdwa: Any = None, + comment: str = "", dpp: Any = None): + super().__init__( + instType=InstType.INST_B32, + dst=dst, + srcs=[src], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst("s_mov_b32") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src_reg = _to_stinky_register(self.srcs[0]) + return _st.SMovB32(dst_reg, src_reg, self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + +class SMovB64(CommonInstruction): + """``s_mov_b64 dst, src`` shim with stinkytofu left-path bridge.""" + + def __init__(self, dst: Any, src: Any, sdwa: Any = None, + comment: str = "", dpp: Any = None): + super().__init__( + instType=InstType.INST_B64, + dst=dst, + srcs=[src], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst("s_mov_b64") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src_reg = _to_stinky_register(self.srcs[0]) + return _st.SMovB64(dst_reg, src_reg, self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + +# ========================================================================== +# SNop -- scalar NOP (SOPP wait field) +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/common.hpp:1750-1794 +# logicalIR: ``SNop`` in LogicalInstructionDefs.inc +# (generated ``_stinkytofu.SNop(src0, comment)`` with ``src0`` = wait +# count as a literal ``Register``). + + +class SNop(Instruction): + """``s_nop `` shim with stinkytofu left-path bridge.""" + + __slots__ = ("wait_state",) + + def __init__(self, waitState: int = 0, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.wait_state = int(waitState) + self.setInst("s_nop") + + def getParams(self): + return [self.wait_state] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.wait_state] + + def toString(self) -> str: + kstr = self.preStr() + " " + _input_to_str(self.wait_state) + return self.formatWithComment(kstr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SNop(_to_stinky_register(self.wait_state), self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = SNop(waitState=self.wait_state, comment=self.comment) + memo[id(self)] = dup + return dup + + +# ========================================================================== +# Scalar ALU -- arithmetic, shift, bitwise (Phase 6 Step 1). +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/common.hpp +# logicalIR: All have entries in LogicalInstructionDefs.inc (2 srcs, hasDest=true). +# +# Pattern: CommonInstruction(dst, src0, src1, comment) with to_stinky_logical() +# forwarding to the matching _stinkytofu.(dst, src0, src1, comment). + + +def _make_scalar_alu_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for scalar/vector ALU instruction shim classes (dst, src0, src1).""" + + def __init__(self, dst: Any, src0: Any = None, src1: Any = None, + comment: str = "", sdwa: Any = None, dpp: Any = None, **kw): + _ = kw + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[src0, src1], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src0_reg = _to_stinky_register(self.srcs[0]) + src1_reg = _to_stinky_register(self.srcs[1]) + factory = getattr(_st, class_name) + return factory(dst_reg, src0_reg, src1_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__doc__": f"``{mnemonic} dst, src0, src1`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +def _make_scalar_unary_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for scalar/vector unary instruction shim classes (dst, src) — 1 source.""" + + def __init__(self, dst: Any, src: Any = None, + comment: str = "", sdwa: Any = None, dpp: Any = None, **kw): + _ = kw + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[src], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src_reg = _to_stinky_register(self.srcs[0]) + factory = getattr(_st, class_name) + inst = factory(dst_reg, src_reg, comment=self.comment) + if getattr(self, 'vop3', None) is not None: + inst.set_vop3(op_sel=self.vop3.op_sel) + return inst + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__doc__": f"``{mnemonic} dst, src`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +def _make_no_operand_class(class_name: str, mnemonic: str): + """Factory for zero-operand instructions (no dst, no srcs): VNop, GlobalInv, etc.""" + + def __init__(self, comment: str = "", **kw): + _ = kw + Instruction.__init__(self, InstType.INST_NOTYPE, comment) + self.setInst(mnemonic) + + def getParams(self): + return [] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [] + + def toString(self) -> str: + return self.formatWithComment(self.instStr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + factory = getattr(_st, class_name) + return factory(self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = object.__new__(type(self)) + memo[id(self)] = dup + Instruction.__init__(dup, InstType.INST_NOTYPE, self.comment) + dup.setInst(mnemonic) + return dup + + cls = type(class_name, (Instruction,), { + "__doc__": f"``{mnemonic}`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "__slots__": (), + "getParams": getParams, + "getDstParams": getDstParams, + "getSrcParams": getSrcParams, + "toString": toString, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + cls.__qualname__ = class_name + cls.__module__ = __name__ + return cls + + +def _make_imm_no_dest_class(class_name: str, mnemonic: str, param_name: str = "simm16"): + """Factory for 1-imm, no-dest instructions: SSleep, SSetPrior, SDelayAlu, etc.""" + + def __init__(self, value: int = 0, comment: str = "", **kw): + _ = kw + Instruction.__init__(self, InstType.INST_NOTYPE, comment) + self._imm_value = int(value) + self.setInst(mnemonic) + + def getParams(self): + return [self._imm_value] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self._imm_value] + + def toString(self) -> str: + kstr = self.instStr + " " + _input_to_str(self._imm_value) + return self.formatWithComment(kstr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + factory = getattr(_st, class_name) + return factory(_to_stinky_register(self._imm_value), self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = object.__new__(type(self)) + memo[id(self)] = dup + Instruction.__init__(dup, InstType.INST_NOTYPE, self.comment) + dup._imm_value = self._imm_value + dup.setInst(mnemonic) + return dup + + cls = type(class_name, (Instruction,), { + "__doc__": f"``{mnemonic} {{imm}}`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "__slots__": ("_imm_value",), + "getParams": getParams, + "getDstParams": getDstParams, + "getSrcParams": getSrcParams, + "toString": toString, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + cls.__qualname__ = class_name + cls.__module__ = __name__ + return cls + + +def _make_reg_jump_class(class_name: str, mnemonic: str, has_dest: bool = False): + """Factory for register-indirect jump/call: SSetPCB64, SSwapPCB64.""" + + if has_dest: + def __init__(self, dst: Any = None, src: Any = None, comment: str = "", **kw): + _ = kw + Instruction.__init__(self, InstType.INST_NOTYPE, comment) + self.dst = dst + self.src = src + self.setInst(mnemonic) + + def getParams(self): + return [self.dst, self.src] + + def getDstParams(self): + return [self.dst] + + def getSrcParams(self): + return [self.src] + + def toString(self) -> str: + kstr = self.instStr + " " + _input_to_str(self.dst) + ", " + _input_to_str(self.src) + return self.formatWithComment(kstr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + factory = getattr(_st, class_name) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.src), + self.comment, + ) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = object.__new__(type(self)) + memo[id(self)] = dup + Instruction.__init__(dup, InstType.INST_NOTYPE, self.comment) + dup.dst = copy.deepcopy(self.dst, memo) + dup.src = copy.deepcopy(self.src, memo) + dup.setInst(mnemonic) + return dup + + slots = ("dst", "src") + else: + def __init__(self, src: Any = None, comment: str = "", **kw): + _ = kw + Instruction.__init__(self, InstType.INST_NOTYPE, comment) + self.src = src + self.setInst(mnemonic) + + def getParams(self): + return [self.src] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.src] + + def toString(self) -> str: + kstr = self.instStr + " " + _input_to_str(self.src) + return self.formatWithComment(kstr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + factory = getattr(_st, class_name) + return factory(_to_stinky_register(self.src), self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = object.__new__(type(self)) + memo[id(self)] = dup + Instruction.__init__(dup, InstType.INST_NOTYPE, self.comment) + dup.src = copy.deepcopy(self.src, memo) + dup.setInst(mnemonic) + return dup + + slots = ("src",) + + cls = type(class_name, (Instruction,), { + "__doc__": f"``{mnemonic}`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "__slots__": slots, + "getParams": getParams, + "getDstParams": getDstParams, + "getSrcParams": getSrcParams, + "toString": toString, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + cls.__qualname__ = class_name + cls.__module__ = __name__ + return cls + + +def _make_zero_src_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for zero-source instruction shim classes (dst only, no srcs).""" + + def __init__(self, dst: Any, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[], + dpp=None, + sdwa=None, + vop3=None, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + factory = getattr(_st, class_name) + return factory(dst_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__doc__": f"``{mnemonic} dst`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +# -- Scalar Arithmetic -- +# logicalIR: SAddI32 +SAddI32 = _make_scalar_alu_class("SAddI32", "s_add_i32", InstType.INST_I32) +# logicalIR: SAddU32 +SAddU32 = _make_scalar_alu_class("SAddU32", "s_add_u32", InstType.INST_U32) +# logicalIR: SAddCU32 +SAddCU32 = _make_scalar_alu_class("SAddCU32", "s_addc_u32", InstType.INST_U32) +# logicalIR: SMulI32 +SMulI32 = _make_scalar_alu_class("SMulI32", "s_mul_i32", InstType.INST_I32) +# logicalIR: SMulHII32 +SMulHII32 = _make_scalar_alu_class("SMulHII32", "s_mul_hi_i32", InstType.INST_I32) +# logicalIR: SMulHIU32 +SMulHIU32 = _make_scalar_alu_class("SMulHIU32", "s_mul_hi_u32", InstType.INST_U32) +# logicalIR: SMulLOU32 +SMulLOU32 = _make_scalar_alu_class("SMulLOU32", "s_mul_lo_u32", InstType.INST_U32) +# logicalIR: SSubI32 +SSubI32 = _make_scalar_alu_class("SSubI32", "s_sub_i32", InstType.INST_I32) +# logicalIR: SSubU32 +SSubU32 = _make_scalar_alu_class("SSubU32", "s_sub_u32", InstType.INST_U32) +# logicalIR: SSubBU32 +SSubBU32 = _make_scalar_alu_class("SSubBU32", "s_subb_u32", InstType.INST_U32) + +# -- Scalar Shift -- +# NOTE: Native rocisa uses (dst, shiftHex, src) API where shiftHex is the shift +# amount and src is the value. Internally srcs=[src, shiftHex] (value first). +# Logical IR factory expects (dst, src0=value, src1=shiftAmount). + + +def _make_scalar_shift_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for scalar shift instruction shims matching rocisa (dst, shiftHex, src) API.""" + + def __init__(self, dst: Any, shiftHex: Any = None, src: Any = None, + comment: str = "", + src0: Any = None, src1: Any = None, + sdwa: Any = None, dpp: Any = None, **kw): + _ = kw + # Accept either (shiftHex=, src=) or (src0=, src1=) calling conventions. + if src is not None or shiftHex is not None: + value = src + shift_amount = shiftHex + else: + value = src0 + shift_amount = src1 + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[value, shift_amount], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src0_reg = _to_stinky_register(self.srcs[0]) # value + src1_reg = _to_stinky_register(self.srcs[1]) # shift amount + factory = getattr(_st, class_name) + return factory(dst_reg, src0_reg, src1_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__doc__": f"``{mnemonic} dst, src, shiftHex`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +# logicalIR: SLShiftLeftB32 +SLShiftLeftB32 = _make_scalar_shift_class("SLShiftLeftB32", "s_lshl_b32", InstType.INST_B32) +# logicalIR: SLShiftRightB32 +SLShiftRightB32 = _make_scalar_shift_class("SLShiftRightB32", "s_lshr_b32", InstType.INST_B32) +# logicalIR: SLShiftLeftB64 +SLShiftLeftB64 = _make_scalar_shift_class("SLShiftLeftB64", "s_lshl_b64", InstType.INST_B64) +# logicalIR: SLShiftRightB64 +SLShiftRightB64 = _make_scalar_shift_class("SLShiftRightB64", "s_lshr_b64", InstType.INST_B64) +# logicalIR: SAShiftRightI32 +SAShiftRightI32 = _make_scalar_shift_class("SAShiftRightI32", "s_ashr_i32", InstType.INST_I32) +# logicalIR: SLShiftLeft1AddU32 +SLShiftLeft1AddU32 = _make_scalar_alu_class("SLShiftLeft1AddU32", "s_lshl1_add_u32", InstType.INST_U32) +# logicalIR: SLShiftLeft2AddU32 +SLShiftLeft2AddU32 = _make_scalar_alu_class("SLShiftLeft2AddU32", "s_lshl2_add_u32", InstType.INST_U32) +# logicalIR: SLShiftLeft3AddU32 +SLShiftLeft3AddU32 = _make_scalar_alu_class("SLShiftLeft3AddU32", "s_lshl3_add_u32", InstType.INST_U32) +# logicalIR: SLShiftLeft4AddU32 +SLShiftLeft4AddU32 = _make_scalar_alu_class("SLShiftLeft4AddU32", "s_lshl4_add_u32", InstType.INST_U32) + +# -- Scalar Bitwise -- +# logicalIR: SAndB32 +SAndB32 = _make_scalar_alu_class("SAndB32", "s_and_b32", InstType.INST_B32) +# logicalIR: SAndB64 +SAndB64 = _make_scalar_alu_class("SAndB64", "s_and_b64", InstType.INST_B64) +# logicalIR: SAndN2B32 +SAndN2B32 = _make_scalar_alu_class("SAndN2B32", "s_andn2_b32", InstType.INST_B32) +# logicalIR: SOrB32 +SOrB32 = _make_scalar_alu_class("SOrB32", "s_or_b32", InstType.INST_B32) +# logicalIR: SOrB64 +SOrB64 = _make_scalar_alu_class("SOrB64", "s_or_b64", InstType.INST_B64) +# logicalIR: SXorB32 +SXorB32 = _make_scalar_alu_class("SXorB32", "s_xor_b32", InstType.INST_B32) +# logicalIR: SAndSaveExecB32 (1 src) +SAndSaveExecB32 = _make_scalar_unary_class("SAndSaveExecB32", "s_and_saveexec_b32", InstType.INST_B32) +# logicalIR: SAndSaveExecB64 (1 src) +SAndSaveExecB64 = _make_scalar_unary_class("SAndSaveExecB64", "s_and_saveexec_b64", InstType.INST_B64) +# logicalIR: SOrSaveExecB32 (1 src) +SOrSaveExecB32 = _make_scalar_unary_class("SOrSaveExecB32", "s_or_saveexec_b32", InstType.INST_B32) +# logicalIR: SOrSaveExecB64 (1 src) +SOrSaveExecB64 = _make_scalar_unary_class("SOrSaveExecB64", "s_or_saveexec_b64", InstType.INST_B64) + + +# ========================================================================== +# Vector ALU -- arithmetic, shift, bitwise, other (Phase 6 Step 4). +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/common.hpp +# logicalIR: All have entries in LogicalInstructionDefs.inc. +# +# Pattern: reuse the scalar factories where the API shape is identical +# (dst, src0, src1) or create specialized ones for ternary / vector-shift. + + +def _make_ternary_class(class_name: str, mnemonic: str, inst_type: "InstType", + shift_position: int = 0): + """Factory for ternary instruction shim classes (dst, src0, src1, src2). + + Also accepts native rocisa shift-ternary API ``(dst, shiftHex, src0, src1)`` + where ``shiftHex`` is placed at ISA operand position ``shift_position``: + - 0: srcs = [shiftHex, src0, src1] (default, legacy) + - 1: srcs = [src0, shiftHex, src1] (v_lshl_add, v_lshl_or) + - 2: srcs = [src0, src1, shiftHex] (v_add_lshl) + """ + + def __init__(self, dst: Any, src0: Any = None, src1: Any = None, + src2: Any = None, shiftHex: Any = None, + comment: str = "", sdwa: Any = None, dpp: Any = None, + vop3: Any = None, **kw): + _ = kw + if shiftHex is not None: + if shift_position == 1: + s0, s1, s2 = src0, shiftHex, src1 + elif shift_position == 2: + s0, s1, s2 = src0, src1, shiftHex + else: + s0, s1, s2 = shiftHex, src0, src1 + else: + s0, s1, s2 = src0, src1, src2 + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[s0, s1, s2], + dpp=dpp, + sdwa=sdwa, + vop3=vop3, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src0_reg = _to_stinky_register(self.srcs[0]) + src1_reg = _to_stinky_register(self.srcs[1]) + src2_reg = _to_stinky_register(self.srcs[2]) + factory = getattr(_st, class_name) + return factory(dst_reg, src0_reg, src1_reg, src2_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__doc__": f"``{mnemonic} dst, src0, src1, src2`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +def _make_vector_shift_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for vector shift instruction shims matching rocisa (dst, shiftHex, src) API. + + Unlike scalar shifts (which internally reorder srcs to [value, shift]), vector + shifts store srcs in the native order [shiftHex, src] and emit as-is. The + stinkytofu factory also expects (dst, src0=shift, src1=value, comment). + """ + + def __init__(self, dst: Any, shiftHex: Any = None, src: Any = None, + comment: str = "", + src0: Any = None, src1: Any = None, + sdwa: Any = None, dpp: Any = None, **kw): + _ = kw + if shiftHex is not None or src is not None: + s0 = shiftHex + s1 = src + else: + s0 = src0 + s1 = src1 + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[s0, s1], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src0_reg = _to_stinky_register(self.srcs[0]) + src1_reg = _to_stinky_register(self.srcs[1]) + factory = getattr(_st, class_name) + return factory(dst_reg, src0_reg, src1_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__doc__": f"``{mnemonic} dst, shiftHex, src`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +# -- Vector Arithmetic (binary: dst, src0, src1) -- +# logicalIR: VAddU32 +VAddU32 = _make_scalar_alu_class("VAddU32", "v_add_nc_u32", InstType.INST_U32) +# logicalIR: VAddF32 +VAddF32 = _make_scalar_alu_class("VAddF32", "v_add_f32", InstType.INST_F32) +# logicalIR: VSubF32 +VSubF32 = _make_scalar_alu_class("VSubF32", "v_sub_f32", InstType.INST_F32) +# logicalIR: VSubI32 +VSubI32 = _make_scalar_alu_class("VSubI32", "v_sub_nc_i32", InstType.INST_I32) +# logicalIR: VSubU32 +VSubU32 = _make_scalar_alu_class("VSubU32", "v_sub_nc_u32", InstType.INST_U32) +# logicalIR: VMulF32 +VMulF32 = _make_scalar_alu_class("VMulF32", "v_mul_f32", InstType.INST_F32) +# logicalIR: VMulLOU32 +VMulLOU32 = _make_scalar_alu_class("VMulLOU32", "v_mul_lo_u32", InstType.INST_LO_U32) +# logicalIR: VMulHIU32 +VMulHIU32 = _make_scalar_alu_class("VMulHIU32", "v_mul_hi_u32", InstType.INST_HI_U32) +# logicalIR: VMulHII32 +VMulHII32 = _make_scalar_alu_class("VMulHII32", "v_mul_hi_i32", InstType.INST_HI_I32) +# logicalIR: VMulI32I24 +VMulI32I24 = _make_scalar_alu_class("VMulI32I24", "v_mul_i32_i24", InstType.INST_I32) +# logicalIR: VMulU32U24 +VMulU32U24 = _make_scalar_alu_class("VMulU32U24", "v_mul_u32_u24", InstType.INST_U32) +# logicalIR: VAndB32 +VAndB32 = _make_scalar_alu_class("VAndB32", "v_and_b32", InstType.INST_B32) +# logicalIR: VOrB32 +VOrB32 = _make_scalar_alu_class("VOrB32", "v_or_b32", InstType.INST_B32) +# logicalIR: VXorB32 +VXorB32 = _make_scalar_alu_class("VXorB32", "v_xor_b32", InstType.INST_B32) + +# -- Vector Shift (dst, shiftHex, src) → rev mnemonic -- +# logicalIR: VLShiftLeftB32 +VLShiftLeftB32 = _make_vector_shift_class("VLShiftLeftB32", "v_lshlrev_b32", InstType.INST_B32) +# logicalIR: VLShiftRightB32 +VLShiftRightB32 = _make_vector_shift_class("VLShiftRightB32", "v_lshrrev_b32", InstType.INST_B32) +# logicalIR: VLShiftLeftB64 +VLShiftLeftB64 = _make_vector_shift_class("VLShiftLeftB64", "v_lshlrev_b64", InstType.INST_B64) +# logicalIR: VLShiftRightB64 +VLShiftRightB64 = _make_vector_shift_class("VLShiftRightB64", "v_lshrrev_b64", InstType.INST_B64) + +# -- Vector Ternary (dst, src0, src1, src2) -- +# logicalIR: VFmaF32 +VFmaF32 = _make_ternary_class("VFmaF32", "v_fma_f32", InstType.INST_F32) +# logicalIR: VFmaMixF32 +VFmaMixF32 = _make_ternary_class("VFmaMixF32", "v_fma_mix_f32", InstType.INST_F32) +# logicalIR: VAndOrB32 +VAndOrB32 = _make_ternary_class("VAndOrB32", "v_and_or_b32", InstType.INST_B32) + +# -- VCndMaskB32 (native: dst, src0, src1, src2=VCC; logical IR: 2 srcs) -- +# logicalIR: VCndMaskB32 + + +class VCndMaskB32(CommonInstruction): + """``v_cndmask_b32 dst, src0, src1, vcc`` shim with stinkytofu left-path bridge. + + Native rocisa takes (dst, src0, src1, src2=VCC). The logical IR version has + only 2 sources (the VCC mask is implicit); ``to_stinky_logical`` forwards + only (dst, src0, src1). + """ + + def __init__(self, dst: Any, src0: Any = None, src1: Any = None, + src2: Any = None, + sdwa: Any = None, comment: str = "", dpp: Any = None, **kw): + _ = kw + srcs = [src0, src1] + if src2 is not None: + srcs.append(src2) + super().__init__( + instType=InstType.INST_B32, + dst=dst, + srcs=srcs, + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst("v_cndmask_b32") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src0_reg = _to_stinky_register(self.srcs[0]) + src1_reg = _to_stinky_register(self.srcs[1]) + src2_reg = _to_stinky_register(self.srcs[2]) if len(self.srcs) > 2 else _st.Register("vcc_lo") + return _st.VCndMaskB32(dst_reg, src0_reg, src1_reg, src2_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + +# -- VReadfirstlaneB32 (unary: dst, src) -- +# logicalIR: VReadfirstlaneB32 +VReadfirstlaneB32 = _make_scalar_unary_class("VReadfirstlaneB32", "v_readfirstlane_b32", InstType.INST_B32) + + +# ========================================================================== +# SBarrier -- workgroup barrier (Phase 6 Step 2). +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/common.hpp:1470-1548 +# logicalIR: ``SBarrier`` in LogicalInstructionDefs.inc (0 srcs, no dest). +# +# Native rocisa SBarrier has complex logic (separate signal/wait, cluster +# barrier) gated on HasNewBarrier/HasClusterBarrier caps. For gfx1250 the +# logical IR lowering pass handles the arch-specific emit; we only bridge +# the constructor parameters through. + + +class SBarrier(Instruction): + """``s_barrier`` shim with stinkytofu left-path bridge.""" + + __slots__ = ("separate", "wait_flag", "cluster_barrier") + + def __init__(self, separate: bool = False, wait: bool = False, + clusterBarrier: bool = False, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.separate = bool(separate) + self.wait_flag = bool(wait) + self.cluster_barrier = bool(clusterBarrier) + self.setInst("s_barrier") + + def getParams(self): + return [] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [] + + def toString(self) -> str: + return self.formatWithComment(self.instStr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SBarrier(self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = SBarrier( + separate=self.separate, wait=self.wait_flag, + clusterBarrier=self.cluster_barrier, comment=self.comment, + ) + memo[id(self)] = dup + return dup + + +# ========================================================================== +# SGetRegB32 / SSetRegB32 / SSetRegIMM32B32 -- HW register access (Step 2). +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/common.hpp:1990-2054 +# logicalIR: 1 src, hasDest=true, "Scalar Control" + +# logicalIR: SGetRegB32 +SGetRegB32 = _make_scalar_unary_class("SGetRegB32", "s_getreg_b32", InstType.INST_B32) +# logicalIR: SSetRegB32 +SSetRegB32 = _make_scalar_unary_class("SSetRegB32", "s_setreg_b32", InstType.INST_B32) +# logicalIR: SSetRegIMM32B32 +SSetRegIMM32B32 = _make_scalar_unary_class("SSetRegIMM32B32", "s_setreg_IMM32_b32", InstType.INST_B32) + + +# ========================================================================== +# SMemLoadInstruction / SLoadB* -- scalar memory loads (Phase A). +# ========================================================================== +# +# source: rocisa/rocisa/include/instruction/mem.hpp:514-573 +# logicalIR: ``SLoadB32`` … ``SLoadB512`` in LogicalInstructionDefs.inc +# (auto-generated ``_stinkytofu.SLoadB32(dest, src0, src1, comment)``). +# +# ``SMEMModifiers`` on the logical-IR Python binding is not wired yet; +# ``to_stinky_logical`` raises if ``smem`` is not ``None``. + + +class SMemLoadInstruction(Instruction): + """``s_load_ dst, base, soffset`` base (rocisa ``SMemLoadInstruction``).""" + + __slots__ = ("dst", "base", "soffset", "smem") + + def __init__( + self, + inst_type: Any, + dst: Any = None, + base: Any = None, + soffset: Any = None, + smem: Any = None, + comment: str = "", + **kwargs: Any, + ): + _ = kwargs # rocisa binding may forward ignored kwargs + super().__init__(inst_type, comment) + self.dst = dst + self.base = base + self.soffset = soffset + self.smem = smem + self.setInst("s_load_") + + def preStr(self) -> str: + return self.instStr + _smem_load_type_suffix(self.instType) + + def getArgStr(self) -> str: + parts: List[str] = [] + if self.dst is not None: + parts.append( + self.dst.toString() if hasattr(self.dst, "toString") else str(self.dst)) + if self.base is not None: + parts.append( + self.base.toString() if hasattr(self.base, "toString") else str(self.base)) + parts.append(_input_to_str(self.soffset)) + return ", ".join(parts) + + def toString(self) -> str: + kstr = self.preStr() + " " + self.getArgStr() + if self.smem is not None and hasattr(self.smem, "toString"): + kstr += self.smem.toString() + return self.formatWithComment(kstr) + + def getParams(self): + return [self.dst, self.base, self.soffset] + + def getDstParams(self): + return [self.dst] if self.dst is not None else [] + + def getSrcParams(self): + return [self.base, self.soffset] + + def to_stinky_logical(self) -> Any: + if self.smem is not None: + raise NotImplementedError( + "rocisa_stinkytofu_adaptor: SMemLoad with non-None SMEMModifiers " + "is not yet supported on the stinkytofu logical-IR path.", + ) + import stinkytofu as _st # noqa: WPS433 + + try: + fac_name = _ST_SLOAD_LOGICAL_BY_INST_TYPE[self.instType] + except KeyError as e: + raise ValueError(f"SMemLoad: unsupported instType {self.instType!r}") from e + factory = getattr(_st, fac_name, None) + if factory is None: + raise ImportError( + f"stinkytofu binding has no factory {fac_name!r}; rebuild stinkytofu " + f"(tablegen) after adding SLoad* to LogicalInstructionDefs.inc.", + ) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.base), + _to_stinky_register(self.soffset), + self.comment, + ) + + def __deepcopy__(self, memo): + clone = self.__class__.__new__(self.__class__) + memo[id(self)] = clone + Instruction.__init__(clone, self.instType, self.comment) + clone.outputInlineAsm = self.outputInlineAsm + clone.instStr = self.instStr + clone.m_memToken = ( + _deepcopy(self.m_memToken, memo) if self.m_memToken is not None else None + ) + clone.dst = _deepcopy(self.dst, memo) if self.dst is not None else None + clone.base = _deepcopy(self.base, memo) if self.base is not None else None + if isinstance(self.soffset, (int, float, str, bool)): + clone.soffset = self.soffset + else: + clone.soffset = _deepcopy(self.soffset, memo) + clone.smem = _deepcopy(self.smem, memo) if self.smem is not None else None + return clone + + +class SLoadB32(SMemLoadInstruction): + """``s_load_b32`` shim.""" + + def __init__( + self, + dst: Any = None, + base: Any = None, + soffset: Any = None, + smem: Any = None, + comment: str = "", + **kwargs: Any, + ): + super().__init__(InstType.INST_B32, dst, base, soffset, smem, comment, **kwargs) + + +class SLoadB64(SMemLoadInstruction): + """``s_load_b64`` shim.""" + + def __init__( + self, + dst: Any = None, + base: Any = None, + soffset: Any = None, + smem: Any = None, + comment: str = "", + **kwargs: Any, + ): + super().__init__(InstType.INST_B64, dst, base, soffset, smem, comment, **kwargs) + + +class SLoadB128(SMemLoadInstruction): + """``s_load_b128`` shim.""" + + def __init__( + self, + dst: Any = None, + base: Any = None, + soffset: Any = None, + smem: Any = None, + comment: str = "", + **kwargs: Any, + ): + super().__init__(InstType.INST_B128, dst, base, soffset, smem, comment, **kwargs) + + +class SLoadB256(SMemLoadInstruction): + """``s_load_b256`` shim.""" + + def __init__( + self, + dst: Any = None, + base: Any = None, + soffset: Any = None, + smem: Any = None, + comment: str = "", + **kwargs: Any, + ): + super().__init__(InstType.INST_B256, dst, base, soffset, smem, comment, **kwargs) + + +class SLoadB512(SMemLoadInstruction): + """``s_load_b512`` shim.""" + + def __init__( + self, + dst: Any = None, + base: Any = None, + soffset: Any = None, + smem: Any = None, + comment: str = "", + **kwargs: Any, + ): + super().__init__(InstType.INST_B512, dst, base, soffset, smem, comment, **kwargs) + + +_ST_SSTORE_LOGICAL_BY_INST_TYPE: Dict[Any, str] = { + InstType.INST_B32: "SStoreB32", + InstType.INST_B64: "SStoreB64", + InstType.INST_B128: "SStoreB128", + InstType.INST_B256: "SStoreB256", + InstType.INST_B512: "SStoreB512", +} + + +class SMemStoreInstruction(Instruction): + """``s_store_ src, base, soffset`` base class.""" + + __slots__ = ("src", "base", "soffset", "smem") + + def __init__( + self, + inst_type: Any, + src: Any = None, + base: Any = None, + soffset: Any = None, + smem: Any = None, + comment: str = "", + **kwargs: Any, + ): + _ = kwargs + super().__init__(inst_type, comment) + self.src = src + self.base = base + self.soffset = soffset + self.smem = smem + self.setInst("s_store_") + + def toString(self) -> str: + parts: List[str] = [] + if self.src is not None: + parts.append( + self.src.toString() if hasattr(self.src, "toString") else str(self.src)) + if self.base is not None: + parts.append( + self.base.toString() if hasattr(self.base, "toString") else str(self.base)) + parts.append(_input_to_str(self.soffset)) + kstr = self.instStr + _smem_load_type_suffix(self.instType) + " " + ", ".join(parts) + if self.smem is not None and hasattr(self.smem, "toString"): + kstr += self.smem.toString() + return self.formatWithComment(kstr) + + def getParams(self): + return [self.src, self.base, self.soffset] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.src, self.base, self.soffset] + + def to_stinky_logical(self) -> Any: + if self.smem is not None: + raise NotImplementedError( + "rocisa_stinkytofu_adaptor: SMemStore with non-None SMEMModifiers " + "is not yet supported on the stinkytofu logical-IR path.", + ) + import stinkytofu as _st # noqa: WPS433 + + fac_name = _ST_SSTORE_LOGICAL_BY_INST_TYPE.get(self.instType) + if fac_name is None: + raise ValueError(f"SMemStore: unsupported instType {self.instType!r}") + factory = getattr(_st, fac_name) + return factory( + _to_stinky_register(self.src), + _to_stinky_register(self.base), + _to_stinky_register(self.soffset), + self.comment, + ) + + def __deepcopy__(self, memo): + clone = self.__class__.__new__(self.__class__) + memo[id(self)] = clone + Instruction.__init__(clone, self.instType, self.comment) + clone.outputInlineAsm = self.outputInlineAsm + clone.instStr = self.instStr + clone.m_memToken = ( + _deepcopy(self.m_memToken, memo) if self.m_memToken is not None else None + ) + clone.src = _deepcopy(self.src, memo) if self.src is not None else None + clone.base = _deepcopy(self.base, memo) if self.base is not None else None + if isinstance(self.soffset, (int, float, str, bool)): + clone.soffset = self.soffset + else: + clone.soffset = _deepcopy(self.soffset, memo) + clone.smem = _deepcopy(self.smem, memo) if self.smem is not None else None + return clone + + +class SStoreB32(SMemStoreInstruction): + def __init__(self, src=None, base=None, soffset=None, smem=None, comment="", **kw): + super().__init__(InstType.INST_B32, src, base, soffset, smem, comment, **kw) + + +class SStoreB64(SMemStoreInstruction): + def __init__(self, src=None, base=None, soffset=None, smem=None, comment="", **kw): + super().__init__(InstType.INST_B64, src, base, soffset, smem, comment, **kw) + + +class SStoreB128(SMemStoreInstruction): + def __init__(self, src=None, base=None, soffset=None, smem=None, comment="", **kw): + super().__init__(InstType.INST_B128, src, base, soffset, smem, comment, **kw) + + +class SStoreB256(SMemStoreInstruction): + def __init__(self, src=None, base=None, soffset=None, smem=None, comment="", **kw): + super().__init__(InstType.INST_B256, src, base, soffset, smem, comment, **kw) + + +class SStoreB512(SMemStoreInstruction): + def __init__(self, src=None, base=None, soffset=None, smem=None, comment="", **kw): + super().__init__(InstType.INST_B512, src, base, soffset, smem, comment, **kw) + + +# ========================================================================== +# Branch instructions +# source: rocisa/rocisa/src/instruction/branch.cpp +# logicalIR: SBranch, SCBranchSCC0, SCBranchSCC1, SCBranchVCCNZ, +# SCBranchVCCZ, SCBranchExecZ, SCBranchExecNZ +# ========================================================================== + + +class BranchInstruction(Instruction): + """Base class for branch instructions (labelName target).""" + + __slots__ = ("labelName",) + + def __init__(self, labelName: str = "", comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.labelName = str(labelName) + + def getParams(self): + return [self.labelName] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.labelName] + + def toString(self) -> str: + return self.formatWithComment(self.instStr + " " + self.labelName) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = self.__class__(labelName=self.labelName, comment=self.comment) + memo[id(self)] = dup + return dup + + +def _make_branch_class(class_name: str, mnemonic: str): + """Factory for branch instruction adaptor classes.""" + + def _init(self, labelName: str = "", comment: str = ""): + BranchInstruction.__init__(self, labelName, comment) + self.setInst(mnemonic) + + def _to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + factory = getattr(_st, class_name) + return factory(_to_stinky_register(self.labelName), self.comment) + + cls = type(class_name, (BranchInstruction,), { + "__init__": _init, + "to_stinky_logical": _to_stinky_logical, + "__slots__": (), + }) + cls.__qualname__ = class_name + cls.__module__ = __name__ + return cls + + +# logicalIR: SBranch +SBranch = _make_branch_class("SBranch", "s_branch") +# logicalIR: SCBranchSCC0 +SCBranchSCC0 = _make_branch_class("SCBranchSCC0", "s_cbranch_scc0") +# logicalIR: SCBranchSCC1 +SCBranchSCC1 = _make_branch_class("SCBranchSCC1", "s_cbranch_scc1") +SAddPCI64_SIMM = _make_branch_class("SAddPCI64_SIMM", "s_add_pc_i64") + + +SAddPCI64_SIMM.to_stinky_logical = lambda self: None # no logicalIR mapping +# logicalIR: SCBranchVCCNZ +SCBranchVCCNZ = _make_branch_class("SCBranchVCCNZ", "s_cbranch_vccnz") +# logicalIR: SCBranchVCCZ +SCBranchVCCZ = _make_branch_class("SCBranchVCCZ", "s_cbranch_vccz") +# logicalIR: SSetPCB64 +SSetPCB64 = _make_reg_jump_class("SSetPCB64", "s_setpc_b64", has_dest=False) +# logicalIR: SSwapPCB64 +SSwapPCB64 = _make_reg_jump_class("SSwapPCB64", "s_swappc_b64", has_dest=True) +# logicalIR: SCBranchExecZ +SCBranchExecZ = _make_branch_class("SCBranchExecZ", "s_cbranch_execz") +# logicalIR: SCBranchExecNZ +SCBranchExecNZ = _make_branch_class("SCBranchExecNZ", "s_cbranch_execnz") + + +# ========================================================================== +# Compare instructions (Phase 6 Step 5) +# source: rocisa/rocisa/src/instruction/cmp.cpp +# ========================================================================== +# +# Three shapes: +# - Scalar Compare (SCmp*, SBitcmp1B32): no dst, 2 srcs. +# Native API: (src0, src1, comment="") +# Stinkytofu binding: (src0, src1, comment="") +# - Vector Compare (VCmp*): dst, src0, src1. +# Native API: (dst, src0, src1, sdwa=None, comment="") +# Stinkytofu binding: (dest, src0, src1, dpp=None, sdwa=None, comment="") +# - Vector CompareX (VCmpX*): same shape as VCmp. +# +# VCmpInstruction / VCmpXInstruction base classes remain dummies (not in +# logical IR); concrete subclasses are what matters. +VCmpInstruction = make_dummy_class(f"{_P}.VCmpInstruction") +VCmpXInstruction = make_dummy_class(f"{_P}.VCmpXInstruction") + + +def _make_scalar_cmp_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for scalar compare instruction shims (no dst, 2 srcs).""" + + def __init__(self, src0: Any = None, src1: Any = None, + comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, + instType=inst_type, + dst=None, + srcs=[src0, src1], + dpp=None, + sdwa=None, + vop3=None, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + src0_reg = _to_stinky_register(self.srcs[0]) + src1_reg = _to_stinky_register(self.srcs[1]) + factory = getattr(_st, class_name) + return factory(src0_reg, src1_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__doc__": f"``{mnemonic} src0, src1`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +def _make_vcmp_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for vector compare instruction shims (dst, src0, src1).""" + + def __init__(self, dst: Any = None, src0: Any = None, src1: Any = None, + sdwa: Any = None, comment: str = "", dpp: Any = None, **kw): + _ = kw + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[src0, src1], + dpp=dpp, + sdwa=sdwa, + vop3=None, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src0_reg = _to_stinky_register(self.srcs[0]) + src1_reg = _to_stinky_register(self.srcs[1]) + factory = getattr(_st, class_name) + return factory(dst_reg, src0_reg, src1_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__doc__": f"``{mnemonic} dst, src0, src1`` shim with stinkytofu left-path bridge.", + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +# -- Scalar Compare (no dst, 2 srcs) -- +# logicalIR: SCmpEQI32 +SCmpEQI32 = _make_scalar_cmp_class("SCmpEQI32", "s_cmp_eq_i32", InstType.INST_I32) +# logicalIR: SCmpEQU32 +SCmpEQU32 = _make_scalar_cmp_class("SCmpEQU32", "s_cmp_eq_u32", InstType.INST_U32) +# logicalIR: SCmpEQU64 +SCmpEQU64 = _make_scalar_cmp_class("SCmpEQU64", "s_cmp_eq_u64", InstType.INST_U64) +# logicalIR: SCmpGeI32 +SCmpGeI32 = _make_scalar_cmp_class("SCmpGeI32", "s_cmp_ge_i32", InstType.INST_I32) +# logicalIR: SCmpGeU32 +SCmpGeU32 = _make_scalar_cmp_class("SCmpGeU32", "s_cmp_ge_u32", InstType.INST_U32) +# logicalIR: SCmpGtI32 +SCmpGtI32 = _make_scalar_cmp_class("SCmpGtI32", "s_cmp_gt_i32", InstType.INST_I32) +# logicalIR: SCmpGtU32 +SCmpGtU32 = _make_scalar_cmp_class("SCmpGtU32", "s_cmp_gt_u32", InstType.INST_U32) +# logicalIR: SCmpLeI32 +SCmpLeI32 = _make_scalar_cmp_class("SCmpLeI32", "s_cmp_le_i32", InstType.INST_I32) +# logicalIR: SCmpLeU32 +SCmpLeU32 = _make_scalar_cmp_class("SCmpLeU32", "s_cmp_le_u32", InstType.INST_U32) +# logicalIR: SCmpLgU32 +SCmpLgU32 = _make_scalar_cmp_class("SCmpLgU32", "s_cmp_lg_u32", InstType.INST_U32) +# logicalIR: SCmpLgI32 +SCmpLgI32 = _make_scalar_cmp_class("SCmpLgI32", "s_cmp_lg_i32", InstType.INST_I32) +# logicalIR: SCmpLgU64 +SCmpLgU64 = _make_scalar_cmp_class("SCmpLgU64", "s_cmp_lg_u64", InstType.INST_U64) +# logicalIR: SCmpLtI32 +SCmpLtI32 = _make_scalar_cmp_class("SCmpLtI32", "s_cmp_lt_i32", InstType.INST_I32) +# logicalIR: SCmpLtU32 +SCmpLtU32 = _make_scalar_cmp_class("SCmpLtU32", "s_cmp_lt_u32", InstType.INST_U32) +# logicalIR: SBitcmp1B32 +SBitcmp1B32 = _make_scalar_cmp_class("SBitcmp1B32", "s_bitcmp1_b32", InstType.INST_B32) +# logicalIR: SCmpKEQU32 +SCmpKEQU32 = _make_scalar_cmp_class("SCmpKEQU32", "s_cmpk_eq_u32", InstType.INST_U32) +# logicalIR: SCmpKGeU32 +SCmpKGeU32 = _make_scalar_cmp_class("SCmpKGeU32", "s_cmpk_ge_u32", InstType.INST_U32) +# logicalIR: SCmpKGtU32 +SCmpKGtU32 = _make_scalar_cmp_class("SCmpKGtU32", "s_cmpk_gt_u32", InstType.INST_U32) +# logicalIR: SCmpKLGU32 +SCmpKLGU32 = _make_scalar_cmp_class("SCmpKLGU32", "s_cmpk_lg_u32", InstType.INST_U32) + +# -- Vector Compare (dst, src0, src1) -- +# logicalIR: VCmpEQF32 +VCmpEQF32 = _make_vcmp_class("VCmpEQF32", "v_cmp_eq_f32", InstType.INST_F32) +# logicalIR: VCmpEQF64 +VCmpEQF64 = _make_vcmp_class("VCmpEQF64", "v_cmp_eq_f64", InstType.INST_F64) +# logicalIR: VCmpEQU32 +VCmpEQU32 = _make_vcmp_class("VCmpEQU32", "v_cmp_eq_u32", InstType.INST_U32) +# logicalIR: VCmpEQI32 +VCmpEQI32 = _make_vcmp_class("VCmpEQI32", "v_cmp_eq_i32", InstType.INST_I32) +# logicalIR: VCmpGEF16 +VCmpGEF16 = _make_vcmp_class("VCmpGEF16", "v_cmp_ge_f16", InstType.INST_F16) +# logicalIR: VCmpGTF16 +VCmpGTF16 = _make_vcmp_class("VCmpGTF16", "v_cmp_gt_f16", InstType.INST_F16) +# logicalIR: VCmpGEF32 +VCmpGEF32 = _make_vcmp_class("VCmpGEF32", "v_cmp_ge_f32", InstType.INST_F32) +# logicalIR: VCmpGTF32 +VCmpGTF32 = _make_vcmp_class("VCmpGTF32", "v_cmp_gt_f32", InstType.INST_F32) +# logicalIR: VCmpGEF64 +VCmpGEF64 = _make_vcmp_class("VCmpGEF64", "v_cmp_ge_f64", InstType.INST_F64) +# logicalIR: VCmpGTF64 +VCmpGTF64 = _make_vcmp_class("VCmpGTF64", "v_cmp_gt_f64", InstType.INST_F64) +# logicalIR: VCmpGEI32 +VCmpGEI32 = _make_vcmp_class("VCmpGEI32", "v_cmp_ge_i32", InstType.INST_I32) +# logicalIR: VCmpGTI32 +VCmpGTI32 = _make_vcmp_class("VCmpGTI32", "v_cmp_gt_i32", InstType.INST_I32) +# logicalIR: VCmpGEU32 +VCmpGEU32 = _make_vcmp_class("VCmpGEU32", "v_cmp_ge_u32", InstType.INST_U32) +# logicalIR: VCmpGtU32 +VCmpGtU32 = _make_vcmp_class("VCmpGtU32", "v_cmp_gt_u32", InstType.INST_U32) +# logicalIR: VCmpLeU32 +VCmpLeU32 = _make_vcmp_class("VCmpLeU32", "v_cmp_le_u32", InstType.INST_U32) +# logicalIR: VCmpLeI32 +VCmpLeI32 = _make_vcmp_class("VCmpLeI32", "v_cmp_le_i32", InstType.INST_I32) +# logicalIR: VCmpLtI32 +VCmpLtI32 = _make_vcmp_class("VCmpLtI32", "v_cmp_lt_i32", InstType.INST_I32) +# logicalIR: VCmpLtU32 +VCmpLtU32 = _make_vcmp_class("VCmpLtU32", "v_cmp_lt_u32", InstType.INST_U32) +# logicalIR: VCmpUF32 +VCmpUF32 = _make_vcmp_class("VCmpUF32", "v_cmp_u_f32", InstType.INST_F32) +# logicalIR: VCmpNeI32 +VCmpNeI32 = _make_vcmp_class("VCmpNeI32", "v_cmp_ne_i32", InstType.INST_I32) +# logicalIR: VCmpNeU32 +VCmpNeU32 = _make_vcmp_class("VCmpNeU32", "v_cmp_ne_u32", InstType.INST_U32) +# logicalIR: VCmpNeU64 +VCmpNeU64 = _make_vcmp_class("VCmpNeU64", "v_cmp_ne_u64", InstType.INST_U64) +# logicalIR: VCmpClassF32 +VCmpClassF32 = _make_vcmp_class("VCmpClassF32", "v_cmp_class_f32", InstType.INST_F32) + +# -- Vector CompareX (dst, src0, src1) -- +# logicalIR: VCmpXClassF32 +VCmpXClassF32 = _make_vcmp_class("VCmpXClassF32", "v_cmpx_class_f32", InstType.INST_F32) +# logicalIR: VCmpXEqU32 +VCmpXEqU32 = _make_vcmp_class("VCmpXEqU32", "v_cmpx_eq_u32", InstType.INST_U32) +# logicalIR: VCmpXGeU32 +VCmpXGeU32 = _make_vcmp_class("VCmpXGeU32", "v_cmpx_ge_u32", InstType.INST_U32) +# logicalIR: VCmpXGtU32 +VCmpXGtU32 = _make_vcmp_class("VCmpXGtU32", "v_cmpx_gt_u32", InstType.INST_U32) +# logicalIR: VCmpXLeU32 +VCmpXLeU32 = _make_vcmp_class("VCmpXLeU32", "v_cmpx_le_u32", InstType.INST_U32) +# logicalIR: VCmpXLeI32 +VCmpXLeI32 = _make_vcmp_class("VCmpXLeI32", "v_cmpx_le_i32", InstType.INST_I32) +# logicalIR: VCmpXLtF32 +VCmpXLtF32 = _make_vcmp_class("VCmpXLtF32", "v_cmpx_lt_f32", InstType.INST_F32) +# logicalIR: VCmpXLtI32 +VCmpXLtI32 = _make_vcmp_class("VCmpXLtI32", "v_cmpx_lt_i32", InstType.INST_I32) +# logicalIR: VCmpXLtU32 +VCmpXLtU32 = _make_vcmp_class("VCmpXLtU32", "v_cmpx_lt_u32", InstType.INST_U32) +# logicalIR: VCmpXLtU64 +VCmpXLtU64 = _make_vcmp_class("VCmpXLtU64", "v_cmpx_lt_u64", InstType.INST_U64) +# logicalIR: VCmpXNeU16 +VCmpXNeU16 = _make_vcmp_class("VCmpXNeU16", "v_cmpx_ne_u16", InstType.INST_U16) +# logicalIR: VCmpXNeU32 +VCmpXNeU32 = _make_vcmp_class("VCmpXNeU32", "v_cmpx_ne_u32", InstType.INST_U32) + + +# ========================================================================== +# Common ALU / control instructions +# source: rocisa/rocisa/src/instruction/common.cpp +# ========================================================================== +# logicalIR: SAbsI32 +SAbsI32 = _make_scalar_unary_class("SAbsI32", "s_abs_i32", InstType.INST_I32) +# logicalIR: SMaxI32 +SMaxI32 = _make_scalar_alu_class("SMaxI32", "s_max_i32", InstType.INST_I32) +# logicalIR: SMaxU32 +SMaxU32 = _make_scalar_alu_class("SMaxU32", "s_max_u32", InstType.INST_U32) +# logicalIR: SMinI32 +SMinI32 = _make_scalar_alu_class("SMinI32", "s_min_i32", InstType.INST_I32) +# logicalIR: SMinU32 +SMinU32 = _make_scalar_alu_class("SMinU32", "s_min_u32", InstType.INST_U32) +# SAddI32 — real class (see Scalar ALU section above) +# SAddU32 — real class (see Scalar ALU section above) +# SAddCU32 — real class (see Scalar ALU section above) +_SAddU64 = _make_scalar_alu_class("SAddU64", "s_add_u64", InstType.INST_U64) +# logicalIR: SAddU64 (composite) +SAddU64 = _make_scalar_alu_class("SAddU64", "s_add_u64", InstType.INST_U64) +# SMulI32 — real class (see Scalar ALU section above) +# SMulHII32 — real class (see Scalar ALU section above) +# SMulHIU32 — real class (see Scalar ALU section above) +# SMulLOU32 — real class (see Scalar ALU section above) +# SSubI32 — real class (see Scalar ALU section above) +# SSubU32 — real class (see Scalar ALU section above) +# SSubBU32 — real class (see Scalar ALU section above) +# logicalIR: SCSelectB32 +SCSelectB32 = _make_scalar_alu_class("SCSelectB32", "s_cselect_b32", InstType.INST_B32) +# logicalIR: SCSelectB64 +SCSelectB64 = _make_scalar_alu_class("SCSelectB64", "s_cselect_b64", InstType.INST_B64) +# SAndB32 — real class (see Scalar ALU section above) +# SAndB64 — real class (see Scalar ALU section above) +# SAndN2B32 — real class (see Scalar ALU section above) +# SOrB32 — real class (see Scalar ALU section above) +# SXorB32 — real class (see Scalar ALU section above) +# SOrB64 — real class (see Scalar ALU section above) +# logicalIR: SSubU64 +SSubU64 = _make_scalar_alu_class("SSubU64", "s_sub_u64", InstType.INST_U64) +# logicalIR: SGetPCB64 +SGetPCB64 = _make_zero_src_class("SGetPCB64", "s_getpc_b64", InstType.INST_B64) +# SLShiftLeftB32 — real class (see Scalar ALU section above) +# SLShiftRightB32 — real class (see Scalar ALU section above) +# SLShiftLeftB64 — real class (see Scalar ALU section above) +# SLShiftRightB64 — real class (see Scalar ALU section above) +# SAShiftRightI32 — real class (see Scalar ALU section above) +# SLShiftLeft1AddU32 — real class (see Scalar ALU section above) +# SLShiftLeft2AddU32 — real class (see Scalar ALU section above) +# SLShiftLeft3AddU32 — real class (see Scalar ALU section above) +# SLShiftLeft4AddU32 — real class (see Scalar ALU section above) +# logicalIR: SSetMask +SSetMask = _make_scalar_unary_class("SSetMask", "s_mov_b32", InstType.INST_B32) +# logicalIR: SCMovB32 +SCMovB32 = _make_scalar_unary_class("SCMovB32", "s_cmov_b32", InstType.INST_B32) +# logicalIR: SCMovB64 +SCMovB64 = _make_scalar_unary_class("SCMovB64", "s_cmov_b64", InstType.INST_B64) +# logicalIR: SFf1B32 +SFf1B32 = _make_scalar_unary_class("SFf1B32", "s_ff1_i32_b32", InstType.INST_B32) +# logicalIR: SBfmB32 +SBfmB32 = _make_scalar_alu_class("SBfmB32", "s_bfm_b32", InstType.INST_B32) +# logicalIR: SBfeU32 +SBfeU32 = _make_scalar_alu_class("SBfeU32", "s_bfe_u32", InstType.INST_U32) +# logicalIR: SFlbitI32B32 +SFlbitI32B32 = _make_scalar_unary_class("SFlbitI32B32", "s_flbit_i32_b32", InstType.INST_B32) +# logicalIR: SMovkI32 +SMovkI32 = _make_scalar_unary_class("SMovkI32", "s_movk_i32", InstType.INST_I32) +# logicalIR: SSExtI16toI32 +SSExtI16toI32 = _make_scalar_unary_class("SSExtI16toI32", "s_sext_i32_i16", InstType.INST_I32) +# SAndSaveExecB32 — real class (see Scalar ALU section above) +# SAndSaveExecB64 — real class (see Scalar ALU section above) +# SOrSaveExecB32 — real class (see Scalar ALU section above) +# SOrSaveExecB64 — real class (see Scalar ALU section above) +# logicalIR: SSetPrior +SSetPrior = _make_imm_no_dest_class("SSetPrior", "s_setprio") +# SBarrier — real class (see SBarrier section above) +# logicalIR: SDcacheWb +SDcacheWb = _make_no_operand_class("SDcacheWb", "s_dcache_wb") +# logicalIR: GlobalWb +GlobalWb = _make_no_operand_class("GlobalWb", "global_wb") +# logicalIR: GlobalInv +GlobalInv = _make_no_operand_class("GlobalInv", "global_inv") +# SNop — real class (see class SNop above, after SMovB64). +# logicalIR: VNop +class VNop(Instruction): + """``v_nop`` shim — emits *count* copies of ``v_nop``.""" + + __slots__ = ("count",) + + def __init__(self, count: int = 1, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.count = int(count) + self.setInst("v_nop") + + def getParams(self): + return [self.count] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.count] + + def to_stinky_logical(self, _module=None): + import stinkytofu as _st + if self.count <= 1: + return _st.VNop(self.comment) + return [_st.VNop(self.comment) for _ in range(self.count)] + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = VNop(count=self.count, comment=self.comment) + memo[id(self)] = dup + return dup + + +# logicalIR: SEndpgm +class SEndpgm(Instruction): + """``s_endpgm`` shim with stinkytofu left-path bridge.""" + + __slots__ = () + + def __init__(self, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.setInst("s_endpgm") + + def getParams(self): + return [] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [] + + def toString(self) -> str: + return self.formatWithComment(self.instStr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SEndpgm(self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = SEndpgm(comment=self.comment) + memo[id(self)] = dup + return dup + + +# logicalIR: SSleep +SSleep = _make_imm_no_dest_class("SSleep", "s_sleep") +# logicalIR: SSetVgprMsb +SSetVgprMsb = _make_imm_no_dest_class("SSetVgprMsb", "s_set_vgpr_msb") +# SGetRegB32 — real class (see Scalar Control section above) +# SSetRegB32 — real class (see Scalar Control section above) +# SSetRegIMM32B32 — real class (see Scalar Control section above) + + +# ========================================================================== +# Wait-count instructions +# source: rocisa/rocisa/include/instruction/common.hpp +# logicalIR: SWaitCnt, SWaitTensorcnt, SWaitXCnt +# ========================================================================== + +# Markers embedded in the comment field of SWaitCnt logical instructions to +# encode which gfx12+ wait opcode the instruction should lower to. The +# post-processing step in Module.to_stinky_asm() (code.py) uses these to +# replace generic ``s_waitcnt N`` assembly text with the correct instruction. +_WAIT_MARKER_LOADCNT = "\x00@WL@" +_WAIT_MARKER_STORECNT = "\x00@WS@" +_WAIT_MARKER_DSCNT = "\x00@WD@" +_WAIT_MARKER_KMCNT = "\x00@WK@" + + +class _SWaitCnt(Instruction): + """``s_waitcnt`` primitive (lgkmcnt/vmcnt combined).""" + + __slots__ = ("lgkmcnt", "vmcnt") + + def __init__(self, lgkmcnt: int = -1, vmcnt: int = -1, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.lgkmcnt = int(lgkmcnt) + self.vmcnt = int(vmcnt) + self.setInst("s_waitcnt") + + def getParams(self): + return [self.lgkmcnt, self.vmcnt] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.lgkmcnt, self.vmcnt] + + def toString(self) -> str: + if self.lgkmcnt == 0 and self.vmcnt == 0: + wait_str = "0" + else: + parts: List[str] = [] + if self.lgkmcnt != -1: + parts.append(f"lgkmcnt({self.lgkmcnt})") + if self.vmcnt != -1: + parts.append(f"vmcnt({self.vmcnt})") + wait_str = ", ".join(parts) + return self.formatWithComment("s_waitcnt " + wait_str) + + def to_stinky_logical(self) -> Any: + """Map legacy _SWaitCnt to gfx12+ typed waits. + + On gfx12+, lgkmcnt maps to dscnt (DS/LDS counter) and vmcnt maps + to loadcnt (VMEM load counter), mirroring the native C++ + AllHwMappings.cpp logic that sets dlcnt=lgkmcnt, vlcnt=vmcnt. + """ + import stinkytofu as _st # noqa: WPS433 + + insts: List[Any] = [] + if self.lgkmcnt != -1: + insts.append(_st.SWaitCnt( + _to_stinky_register(self.lgkmcnt), + _WAIT_MARKER_DSCNT + self.comment, + )) + if self.vmcnt != -1: + insts.append(_st.SWaitCnt( + _to_stinky_register(self.vmcnt), + _WAIT_MARKER_LOADCNT + self.comment, + )) + if not insts: + return _st.SWaitCnt(_to_stinky_register(0), self.comment) + return insts + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = _SWaitCnt(lgkmcnt=self.lgkmcnt, vmcnt=self.vmcnt, comment=self.comment) + memo[id(self)] = dup + return dup + + +class _SWaitCntVscnt(Instruction): + """``s_waitcnt_vscnt`` primitive (vscnt counter only, gfx10+).""" + + __slots__ = ("cnt",) + + def __init__(self, cnt: int = 0, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.cnt = int(cnt) + self.setInst("s_waitcnt_vscnt") + + def getParams(self): + return [self.cnt] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.cnt] + + def toString(self) -> str: + return self.formatWithComment(f"s_waitcnt_vscnt null, {self.cnt}") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SWaitCnt( + _to_stinky_register(self.cnt), + _WAIT_MARKER_STORECNT + self.comment, + ) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = _SWaitCntVscnt(cnt=self.cnt, comment=self.comment) + memo[id(self)] = dup + return dup + + +class _SWaitStorecnt(Instruction): + """``s_wait_storecnt`` primitive (store counter, gfx11+).""" + + __slots__ = ("cnt",) + + def __init__(self, cnt: int = 0, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.cnt = int(cnt) + self.setInst("s_wait_storecnt") + + def getParams(self): + return [self.cnt] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.cnt] + + def toString(self) -> str: + return self.formatWithComment(f"s_wait_storecnt {self.cnt}") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SWaitCnt( + _to_stinky_register(self.cnt), + _WAIT_MARKER_STORECNT + self.comment, + ) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = _SWaitStorecnt(cnt=self.cnt, comment=self.comment) + memo[id(self)] = dup + return dup + + +class _SWaitLoadcnt(Instruction): + """``s_wait_loadcnt`` primitive (load counter, gfx11+).""" + + __slots__ = ("cnt",) + + def __init__(self, cnt: int = 0, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.cnt = int(cnt) + self.setInst("s_wait_loadcnt") + + def getParams(self): + return [self.cnt] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.cnt] + + def toString(self) -> str: + return self.formatWithComment(f"s_wait_loadcnt {self.cnt}") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SWaitCnt( + _to_stinky_register(self.cnt), + _WAIT_MARKER_LOADCNT + self.comment, + ) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = _SWaitLoadcnt(cnt=self.cnt, comment=self.comment) + memo[id(self)] = dup + return dup + + +class _SWaitKMcnt(Instruction): + """``s_wait_kmcnt`` primitive (kernel memory counter, gfx11+).""" + + __slots__ = ("cnt",) + + def __init__(self, cnt: int = 0, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.cnt = int(cnt) + self.setInst("s_wait_kmcnt") + + def getParams(self): + return [self.cnt] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.cnt] + + def toString(self) -> str: + return self.formatWithComment(f"s_wait_kmcnt {self.cnt}") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SWaitCnt( + _to_stinky_register(self.cnt), + _WAIT_MARKER_KMCNT + self.comment, + ) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = _SWaitKMcnt(cnt=self.cnt, comment=self.comment) + memo[id(self)] = dup + return dup + + +class _SWaitDscnt(Instruction): + """``s_wait_dscnt`` primitive (DS/LDS counter, gfx11+).""" + + __slots__ = ("cnt",) + + def __init__(self, cnt: int = 0, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.cnt = int(cnt) + self.setInst("s_wait_dscnt") + + def getParams(self): + return [self.cnt] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.cnt] + + def toString(self) -> str: + return self.formatWithComment(f"s_wait_dscnt {self.cnt}") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SWaitCnt( + _to_stinky_register(self.cnt), + _WAIT_MARKER_DSCNT + self.comment, + ) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = _SWaitDscnt(cnt=self.cnt, comment=self.comment) + memo[id(self)] = dup + return dup + + +class SWaitCnt(Instruction): + """High-level ``s_waitcnt`` composite (vlcnt/vscnt/dscnt/kmcnt). + + Mirrors rocisa::SWaitCnt which is a CompositeInstruction that + decomposes into _SWaitCnt/_SWaitCntVscnt. For the logical IR path, + we decompose into individual gfx12+ typed wait instructions. + """ + + __slots__ = ("vlcnt", "vscnt", "dscnt", "kmcnt", "waitAll") + + def __init__(self, vlcnt: int = -1, vscnt: int = -1, + dscnt: int = -1, kmcnt: int = -1, + comment: str = "", waitAll: bool = False): + super().__init__(InstType.INST_NOTYPE, comment) + self.vlcnt = int(vlcnt) + self.vscnt = int(vscnt) + self.dscnt = int(dscnt) + self.kmcnt = int(kmcnt) + self.waitAll = bool(waitAll) + self.setInst("s_waitcnt") + + def getParams(self): + return [] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [] + + def toString(self) -> str: + return self.formatWithComment(self.instStr) + + def to_stinky_logical(self) -> Any: + """Decompose into individual gfx12+ wait logical instructions. + + Returns a list of SWaitCnt logical instructions with type markers + so the post-processing step can emit the correct opcodes. + """ + import stinkytofu as _st # noqa: WPS433 + + insts: List[Any] = [] + if self.dscnt != -1: + insts.append(_st.SWaitCnt( + _to_stinky_register(self.dscnt), + _WAIT_MARKER_DSCNT + self.comment, + )) + if self.kmcnt != -1: + insts.append(_st.SWaitCnt( + _to_stinky_register(self.kmcnt), + _WAIT_MARKER_KMCNT + self.comment, + )) + if self.vlcnt != -1: + insts.append(_st.SWaitCnt( + _to_stinky_register(self.vlcnt), + _WAIT_MARKER_LOADCNT + self.comment, + )) + if self.vscnt != -1: + insts.append(_st.SWaitCnt( + _to_stinky_register(self.vscnt), + _WAIT_MARKER_STORECNT + self.comment, + )) + if not insts: + return _st.SWaitCnt(_to_stinky_register(0), self.comment) + return insts + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = SWaitCnt( + vlcnt=self.vlcnt, vscnt=self.vscnt, dscnt=self.dscnt, + kmcnt=self.kmcnt, comment=self.comment, waitAll=self.waitAll, + ) + memo[id(self)] = dup + return dup + + +class SWaitXCnt(Instruction): + """``s_wait_xcnt`` shim.""" + + __slots__ = ("cnt",) + + def __init__(self, cnt: int = 0, comment: str = ""): + super().__init__(InstType.INST_NOTYPE, comment) + self.cnt = int(cnt) + self.setInst("s_wait_xcnt") + + def getParams(self): + return [self.cnt] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.cnt] + + def toString(self) -> str: + return self.formatWithComment(f"s_wait_xcnt {self.cnt}") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SWaitXCnt(_to_stinky_register(self.cnt), self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = SWaitXCnt(cnt=self.cnt, comment=self.comment) + memo[id(self)] = dup + return dup + + +class SWaitTensorcnt(Instruction): + """``s_wait_tensorcnt`` shim.""" + + __slots__ = ("cnt",) + + def __init__(self, cnt: int = 0, tensorcnt: int = None, comment: str = ""): + if tensorcnt is not None: + cnt = tensorcnt + super().__init__(InstType.INST_NOTYPE, comment) + self.cnt = int(cnt) + self.setInst("s_wait_tensorcnt") + + def getParams(self): + return [self.cnt] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [self.cnt] + + def toString(self) -> str: + return self.formatWithComment(f"s_wait_tensorcnt {self.cnt}") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + return _st.SWaitTensorcnt(_to_stinky_register(self.cnt), self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = SWaitTensorcnt(cnt=self.cnt, comment=self.comment) + memo[id(self)] = dup + return dup + + +class SWaitAlu(Instruction): + """SWaitAlu — dependency counter wait instruction. + + Carries 7 optional counter fields; -1 means "not specified". + Emits a LogicalIR SWaitAlu instruction with specialData. + """ + + __slots__ = ("va_vdst", "va_sdst", "va_ssrc", "hold_cnt", "vm_vsrc", "va_vcc", "sa_sdst") + + def __init__(self, va_vdst=-1, va_sdst=-1, va_ssrc=-1, + hold_cnt=-1, vm_vsrc=-1, va_vcc=-1, sa_sdst=-1, + comment=""): + super().__init__(InstType.INST_NOTYPE, comment) + self.va_vdst = va_vdst + self.va_sdst = va_sdst + self.va_ssrc = va_ssrc + self.hold_cnt = hold_cnt + self.vm_vsrc = vm_vsrc + self.va_vcc = va_vcc + self.sa_sdst = sa_sdst + self.setInst("s_wait_alu") + + def getParams(self): + return [self.va_vdst, self.va_sdst, self.va_ssrc, + self.hold_cnt, self.vm_vsrc, self.va_vcc, self.sa_sdst] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [] + + def to_stinky_logical(self, _module=None): + import stinkytofu as st + return st.SWaitAlu( + va_vdst=self.va_vdst, + va_sdst=self.va_sdst, + va_ssrc=self.va_ssrc, + hold_cnt=self.hold_cnt, + vm_vsrc=self.vm_vsrc, + va_vcc=self.va_vcc, + sa_sdst=self.sa_sdst, + comment=self.comment, + ) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = SWaitAlu( + va_vdst=self.va_vdst, + va_sdst=self.va_sdst, + va_ssrc=self.va_ssrc, + hold_cnt=self.hold_cnt, + vm_vsrc=self.vm_vsrc, + va_vcc=self.va_vcc, + sa_sdst=self.sa_sdst, + comment=self.comment, + ) + memo[id(self)] = dup + return dup +# logicalIR: SDelayAlu +SDelayAlu = _make_imm_no_dest_class("SDelayAlu", "s_delay_alu") + + +def _sdelayalu_to_stinky_logical(self) -> Any: + """SNop placeholder workaround for stinkytofu SDelayAluData assertion bug.""" + import stinkytofu as _st # noqa: WPS433 + + # The raw immediate encodes the full s_delay_alu operand; emit it verbatim + # so post-processing can restore the original instruction text. + alu_text = _input_to_str(self._imm_value) + return _st.SNop(_st.Register(0), "DELAY_ALU:" + alu_text) + + +SDelayAlu.to_stinky_logical = _sdelayalu_to_stinky_logical +# logicalIR: VAddF16 +VAddF16 = _make_scalar_alu_class("VAddF16", "v_add_f16", InstType.INST_F16) +# VAddF32 — real class (see Vector ALU section above) +# logicalIR: VAddF64 +VAddF64 = _make_scalar_alu_class("VAddF64", "v_add_f64", InstType.INST_F64) +# logicalIR: VAddI32 +VAddI32 = _make_scalar_alu_class("VAddI32", "v_add_nc_i32", InstType.INST_I32) +# VAddU32 — real class (see Vector ALU section above) +# logicalIR: VAddCOU32 +VAddCOU32 = _make_scalar_alu_class("VAddCOU32", "v_add_co_u32", InstType.INST_U32) +# logicalIR: VAddCCOU32 +VAddCCOU32 = _make_scalar_alu_class("VAddCCOU32", "v_add_co_ci_u32", InstType.INST_U32) +_VAddNCU64 = _make_scalar_alu_class("VAddNCU64", "v_add_nc_u64", InstType.INST_U64) +# logicalIR: VAddNCU64 (composite) +VAddNCU64 = _make_scalar_alu_class("VAddNCU64", "v_add_nc_u64", InstType.INST_U64) +# logicalIR: VAddPKF16 +VAddPKF16 = _make_scalar_alu_class("VAddPKF16", "v_pk_add_f16", InstType.INST_F16) +_VAddPKF32 = _make_scalar_alu_class("VAddPKF32", "v_pk_add_f32", InstType.INST_F32) +# logicalIR: VAddPKF32 +VAddPKF32 = _make_scalar_alu_class("VAddPKF32", "v_pk_add_f32", InstType.INST_F32) +# logicalIR: VAdd3U32 +VAdd3U32 = _make_scalar_alu_class("VAdd3U32", "v_add3_u32", InstType.INST_U32) +# logicalIR: VMulF16 +VMulF16 = _make_scalar_alu_class("VMulF16", "v_mul_f16", InstType.INST_F16) +# VMulF32 — real class (see Vector ALU section above) +# logicalIR: VMulF64 +VMulF64 = _make_scalar_alu_class("VMulF64", "v_mul_f64", InstType.INST_F64) +# logicalIR: VMulPKF16 +VMulPKF16 = _make_scalar_alu_class("VMulPKF16", "v_pk_mul_f16", InstType.INST_F16) +# logicalIR: VMulPKF32S +VMulPKF32S = _make_scalar_alu_class("VMulPKF32S", "v_pk_mul_f32", InstType.INST_F32) +_VMulPKF32 = _make_scalar_alu_class("VMulPKF32", "v_pk_mul_f32", InstType.INST_F32) +# logicalIR: VMulPKF32 +VMulPKF32 = _make_scalar_alu_class("VMulPKF32", "v_pk_mul_f32", InstType.INST_F32) +# VMulLOU32 — real class (see Vector ALU section above) +# VMulHII32 — real class (see Vector ALU section above) +# VMulHIU32 — real class (see Vector ALU section above) +# VMulI32I24 — real class (see Vector ALU section above) +# VMulU32U24 — real class (see Vector ALU section above) +# VSubF32 — real class (see Vector ALU section above) +# VSubI32 — real class (see Vector ALU section above) +# VSubU32 — real class (see Vector ALU section above) +# logicalIR: VSubCoU32 +VSubCoU32 = _make_scalar_alu_class("VSubCoU32", "v_sub_co_u32", InstType.INST_U32) +# logicalIR: VMacF32 +VMacF32 = _make_scalar_alu_class("VMacF32", "v_fmac_f32", InstType.INST_F32) + + +class VDualFMACF32(CommonInstruction): + """VOPD dual-issue FMA: two independent v_fmac_f32 in one slot.""" + + def __init__(self, dstX, src0X, src1X, dstY, src0Y, src1Y, comment=""): + super().__init__(InstType.INST_F32, dstX, [src0X, src1X, src0Y, src1Y], comment=comment) + self.dst1 = dstY + self.setInst("v_dual_fmac_f32") + + def getArgStr(self) -> str: + dstX_s = self.dst.toString() if hasattr(self.dst, "toString") else str(self.dst) + s0X = self.srcs[0].toString() if hasattr(self.srcs[0], "toString") else str(self.srcs[0]) + s1X = self.srcs[1].toString() if hasattr(self.srcs[1], "toString") else str(self.srcs[1]) + dstY_s = self.dst1.toString() if hasattr(self.dst1, "toString") else str(self.dst1) + s0Y = self.srcs[2].toString() if hasattr(self.srcs[2], "toString") else str(self.srcs[2]) + s1Y = self.srcs[3].toString() if hasattr(self.srcs[3], "toString") else str(self.srcs[3]) + return f"{dstX_s}, {s0X}, {s1X} :: v_dual_fmac_f32 {dstY_s}, {s0Y}, {s1Y}" + + def getSrcParams(self): + params = list(self.srcs) + if self.dst is not None: + params.append(self.dst) + if self.dst1 is not None: + params.append(self.dst1) + return params + + +# logicalIR: VDot2CF32F16 +VDot2CF32F16 = _make_ternary_class("VDot2CF32F16", "v_dot2_c_f32_f16", InstType.INST_F32) +# logicalIR: VDot2CF32BF16 +VDot2CF32BF16 = _make_ternary_class("VDot2CF32BF16", "v_dot2_c_f32_b_f16", InstType.INST_F32) +# logicalIR: VDot2F32F16 +VDot2F32F16 = _make_ternary_class("VDot2F32F16", "v_dot2_f32_f16", InstType.INST_F32) +# logicalIR: VDot2F32BF16 +VDot2F32BF16 = _make_ternary_class("VDot2F32BF16", "v_dot2_f32_b_f16", InstType.INST_F32) +# logicalIR: VFmaF16 +VFmaF16 = _make_ternary_class("VFmaF16", "v_fma_f16", InstType.INST_F16) +# VFmaF32 — real class (see Vector ALU section above) +# logicalIR: VFmaF64 +VFmaF64 = _make_ternary_class("VFmaF64", "v_fma_f64", InstType.INST_F64) +# logicalIR: VFmaPKF16 +VFmaPKF16 = _make_ternary_class("VFmaPKF16", "v_pk_fma_f16", InstType.INST_F16) +# VFmaMixF32 — real class (see Vector ALU section above) +# logicalIR: VMadI32I24 +VMadI32I24 = _make_ternary_class("VMadI32I24", "v_mad_i32_i24", InstType.INST_I32) +# logicalIR: VMadU32U24 +VMadU32U24 = _make_ternary_class("VMadU32U24", "v_mad_u32_u24", InstType.INST_U32) +# logicalIR: VMadMixF32 +VMadMixF32 = _make_ternary_class("VMadMixF32", "v_mad_mix_f32", InstType.INST_F32) +# logicalIR: VExpF16 +VExpF16 = _make_scalar_unary_class("VExpF16", "v_exp_f16", InstType.INST_F16) +# logicalIR: VExpF32 +VExpF32 = _make_scalar_unary_class("VExpF32", "v_exp_f32", InstType.INST_F32) +# logicalIR: VRcpF16 +VRcpF16 = _make_scalar_unary_class("VRcpF16", "v_rcp_f16", InstType.INST_F16) +# logicalIR: VRcpF32 +VRcpF32 = _make_scalar_unary_class("VRcpF32", "v_rcp_f32", InstType.INST_F32) +# logicalIR: VRcpIFlagF32 +VRcpIFlagF32 = _make_scalar_unary_class("VRcpIFlagF32", "v_rcp_iflag_f32", InstType.INST_F32) +# logicalIR: VRcpF64 +VRcpF64 = _make_scalar_unary_class("VRcpF64", "v_rcp_f64", InstType.INST_F64) +# logicalIR: VRsqF16 +VRsqF16 = _make_scalar_unary_class("VRsqF16", "v_rsq_f16", InstType.INST_F16) +# logicalIR: VRsqF32 +VRsqF32 = _make_scalar_unary_class("VRsqF32", "v_rsq_f32", InstType.INST_F32) +# logicalIR: VRsqIFlagF32 +VRsqIFlagF32 = _make_scalar_unary_class("VRsqIFlagF32", "v_rsq_iflag_f32", InstType.INST_F32) +# logicalIR: VMaxF16 +VMaxF16 = _make_scalar_alu_class("VMaxF16", "v_max_f16", InstType.INST_F16) +# logicalIR: VMaxF32 +VMaxF32 = _make_scalar_alu_class("VMaxF32", "v_max_f32", InstType.INST_F32) +# logicalIR: VMaxF64 +VMaxF64 = _make_scalar_alu_class("VMaxF64", "v_max_f64", InstType.INST_F64) +# logicalIR: VMaxI32 +VMaxI32 = _make_scalar_alu_class("VMaxI32", "v_max_i32", InstType.INST_I32) +# logicalIR: VMaxPKF16 +VMaxPKF16 = _make_scalar_alu_class("VMaxPKF16", "v_pk_max_f16", InstType.INST_F16) +# logicalIR: VMed3I32 +VMed3I32 = _make_ternary_class("VMed3I32", "v_med3_i32", InstType.INST_I32) +# logicalIR: VMed3F32 +VMed3F32 = _make_ternary_class("VMed3F32", "v_med3_f32", InstType.INST_F32) +# logicalIR: VMinF16 +VMinF16 = _make_scalar_alu_class("VMinF16", "v_min_f16", InstType.INST_F16) +# logicalIR: VMinF32 +VMinF32 = _make_scalar_alu_class("VMinF32", "v_min_f32", InstType.INST_F32) +# logicalIR: VMinF64 +VMinF64 = _make_scalar_alu_class("VMinF64", "v_min_f64", InstType.INST_F64) +# logicalIR: VMinI32 +VMinI32 = _make_scalar_alu_class("VMinI32", "v_min_i32", InstType.INST_I32) +# VAndB32 — real class (see Vector ALU section above) +# VAndOrB32 — real class (see Vector ALU section above) +# logicalIR: VNotB32 +VNotB32 = _make_scalar_unary_class("VNotB32", "v_not_b32", InstType.INST_B32) +# VOrB32 — real class (see Vector ALU section above) +# VXorB32 — real class (see Vector ALU section above) +# logicalIR: VPrngB32 +VPrngB32 = _make_scalar_unary_class("VPrngB32", "v_prng_b32", InstType.INST_B32) +# VCndMaskB32 — real class (see Vector ALU section above) +# logicalIR: VLShiftLeftB16 +VLShiftLeftB16 = _make_vector_shift_class("VLShiftLeftB16", "v_lshlrev_b16", InstType.INST_B16) +# VLShiftLeftB32 — real class (see Vector ALU section above) +# VLShiftRightB32 — real class (see Vector ALU section above) +# VLShiftLeftB64 — real class (see Vector ALU section above) +# VLShiftRightB64 — real class (see Vector ALU section above) +_VLShiftLeftOrB32 = _make_ternary_class("VLShiftLeftOrB32", "v_lshl_or_b32", InstType.INST_B32, shift_position=1) +# logicalIR: VAShiftRightI32 +VAShiftRightI32 = _make_vector_shift_class("VAShiftRightI32", "v_ashrrev_i32", InstType.INST_I32) +# logicalIR: VLShiftLeftOrB32 +VLShiftLeftOrB32 = _make_ternary_class("VLShiftLeftOrB32", "v_lshl_or_b32", InstType.INST_B32, shift_position=1) +_VAddLShiftLeftU32 = _make_ternary_class("VAddLShiftLeftU32", "v_add_lshl_u32", InstType.INST_U32, shift_position=2) +# logicalIR: VAddLShiftLeftU32 (composite) +VAddLShiftLeftU32 = _make_ternary_class("VAddLShiftLeftU32", "v_add_lshl_u32", InstType.INST_U32, shift_position=2) +_VLShiftLeftAddU32 = _make_ternary_class("VLShiftLeftAddU32", "v_lshl_add_u32", InstType.INST_U32, shift_position=1) +# logicalIR: VLShiftLeftAddU32 (composite) +VLShiftLeftAddU32 = _make_ternary_class("VLShiftLeftAddU32", "v_lshl_add_u32", InstType.INST_U32, shift_position=1) +# logicalIR: VMovB32 -- real class defined at the bottom of this file +# (after ``CommonInstruction`` / ``_to_stinky_register`` are in scope). +# Intentionally NOT declared here so ``from rocisa.instruction import +# VMovB32`` resolves to the real class via the module-scope binding. +_VMovB64 = _make_scalar_unary_class("VMovB64", "v_mov_b64", InstType.INST_B64) +# logicalIR: VMovB64 +VMovB64 = _make_scalar_unary_class("VMovB64", "v_mov_b64", InstType.INST_B64) +# logicalIR: VSwapB32 +VSwapB32 = _make_scalar_unary_class("VSwapB32", "v_swap_b32", InstType.INST_B32) +# logicalIR: VBfeI32 +VBfeI32 = _make_ternary_class("VBfeI32", "v_bfe_i32", InstType.INST_I32) +# logicalIR: VBfeU32 +VBfeU32 = _make_ternary_class("VBfeU32", "v_bfe_u32", InstType.INST_U32) +# logicalIR: VBfiB32 +VBfiB32 = _make_ternary_class("VBfiB32", "v_bfi_b32", InstType.INST_B32) +# logicalIR: VPackF16toB32 +VPackF16toB32 = _make_scalar_alu_class("VPackF16toB32", "v_pack_b32_f16", InstType.INST_B32) +# logicalIR: VAccvgprReadB32 +VAccvgprReadB32 = _make_scalar_unary_class("VAccvgprReadB32", "v_accvgpr_read_b32", InstType.INST_B32) +# logicalIR: VAccvgprWrite +VAccvgprWrite = _make_scalar_unary_class("VAccvgprWrite", "v_accvgpr_write", InstType.INST_B32) +# logicalIR: VAccvgprWriteB32 +VAccvgprWriteB32 = _make_scalar_unary_class("VAccvgprWriteB32", "v_accvgpr_write_b32", InstType.INST_B32) +# VReadfirstlaneB32 — real class (see Vector ALU section above) +# logicalIR: VReadlaneB32 +VReadlaneB32 = _make_scalar_alu_class("VReadlaneB32", "v_readlane_b32", InstType.INST_B32) +# logicalIR: VWritelaneB32 +VWritelaneB32 = _make_scalar_alu_class("VWritelaneB32", "v_writelane_b32", InstType.INST_B32) +# logicalIR: VRndneF32 +VRndneF32 = _make_scalar_unary_class("VRndneF32", "v_rndne_f32", InstType.INST_F32) +# logicalIR: VPermB32 +VPermB32 = _make_ternary_class("VPermB32", "v_perm_b32", InstType.INST_B32) +# logicalIR: VPermlane16SwapB32 +VPermlane16SwapB32 = _make_scalar_unary_class("VPermlane16SwapB32", "v_permlane16_swap_b32", InstType.INST_B32) +# logicalIR: VPermlane32SwapB32 +VPermlane32SwapB32 = _make_scalar_unary_class("VPermlane32SwapB32", "v_permlane32_swap_b32", InstType.INST_B32) +class SSchedulingFence(Instruction): + """SSchedulingFence — scheduling barrier pseudo-instruction. + + Emits a SchedulingFence LogicalIR instruction that lowers to a FENCE + in the hardware scheduler (no assembly output, just a DAG barrier). + """ + + __slots__ = () + + def __init__(self, comment=""): + super().__init__(InstType.INST_NOTYPE, comment) + self.setInst("scheduling_fence") + + def getParams(self): + return [] + + def getDstParams(self): + return [] + + def getSrcParams(self): + return [] + + def to_stinky_logical(self, _module=None): + import stinkytofu as st + return st.SchedulingFence(comment=self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = SSchedulingFence(comment=self.comment) + memo[id(self)] = dup + return dup + + +# ========================================================================== +# Conversion instructions +# source: rocisa/rocisa/src/instruction/cvt.cpp +# ========================================================================== + + +def _make_cvt_scale_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for scale CVT shim classes with (dst, src, scale) rocisa API.""" + + def __init__(self, dst: Any, src: Any = None, scale: Any = None, + sdwa: Any = None, vop3: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[src, scale], + dpp=None, + sdwa=sdwa, + vop3=vop3, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + + dst_reg = _to_stinky_register(self.dst) + src_reg = _to_stinky_register(self.srcs[0]) + scale_reg = _to_stinky_register(self.srcs[1]) + factory = getattr(_st, class_name) + return factory(dst_reg, src_reg, scale_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + cls.__qualname__ = class_name + return cls + + +# VCvtInstruction is a base class — rarely instantiated directly by KernelWriter. +VCvtInstruction = CommonInstruction + +# --- Unary CVTs: (dst, src) → logicalIR(dst, src) --- +VCvtF16toF32 = _make_scalar_unary_class("VCvtF16toF32", "v_cvt_f32_f16", InstType.INST_NOTYPE) +VCvtF32toF16 = _make_scalar_unary_class("VCvtF32toF16", "v_cvt_f16_f32", InstType.INST_NOTYPE) +VCvtF32toU32 = _make_scalar_unary_class("VCvtF32toU32", "v_cvt_u32_f32", InstType.INST_NOTYPE) +VCvtU32toF32 = _make_scalar_unary_class("VCvtU32toF32", "v_cvt_f32_u32", InstType.INST_NOTYPE) +VCvtI32toF32 = _make_scalar_unary_class("VCvtI32toF32", "v_cvt_f32_i32", InstType.INST_NOTYPE) +VCvtF32toI32 = _make_scalar_unary_class("VCvtF32toI32", "v_cvt_i32_f32", InstType.INST_NOTYPE) +VCvtFP8toF32 = _make_scalar_unary_class("VCvtFP8toF32", "v_cvt_f32_fp8", InstType.INST_NOTYPE) +VCvtBF8toF32 = _make_scalar_unary_class("VCvtBF8toF32", "v_cvt_f32_bf8", InstType.INST_NOTYPE) +VCvtPkFP8toF32 = _make_scalar_unary_class("VCvtPkFP8toF32", "v_cvt_pk_f32_fp8", InstType.INST_NOTYPE) +VCvtPkBF8toF32 = _make_scalar_unary_class("VCvtPkBF8toF32", "v_cvt_pk_f32_bf8", InstType.INST_NOTYPE) + +# --- Binary CVTs: (dst, src0, src1) → logicalIR(dst, src0, src1) --- +VCvtPkF32toBF8 = _make_scalar_alu_class("VCvtPkF32toBF8", "v_cvt_pk_bf8_f32", InstType.INST_NOTYPE) +VCvtSRF32toFP8 = _make_scalar_alu_class("VCvtSRF32toFP8", "v_cvt_sr_fp8_f32", InstType.INST_NOTYPE) +VCvtSRF32toBF8 = _make_scalar_alu_class("VCvtSRF32toBF8", "v_cvt_sr_bf8_f32", InstType.INST_NOTYPE) +VCvtPkF32toFP8 = _make_scalar_alu_class("VCvtPkF32toFP8", "v_cvt_pk_fp8_f32", InstType.INST_NOTYPE) +VCvtPkF32toBF16 = _make_scalar_alu_class("VCvtPkF32toBF16", "v_cvt_pk_bf16_f32", InstType.INST_NOTYPE) + +# --- Scale CVTs: (dst, src, scale) → logicalIR(dst, src, scale) --- +VCvtScalePkFP8toF16 = _make_cvt_scale_class("VCvtScalePkFP8toF16", "v_cvt_scalef32_pk_f16_fp8", InstType.INST_NOTYPE) +VCvtScalePkBF8toF16 = _make_cvt_scale_class("VCvtScalePkBF8toF16", "v_cvt_scalef32_pk_f16_bf8", InstType.INST_NOTYPE) +VCvtScaleFP8toF16 = _make_cvt_scale_class("VCvtScaleFP8toF16", "v_cvt_scalef32_f16_fp8", InstType.INST_NOTYPE) +VCvtScalePkF16toFP8 = _make_cvt_scale_class("VCvtScalePkF16toFP8", "v_cvt_scalef32_pk_fp8_f16", InstType.INST_NOTYPE) +VCvtScalePkF16toBF8 = _make_cvt_scale_class("VCvtScalePkF16toBF8", "v_cvt_scalef32_pk_bf8_f16", InstType.INST_NOTYPE) +VCvtScaleSRF16toFP8 = _make_cvt_scale_class("VCvtScaleSRF16toFP8", "v_cvt_scalef32_sr_fp8_f16", InstType.INST_NOTYPE) +VCvtScaleSRF16toBF8 = _make_cvt_scale_class("VCvtScaleSRF16toBF8", "v_cvt_scalef32_sr_bf8_f16", InstType.INST_NOTYPE) +VCvtScalePk8F32toFP8 = _make_cvt_scale_class("VCvtScalePk8F32toFP8", "v_cvt_scalef32_pk8_fp8_f32", InstType.INST_NOTYPE) +VCvtScalePk8F32toBF8 = _make_cvt_scale_class("VCvtScalePk8F32toBF8", "v_cvt_scalef32_pk8_bf8_f32", InstType.INST_NOTYPE) + + +def _make_cvt_scale_sr_class(class_name: str, mnemonic: str, inst_type: "InstType"): + """Factory for stochastic-rounding scale CVT shims with (dst, src0, src1, scale) API.""" + + def __init__(self, dst: Any, src0: Any = None, src1: Any = None, + scale: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, + instType=inst_type, + dst=dst, + srcs=[src0, src1, scale], + dpp=None, + sdwa=None, + vop3=None, + comment=comment, + ) + self.setInst(mnemonic) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + dst_reg = _to_stinky_register(self.dst) + src0_reg = _to_stinky_register(self.srcs[0]) + src1_reg = _to_stinky_register(self.srcs[1]) + scale_reg = _to_stinky_register(self.srcs[2]) + factory = getattr(_st, class_name) + return factory(dst_reg, src0_reg, src1_reg, scale_reg, comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + cls.__qualname__ = class_name + return cls + + +VCvtScaleSRPkF32toFP8 = _make_cvt_scale_sr_class("VCvtScaleSRPkF32toFP8", "v_cvt_scalef32_sr_pk8_fp8_f32", InstType.INST_NOTYPE) + + +# ========================================================================== +# FlatAtomicDecU32 -- flat atomic decrement (MBSK GSU kernel). +# ========================================================================== +class FlatAtomicDecU32(CommonInstruction): + """``flat_atomic_dec_u32 dst, addr, data`` -- flat atomic decrement.""" + + def __init__(self, dst: Any, addr: Any, data: Any, + modifier: Any = None, comment: str = ""): + super().__init__( + instType=InstType.INST_B32, + dst=dst, + srcs=[addr, data], + dpp=None, + sdwa=None, + vop3=None, + comment=comment, + ) + self.setInst("flat_atomic_dec_u32") + self.flat = modifier + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st # noqa: WPS433 + dst_reg = _to_stinky_register(self.dst) + addr_reg = _to_stinky_register(self.srcs[0]) + data_reg = _to_stinky_register(self.srcs[1]) + return _st.FlatAtomicDecU32(dst_reg, addr_reg, data_reg, comment=self.comment) + + def __deepcopy__(self, memo): + clone = CommonInstruction.__deepcopy__(self, memo) + return clone + + +# --- Gfx1250 vector conversions --- +# logicalIR: VCvtPkF32toF16 +VCvtPkF32toF16 = _make_scalar_alu_class("VCvtPkF32toF16", "v_cvt_pk_f16_f32", InstType.INST_NOTYPE) +# logicalIR: VCvtF64toU32 +VCvtF64toU32 = _make_scalar_unary_class("VCvtF64toU32", "v_cvt_u32_f64", InstType.INST_U32) +# logicalIR: VCvtU32toF64 +VCvtU32toF64 = _make_scalar_unary_class("VCvtU32toF64", "v_cvt_f64_u32", InstType.INST_F64) +# logicalIR: PVCvtBF16toFP32 +PVCvtBF16toFP32 = _make_scalar_unary_class("PVCvtBF16toFP32", "v_cvt_f32_bf16", InstType.INST_F32) +# logicalIR: VCvtPkF32toFP16 +VCvtPkF32toFP16 = _make_scalar_alu_class("VCvtPkF32toFP16", "v_cvt_pk_f16_f32", InstType.INST_NOTYPE) +# logicalIR: VCvtFP8toF16 +VCvtFP8toF16 = _make_scalar_unary_class("VCvtFP8toF16", "v_cvt_f16_fp8", InstType.INST_F16) + + +# ========================================================================== +# Memory (Buffer/Flat/Global/DS/SMEM) instructions +# source: rocisa/rocisa/src/instruction/mem.cpp +# ========================================================================== + + +def _make_buffer_load_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for MUBUF load shims: rocisa(dst, vaddr, saddr, soffset, mubuf, comment).""" + + def __init__(self, dst: Any = None, vaddr: Any = None, saddr: Any = None, + soffset: Any = None, mubuf: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dst, + srcs=[vaddr, saddr, soffset], dpp=None, sdwa=None, vop3=None, + comment=comment) + self.setInst(mnemonic) + self.mubuf = mubuf + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + inst = factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + comment=self.comment) + if self.srcs[1] is not None: + inst.add_src(_to_stinky_register(self.srcs[1])) + if self.srcs[2] is not None: + inst.add_src(_to_stinky_register(self.srcs[2])) + elif self.srcs[1] is not None: + inst.add_src(_st.Register("null")) + if self.mubuf is not None: + inst.set_mubuf( + offen=getattr(self.mubuf, "offen", False), + offset=getattr(self.mubuf, "offset12", 0), + glc=getattr(self.mubuf, "glc", False), + slc=getattr(self.mubuf, "slc", False), + nt=getattr(self.mubuf, "nt", False), + scope=getattr(self.mubuf, "scope", 0) if isinstance(getattr(self.mubuf, "scope", 0), int) else getattr(self.mubuf, "scope", 0).value, + th=int(getattr(self.mubuf, "th", -1)), + is_store=getattr(self.mubuf, "isStore", False), + ) + return inst + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +def _make_buffer_store_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for MUBUF store/atomic shims: rocisa(src, vaddr, saddr, soffset, mubuf, comment).""" + + def __init__(self, src: Any = None, vaddr: Any = None, saddr: Any = None, + soffset: Any = None, mubuf: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=src, + srcs=[vaddr, saddr, soffset], dpp=None, sdwa=None, vop3=None, + comment=comment) + self.setInst(mnemonic) + self.mubuf = mubuf + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + inst = factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + _to_stinky_register(self.srcs[1]), + comment=self.comment) + if self.srcs[2] is not None: + inst.add_src(_to_stinky_register(self.srcs[2])) + elif self.srcs[1] is not None: + inst.add_src(_st.Register("null")) + if self.mubuf is not None: + inst.set_mubuf( + offen=getattr(self.mubuf, "offen", False), + offset=getattr(self.mubuf, "offset12", 0), + glc=getattr(self.mubuf, "glc", False), + slc=getattr(self.mubuf, "slc", False), + nt=getattr(self.mubuf, "nt", False), + scope=getattr(self.mubuf, "scope", 0) if isinstance(getattr(self.mubuf, "scope", 0), int) else getattr(self.mubuf, "scope", 0).value, + th=int(getattr(self.mubuf, "th", -1)), + is_store=getattr(self.mubuf, "isStore", False), + ) + return inst + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +def _make_flat_load_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for Flat load shims: rocisa(dst, vaddr, flat, comment).""" + + def __init__(self, dst: Any = None, vaddr: Any = None, + flat: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dst, + srcs=[vaddr], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self.flat = flat + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +def _make_flat_store_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for Flat store shims: rocisa(src, vaddr, flat, comment).""" + + def __init__(self, src: Any = None, vaddr: Any = None, + flat: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=src, + srcs=[vaddr], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self.flat = flat + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + _to_stinky_register(self.dst), + comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +def _make_flat_atomic_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for Flat atomic shims: rocisa(vaddr, tmp, src, flat, comment).""" + + def __init__(self, vaddr: Any = None, tmp: Any = None, src: Any = None, + flat: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=vaddr, + srcs=[tmp, src], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self.flat = flat + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + _to_stinky_register(self.srcs[1]), + comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +def _make_ds_load_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for DS load shims: rocisa(dst, src, ds, comment).""" + + def __init__(self, dst: Any = None, src: Any = None, + ds: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dst, + srcs=[src], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self.ds = ds + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + inst = factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + comment=self.comment) + if self.ds is not None: + inst.set_ds(offset=getattr(self.ds, "offset", 0)) + return inst + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +def _make_ds_store_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for DS store (binary) shims: rocisa(dstAddr, src, ds, comment).""" + + def __init__(self, dstAddr: Any = None, src: Any = None, + ds: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dstAddr, + srcs=[src], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self.ds = ds + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + inst = factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + comment=self.comment) + if self.ds is not None: + inst.set_ds(offset=getattr(self.ds, "offset", 0)) + return inst + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +def _make_ds_store2_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for DS store2/permute (ternary) shims: rocisa(dstAddr, src0, src1, ds, comment).""" + + def __init__(self, dstAddr: Any = None, src0: Any = None, src1: Any = None, + ds: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dstAddr, + srcs=[src0, src1], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self.ds = ds + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + inst = factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + _to_stinky_register(self.srcs[1]), + comment=self.comment) + if self.ds is not None: + inst.set_ds(offset=getattr(self.ds, "offset", 0)) + return inst + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +# Base classes — aliased to CommonInstruction for isinstance checks +ReadWriteInstruction = CommonInstruction +GlobalReadInstruction = CommonInstruction +FLATReadInstruction = CommonInstruction +GLOBALLoadInstruction = CommonInstruction +MUBUFReadInstruction = CommonInstruction +AtomicReadWriteInstruction = CommonInstruction +SMemAtomicIncInstruction = make_dummy_class(f"{_P}.SMemAtomicIncInstruction") +SMemAtomicDecInstruction = make_dummy_class(f"{_P}.SMemAtomicDecInstruction") +# SMemLoadInstruction / SLoadB* — real classes (see above after SMovB64). +GlobalWriteInstruction = CommonInstruction +SMemStoreInstruction = make_dummy_class(f"{_P}.SMemStoreInstruction") +FLATStoreInstruction = CommonInstruction +MUBUFStoreInstruction = CommonInstruction +LocalReadInstruction = CommonInstruction +DSLoadInstruction = CommonInstruction +LocalWriteInstruction = CommonInstruction +DSStoreInstruction = CommonInstruction + +# --- Buffer Load (MUBUF): rocisa(dst, vaddr, saddr, soffset, mubuf, comment) --- +BufferLoadU8 = _make_buffer_load_class("BufferLoadU8", "buffer_load_u8") +BufferLoadI8 = _make_buffer_load_class("BufferLoadI8", "buffer_load_i8") +BufferLoadD16HIU8 = _make_buffer_load_class("BufferLoadD16HIU8", "buffer_load_d16_hi_u8") +BufferLoadD16U8 = _make_buffer_load_class("BufferLoadD16U8", "buffer_load_d16_u8") +BufferLoadD16I8 = _make_buffer_load_class("BufferLoadD16I8", "buffer_load_d16_i8") +BufferLoadD16HII8 = _make_buffer_load_class("BufferLoadD16HII8", "buffer_load_d16_hi_i8") +BufferLoadD16HIB16 = _make_buffer_load_class("BufferLoadD16HIB16", "buffer_load_d16_hi_b16") +BufferLoadD16B16 = _make_buffer_load_class("BufferLoadD16B16", "buffer_load_d16_b16") +# logicalIR: BufferLoadB16 +BufferLoadB16 = _make_buffer_load_class("BufferLoadB16", "buffer_load_b16") +BufferLoadI16 = _make_buffer_load_class("BufferLoadI16", "buffer_load_i16") +BufferLoadU16 = _make_buffer_load_class("BufferLoadU16", "buffer_load_u16") +BufferLoadB32 = _make_buffer_load_class("BufferLoadB32", "buffer_load_b32") +BufferLoadB64 = _make_buffer_load_class("BufferLoadB64", "buffer_load_b64") +BufferLoadB96 = _make_buffer_load_class("BufferLoadB96", "buffer_load_b96") +BufferLoadB128 = _make_buffer_load_class("BufferLoadB128", "buffer_load_b128") +# logicalIR: BufferLoadB192 +BufferLoadB192 = _make_buffer_load_class("BufferLoadB192", "buffer_load_b192") + +# --- Flat Load: rocisa(dst, vaddr, flat, comment) --- +FlatLoadU8 = _make_flat_load_class("FlatLoadU8", "flat_load_u8") +FlatLoadI8 = _make_flat_load_class("FlatLoadI8", "flat_load_i8") +FlatLoadD16HIU8 = _make_flat_load_class("FlatLoadD16HIU8", "flat_load_d16_hi_u8") +FlatLoadD16U8 = _make_flat_load_class("FlatLoadD16U8", "flat_load_d16_u8") +FlatLoadD16I8 = _make_flat_load_class("FlatLoadD16I8", "flat_load_d16_i8") +FlatLoadD16HII8 = _make_flat_load_class("FlatLoadD16HII8", "flat_load_d16_hi_i8") +FlatLoadD16HIB16 = _make_flat_load_class("FlatLoadD16HIB16", "flat_load_d16_hi_b16") +FlatLoadD16B16 = _make_flat_load_class("FlatLoadD16B16", "flat_load_d16_b16") +FlatLoadU16 = _make_flat_load_class("FlatLoadU16", "flat_load_u16") +FlatLoadI16 = _make_flat_load_class("FlatLoadI16", "flat_load_i16") +FlatLoadB32 = _make_flat_load_class("FlatLoadB32", "flat_load_b32") +FlatLoadB64 = _make_flat_load_class("FlatLoadB64", "flat_load_b64") +FlatLoadB96 = _make_flat_load_class("FlatLoadB96", "flat_load_b96") +FlatLoadB128 = _make_flat_load_class("FlatLoadB128", "flat_load_b128") +# logicalIR: FlatLoadB192 +FlatLoadB192 = _make_flat_load_class("FlatLoadB192", "flat_load_b192") +# --- Global Load: rocisa(dst, vaddr, saddr, modifier, comment) --- +def _make_global_load_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for Global load shims: rocisa(dst, vaddr, saddr, modifier, comment).""" + + def __init__(self, dst: Any = None, vaddr: Any = None, + saddr: Any = None, modifier: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dst, + srcs=[vaddr, saddr], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self._modifier = modifier + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + _to_stinky_register(self.srcs[1]), + comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +GlobalLoadB32 = _make_global_load_class("GlobalLoadB32", "global_load_b32") +GlobalLoadB64 = _make_global_load_class("GlobalLoadB64", "global_load_b64") +GlobalLoadB96 = _make_global_load_class("GlobalLoadB96", "global_load_b96") +GlobalLoadB128 = _make_global_load_class("GlobalLoadB128", "global_load_b128") +GlobalLoadB192 = _make_global_load_class("GlobalLoadB192", "global_load_b192") +GlobalLoadD16B16 = _make_global_load_class("GlobalLoadD16B16", "global_load_d16_b16") +GlobalLoadD16HIB16 = _make_global_load_class("GlobalLoadD16HIB16", "global_load_d16_hi_b16") +GlobalLoadD16U8 = _make_global_load_class("GlobalLoadD16U8", "global_load_d16_u8") +GlobalLoadD16HIU8 = _make_global_load_class("GlobalLoadD16HIU8", "global_load_d16_hi_u8") + + +# --- Global Store: rocisa(vaddr, src, saddr, modifier, comment) --- +def _make_global_store_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for Global store shims: rocisa(vaddr, src, saddr, modifier, comment).""" + + def __init__(self, vaddr: Any = None, src: Any = None, + saddr: Any = None, modifier: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=src, + srcs=[vaddr, saddr], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self._modifier = modifier + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + _to_stinky_register(self.srcs[1]), + comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +GlobalStoreB8 = _make_global_store_class("GlobalStoreB8", "global_store_b8") +GlobalStoreB16 = _make_global_store_class("GlobalStoreB16", "global_store_b16") +GlobalStoreD16HIB16 = _make_global_store_class("GlobalStoreD16HIB16", "global_store_d16_hi_b16") +GlobalStoreB32 = _make_global_store_class("GlobalStoreB32", "global_store_b32") +GlobalStoreB64 = _make_global_store_class("GlobalStoreB64", "global_store_b64") +GlobalStoreB128 = _make_global_store_class("GlobalStoreB128", "global_store_b128") + + +# logicalIR: GlobalLoadTR8B64 +def _make_global_load_tr_class(class_name: str, mnemonic: str): + """Factory for global_load_tr* shims: rocisa(dst, vaddr, saddr, modifier, comment).""" + + def __init__(self, dst: Any = None, vaddr: Any = None, + saddr: Any = None, modifier: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dst, + srcs=[vaddr, saddr], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self._modifier = modifier + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + _to_stinky_register(self.srcs[1]), + self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + }) + return cls + + +GlobalLoadTR8B64 = _make_global_load_tr_class("GlobalLoadTR8B64", "global_load_tr_b64_b8") +# logicalIR: GlobalLoadTR16B128 +GlobalLoadTR16B128 = _make_global_load_tr_class("GlobalLoadTR16B128", "global_load_tr_b128_b16") + +# --- Buffer Store / Atomic: rocisa(src, vaddr, saddr, soffset, mubuf, comment) --- +BufferStoreB8 = _make_buffer_store_class("BufferStoreB8", "buffer_store_b8") +BufferStoreD16HIU8 = _make_buffer_store_class("BufferStoreD16HIU8", "buffer_store_d16_hi_b8") +# logicalIR: BufferStoreD16U8 +BufferStoreD16U8 = _make_buffer_store_class("BufferStoreD16U8", "buffer_store_d16_u8") +BufferStoreD16HIB16 = _make_buffer_store_class("BufferStoreD16HIB16", "buffer_store_d16_hi_b16") +# logicalIR: BufferStoreD16B16 +BufferStoreD16B16 = _make_buffer_store_class("BufferStoreD16B16", "buffer_store_d16_b16") +BufferStoreB16 = _make_buffer_store_class("BufferStoreB16", "buffer_store_b16") +BufferStoreB32 = _make_buffer_store_class("BufferStoreB32", "buffer_store_b32") +BufferStoreB64 = _make_buffer_store_class("BufferStoreB64", "buffer_store_b64") +BufferStoreB96 = _make_buffer_store_class("BufferStoreB96", "buffer_store_b96") +BufferStoreB128 = _make_buffer_store_class("BufferStoreB128", "buffer_store_b128") +BufferAtomicAddF32 = _make_buffer_load_class("BufferAtomicAddF32", "buffer_atomic_add_f32") +BufferAtomicCmpswapB32 = _make_buffer_store_class("BufferAtomicCmpswapB32", "buffer_atomic_cmpswap_b32") +BufferAtomicCmpswapB64 = _make_buffer_store_class("BufferAtomicCmpswapB64", "buffer_atomic_cmpswap_b64") + +# --- Flat Store: rocisa(src, vaddr, flat, comment) --- +FlatStoreB8 = _make_flat_store_class("FlatStoreB8", "flat_store_b8") +FlatStoreD16HIB8 = _make_flat_store_class("FlatStoreD16HIB8", "flat_store_d16_hi_b8") +FlatStoreB16 = _make_flat_store_class("FlatStoreB16", "flat_store_b16") +FlatStoreD16HIB16 = _make_flat_store_class("FlatStoreD16HIB16", "flat_store_d16_hi_b16") +# logicalIR: FlatStoreD16B16 +FlatStoreD16B16 = _make_flat_store_class("FlatStoreD16B16", "flat_store_d16_b16") +FlatStoreB32 = _make_flat_store_class("FlatStoreB32", "flat_store_b32") +FlatStoreB64 = _make_flat_store_class("FlatStoreB64", "flat_store_b64") +FlatStoreB96 = _make_flat_store_class("FlatStoreB96", "flat_store_b96") +FlatStoreB128 = _make_flat_store_class("FlatStoreB128", "flat_store_b128") + +# --- Flat Atomic: rocisa(vaddr, tmp, src, flat, comment) --- +FlatAtomicCmpswapB32 = _make_flat_atomic_class("FlatAtomicCmpswapB32", "flat_atomic_cmpswap_b32") + +# --- DS Load: rocisa(dst, src, ds, comment) --- +DSLoadU8 = _make_ds_load_class("DSLoadU8", "ds_load_u8") +DSLoadI8 = _make_ds_load_class("DSLoadI8", "ds_load_i8") +# logicalIR: DSLoadD16HIU8 +DSLoadD16HIU8 = _make_ds_load_class("DSLoadD16HIU8", "ds_load_d16_hi_u8") +DSLoadU16 = _make_ds_load_class("DSLoadU16", "ds_load_u16") +DSLoadI16 = _make_ds_load_class("DSLoadI16", "ds_load_i16") +# logicalIR: DSLoadD16HIU16 +DSLoadD16HIU16 = _make_ds_load_class("DSLoadD16HIU16", "ds_load_d16_hi_u16") +# logicalIR: DSLoadB16 +DSLoadB16 = _make_ds_load_class("DSLoadB16", "ds_load_b16") +DSLoadB32 = _make_ds_load_class("DSLoadB32", "ds_load_b32") +DSLoadB64 = _make_ds_load_class("DSLoadB64", "ds_load_b64") +DSLoadB96 = _make_ds_load_class("DSLoadB96", "ds_load_b96") +# logicalIR: DSLoadB96TrB6 +DSLoadB96TrB6 = _make_ds_load_class("DSLoadB96TrB6", "ds_load_tr6_b96") +# logicalIR: DSLoadB64TrB4 +DSLoadB64TrB4 = _make_ds_load_class("DSLoadB64TrB4", "ds_load_tr4_b64") +# logicalIR: DSLoadB64TrB16 +DSLoadB64TrB16 = _make_ds_load_class("DSLoadB64TrB16", "ds_load_tr16_b64") +# logicalIR: DSLoadB128TrB16 +DSLoadB128TrB16 = _make_ds_load_class("DSLoadB128TrB16", "ds_load_tr16_b128") +# logicalIR: DSLoadB64TrB8 +DSLoadB64TrB8 = _make_ds_load_class("DSLoadB64TrB8", "ds_load_tr8_b64") +DSLoadB128 = _make_ds_load_class("DSLoadB128", "ds_load_b128", latency=2) +# logicalIR: DSLoadB192 +DSLoadB192 = _make_ds_load_class("DSLoadB192", "ds_load_b192", latency=2) +def _make_ds_load2_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for DS load2 (dual-address): rocisa(dst, src, ds, comment) → logicalIR 3 args.""" + + def __init__(self, dst: Any = None, src: Any = None, + ds: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dst, + srcs=[src], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self.ds = ds + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + src_reg = _to_stinky_register(self.srcs[0]) + inst = factory( + _to_stinky_register(self.dst), + src_reg, + src_reg, + comment=self.comment) + if self.ds is not None: + inst.set_ds( + na=getattr(self.ds, "na", 2), + offset=getattr(self.ds, "offset", 0), + offset0=getattr(self.ds, "offset0", 0), + offset1=getattr(self.ds, "offset1", 0), + gds=getattr(self.ds, "gds", False), + ) + return inst + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +DSLoad2B32 = _make_ds_load2_class("DSLoad2B32", "ds_load2_b32") +DSLoad2B64 = _make_ds_load2_class("DSLoad2B64", "ds_load2_b64") + +# --- DS Store (binary): rocisa(dstAddr, src, ds, comment) --- +# logicalIR: DSStoreU16 +DSStoreU16 = _make_ds_store_class("DSStoreU16", "ds_store_u16") +DSStoreB8 = _make_ds_store_class("DSStoreB8", "ds_store_b8") +DSStoreB16 = _make_ds_store_class("DSStoreB16", "ds_store_b16") +# logicalIR: DSStoreB8HID16 +DSStoreB8HID16 = _make_ds_store_class("DSStoreB8HID16", "ds_store_b8_d16_hi") +# logicalIR: DSStoreD16HIB16 +DSStoreD16HIB16 = _make_ds_store_class("DSStoreD16HIB16", "ds_store_b16_d16_hi") +DSStoreB32 = _make_ds_store_class("DSStoreB32", "ds_store_b32", latency=2) +DSStoreB64 = _make_ds_store_class("DSStoreB64", "ds_store_b64", latency=3) +DSStoreB96 = _make_ds_store_class("DSStoreB96", "ds_store_b96", latency=4) +DSStoreB128 = _make_ds_store_class("DSStoreB128", "ds_store_b128", latency=5) +# logicalIR: DSStoreB192 +DSStoreB192 = _make_ds_store_class("DSStoreB192", "ds_store_b192", latency=6) +# logicalIR: DSStoreB256 +DSStoreB256 = _make_ds_store_class("DSStoreB256", "ds_store_b256", latency=7) + +# --- DS Store2 / Permute (ternary): rocisa(dstAddr, src0, src1, ds, comment) --- +DSStore2B32 = _make_ds_store2_class("DSStore2B32", "ds_store2_b32", latency=3) +DSStore2B64 = _make_ds_store2_class("DSStore2B64", "ds_store2_b64", latency=3) + + +def _make_ds_permute_class(class_name: str, mnemonic: str, latency: int = 1): + """Factory for DS permute: rocisa(dst, src0, src1, ds, comment) → logicalIR 2 args.""" + + def __init__(self, dstAddr: Any = None, src0: Any = None, src1: Any = None, + ds: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=dstAddr, + srcs=[src0, src1], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst(mnemonic) + self.ds = ds + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, class_name) + return factory( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type(class_name, (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: latency), + }) + cls.__qualname__ = class_name + return cls + + +DSBPermuteB32 = _make_ds_permute_class("DSBPermuteB32", "ds_bpermute_b32") + +# --- SMEM Store / Atomic --- +# SStoreB32 … SStoreB512 — real classes (``SMemStoreInstruction`` subclasses, defined above). +# logicalIR: SAtomicInc +class SAtomicInc(Instruction): + """``s_atomic_inc dst, base, soffset`` shim.""" + + __slots__ = ("dst", "base", "soffset", "smem") + + def __init__(self, dst=None, base=None, soffset=None, smem=None, comment="", **kw): + _ = kw + super().__init__(InstType.INST_B32, comment) + self.dst = dst + self.base = base + self.soffset = soffset + self.smem = smem + self.setInst("s_atomic_inc") + + def getParams(self): + return [self.dst, self.base, self.soffset] + + def getDstParams(self): + return [self.dst] if self.dst else [] + + def getSrcParams(self): + return [self.base, self.soffset] + + def toString(self) -> str: + parts = [_input_to_str(self.dst), _input_to_str(self.base), _input_to_str(self.soffset)] + kstr = self.instStr + " " + ", ".join(parts) + if self.smem is not None and hasattr(self.smem, "toString"): + kstr += self.smem.toString() + return self.formatWithComment(kstr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + return _st.SAtomicInc( + _to_stinky_register(self.dst), + _to_stinky_register(self.base), + _to_stinky_register(self.soffset), + self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = self.__class__.__new__(self.__class__) + memo[id(self)] = dup + Instruction.__init__(dup, self.instType, self.comment) + dup.instStr = self.instStr + dup.dst = _deepcopy(self.dst, memo) if self.dst is not None else None + dup.base = _deepcopy(self.base, memo) if self.base is not None else None + dup.soffset = self.soffset if isinstance(self.soffset, (int, float, str, bool)) else _deepcopy(self.soffset, memo) + dup.smem = _deepcopy(self.smem, memo) if self.smem is not None else None + return dup + + +# logicalIR: SAtomicDec +class SAtomicDec(Instruction): + """``s_atomic_dec dst, base`` shim (no soffset).""" + + __slots__ = ("dst", "base", "smem") + + def __init__(self, dst=None, base=None, smem=None, comment="", **kw): + _ = kw + super().__init__(InstType.INST_B32, comment) + self.dst = dst + self.base = base + self.smem = smem + self.setInst("s_atomic_dec") + + def getParams(self): + return [self.dst, self.base] + + def getDstParams(self): + return [self.dst] if self.dst else [] + + def getSrcParams(self): + return [self.base] + + def toString(self) -> str: + parts = [_input_to_str(self.dst), _input_to_str(self.base)] + kstr = self.instStr + " " + ", ".join(parts) + if self.smem is not None and hasattr(self.smem, "toString"): + kstr += self.smem.toString() + return self.formatWithComment(kstr) + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + return _st.SAtomicDec( + _to_stinky_register(self.dst), + _to_stinky_register(self.base), + self.comment) + + def __deepcopy__(self, memo): + if id(self) in memo: + return memo[id(self)] + dup = self.__class__.__new__(self.__class__) + memo[id(self)] = dup + Instruction.__init__(dup, self.instType, self.comment) + dup.instStr = self.instStr + dup.dst = _deepcopy(self.dst, memo) if self.dst is not None else None + dup.base = _deepcopy(self.base, memo) if self.base is not None else None + dup.smem = _deepcopy(self.smem, memo) if self.smem is not None else None + return dup + +# --- TensorLoadToLds: rocisa(group0, group1, group2, group3, comment) --- +def _make_tensor_load_class(): + def __init__(self, group0: Any = None, group1: Any = None, + group2: Any = None, group3: Any = None, + comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=group0, + srcs=[group1, group2, group3], dpp=None, sdwa=None, + vop3=None, comment=comment) + self.setInst("tensor_load_to_lds") + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + return _st.TensorLoadToLds( + _to_stinky_register(self.dst), + _to_stinky_register(self.srcs[0]), + comment=self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + cls = type("TensorLoadToLds", (CommonInstruction,), { + "__init__": __init__, + "to_stinky_logical": to_stinky_logical, + "__deepcopy__": __deepcopy__, + "issueLatency": staticmethod(lambda: 1), + }) + cls.__qualname__ = "TensorLoadToLds" + return cls + + +TensorLoadToLds = _make_tensor_load_class() +# logicalIR: GlobalPrefetchB8 — 2 srcs (vaddr, saddr), no dest +def _make_global_prefetch_class(): + """GlobalPrefetchB8: (vaddr, saddr, globalModifiers) → logicalIR 2 srcs.""" + + class GlobalPrefetchB8(CommonInstruction): + __slots__ = ("_modifiers",) + + def __init__(self, vaddr: Any = None, saddr: Any = None, + modifiers: Any = None, comment: str = "", **kw): + _ = kw + CommonInstruction.__init__( + self, instType=InstType.INST_NOTYPE, dst=None, + srcs=[vaddr, saddr], dpp=None, sdwa=None, vop3=None, comment=comment) + self.setInst("global_prefetch_b8") + self._modifiers = modifiers + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + factory = getattr(_st, "GlobalPrefetchB8") + return factory( + _to_stinky_register(self.srcs[0]), + _to_stinky_register(self.srcs[1]), + self.comment) + + def __deepcopy__(self, memo): + return CommonInstruction.__deepcopy__(self, memo) + + return GlobalPrefetchB8 + +GlobalPrefetchB8 = _make_global_prefetch_class() + + +# ========================================================================== +# MFMA / SMFMA / MXMFMA instructions +# source: rocisa/rocisa/src/instruction/mfma.cpp +# ========================================================================== + +_INST_TYPE_TO_STR: Dict[Any, str] = {} + + +def _inst_type_to_str(it: Any) -> str: + """Convert an InstType enum value to the string used by logicalIR MFMA factories.""" + if not _INST_TYPE_TO_STR: + _INST_TYPE_TO_STR.update({ + InstType.INST_F8: "fp8", InstType.INST_F16: "f16", + InstType.INST_F32: "f32", InstType.INST_F64: "f64", + InstType.INST_BF16: "bf16", InstType.INST_XF32: "xf32", + InstType.INST_BF8: "bf8", InstType.INST_I8: "i8", + InstType.INST_I32: "i32", InstType.INST_U8: "u8", + InstType.INST_F4: "f4", InstType.INST_F6: "f6", + InstType.INST_BF6: "bf6", InstType.INST_B8: "b8", + InstType.INST_E5M3: "e5m3", InstType.INST_E8: "e8", + }) + if it in _INST_TYPE_TO_STR: + return _INST_TYPE_TO_STR[it] + s = str(it) + if "INST_" in s: + return s.split("INST_")[-1].lower() + return s + + +class MFMAInstruction(Instruction): + """``v_mfma_*`` shim (rocisa ``MFMAInstruction``).""" + + __slots__ = ("accType", "variant", "mfma1k", "acc", "a", "b", "acc2", "acc2_imm", "neg") + + def __init__(self, instType: Any = None, accType: Any = None, + variant: Any = None, mfma1k: bool = False, + acc: Any = None, a: Any = None, b: Any = None, + acc2: Any = None, acc2_imm: Any = None, neg: bool = False, + comment: str = "", **kw): + _ = kw + super().__init__(instType, comment) + self.accType = accType + self.variant = variant if variant is not None else [] + self.mfma1k = mfma1k + self.acc = acc + self.a = a + self.b = b + self.acc2 = acc2 + self.acc2_imm = acc2_imm + self.neg = neg + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + m = self.variant[0] if len(self.variant) > 0 else 0 + n = self.variant[1] if len(self.variant) > 1 else 0 + k = self.variant[2] if len(self.variant) > 2 else 0 + blocks = self.variant[3] if len(self.variant) > 3 else 1 + acc2_reg = None + if self.acc2_imm is not None: + acc2_reg = _to_stinky_register(self.acc2_imm) + elif self.acc2 is not None: + acc2_reg = _to_stinky_register(self.acc2) + return _st.MFMA( + _inst_type_to_str(self.instType), + _inst_type_to_str(self.accType), + m, n, k, blocks, self.neg, + _to_stinky_register(self.acc), + _to_stinky_register(self.a), + _to_stinky_register(self.b), + acc2=acc2_reg, + comment=self.comment) + + def getParams(self): + return [self.acc, self.a, self.b] + + def getDstParams(self): + return [self.acc] + + def getSrcParams(self): + return [self.a, self.b] + + def getIssueLatency(self) -> int: + m = self.variant[0] if len(self.variant) > 0 else 16 + blocks = self.variant[3] if len(self.variant) > 3 else 1 + return getMFMAIssueLatency(None, m, blocks)[0] + + def __deepcopy__(self, memo): + clone = self.__class__.__new__(self.__class__) + memo[id(self)] = clone + Instruction.__init__(clone, self.instType, self.comment) + clone.outputInlineAsm = self.outputInlineAsm + clone.instStr = self.instStr + clone.m_memToken = None + clone.accType = self.accType + clone.variant = list(self.variant) + clone.mfma1k = self.mfma1k + clone.acc = _deepcopy(self.acc, memo) if self.acc is not None else None + clone.a = _deepcopy(self.a, memo) if self.a is not None else None + clone.b = _deepcopy(self.b, memo) if self.b is not None else None + clone.acc2 = _deepcopy(self.acc2, memo) if self.acc2 is not None else None + clone.acc2_imm = self.acc2_imm + clone.neg = self.neg + return clone + + +class MXMFMAInstruction(Instruction): + """``v_wmma_scale_*`` / ``v_mfma_scale_*`` shim (rocisa ``MXMFMAInstruction``).""" + + __slots__ = ("accType", "mxScaleAType", "mxScaleBType", "variant", + "acc", "a", "b", "acc2", "mxsa", "mxsb", "vop3", "mxCBSZ") + + def __init__(self, *, instType: Any = None, accType: Any = None, + variant: Any = None, acc: Any = None, + a: Any = None, b: Any = None, + acc2: Any = None, mxsa: Any = None, mxsb: Any = None, + vop3: Any = None, + mxScaleAType: Any = None, mxScaleBType: Any = None, + mxCBSZ: int = 0, + comment: str = "", **kw): + _ = kw + super().__init__(instType, comment) + self.accType = accType + self.mxScaleAType = mxScaleAType + self.mxScaleBType = mxScaleBType + self.variant = variant if variant is not None else [] + self.acc = acc + self.a = a + self.b = b + self.acc2 = acc2 + self.mxsa = mxsa + self.mxsb = mxsb + self.vop3 = vop3 + self.mxCBSZ = mxCBSZ + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + m = self.variant[0] if len(self.variant) > 0 else 0 + n = self.variant[1] if len(self.variant) > 1 else 0 + k = self.variant[2] if len(self.variant) > 2 else 0 + blocks = self.variant[3] if len(self.variant) > 3 else 1 + return _st.MXMFMA( + _inst_type_to_str(self.instType), + _inst_type_to_str(self.accType), + _inst_type_to_str(self.mxScaleAType) if self.mxScaleAType else "f32", + _inst_type_to_str(self.mxScaleBType) if self.mxScaleBType else "f32", + m, n, k, blocks, + _to_stinky_register(self.acc), + _to_stinky_register(self.a), + _to_stinky_register(self.b), + _to_stinky_register(self.acc2) if self.acc2 else None, + _to_stinky_register(self.mxsa) if self.mxsa else None, + _to_stinky_register(self.mxsb) if self.mxsb else None, + comment=self.comment) + + def getParams(self): + return [self.acc, self.a, self.b] + + def getDstParams(self): + return [self.acc] + + def getSrcParams(self): + return [self.a, self.b] + + def getIssueLatency(self) -> int: + m = self.variant[0] if len(self.variant) > 0 else 16 + blocks = self.variant[3] if len(self.variant) > 3 else 1 + return getMFMAIssueLatency(None, m, blocks)[0] + + def __deepcopy__(self, memo): + clone = self.__class__.__new__(self.__class__) + memo[id(self)] = clone + Instruction.__init__(clone, self.instType, self.comment) + clone.outputInlineAsm = self.outputInlineAsm + clone.instStr = self.instStr + clone.m_memToken = None + clone.accType = self.accType + clone.mxScaleAType = self.mxScaleAType + clone.mxScaleBType = self.mxScaleBType + clone.variant = list(self.variant) + clone.acc = _deepcopy(self.acc, memo) if self.acc is not None else None + clone.a = _deepcopy(self.a, memo) if self.a is not None else None + clone.b = _deepcopy(self.b, memo) if self.b is not None else None + clone.acc2 = _deepcopy(self.acc2, memo) if self.acc2 is not None else None + clone.mxsa = _deepcopy(self.mxsa, memo) if self.mxsa is not None else None + clone.mxsb = _deepcopy(self.mxsb, memo) if self.mxsb is not None else None + clone.vop3 = self.vop3 + clone.mxCBSZ = self.mxCBSZ + return clone + + +class SMFMAInstruction(Instruction): + """``v_smfma_*`` shim (rocisa ``SMFMAInstruction``).""" + + __slots__ = ("accType", "variant", "mfma1k", "acc", "a", "b", "metadata", "neg") + + def __init__(self, instType: Any = None, accType: Any = None, + variant: Any = None, mfma1k: bool = False, + acc: Any = None, a: Any = None, b: Any = None, + metadata: Any = None, neg: bool = False, + comment: str = "", **kw): + _ = kw + super().__init__(instType, comment) + self.accType = accType + self.variant = variant if variant is not None else [] + self.mfma1k = mfma1k + self.acc = acc + self.a = a + self.b = b + self.metadata = metadata + self.neg = neg + + def to_stinky_logical(self) -> Any: + import stinkytofu as _st + m = self.variant[0] if len(self.variant) > 0 else 0 + n = self.variant[1] if len(self.variant) > 1 else 0 + k = self.variant[2] if len(self.variant) > 2 else 0 + blocks = self.variant[3] if len(self.variant) > 3 else 1 + return _st.SMFMA( + _inst_type_to_str(self.instType), + _inst_type_to_str(self.accType), + m, n, k, blocks, self.neg, + _to_stinky_register(self.acc), + _to_stinky_register(self.a), + _to_stinky_register(self.b), + _to_stinky_register(self.metadata), + comment=self.comment) + + def getParams(self): + return [self.acc, self.a, self.b, self.metadata] + + def getDstParams(self): + return [self.acc] + + def getSrcParams(self): + return [self.a, self.b, self.metadata] + + def getIssueLatency(self) -> int: + m = self.variant[0] if len(self.variant) > 0 else 16 + blocks = self.variant[3] if len(self.variant) > 3 else 1 + return getSMFMAIssueLatency(None, m, blocks)[0] + + def __deepcopy__(self, memo): + clone = self.__class__.__new__(self.__class__) + memo[id(self)] = clone + Instruction.__init__(clone, self.instType, self.comment) + clone.outputInlineAsm = self.outputInlineAsm + clone.instStr = self.instStr + clone.m_memToken = None + clone.accType = self.accType + clone.variant = list(self.variant) + clone.mfma1k = self.mfma1k + clone.acc = _deepcopy(self.acc, memo) if self.acc is not None else None + clone.a = _deepcopy(self.a, memo) if self.a is not None else None + clone.b = _deepcopy(self.b, memo) if self.b is not None else None + clone.metadata = _deepcopy(self.metadata, memo) if self.metadata is not None else None + clone.neg = self.neg + return clone +def getMFMAIssueLatency(dataType, matrixInstM, matrixInstB): + """Workaround port of ``rocisa::getMFMAIssueLatency``. + + Returns ``(matrixInstM // mi_divisor, miIssueLatency)`` matching the + C++ default branch (``mi_divisor=2``, ``miIssueLatency=2``). The C++ + template has ISA-specific overrides (gfx940/941/942/950 + matrixInstB==1 + + halve-precision → divisor=4 / latency=1; XFloat32 → divisor=4; + gfx950 8-bit float → divisor=2 again). None of those branches fire for + gfx1250, so the default values are byte-for-byte equivalent for the + only ISA the logicalIR backend supports today. + + TODO: re-derive ``mi_divisor`` from + ``rocIsa.getInstance().getKernel().isa`` when extending beyond gfx1250. + """ + return (matrixInstM // 2, 2) + + +def getSMFMAIssueLatency(dataType, matrixInstM, matrixInstB): + """Workaround port of the sparse branch of + ``rocisa::getMFMAIssueLatency`` (mfma.hpp:55-58 hard-codes + ``mi_divisor = 4`` when ``isSparse`` is true). Same ISA caveats as + ``getMFMAIssueLatency``. + """ + return (matrixInstM // 4, 2) + + +# ========================================================================== +# Extension helpers (exposed as functions) +# source: rocisa/rocisa/src/instruction/extension.cpp +# ========================================================================== + +def _ext_lazy(): + """Lazy imports to avoid circular dependency (code.py imports instruction.py).""" + from .code import Module, Label # noqa: F811 + from .container import ContinuousRegister, sgpr # noqa: F811 + return Module, Label, ContinuousRegister, sgpr + + +def SGetPositivePCOffset(sgprIdx, label, tmpSgprRes): + """Port of ``rocisa::SGetPositivePCOffset`` (extension.hpp).""" + Module, Label, ContinuousRegister, sgpr = _ext_lazy() + if isinstance(tmpSgprRes, int): + tmpSgprRes = ContinuousRegister(tmpSgprRes, 1) + labelName = label.getLabelName() + module = Module("SGetPositivePCOffset " + labelName) + if tmpSgprRes.size < 1: + raise RuntimeError("ContinuousRegister size must be at least 1.") + tmpSgpr = tmpSgprRes.idx + module.add(SGetPCB64(dst=sgpr(sgprIdx, 2), comment="addr of next instr")) + module.add(SAddI32(dst=sgpr(tmpSgpr), src0=labelName, src1=4, + comment="target branch offset")) + module.add(SAddU32(dst=sgpr(sgprIdx), src0=sgpr(sgprIdx), + src1=sgpr(tmpSgpr), comment="add target branch offset")) + module.add(SAddCU32(dst=sgpr(sgprIdx + 1), src0=sgpr(sgprIdx + 1), + src1=0, comment="add high and carry")) + return module + + +def _split_tmp_regs(tmpSgprRes): + """Split a >= 3-register ContinuousRegister into (aligned_pair_idx, off_idx).""" + if tmpSgprRes.idx % 2 == 0: + return tmpSgprRes.idx, tmpSgprRes.idx + 2 + else: + return tmpSgprRes.idx + 1, tmpSgprRes.idx + + +def SLongBranch(label, tmpSgprRes_or_pcPair, offSgpr_or_posLabel=None, + positiveLabelStr=None, comment=""): + """Port of ``rocisa::SLongBranch`` (extension.hpp). + + Two calling conventions: + SLongBranch(label, tmpSgprRes, positiveLabelStr, comment="") + SLongBranch(label, pcPair, offSgpr, positiveLabelStr, comment="") + """ + if isinstance(offSgpr_or_posLabel, str) or offSgpr_or_posLabel is None: + tmpSgprRes = tmpSgprRes_or_pcPair + posLabel = offSgpr_or_posLabel or "" + cmt = positiveLabelStr if positiveLabelStr is not None else comment + if tmpSgprRes.size < 3: + raise RuntimeError("ContinuousRegister size must be at least 3.") + tmpSgprX2, tmpSgprX1 = _split_tmp_regs(tmpSgprRes) + return _SLongBranchImpl(label, tmpSgprX2, tmpSgprX1, posLabel, cmt) + else: + pcPair = tmpSgprRes_or_pcPair + offSgpr = offSgpr_or_posLabel + posLabel = positiveLabelStr or "" + cmt = comment + if pcPair.size < 2 or pcPair.idx % 2 != 0: + raise RuntimeError("pcPair must be a 2-aligned pair.") + if offSgpr.size < 1: + raise RuntimeError("offSgpr must have at least 1 register.") + return _SLongBranchImpl(label, pcPair.idx, offSgpr.idx, posLabel, cmt) + + +def _SLongBranchImpl(label, tmpSgprX2, tmpSgprX1, positiveLabelStr, comment): + Module, Label, ContinuousRegister, sgpr = _ext_lazy() + labelName = label.getLabelName() + module = Module("SLongBranch " + labelName) + if comment: + module.addComment(comment) + positiveLabel = Label(positiveLabelStr, "") + module.add(SGetPCB64(dst=sgpr(tmpSgprX2, 2), comment="addr of next instr")) + module.add(SAddI32(dst=sgpr(tmpSgprX1), src0=labelName, src1=4, + comment="target branch offset")) + module.add(SCmpGeI32(src0=sgpr(tmpSgprX1), src1=0, + comment="check positive or negative")) + module.add(SCBranchSCC1(labelName=positiveLabel.getLabelName(), + comment="jump when positive")) + module.add(SAbsI32(dst=sgpr(tmpSgprX1), src=sgpr(tmpSgprX1), + comment="abs offset")) + module.add(SSubU32(dst=sgpr(tmpSgprX2), src0=sgpr(tmpSgprX2), + src1=sgpr(tmpSgprX1), comment="sub target branch offset")) + module.add(SSubBU32(dst=sgpr(tmpSgprX2 + 1), src0=sgpr(tmpSgprX2 + 1), + src1=0, comment="sub high and carry")) + module.add(SSetPCB64(src=sgpr(tmpSgprX2, 2), + comment="branch to " + labelName)) + module.add(positiveLabel) + module.add(SAddU32(dst=sgpr(tmpSgprX2), src0=sgpr(tmpSgprX2), + src1=sgpr(tmpSgprX1), comment="add target branch offset")) + module.add(SAddCU32(dst=sgpr(tmpSgprX2 + 1), src0=sgpr(tmpSgprX2 + 1), + src1=0, comment="add high and carry")) + module.add(SSetPCB64(src=sgpr(tmpSgprX2, 2), + comment="branch to " + labelName)) + return module + + +def SLongBranchPositive(label, tmpSgprRes_or_pcPair, offSgpr_or_comment=None, + comment=""): + """Port of ``rocisa::SLongBranchPositive`` (extension.hpp). + + Two calling conventions: + SLongBranchPositive(label, tmpSgprRes, comment="") + SLongBranchPositive(label, pcPair, offSgpr, comment="") + """ + from .base import getAsmCaps + Module, Label, ContinuousRegister, sgpr = _ext_lazy() + + if isinstance(offSgpr_or_comment, str) or offSgpr_or_comment is None: + cmt = offSgpr_or_comment or "" + labelName = label.getLabelName() + module = Module("SLongBranchPositive " + labelName) + if cmt: + module.addComment(cmt) + if getAsmCaps().get("HasAdd_PC_i64", 0): + module.add(SAddPCI64_SIMM( + labelName=labelName + "-.-12", + comment="Add PC to " + labelName + + ", the constant correction is dependent on the" + " current assembler behavior.")) + else: + tmpSgprRes = tmpSgprRes_or_pcPair + if tmpSgprRes.size < 3: + raise RuntimeError( + "ContinuousRegister size must be at least 3.") + tmpSgprX2, tmpSgprX1 = _split_tmp_regs(tmpSgprRes) + cr = ContinuousRegister(tmpSgprX1, 1) + module.add(SGetPositivePCOffset(tmpSgprX2, label, cr)) + module.add(SSetPCB64(src=sgpr(tmpSgprX2, 2), + comment="branch to " + labelName)) + return module + else: + pcPair = tmpSgprRes_or_pcPair + offSgpr = offSgpr_or_comment + cmt = comment + labelName = label.getLabelName() + module = Module("SLongBranchPositive " + labelName) + if cmt: + module.addComment(cmt) + if pcPair.size < 2 or pcPair.idx % 2 != 0: + raise RuntimeError("pcPair must be a 2-aligned pair.") + if offSgpr.size < 1: + raise RuntimeError("offSgpr must have at least 1 register.") + module.add(SGetPositivePCOffset(pcPair.idx, label, offSgpr.idx)) + module.add(SSetPCB64(src=sgpr(pcPair.idx, 2), + comment="branch to " + labelName)) + return module + + +def SLongBranchNegative(label, tmpSgprRes_or_pcPair, offSgpr_or_comment=None, + comment=""): + """Port of ``rocisa::SLongBranchNegative`` (extension.hpp). + + Two calling conventions: + SLongBranchNegative(label, tmpSgprRes, comment="") + SLongBranchNegative(label, pcPair, offSgpr, comment="") + """ + if isinstance(offSgpr_or_comment, str) or offSgpr_or_comment is None: + tmpSgprRes = tmpSgprRes_or_pcPair + cmt = offSgpr_or_comment or "" + if tmpSgprRes.size < 3: + raise RuntimeError( + "ContinuousRegister size must be at least 3.") + tmpSgprX2, tmpSgprX1 = _split_tmp_regs(tmpSgprRes) + return _SLongBranchNegativeImpl(label, tmpSgprX2, tmpSgprX1, cmt) + else: + pcPair = tmpSgprRes_or_pcPair + offSgpr = offSgpr_or_comment + cmt = comment + if pcPair.size < 2 or pcPair.idx % 2 != 0: + raise RuntimeError("pcPair must be a 2-aligned pair.") + if offSgpr.size < 1: + raise RuntimeError("offSgpr must have at least 1 register.") + return _SLongBranchNegativeImpl( + label, pcPair.idx, offSgpr.idx, cmt) + + +def _SLongBranchNegativeImpl(label, tmpSgprX2, tmpSgprX1, comment): + Module, Label, ContinuousRegister, sgpr = _ext_lazy() + labelName = label.getLabelName() + module = Module("SLongBranchNegative " + labelName) + if comment: + module.addComment(comment) + module.add(SGetPCB64(dst=sgpr(tmpSgprX2, 2), comment="addr of next instr")) + module.add(SAddI32(dst=sgpr(tmpSgprX1), src0=labelName, src1=4, + comment="target branch offset")) + module.add(SAbsI32(dst=sgpr(tmpSgprX1), src=sgpr(tmpSgprX1), + comment="abs offset")) + module.add(SSubU32(dst=sgpr(tmpSgprX2), src0=sgpr(tmpSgprX2), + src1=sgpr(tmpSgprX1), comment="sub target branch offset")) + module.add(SSubBU32(dst=sgpr(tmpSgprX2 + 1), src0=sgpr(tmpSgprX2 + 1), + src1=0, comment="sub high and carry")) + module.add(SSetPCB64(src=sgpr(tmpSgprX2, 2), + comment="branch to " + labelName)) + return module + + +def SCLongBranchScc0(label, tmpSgprRes, noBranchLabelStr, + positiveLabelStr, posNeg=0, comment=""): + """Port of ``rocisa::SCLongBranchScc0`` (extension.hpp).""" + Module, Label, ContinuousRegister, sgpr = _ext_lazy() + module = Module("SCLongBranchScc0 " + label.getLabelName()) + noBranchLabel = Label(noBranchLabelStr, "") + module.add(SCBranchSCC1(labelName=noBranchLabel.getLabelName(), + comment="Only branch on scc0")) + if posNeg > 0: + module.add(SLongBranchPositive(label, tmpSgprRes, comment)) + elif posNeg < 0: + module.add(SLongBranchNegative(label, tmpSgprRes, comment)) + else: + module.add(SLongBranch(label, tmpSgprRes, positiveLabelStr, comment)) + module.add(noBranchLabel) + return module + + +def SCLongBranchScc1(label, tmpSgprRes, noBranchLabelStr, + positiveLabelStr, posNeg=0, comment=""): + """Port of ``rocisa::SCLongBranchScc1`` (extension.hpp).""" + Module, Label, ContinuousRegister, sgpr = _ext_lazy() + module = Module("SCLongBranchScc1 " + label.getLabelName()) + noBranchLabel = Label(noBranchLabelStr, "") + module.add(SCBranchSCC0(labelName=noBranchLabel.getLabelName(), + comment="Only branch on scc1")) + if posNeg > 0: + module.add(SLongBranchPositive(label, tmpSgprRes, comment)) + elif posNeg < 0: + module.add(SLongBranchNegative(label, tmpSgprRes, comment)) + else: + module.add(SLongBranch(label, tmpSgprRes, positiveLabelStr, comment)) + module.add(noBranchLabel) + return module + + +def SCLongBranchVccnz(label, tmpSgprRes, noBranchLabelStr, + positiveLabelStr, posNeg=0, comment=""): + """Port of ``rocisa::SCLongBranchVccnz`` (extension.hpp).""" + Module, Label, ContinuousRegister, sgpr = _ext_lazy() + module = Module("SCLongBranchVccnz " + label.getLabelName()) + noBranchLabel = Label(noBranchLabelStr, "") + module.add(SCBranchVCCZ(labelName=noBranchLabel.getLabelName(), + comment="Only branch on vccz")) + if posNeg > 0: + module.add(SLongBranchPositive(label, tmpSgprRes, comment)) + elif posNeg < 0: + module.add(SLongBranchNegative(label, tmpSgprRes, comment)) + else: + module.add(SLongBranch(label, tmpSgprRes, positiveLabelStr, comment)) + module.add(noBranchLabel) + return module +def SMulInt64to32(dst0: Any, dst1: Any, src0: Any, src1: Any, + tmpVgprRes: Any, hasSMulHi: bool, sign: bool = False, + comment: str = "") -> Any: + """64-bit = 32×32 multiply, result in dst0:dst1 (extension.hpp:391).""" + Module, _, ContinuousRegister, _ = _ext_lazy() + from .container import vgpr # noqa: WPS433 + + module = Module("SMulInt64to32") + if hasattr(tmpVgprRes, "size") and tmpVgprRes.size < 2: + raise RuntimeError("ContinuousRegister size must be at least 2.") + + if hasSMulHi: + if sign: + module.add(SMulHII32(dst=dst1, src0=src0, src1=src1, comment=comment)) + else: + module.add(SMulHIU32(dst=dst1, src0=src0, src1=src1, comment=comment)) + module.add(SMulI32(dst=dst0, src0=src0, src1=src1, comment=comment)) + else: + idx = tmpVgprRes.idx if hasattr(tmpVgprRes, "idx") else int(tmpVgprRes) + vgprTmp = vgpr(idx) + vgprTmp1 = vgpr(idx + 1) + swapSrc0, swapSrc1 = (src0, src1) if _is_container(src1) else (src1, src0) + module.add(VMovB32(dst=vgprTmp, src=swapSrc0, comment=comment)) + if sign: + module.add(VMulHII32(dst=vgprTmp1, src0=vgprTmp, src1=swapSrc1, + comment=comment)) + else: + module.add(VMulHIU32(dst=vgprTmp1, src0=vgprTmp, src1=swapSrc1, + comment=comment)) + module.add(VReadfirstlaneB32(dst=dst1, src=vgprTmp1, comment=comment)) + module.add(VMulLOU32(dst=vgprTmp1, src0=vgprTmp, src1=swapSrc1, + comment=comment)) + module.add(VReadfirstlaneB32(dst=dst0, src=vgprTmp1, comment=comment)) + return module + + +def _is_container(val: Any) -> bool: + """Check if val is a Container-like object (not a scalar int/hex).""" + return hasattr(val, "toString") +# ========================================================================== +# True16 / conversion helpers (extension.hpp:473-627) +# +# Each factory checks hardware capabilities and returns a single instruction +# with the appropriate SDWA, VOP3P, or True16 modifiers. +# ========================================================================== + + +class _True16Wrap: + """Wraps a register/input and appends a True16 ``.h``/``.l`` suffix.""" + + __slots__ = ("_inner", "_suffix") + + def __init__(self, inner: Any, sel: Any) -> None: + from .enum import HighBitSel # noqa: WPS433 + self._inner = inner + self._suffix = ".h" if sel == HighBitSel.HIGH else ".l" + + def toString(self) -> str: + return _input_to_str(self._inner) + self._suffix + + def to_stinky(self) -> Any: + """Return the inner register as a proper StinkyRegister. + + The True16 .h/.l selection is encoded via the VOP3 op_sel modifier + on the instruction, NOT as a string suffix on the register. Returning + the structured register preserves the physical index needed by + InsertVgprMsbPass for correct MSB computation. + """ + if hasattr(self._inner, "to_stinky"): + return self._inner.to_stinky() + import stinkytofu as _st # noqa: WPS433 + return _st.Register(self.toString()) + + def __str__(self) -> str: + return self.toString() + + +def ECvtF16toF32(dst: Any, src: Any, sel: Any, comment: str = "") -> Any: + """Convert F16 → F32, selecting src half-word by *sel* (extension.hpp:474).""" + from .base import getArchCaps # noqa: WPS433 + from .container import SDWAModifiers # noqa: WPS433 + from .enum import HighBitSel, SelectBit # noqa: WPS433 + + if getArchCaps().get("NoSDWA", 0): + return VCvtF16toF32(dst=dst, src=_True16Wrap(src, sel), comment=comment) + + src0_sel = SelectBit.WORD_1 if sel == HighBitSel.HIGH else SelectBit.WORD_0 + return VCvtF16toF32( + dst=dst, src=src, sdwa=SDWAModifiers(src0_sel=src0_sel), comment=comment, + ) + + +def ECvtF32toF16(dst: Any, src: Any, sel: Any = None, comment: str = "") -> Any: + """Convert F32 → F16 with optional half-word packing (extension.hpp:506).""" + from .base import getArchCaps # noqa: WPS433 + from .container import SDWAModifiers # noqa: WPS433 + from .enum import HighBitSel, SelectBit # noqa: WPS433 + + if sel is None: + return VCvtF32toF16(dst=dst, src=src, comment=comment) + + if getArchCaps().get("NoSDWA", 0): + return VCvtF32toF16(dst=_True16Wrap(dst, sel), src=src, comment=comment) + + dst_sel = SelectBit.WORD_1 if sel == HighBitSel.HIGH else SelectBit.WORD_0 + return VCvtF32toF16( + dst=dst, src=src, sdwa=SDWAModifiers(dst_sel=dst_sel), comment=comment, + ) + + +def ECvtPkFP8toF32(dst: Any, src: Any, sel: Any, comment: str = "") -> Any: + """Unpack packed-FP8 → 2×F32, selecting src half-word (extension.hpp:537).""" + from .base import getArchCaps # noqa: WPS433 + from .container import SDWAModifiers, VOP3PModifiers # noqa: WPS433 + from .enum import HighBitSel, SelectBit # noqa: WPS433 + + sel_int = 1 if sel == HighBitSel.HIGH else 0 + if getArchCaps().get("NoSDWA", 0): + inst = VCvtPkFP8toF32( + dst=dst, src=_True16Wrap(src, sel), comment=comment, + ) + inst.vop3 = VOP3PModifiers(op_sel=[sel_int]) + return inst + + src0_sel = SelectBit.WORD_1 if sel == HighBitSel.HIGH else SelectBit.WORD_0 + return VCvtPkFP8toF32( + dst=dst, src=src, sdwa=SDWAModifiers(src0_sel=src0_sel), comment=comment, + ) + + +def ECvtPkBF8toF32(dst: Any, src: Any, sel: Any, comment: str = "") -> Any: + """Unpack packed-BF8 → 2×F32, selecting src half-word (extension.hpp:566).""" + from .base import getArchCaps # noqa: WPS433 + from .container import SDWAModifiers, VOP3PModifiers # noqa: WPS433 + from .enum import HighBitSel, SelectBit # noqa: WPS433 + + sel_int = 1 if sel == HighBitSel.HIGH else 0 + if getArchCaps().get("NoSDWA", 0): + inst = VCvtPkBF8toF32( + dst=dst, src=_True16Wrap(src, sel), comment=comment, + ) + inst.vop3 = VOP3PModifiers(op_sel=[sel_int]) + return inst + + src0_sel = SelectBit.WORD_1 if sel == HighBitSel.HIGH else SelectBit.WORD_0 + return VCvtPkBF8toF32( + dst=dst, src=src, sdwa=SDWAModifiers(src0_sel=src0_sel), comment=comment, + ) + + +def VCvtBF16toFP32(dst: Any, src: Any, vgprMask: Any, vi: int, + comment: str = "") -> Any: + """BF16 → FP32 conversion with architecture dispatch (extension.hpp:594).""" + from .base import getAsmCaps, getArchCaps # noqa: WPS433 + from .container import SDWAModifiers, VOP3PModifiers # noqa: WPS433 + from .enum import HighBitSel, SelectBit # noqa: WPS433 + + if not getAsmCaps().get("HasBF16CVT", 0): + if (vi % 2) == 1: + if vgprMask is None: + raise RuntimeError("vgprMask is null") + return VAndB32( + dst=dst, src0=src, src1=vgprMask, + comment="cvt bf16 to fp32. " + comment, + ) + return VLShiftLeftB32( + dst=dst, src0=16, src1=src, + comment="cvt bf16 to fp32. " + comment, + ) + + if getArchCaps().get("NoSDWA", 0): + sel = HighBitSel.HIGH if (vi % 2) == 1 else HighBitSel.LOW + inst = PVCvtBF16toFP32( + dst=dst, src=_True16Wrap(src, sel), comment="cvt bf16 to f32", + ) + inst.vop3 = VOP3PModifiers(op_sel=[vi % 2]) + return inst + + src0_sel = SelectBit.WORD_1 if (vi % 2) == 1 else SelectBit.WORD_0 + return PVCvtBF16toFP32( + dst=dst, src=src, sdwa=SDWAModifiers(src0_sel=src0_sel), + comment="cvt bf16 to f32", + ) + diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/label.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/label.py new file mode 100644 index 000000000000..5917f092c636 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/label.py @@ -0,0 +1,233 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Assembly label management (LabelManager + magicGenerator). + +Fully implemented; no stinkytofu dependency. +""" + +from __future__ import annotations + +import random + + +# --------------------------------------------------------------------------- +# magicGenerator -- 16-char random label tag. +# --------------------------------------------------------------------------- +# +# 36-char alphabet from rocisa (uppercase letters + digits, no +# punctuation, no lowercase -- assembler-safe in every dialect we +# target). + +_MAGIC_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +# Effective output length. +# +# rocisa names the constant ``LABEL_NAME_LENGTH = 17`` but the +# generator loop iterates ``LABEL_NAME_LENGTH - 1 == 16`` times -- +# so the returned string is 16 chars (NOT 17). We mirror the actual +# behavior here, not the misleading constant name. + +_MAGIC_LENGTH = 16 + + +def magicGenerator() -> str: # noqa: N802 (public name; matches rocisa) + """Return a 16-character random ``[A-Z0-9]`` string. + + Mirror of ``rocisa::magicGenerator``. Used by + ``LabelManager.getUniqueName`` / ``getUniqueNamePrefix`` to seed + fresh label names. + + Parity / non-parity: + * Output character set and length are byte-identical to + rocisa. + * Randomness source is Python's ``random.choices`` (Mersenne + Twister), NOT C++ ``rand()`` -- the bit pattern of any + given draw will not match. This is intentional: the values + flow into label names that live only inside a single kernel + emission, never get persisted in golden asm files, so + per-byte determinism with a particular seed is not part of + the contract. Tests that need deterministic output should + monkeypatch this function. + """ + return "".join(random.choices(_MAGIC_CHARS, k=_MAGIC_LENGTH)) + + +# --------------------------------------------------------------------------- +# LabelManager -- per-kernel label registry. +# --------------------------------------------------------------------------- +# +# Tracks per-name use counts so labels emitted into the same kernel +# are guaranteed unique. KernelWriter creates one ``LabelManager`` +# per kernel and routes EVERY label name through it via +# ``getName`` / ``getNameInc`` / ``getUniqueName*``. +# +# Counter semantics (the only subtle bit): +# * ``addName`` on a fresh name sets the count to 0; on an +# existing name it adds 1. +# * The qualified name returned for count ``c`` is: +# c == 0 -> name +# c > 0 -> name_ +# * So the FIRST occurrence emits the bare name, the SECOND +# emits ``name_1``, the THIRD ``name_2``, and so on. The count +# is therefore "occurrences minus one", not "occurrences". +# +# Public API parity: +# * Methods exposed = the methods rocisa's nanobind binding exposes +# (label.cpp:38-64): ``addName / getName / getNameInc / +# getNameIndex / getUniqueName / getUniqueNamePrefix``. +# * ``getData`` is NOT exposed -- it's an internal C++ accessor +# used only by deepcopy / pickle on the rocisa side. We mirror +# that by gating dict access behind ``_labels`` (private) plus +# the dunder protocol methods. + + +class LabelManager: + """Per-kernel label registry; mirror of ``rocisa::LabelManager``. + + See the section header above for counter semantics and the + public-API parity contract. + """ + + __slots__ = ("_labels",) + + def __init__(self) -> None: + # ``dict[str, int]`` matches the rocisa + # ``std::map``. Insertion order is not part + # of the contract (the C++ side iterates in sorted order via + # std::map; nothing in KernelWriter or our public API reads + # the order, so a vanilla dict is fine). + self._labels: dict[str, int] = {} + + def addName(self, name: str) -> None: # noqa: N802 (matches rocisa) + """Bump (or insert at 0) the use-count for ``name``. + + Existing entry → ``+1``; new entry → inserted at ``0``. + Mirror of ``LabelManager::addName``. + """ + if name in self._labels: + self._labels[name] += 1 + else: + self._labels[name] = 0 + + def getName(self, name: str) -> str: # noqa: N802 (matches rocisa) + """Return the qualified label string for ``name``. + + Inserts ``name`` at count 0 if absent (matches the C++ + ``m_labels[name] = 0`` side effect when the key is missing, + triggered by both ``find() == end()`` AND the subsequent + ``operator[]`` read). Returns ``name`` when the count is 0, + else ``name_``. + + This method is IDEMPOTENT for an already-tracked name -- it + does NOT bump the counter. Use ``getNameInc`` for the + bumping variant. + """ + # ``setdefault`` mirrors the C++ "insert-on-miss-then-read" + # pattern in a single call. + count = self._labels.setdefault(name, 0) + if count == 0: + return name + return f"{name}_{count}" + + def getNameInc(self, name: str) -> str: # noqa: N802 (matches rocisa) + """``addName(name)`` then return the qualified name. + + Mirror of ``LabelManager::getNameInc``. KernelWriter calls + this for every NEW label emission so colliding names get + suffix-disambiguated automatically. + """ + self.addName(name) + count = self._labels[name] + if count == 0: + return name + return f"{name}_{count}" + + def getNameIndex(self, name: str, index: int) -> str: # noqa: N802 (matches rocisa) + """Return ``name`` (``index == 0``) or ``name_``. + + Use this when you already know which occurrence you want + (e.g. KernelWriter referring back to a specific iteration of + a loop label). Raises: + + - ``RuntimeError("You have to add a label first ...")`` when + the name has never been added. + - ``RuntimeError("The index N exceeded. (> M)")`` when the + requested index is greater than the recorded count. + + Both messages match ``std::runtime_error`` text from rocisa + byte-for-byte so any consumer that string-matches on the + exception keeps working. + """ + if name not in self._labels: + raise RuntimeError( + "You have to add a label first to get a label name with specific index." + ) + current = self._labels[name] + if index > current: + raise RuntimeError(f"The index {index} exceeded. (> {current})") + if index == 0: + return name + return f"{name}_{index}" + + def getUniqueName(self) -> str: # noqa: N802 (matches rocisa) + """Return a freshly-generated label name not yet tracked. + + Loops on ``magicGenerator`` until the candidate is absent + from the registry, then routes through ``getName`` -- which + inserts the candidate at count 0 and returns the bare name. + Subsequent calls with the same candidate (which can't happen + unless tests monkeypatch ``magicGenerator`` to be + deterministic) would suffix-disambiguate. + """ + name = magicGenerator() + while name in self._labels: + name = magicGenerator() + return self.getName(name) + + def getUniqueNamePrefix(self, prefix: str) -> str: # noqa: N802 (matches rocisa) + """Like ``getUniqueName`` with a fixed ``_`` head. + + KernelWriter uses this for human-readable unique labels such + as ``loop_top_`` or ``waitcnt_``. + """ + name = f"{prefix}_{magicGenerator()}" + while name in self._labels: + name = f"{prefix}_{magicGenerator()}" + return self.getName(name) + + # ------------------------------------------------------------------ + # Pickle / deepcopy plumbing. + # + # rocisa's binding implements ``__deepcopy__`` / + # ``__getstate__`` / ``__setstate__`` directly on the C++ class + # (label.cpp:54-64). They use ``getData()`` (return-by-value of + # the internal map) as the round-trip carrier, so the state + # shape is a 1-tuple ``(dict,)``. + # ------------------------------------------------------------------ + + def __deepcopy__(self, memo) -> "LabelManager": + # Mirrors ``new LabelManager(self.getData())``: a fresh + # manager seeded with a COPY of the underlying map. Counter + # values are immutable ``int`` so a shallow dict copy + # suffices for full isolation between original and clone. + clone = LabelManager() + clone._labels = dict(self._labels) + memo[id(self)] = clone + return clone + + def __getstate__(self): + # 1-tuple shape matches rocisa (label.cpp:60). The inner + # dict is COPIED to defend against post-pickle mutation of + # the live manager bleeding into the pickled bytes (the + # rocisa side gets this for free via return-by-value). + return (dict(self._labels),) + + def __setstate__(self, state) -> None: + # No ``__init__`` was called by pickle -- ``__new__`` gave us + # an empty shell. Rebuild from scratch, mirroring rocisa's + # placement-new ``new(&self) LabelManager(get<0>(state))``. + (data,) = state + self._labels = dict(data) + + def __repr__(self) -> str: + return f"LabelManager({self._labels!r})" diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/macro.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/macro.py new file mode 100644 index 000000000000..fd8fff8d76f5 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/macro.py @@ -0,0 +1,135 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Composite macro builders (MacroVMagicDiv, PseudoRandomGenerator). + +Macro/MacroInstruction IR types are real; builder callables are still dummy. +""" + +from __future__ import annotations + +from typing import Any + +from .code import Macro, Module, TextBlock +from .container import sgpr, vgpr +from .instruction import ( + VAddU32, VAndB32, VLShiftLeftB32, VLShiftRightB32, VLShiftRightB64, + VMulHIU32, VMulLOU32, VMulU32U24, VXorB32, _VLShiftLeftOrB32, +) + + +def MacroVMagicDiv(algo: int) -> Macro: + """Build the V_MAGIC_DIV macro definition (macro.hpp:36-58).""" + macro = Macro( + "V_MAGIC_DIV", + ["vgprDstIdx:req", "dividend:req", "magicNumber:req", + "magicShift:req", "magicA:req"], + ) + if algo == 1: + macro.addT(VMulHIU32, dst=vgpr("DstIdx+1", 1, True), + src0="\\dividend", src1="\\magicNumber") + macro.addT(VMulLOU32, dst=vgpr("DstIdx+0", 1, True), + src0="\\dividend", src1="\\magicNumber") + macro.addT(VLShiftRightB64, dst=vgpr("DstIdx", 2, True), + shiftHex="\\magicShift", src=vgpr("DstIdx", 2, True)) + elif algo == 2: + macro.addT(VMulHIU32, dst=vgpr("DstIdx+1", 1, True), + src0="\\dividend", src1="\\magicNumber") + macro.addT(VMulLOU32, dst=vgpr("DstIdx+0", 1, True), + src0="\\dividend", src1="\\magicA") + macro.addT(VAddU32, dst=vgpr("DstIdx+0", 1, True), + src0=vgpr("DstIdx+0", 1, True), src1=vgpr("DstIdx+1", 1, True)) + macro.addT(VLShiftRightB32, dst=vgpr("DstIdx+0", 1, True), + shiftHex="\\magicShift", src=vgpr("DstIdx+0", 1, True)) + return macro + + +def VMagicDiv(algo: int, dstIdx: int, dividend: Any, + magicNumber: Any, magicShift: Any, magicA: Any) -> Module: + """Inlined V_MAGIC_DIV — module form (macro.hpp:121-149).""" + module = Module("V_MAGIC_DIV") + module.addComment0("V_MAGIC_DIV start") + if algo == 1: + module.add(VMulHIU32(dst=vgpr(dstIdx + 1, 1), src0=dividend, + src1=magicNumber)) + module.add(VMulLOU32(dst=vgpr(dstIdx, 1), src0=dividend, + src1=magicNumber)) + module.add(VLShiftRightB64(dst=vgpr(dstIdx, 2), shiftHex=magicShift, + src=vgpr(dstIdx, 2))) + elif algo == 2: + module.add(VMulHIU32(dst=vgpr(dstIdx + 1, 1), src0=dividend, + src1=magicNumber)) + module.add(VMulLOU32(dst=vgpr(dstIdx, 1), src0=dividend, + src1=magicA)) + module.add(VAddU32(dst=vgpr(dstIdx, 1), src0=vgpr(dstIdx, 1), + src1=vgpr(dstIdx + 1, 1))) + module.add(VLShiftRightB32(dst=vgpr(dstIdx, 1), shiftHex=magicShift, + src=vgpr(dstIdx, 1))) + module.addComment0("V_MAGIC_DIV end") + return module + + +def PseudoRandomGenerator() -> Macro: + """Build the PRND_GENERATOR macro definition (macro.hpp:60-119).""" + macro = Macro( + "PRND_GENERATOR", + ["vgprRand:req", "vgprAcc:req", "vgprTemp0:req", "vgprTemp1:req"], + ) + macro.addComment0("PRND_GENERATOR: vgprRand=RND(vgprAcc, sgprSeed, vgprTid)") + + macro.addT(VAndB32, dst=vgpr("Temp0", 1, True), src0="0xFFFF", + src1=vgpr("Acc", 1, True), comment="vgprTemp0 = vgprAcc & 0xFFFF") + macro.addT(VLShiftRightB32, dst=vgpr("Temp1", 1, True), shiftHex=16, + src=vgpr("Acc", 1, True), comment="vgprTemp1 = vgprAcc >> 16") + macro.addT(VXorB32, dst=vgpr("Temp0", 1, True), src0=vgpr("Temp0", 1, True), + src1=vgpr("Temp1", 1, True), comment="VTemp0 = vgprTemp0 ^ vgprTemp1") + macro.addT(VAndB32, dst=vgpr("Temp1", 1, True), src0=vgpr("Temp0", 1, True), + src1=31, comment="vgprTemp1 = vgprTemp0 & 31") + macro.addT(VLShiftLeftB32, dst=vgpr("Temp1", 1, True), shiftHex=11, + src=vgpr("Temp1", 1, True), comment="vgprTemp1 = vgprTemp1 << 11") + macro.addT(_VLShiftLeftOrB32, dst=vgpr("Temp0", 1, True), + src0=vgpr("Temp0", 1, True), src1=5, src2=vgpr("Temp1", 1, True), + comment="vgprTemp0 = vgprTemp0 << 5 | vgprTemp1") + macro.addT(VMulU32U24, dst=vgpr("Temp0", 1, True), src0="0x700149", + src1=vgpr("Temp0", 1, True), comment="VTemp0 = vgprTemp0 * 0x700149") + macro.addT(VMulU32U24, dst=vgpr("Temp1", 1, True), src0=229791, + src1=vgpr("Serial"), comment="VTemp1 = vTid * 229791") + macro.addT(VXorB32, dst=vgpr("Rand", 1, True), src0="0x1337137", + src1=vgpr("Temp0", 1, True), comment="VRand = vgprTemp0 ^ 0x1337137") + macro.addT(VXorB32, dst=vgpr("Rand", 1, True), src0=vgpr("Rand", 1, True), + src1=vgpr("Temp1", 1, True), comment="VRand = vgprRand ^ vgprTemp1") + macro.addT(VXorB32, dst=vgpr("Rand", 1, True), src0=vgpr("Rand", 1, True), + src1=sgpr("RNDSeed"), comment="VRand = vgprRand ^ sSeed") + return macro + + +def PseudoRandomGeneratorModule(Rand: int, Acc: int, + Temp0: int, Temp1: int) -> Module: + """Inlined PRND_GENERATOR — module form (macro.hpp:151-210).""" + module = Module("PRND_GENERATOR") + module.addComment0("PRND_GENERATOR: vgprRand=RND(vgprAcc, sgprSeed, vgprTid)") + + module.add(VAndB32(dst=vgpr(Temp0, 1), src0="0xFFFF", + src1=vgpr(Acc, 1), comment="vgprTemp0 = vgprAcc & 0xFFFF")) + module.add(VLShiftRightB32(dst=vgpr(Temp1, 1), shiftHex=16, + src=vgpr(Acc, 1), comment="vgprTemp1 = vgprAcc >> 16")) + module.add(VXorB32(dst=vgpr(Temp0, 1), src0=vgpr(Temp0, 1), + src1=vgpr(Temp1, 1), comment="VTemp0 = vgprTemp0 ^ vgprTemp1")) + module.add(VAndB32(dst=vgpr(Temp1, 1), src0=vgpr(Temp0, 1), + src1=31, comment="vgprTemp1 = vgprTemp0 & 31")) + module.add(VLShiftLeftB32(dst=vgpr(Temp1, 1), shiftHex=11, + src=vgpr(Temp1, 1), comment="vgprTemp1 = vgprTemp1 << 11")) + module.add(_VLShiftLeftOrB32(dst=vgpr(Temp0, 1), src0=vgpr(Temp0, 1), + src1=5, src2=vgpr(Temp1, 1), + comment="vgprTemp0 = vgprTemp0 << 5 | vgprTemp1")) + module.add(VMulU32U24(dst=vgpr(Temp0, 1), src0="0x700149", + src1=vgpr(Temp0, 1), comment="VTemp0 = vgprTemp0 * 0x700149")) + module.add(VMulU32U24(dst=vgpr(Temp1, 1), src0=229791, + src1=vgpr("Serial"), comment="VTemp1 = vTid * 229791")) + module.add(VXorB32(dst=vgpr(Rand, 1), src0="0x1337137", + src1=vgpr(Temp0, 1), comment="VRand = vgprTemp0 ^ 0x1337137")) + module.add(VXorB32(dst=vgpr(Rand, 1), src0=vgpr(Rand, 1), + src1=vgpr(Temp1, 1), comment="VRand = vgprRand ^ vgprTemp1")) + module.add(VXorB32(dst=vgpr(Rand, 1), src0=vgpr(Rand, 1), + src1=sgpr("RNDSeed"), comment="VRand = vgprRand ^ sSeed")) + module.addComment0("PRND_GENERATOR end") + return module diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/register.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/register.py new file mode 100644 index 000000000000..d0a9899dcd0e --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/register.py @@ -0,0 +1,529 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Stateful first-fit register allocator (1:1 port of register.hpp/cpp). + +Fully implemented: checkOut/checkIn/growPool/occupancyLimit/deepcopy. +Not yet done: initTmps (returns None until instruction shim wiring is complete). +""" + +from __future__ import annotations + +import enum as _stdenum +from copy import deepcopy +from typing import Dict, List, Optional, Tuple + + +_UNTAGGED = "_untagged_" +_UNTAGGED_ALIGNED = "_untagged_aligned_" + + +class RegisterPool: + """First-fit, growable register allocator. + + Construct one per register class (Sgpr/Vgpr/Accvgpr) and call + ``checkOutAligned`` / ``checkIn`` over its lifetime. + + The pool starts empty (size=0 by convention) and grows from the tail + when ``checkOutAligned`` cannot satisfy a request and ``preventOverflow`` + is false. + """ + + class Status(_stdenum.IntEnum): + Unavailable = 0 + Available = 1 + InUse = 2 + + class Register: + """Per-slot state record. ``tag`` is for debugging only.""" + + __slots__ = ("status", "tag") + + def __init__(self, status: "RegisterPool.Status", tag: str) -> None: + self.status = status + self.tag = tag + + def __repr__(self) -> str: + return f"Register(status={self.status.name}, tag={self.tag!r})" + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, RegisterPool.Register) + and self.status == other.status + and self.tag == other.tag + ) + + def __deepcopy__(self, memo: dict) -> "RegisterPool.Register": + return RegisterPool.Register(self.status, self.tag) + + def __init__( + self, + size: int, + type: "object", # rocisa.enum.RegisterType (Sgpr/Vgpr/Accvgpr/mgpr) + defaultPreventOverflow: bool, + printRP: bool = False, + ) -> None: + self._printRP: bool = bool(printRP) + self._type = type + self._defaultPreventOverflow: bool = bool(defaultPreventOverflow) + self._pool: List[RegisterPool.Register] = [ + RegisterPool.Register(RegisterPool.Status.Unavailable, "init") + for _ in range(int(size)) + ] + self._checkOutSize: Dict[int, int] = {} + self._checkOutSizeTemp: Dict[int, Tuple[int, str]] = {} + self._occupancyLimitSize: int = 0 + self._occupancyLimitMaxSize: int = 0 + + def __deepcopy__(self, memo: dict) -> "RegisterPool": + new = RegisterPool.__new__(RegisterPool) + new._printRP = self._printRP + new._type = self._type + new._defaultPreventOverflow = self._defaultPreventOverflow + new._pool = [deepcopy(r, memo) for r in self._pool] + new._checkOutSize = dict(self._checkOutSize) + new._checkOutSizeTemp = dict(self._checkOutSizeTemp) + new._occupancyLimitSize = self._occupancyLimitSize + new._occupancyLimitMaxSize = self._occupancyLimitMaxSize + return new + + def setOccupancyLimit(self, maxSize: int, size: int) -> None: + self._occupancyLimitSize = int(size) + self._occupancyLimitMaxSize = int(maxSize) + + def resetOccupancyLimit(self) -> None: + self._occupancyLimitSize = 0 + self._occupancyLimitMaxSize = 0 + + def getPool(self) -> List["RegisterPool.Register"]: + return list(self._pool) + + def addRange(self, start: int, stop: int, tag: str = "") -> str: + self.add(start, stop - start + 1, tag) + if start == stop: + return str(start) + return f"{start}-{stop}" + + def add(self, start: int, size: int, tag: str = "") -> None: + start = int(start) + size = int(size) + if self._printRP: + print(f"RP::add({start}..{start + size - 1} for '{tag}')") + + newSize = start + size + oldSize = len(self._pool) + if newSize > oldSize: + self._pool.extend( + RegisterPool.Register(RegisterPool.Status.Unavailable, tag) + for _ in range(newSize - oldSize) + ) + + for i in range(start, start + size): + s = self._pool[i].status + if s == RegisterPool.Status.Unavailable: + self._pool[i].status = RegisterPool.Status.Available + self._pool[i].tag = tag + elif s == RegisterPool.Status.Available: + print( + f"Warning: RegisterPool::add({start},{start + size - 1}) " + f"pool[{i}]({self._pool[i].tag}) already available" + ) + elif s == RegisterPool.Status.InUse: + print( + f"Warning: RegisterPool::add({start},{start + size - 1}) " + f"pool[{i}]({self._pool[i].tag}) already in use" + ) + else: + raise RuntimeError("RegisterPool::add() invalid status") + + if self._printRP: + print(self.state()) + + def addFromCheckOut(self, start: int) -> None: + start = int(start) + if start in self._checkOutSize: + size = self._checkOutSize[start] + for i in range(start, start + size): + if self._pool[i].status != RegisterPool.Status.InUse: + raise RuntimeError( + "RegisterPool::addFromCheckOut() is not in InUse state" + ) + self._pool[i].status = RegisterPool.Status.Available + self._checkOutSizeTemp[start] = (size, self._pool[start].tag) + del self._checkOutSize[start] + if self._printRP: + print( + f"RP::addFromCheckOut('{self._pool[start].tag}') " + f"@ {start} +{size}" + ) + else: + raise RuntimeError( + "RegisterPool::addFromCheckOut() but it was never checked out" + ) + + def remove(self, start: int, size: int, tag: str = "") -> None: + start = int(start) + size = int(size) + if self._printRP: + print(f"RP::remove({start}..{start + size - 1}) for {tag}") + + newSize = start + size + oldSize = len(self._pool) + if newSize > oldSize: + print( + f"Warning: RegisterPool::remove({start},{start + size - 1}) " + f"but poolSize={oldSize}" + ) + + for i in range(start, start + size): + if i >= len(self._pool): + continue + s = self._pool[i].status + if s == RegisterPool.Status.Available: + self._pool[i].status = RegisterPool.Status.Unavailable + elif s == RegisterPool.Status.Unavailable: + print( + f"Warning: RegisterPool::remove({start},{start + size - 1}) " + f"pool[{i}]({self._pool[i].tag}) already unavailable" + ) + elif s == RegisterPool.Status.InUse: + print( + f"Warning: RegisterPool::remove({start},{start + size - 1}) " + f"pool[{i}]({self._pool[i].tag}) still in use" + ) + else: + raise RuntimeError("RegisterPool::remove() invalid status") + + def removeFromCheckOut(self, start: int) -> None: + start = int(start) + if start in self._checkOutSizeTemp: + size, tag = self._checkOutSizeTemp[start] + for i in range(start, start + size): + if self._pool[i].status != RegisterPool.Status.Available: + raise RuntimeError( + "RegisterPool::removeFromCheckOut() is not in Available state" + ) + self._pool[i].status = RegisterPool.Status.InUse + self._pool[i].tag = tag + self._checkOutSize[start] = size + del self._checkOutSizeTemp[start] + if self._printRP: + print( + f"RegisterPool::removeFromCheckOut('{self._pool[start].tag}') " + f"@ {start} +{size}" + ) + else: + raise RuntimeError( + "RegisterPool::removeFromCheckOut() but it was never checked out" + ) + + def checkOut( + self, size: int, tag: str = _UNTAGGED, preventOverflow: int = -1 + ) -> int: + return self.checkOutAligned(size, 1, tag, preventOverflow) + + def checkOutAligned( + self, + size: int, + alignment: int, + tag: str = _UNTAGGED_ALIGNED, + preventOverflow: int = -1, + ) -> int: + size = int(size) + alignment = int(alignment) + + # Mirror the C++ behavior: ``preventOverflow`` is an int with -1 meaning + # "use defaultPreventOverflow". Tensile passes ``False`` explicitly + # (becomes 0) to permit growth. + if int(preventOverflow) == -1: + preventOverflow = int(self._defaultPreventOverflow) + else: + preventOverflow = int(bool(preventOverflow)) + + if size == 0: + raise ValueError("Size must be greater than 0") + + # First-fit scan with the C++ ``i += j`` skip-ahead optimization. We + # use a manual ``while`` loop because Python's ``for ... in range()`` + # cannot mutate the iterator. + found = -1 + i = 0 + n = len(self._pool) + while i < n: + if i % alignment != 0: + i += 1 + continue + if i + size > n: + i += 1 + continue + allAvailable = True + for j in range(size): + if self._pool[i + j].status != RegisterPool.Status.Available: + allAvailable = False + i += j # skip ahead past the known-unavailable slot + break + if allAvailable: + found = i + break + i += 1 + + if found != -1: + for k in range(found, found + size): + self._pool[k].status = RegisterPool.Status.InUse + self._pool[k].tag = tag + self._checkOutSize[found] = size + if self._printRP: + print( + f"RP::checkOut '{tag}' ({size},{alignment}) " + f"@ {found} avail={self.available()}" + ) + return found + + # Overflow path: grow the pool from the tail. + if preventOverflow: + raise RuntimeError( + "RegisterPool::checkOutAligned: register overflow prevented " + "by preventOverflow flag" + ) + + start = len(self._pool) + if start: + # Walk backwards while the tail is Available, claiming the run. + # NOTE: matches C++ ``for (i = size-1; i > 0; --i)`` — index 0 is + # intentionally NOT visited; this is a faithful reproduction of + # the rocisa quirk so downstream allocations stay byte-for-byte + # compatible. + i = len(self._pool) - 1 + while i > 0: + if self._pool[i].status == RegisterPool.Status.Available: + self._pool[i].tag = tag + start = i + i -= 1 + continue + else: + break + start = ((start + alignment - 1) // alignment) * alignment + + newSize = start + size + oldSize = len(self._pool) + if self._occupancyLimitSize > 0: + if ( + newSize > self._occupancyLimitSize + and newSize <= self._occupancyLimitMaxSize + ): + print( + f"newSize {newSize} OldSize {oldSize} " + f"Limit {self._occupancyLimitSize}" + ) + if self._occupancyLimitSize < newSize: + raise RuntimeError( + "RegisterPool::checkOutAligned: occupancy limit exceeded" + ) + + overflow = (newSize - oldSize) if newSize > oldSize else 0 + + for k in range(start, len(self._pool)): + self._pool[k].status = RegisterPool.Status.InUse + self._pool[k].tag = tag + + for _ in range(overflow): + if len(self._pool) < start: + # Padding pushed in to satisfy alignment on the next grow. + self._pool.append( + RegisterPool.Register(RegisterPool.Status.Available, tag) + ) + else: + self._pool.append( + RegisterPool.Register(RegisterPool.Status.InUse, tag) + ) + + self._checkOutSize[start] = size + if self._printRP: + print( + f"RP::checkOut '{tag}' ({size},{alignment}) " + f"@ {start} (overflow)" + ) + return start + + def checkOutMulti( + self, sizes: List[int], alignment: int, tags: List[str] + ) -> List[int]: + if len(sizes) != len(tags): + raise ValueError("Sizes and tags must have the same length") + + totalSize = sum(int(s) for s in sizes) + # ``checkOutAligned(..., preventOverflow=False)`` mirrors C++ which + # passes literal ``false`` here; tag is intentionally empty. + idx = self.checkOutAligned(totalSize, alignment, "", False) + + # Re-partition the lump-sum checkout into per-tag entries. + if idx in self._checkOutSize: + del self._checkOutSize[idx] + idxVec: List[int] = [] + cur = idx + for s, t in zip(sizes, tags): + s = int(s) + idxVec.append(cur) + self._checkOutSize[cur] = s + for k in range(cur, cur + s): + self._pool[k].tag = t + cur += s + return idxVec + + def initTmps( + self, initValue: int, start: int = 0, stop: int = -1 + ) -> Optional[object]: + # Real implementation requires Module + SMovB32 / VMovB32 to be + # functional. Returning ``None`` is intentional during Phase 2 — the + # callers in KernelWriterAssembly do ``module.add(initTmps(...))`` + # and the surrounding ``Module`` shim swallows ``None`` gracefully. + # TODO(Phase 4): build a real Module containing per-slot init insts. + return None + + def checkIn(self, start: int) -> None: + start = int(start) + if start in self._checkOutSize: + size = self._checkOutSize[start] + for i in range(start, start + size): + self._pool[i].status = RegisterPool.Status.Available + del self._checkOutSize[start] + if self._printRP: + print( + f"RP::checkIn('{self._pool[start].tag}') @ {start} +{size}" + ) + else: + tag = self._pool[start].tag if start < len(self._pool) else "?" + print( + f"Warning: RegisterPool::checkIn('{tag}',{start}) " + f"but it was never checked out" + ) + + def size(self) -> int: + return len(self._pool) + + def available(self) -> int: + return sum( + 1 for r in self._pool if r.status == RegisterPool.Status.Available + ) + + def availableBlock(self, blockSize: int, align: int) -> int: + if blockSize == 0: + blockSize = 1 + blocksAvail = 0 + consecAvailable = 0 + for i, reg in enumerate(self._pool): + if reg.status == RegisterPool.Status.Available: + if not (consecAvailable == 0 and i % align != 0): + consecAvailable += 1 + else: + blocksAvail += consecAvailable // blockSize + consecAvailable = 0 + blocksAvail += consecAvailable // blockSize + return blocksAvail * blockSize + + def availableBlockMaxVgpr( + self, maxVgpr: int, blockSize: int, align: int + ) -> int: + if blockSize == 0: + blockSize = 1 + blocksAvail = 0 + consecAvailable = 0 + for i in range(maxVgpr): + if i >= len(self._pool): + if not (consecAvailable == 0 and i % align != 0): + consecAvailable += 1 + else: + reg = self._pool[i] + if reg.status == RegisterPool.Status.Available: + if not (consecAvailable == 0 and i % align != 0): + consecAvailable += 1 + else: + blocksAvail += consecAvailable // blockSize + consecAvailable = 0 + blocksAvail += consecAvailable // blockSize + return blocksAvail * blockSize + + def availableBlockAtEnd(self) -> int: + cnt = 0 + for r in reversed(self._pool): + if r.status == RegisterPool.Status.Available: + cnt += 1 + else: + break + return cnt + + def checkFinalState(self) -> None: + for i, r in enumerate(self._pool): + if r.status == RegisterPool.Status.InUse: + if self._printRP: + print(self.state()) + raise RuntimeError( + f"RegisterPool::checkFinalState: temp ({i}, '{r.tag}') " + "was never checked in." + ) + if self._printRP: + print(f"total vgpr count: {self.size()}") + + def state(self) -> str: + out = "" + place_values = [1000, 100, 10, 1] + for pv_idx in range(1, len(place_values)): + place_value = place_values[pv_idx] + prior_place_value = place_values[pv_idx - 1] + if len(self._pool) >= place_value: + pvs = "" + for i in range(len(self._pool)): + if i % place_value == 0: + pvs += str((i % prior_place_value) // place_value) + else: + pvs += " " + out += pvs + "\n" + for r in self._pool: + if r.status == RegisterPool.Status.Unavailable: + out += "." + elif r.status == RegisterPool.Status.Available: + out += "|" + elif r.status == RegisterPool.Status.InUse: + out += "#" + return out + + def growPool( + self, + rangeStart: int, + rangeEnd: int, + checkOutSize: int, + comment: str = "", + ) -> None: + tl: List[int] = [] + if checkOutSize == 1: + continuous = 0 + availList: List[int] = [] + for r in self._pool: + if r.status != RegisterPool.Status.Available: + if continuous > 0: + availList.append(continuous) + continuous = 0 + else: + continuous += 1 + rangeTotal = rangeEnd - rangeStart + for numGpr in availList: + rangeTurn = numGpr // checkOutSize + if rangeTurn > 0: + tl.append(self.checkOut(checkOutSize * rangeTurn, comment)) + rangeTotal -= rangeTurn + if rangeTotal > 0: + tl.append(self.checkOut(checkOutSize * rangeTotal, comment)) + else: + for _ in range(rangeStart, rangeEnd): + tl.append(self.checkOut(checkOutSize, comment)) + for t in tl: + self.checkIn(t) + + def appendPool(self, newSize: int) -> None: + oldSize = len(self._pool) + if newSize > oldSize: + self._pool.extend( + RegisterPool.Register( + RegisterPool.Status.Available, "append pool" + ) + for _ in range(newSize - oldSize) + ) diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/stinky_interop.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/stinky_interop.py new file mode 100644 index 000000000000..a1bb91254ebb --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/stinky_interop.py @@ -0,0 +1,121 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""StinkyTofu asm-IR wiring: toStinkyTofuModule / emitAssembly entry points. + +Lowers Python code.Module via Module.to_stinky_asm -> stinkytofu.lower_logical_module. +The options dict is accepted for API parity but not yet forwarded to the C++ path. +""" + +from __future__ import annotations + +from typing import Any, List + +from . import caps as _caps +from . import code as _code + + +def _arch_to_list(arch: Any) -> List[int]: + return list(_caps.normalize_isa_key(arch)) + + +class StinkyAsmModuleWithAdapterSignature: + """Behavioural subset of stinkytofu's ``StinkyAsmModuleWithSignature`` (C++). + + Prepends the Python ``SignatureBase.toString()`` banner before the + lowered asm module's ``emitAssembly()``, matching what KernelWriter expects + after ``toStinkyTofuModule`` on the native backend. + """ + + __slots__ = ("_inner", "_signature") + + def __init__(self, inner: Any, signature: Any) -> None: + self._inner = inner + self._signature = signature + + def runOptimizationPipeline(self) -> None: + self._inner.runOptimizationPipeline() + + def emitAssembly(self) -> str: + out = "" + if self._signature is not None: + out += self._signature.toString() + # .set directives go between signature and instruction body. + set_dirs = getattr(self._inner, "getSetDirectives", None) + if set_dirs is not None: + out += set_dirs() + out += self._inner.emitAssembly() + return out + + def getName(self) -> str: + return self._inner.getName() + + def setOutputName(self, name: str) -> None: + self._inner.setOutputName(name) + + def getOutputName(self) -> str: + return self._inner.getOutputName() + + def setOutputDir(self, directory: str) -> None: + self._inner.setOutputDir(directory) + + def getOutputDir(self) -> str: + return self._inner.getOutputDir() + + def getModule(self) -> Any: + return self._inner + + +def _convert_options(options: Any) -> dict: + """Convert adaptor options dict to stinkytofu-binding-compatible dict. + + Replaces Python-shim CloneSpec objects with stinkytofu binding CloneSpec. + """ + import stinkytofu as _st # noqa: WPS433 + + out = {} + for k, v in options.items(): + if k == "CloneList" and isinstance(v, list): + out[k] = [_st.CloneSpec(name=cs.name, startLabel=cs.startLabel) for cs in v] + else: + out[k] = v + return out + + +def toStinkyTofuModule( + module: Any, + arch: Any, + moduleName: str = "", + *, + signature: Any = None, + options: Any = None, +) -> Any: + """Lower ``code.Module`` to stinkytofu asm IR (logical path). + + Args: + module: adapter ``rocisa.code.Module`` instance. + arch: ISA key (tuple / list / gfx string); normalised via ``caps``. + moduleName: forwarded as ``logical_name`` to ``LogicalModule`` / + asm module naming (when non-empty). + signature: optional ``SignatureBase``; when set, return value wraps + the binding module so ``emitAssembly()`` prepends ``signature.toString()``. + options: dict of ``ModuleOptions`` fields (same keys as native + ``toStinkyTofuModule``: CloneList, OptLevel, wavefrontSize, etc.). + Forwarded to ``lower_logical_module`` in the stinkytofu binding. + + Returns: + ``stinkytofu.StinkyAsmModule`` when ``signature`` is ``None``, else + ``StinkyAsmModuleWithAdapterSignature`` delegating pipeline/emit to + the inner module. + """ + if not isinstance(module, _code.Module): + raise TypeError( + "toStinkyTofuModule expects a rocisa.code.Module from this adapter, " + f"got {type(module).__name__!r}", + ) + arch_list = _arch_to_list(arch) + logical_name = moduleName if moduleName else None + st_options = _convert_options(options) if options else None + inner = module.to_stinky_asm(arch_list, logical_name=logical_name, options=st_options) + if signature is None: + return inner + return StinkyAsmModuleWithAdapterSignature(inner, signature) diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test.sh b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test.sh new file mode 100755 index 000000000000..7eaae82bd372 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +################################################################################ +# +# test.sh -- environment wrapper for rocisa_stinkytofu_adaptor tests. +# +# Auto-detects the built ``stinkytofu`` / ``rocisa`` binding directory by +# walking up from this script to the ``tensilelite/`` directory and +# globbing for ``*build*/tensilelite/rocisa`` (the layout cmake creates), +# then exports PYTHONPATH so both the parent unittest runner and the +# per-path subprocesses can ``import stinkytofu`` / ``import rocisa`` / +# ``import rocisa_stinkytofu_adaptor`` without further configuration. +# +# Usage +# ----- +# ./test.sh # discover & run every test in this dir +# ./test.sh test_emission_consistency # run a single file (suffix .py optional) +# ./test.sh test_emission_consistency -v # ... with extra unittest args +# ./test.sh -v # discover & run with -v +# ./test.sh --help # this message +# +# Environment overrides +# --------------------- +# PYTHONPATH if already set, the discovered binding root is *prepended*, +# not replaced, so callers can layer paths. +# STINKY_BUILD_DIR if set, skip glob discovery and use this directory +# (must contain ``stinkytofu/__init__.py`` and +# ``rocisa/__init__.py``). +# +# Exit codes +# ---------- +# 0 tests passed (or all skipped) +# 1 test failures +# 2 binding directory could not be found / validated +# +################################################################################ + +set -euo pipefail + +usage() { + sed -n '4,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' +} + +case "${1:-}" in + -h|--help) + usage + exit 0 + ;; +esac + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ---------------------------------------------------------------------------- +# Find ``tensilelite/`` ancestor of this script (max 6 levels up). +# ---------------------------------------------------------------------------- +tensilelite="" +cur="${SCRIPT_DIR}" +for _ in 1 2 3 4 5 6; do + if [[ "$(basename "${cur}")" == "tensilelite" ]]; then + tensilelite="${cur}" + break + fi + parent="$(dirname "${cur}")" + if [[ "${parent}" == "${cur}" ]]; then + break + fi + cur="${parent}" +done + +if [[ -z "${tensilelite}" ]]; then + echo "ERROR: could not locate a 'tensilelite' ancestor of ${SCRIPT_DIR}" >&2 + exit 2 +fi + +# ---------------------------------------------------------------------------- +# Locate the build's binding root (a directory containing both stinkytofu +# and rocisa Python packages). Override via ``STINKY_BUILD_DIR``. +# ---------------------------------------------------------------------------- +binding_root="" +if [[ -n "${STINKY_BUILD_DIR:-}" ]]; then + binding_root="${STINKY_BUILD_DIR}" +else + shopt -s nullglob + candidates=( + "${tensilelite}"/*build*/tensilelite/rocisa + "${tensilelite}"/build*/tensilelite/rocisa + ) + shopt -u nullglob + for cand in "${candidates[@]}"; do + if [[ -f "${cand}/stinkytofu/__init__.py" \ + && -f "${cand}/rocisa/__init__.py" ]]; then + binding_root="${cand}" + break + fi + done +fi + +if [[ -z "${binding_root}" ]]; then + echo "ERROR: no /tensilelite/rocisa with stinkytofu+rocisa was" >&2 + echo " found under ${tensilelite}." >&2 + echo " Build the bindings first, or point STINKY_BUILD_DIR at one." >&2 + exit 2 +fi + +if [[ ! -f "${binding_root}/stinkytofu/__init__.py" \ + || ! -f "${binding_root}/rocisa/__init__.py" ]]; then + echo "ERROR: STINKY_BUILD_DIR=${binding_root} does not look like a binding root" >&2 + echo " (missing stinkytofu/__init__.py or rocisa/__init__.py)." >&2 + exit 2 +fi + +# ---------------------------------------------------------------------------- +# Export PYTHONPATH for parent unittest runner and inherited subprocesses. +# ``tensilelite`` is needed for ``import rocisa_stinkytofu_adaptor``; +# ``binding_root`` for ``import stinkytofu`` / ``import rocisa``. +# ---------------------------------------------------------------------------- +new_paths="${binding_root}:${tensilelite}" +if [[ -n "${PYTHONPATH:-}" ]]; then + export PYTHONPATH="${new_paths}:${PYTHONPATH}" +else + export PYTHONPATH="${new_paths}" +fi + +cd "${SCRIPT_DIR}" + +# ---------------------------------------------------------------------------- +# Dispatch: +# - no args, or first arg starts with '-' -> unittest discover +# - first arg looks like a test file -> run that file directly +# ---------------------------------------------------------------------------- +if [[ $# -eq 0 ]]; then + exec python3 -m unittest discover -s . +fi + +first="$1" +case "${first}" in + -*) + # leading flag -> treat the whole arg list as flags to discover + exec python3 -m unittest discover -s . "$@" + ;; + *) + shift + first="${first%.py}" + if [[ ! -f "${first}.py" ]]; then + echo "ERROR: no such test file '${first}.py' in ${SCRIPT_DIR}" >&2 + echo "Available:" >&2 + for f in "${SCRIPT_DIR}"/test_*.py; do + echo " $(basename "${f%.py}")" >&2 + done + exit 2 + fi + exec python3 "${first}.py" "$@" + ;; +esac diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_asmpass.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_asmpass.py new file mode 100644 index 000000000000..6a1f2aa331d2 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_asmpass.py @@ -0,0 +1,115 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Tests for ``rocisa_stinkytofu_adaptor.asmpass`` (``rocIsaPass`` Python port).""" + +from __future__ import annotations + +import os +import sys +import unittest + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor import asmpass # noqa: E402 +from rocisa_stinkytofu_adaptor.code import KernelBody, Macro, Module # noqa: E402 +from rocisa_stinkytofu_adaptor.container import sgpr, vgpr # noqa: E402 +from rocisa_stinkytofu_adaptor.instruction import MacroInstruction, VMovB32 # noqa: E402 + + +class TestRocIsaPassOption(unittest.TestCase): + def test_defaults_match_pass_hpp(self): + o = asmpass.rocIsaPassOption() + self.assertFalse(o.insertDelayAlu) + self.assertTrue(o.removeDupFunc) + self.assertTrue(o.removeDupAssign) + self.assertTrue(o.getCycles) + self.assertEqual(o.numWaves, 0) + self.assertTrue(o.doOpt()) + + +class TestRocIsaPassResult(unittest.TestCase): + def test_default_cycles(self): + r = asmpass.rocIsaPassResult() + self.assertEqual(r.cycles, -1) + + +class TestGetActFuncNames(unittest.TestCase): + def test_module_name(self): + self.assertEqual( + asmpass.getActFuncModuleName(8, 12, 3, 4), + "ActFunc_VW8_Sgpr12_Tmp3_4", + ) + + def test_branch_name(self): + self.assertEqual( + asmpass.getActFuncBranchModuleName(), + "InsertActFuncCallAddrCalc", + ) + + +class TestMacroToInstruction(unittest.TestCase): + def test_expands_macro_call_and_drops_definition(self): + body = Module("body") + macro = Macro("FOO", ["vdst:req=v0", "ssrc:req=s0"]) + macro.add( + VMovB32( + dst=vgpr("vdst", isMacro=True), + src=sgpr("ssrc", isMacro=True), + comment="inside", + ) + ) + body.add(macro) + body.add(MacroInstruction(name="FOO", args=[vgpr(7), sgpr(2)])) + + kb = KernelBody("k") + kb.addBody(body) + kb.setGprs(256, 0, 256) + + opt = asmpass.rocIsaPassOption() + opt.removeDupFunc = False + opt.removeDupAssign = False + opt.insertDelayAlu = False + opt.getCycles = False + + res = asmpass.rocIsaPass(kb, opt) + self.assertEqual(res.cycles, -1) + + items = body.items() + self.assertEqual(len(items), 1) + inst = items[0] + self.assertIsInstance(inst, VMovB32) + self.assertEqual(inst.dst.regIdx, 7) + self.assertEqual(inst.srcs[0].regIdx, 2) + self.assertEqual(inst.comment, "inside") + + def test_get_cycles_default_zero(self): + body = Module("b") + kb = KernelBody("k") + kb.addBody(body) + kb.setGprs(256, 0, 256) + opt = asmpass.rocIsaPassOption() + opt.removeDupFunc = False + opt.removeDupAssign = False + opt.insertDelayAlu = False + res = asmpass.rocIsaPass(kb, opt) + self.assertEqual(res.cycles, 0) + + +class TestModuleExports(unittest.TestCase): + def test_public_symbols(self): + for name in ( + "rocIsaPass", + "rocIsaPassOption", + "rocIsaPassResult", + "getActFuncModuleName", + "getActFuncBranchModuleName", + ): + with self.subTest(name=name): + self.assertTrue(hasattr(asmpass, name)) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_base.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_base.py new file mode 100644 index 000000000000..9a1a39b9356b --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_base.py @@ -0,0 +1,662 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Standalone tests for ``rocisa_stinkytofu_adaptor.base``. + +Covers the ``rocIsa`` singleton state sink (``Item``, ``DummyItem``, +``isaToGfx``, forwarding shell) and the ``Item`` inheritance contract +for ``code.py`` subclasses (``TextBlock``, ``Module``). + +Run from any working directory: + + python3 projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_base.py + +Or via the wrapper: + + ./test.sh test_base +""" + +from __future__ import annotations + +import os +import pickle +import sys +import unittest + + +# --------------------------------------------------------------------------- +# Self-contained sys.path bootstrap (mirrors test_code.py). +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor import base as _base # noqa: E402 +from rocisa_stinkytofu_adaptor import ( # noqa: E402 + IsaInfo, + KernelInfo, + OutputOptions, + rocIsa, +) +from rocisa_stinkytofu_adaptor.base import DummyItem, Item # noqa: E402 +from rocisa_stinkytofu_adaptor.code import Module, TextBlock # noqa: E402 + + +# =========================================================================== +# isaToGfx +# =========================================================================== + + +class TestIsaToGfx(unittest.TestCase): + def test_tuple_isa_gfx1250(self): + self.assertEqual(_base.isaToGfx((12, 5, 0)), "gfx1250") + + def test_tuple_stepping_hex_digit(self): + self.assertEqual(_base.isaToGfx((9, 4, 10)), "gfx94a") + + def test_kernel_info_style_object(self): + class _Isa: + major = 12 + minor = 5 + stepping = 0 + + self.assertEqual(_base.isaToGfx(_Isa()), "gfx1250") + + +# =========================================================================== +# Helpers +# =========================================================================== + + +class _StateSaveRestore(unittest.TestCase): + """Snapshot/restore ``base.py`` state so tests cannot leak into one another. + + We grab the live references at setUp time and restore them in + tearDown. Tests can mutate freely; the harness puts everything back + so subsequent test files (and the rest of this file) see a clean + slate. + """ + + def setUp(self) -> None: + # ``getXxx`` accessors return the live object; we copy where + # appropriate so the saved state is independent of in-test + # mutations. + self._saved_kernel = _base.getKernel() + self._saved_current_isa = _base._current_isa + self._saved_is_init = _base._is_init + self._saved_assembler_path = _base._assembler_path + self._saved_data = dict(_base.getData()) + self._saved_vgpr_idx = dict(_base.getVgprIdx()) + self._saved_vgpr_msb = _base.getVgprMsb() + self._saved_opts = OutputOptions(_base.getOutputOptions().outputNoComment) + + def tearDown(self) -> None: + _base.setKernelInfo(self._saved_kernel) + _base._current_isa = self._saved_current_isa + _base._is_init = self._saved_is_init + _base._assembler_path = self._saved_assembler_path + # ``getData`` returns the live dict; mutate in place so anyone + # who captured a reference earlier in the process keeps seeing + # the restored state. + live_data = _base.getData() + live_data.clear() + live_data.update(self._saved_data) + _base._is_init = self._saved_is_init # re-assert (setData would flip it) + live_vgpr = _base.getVgprIdx() + live_vgpr.clear() + live_vgpr.update(self._saved_vgpr_idx) + _base.setVgprMsb(self._saved_vgpr_msb) + _base.setOutputOptions(self._saved_opts) + + +# =========================================================================== +# Class location parity (IsaInfo / KernelInfo / OutputOptions live in base.py +# to mirror rocisa/include/base.hpp) +# =========================================================================== + + +class TestClassLocationParity(unittest.TestCase): + """``rocisa::IsaInfo`` lives in ``base.hpp``; the Python mirror must too.""" + + def test_isainfo_defined_in_base_module(self): + self.assertIs(IsaInfo, _base.IsaInfo) + self.assertEqual(IsaInfo.__module__, _base.__name__) + + def test_kernelinfo_defined_in_base_module(self): + self.assertIs(KernelInfo, _base.KernelInfo) + self.assertEqual(KernelInfo.__module__, _base.__name__) + + def test_outputoptions_defined_in_base_module(self): + self.assertIs(OutputOptions, _base.OutputOptions) + self.assertEqual(OutputOptions.__module__, _base.__name__) + + def test_top_level_reexport_for_isainfo(self): + # ``from rocisa import IsaInfo`` must keep working (Tensile API). + from rocisa_stinkytofu_adaptor import IsaInfo as TopLevelIsaInfo + self.assertIs(TopLevelIsaInfo, _base.IsaInfo) + + +# =========================================================================== +# rocIsa has no instance state (everything moved to base.py) +# =========================================================================== + + +class TestRocIsaIsThinShell(unittest.TestCase): + """``rocIsa`` instance must hold no state -- state lives in ``base.py``.""" + + def test_init_does_not_set_state_fields(self): + inst = rocIsa() + # The instance must not carry the historical state fields. + # ``_vgpr_idx`` and ``_kernel_info`` ARE still readable, but only + # via @property descriptors that forward to base.py (verified + # separately below); they're not instance attributes. + instance_attrs = set(getattr(inst, "__dict__", {})) + forbidden = { + "_current_isa", + "_is_init", + "_assembler_path", + "_data", + } + self.assertEqual(instance_attrs & forbidden, set()) + + def test_singleton_returns_same_instance(self): + a = rocIsa.getInstance() + b = rocIsa.getInstance() + self.assertIs(a, b) + + +# =========================================================================== +# rocIsa methods forward to base.* (the WHOLE point of this commit) +# =========================================================================== + + +class TestRocIsaForwarding(_StateSaveRestore): + """Every ``rocIsa`` method must be an alias for the matching ``base.*``.""" + + # ---- init / isInit ---------------------------------------------------- + + def test_init_forwards(self): + # Wipe the data dict first so we can observe init repopulating it. + live_data = _base.getData() + live_data.clear() + _base._is_init = False + rocIsa.getInstance().init((12, 5, 0), "/fake/path", False) + self.assertTrue(_base.isInit()) + self.assertIn((12, 5, 0), live_data) + self.assertEqual(_base._assembler_path, "/fake/path") + + def test_isinit_forwards(self): + _base._is_init = True + self.assertTrue(rocIsa.getInstance().isInit()) + _base._is_init = False + self.assertFalse(rocIsa.getInstance().isInit()) + + # ---- getIsaInfo + get*Caps ------------------------------------------- + + def test_getisainfo_forwards(self): + info = rocIsa.getInstance().getIsaInfo((12, 5, 0)) + self.assertIsInstance(info, IsaInfo) + # Must be the SAME dict the base module cached -- callers expect + # to be able to mutate getData() and see the result via getIsaInfo. + self.assertIs(info, _base.getData()[(12, 5, 0)]) + + def test_get_caps_forward(self): + rocIsa.getInstance().init((12, 5, 0)) + self.assertEqual( + rocIsa.getInstance().getAsmCaps(), _base.getAsmCaps() + ) + self.assertEqual( + rocIsa.getInstance().getArchCaps(), _base.getArchCaps() + ) + self.assertEqual( + rocIsa.getInstance().getRegCaps(), _base.getRegCaps() + ) + self.assertEqual( + rocIsa.getInstance().getAsmBugs(), _base.getAsmBugs() + ) + + def test_get_caps_raises_without_init(self): + # Hard-reset to "no ISA selected" state. + _base._current_isa = None + with self.assertRaises(RuntimeError): + rocIsa.getInstance().getAsmCaps() + with self.assertRaises(RuntimeError): + _base.getAsmCaps() + + # ---- setKernel / getKernel ------------------------------------------- + + def test_setkernel_forwards(self): + rocIsa.getInstance().setKernel((12, 5, 0), 64) + ki = _base.getKernel() + self.assertEqual(ki.isa, (12, 5, 0)) + self.assertEqual(ki.wavefrontSize, 64) + self.assertEqual(_base._current_isa, (12, 5, 0)) + + def test_getkernel_forwards(self): + custom = KernelInfo(isa=(12, 5, 0), wavefrontSize=32) + _base.setKernelInfo(custom) + self.assertIs(rocIsa.getInstance().getKernel(), custom) + + # ---- OutputOptions ---------------------------------------------------- + + def test_outputoptions_forwards(self): + opts = OutputOptions(outputNoComment=True) + rocIsa.getInstance().setOutputOptions(opts) + self.assertIs(_base.getOutputOptions(), opts) + self.assertTrue(_base.outputNoComment()) + rocIsa.getInstance().setOutputOptions(OutputOptions(False)) + self.assertFalse(_base.outputNoComment()) + + # ---- Data dict -------------------------------------------------------- + + def test_getdata_returns_live_reference(self): + rocIsa.getInstance().init((12, 5, 0)) + d_via_rocisa = rocIsa.getInstance().getData() + d_via_base = _base.getData() + self.assertIs(d_via_rocisa, d_via_base) + + def test_setdata_replaces_dict_and_flips_isinit(self): + snap = { + (12, 5, 0): IsaInfo({"x": 1}, {"y": 2}, {"z": 3}, {"b": True}), + } + rocIsa.getInstance().setData(snap) + self.assertEqual(dict(_base.getData()), snap) + self.assertTrue(_base.isInit()) + rocIsa.getInstance().setData({}) + self.assertFalse(_base.isInit()) + + # ---- VGPR idx --------------------------------------------------------- + + def test_setvgpridx_forwards(self): + rocIsa.getInstance().setVgprIdx("ValuA", 100) + self.assertEqual(_base.getVgprIdx()["ValuA"], 100) + + def test_getvgpridx_forwards(self): + _base.setVgprIdx("ValuB", 42) + self.assertEqual(rocIsa.getInstance().getVgprIdx()["ValuB"], 42) + + def test_getvgpridx_returns_live_reference(self): + d_via_rocisa = rocIsa.getInstance().getVgprIdx() + d_via_base = _base.getVgprIdx() + self.assertIs(d_via_rocisa, d_via_base) + d_via_rocisa["ValuC"] = 7 + self.assertEqual(_base.getVgprIdx()["ValuC"], 7) + + # ---- VGPR MSB (new in this commit) ----------------------------------- + + def test_vgpr_msb_default_is_zero(self): + # Sanity: default must match C++ ``int()`` default-construct. + _base.setVgprMsb(0) + self.assertEqual(rocIsa.getInstance().getVgprMsb(), 0) + + def test_setvgprmsb_forwards(self): + rocIsa.getInstance().setVgprMsb(3) + self.assertEqual(_base.getVgprMsb(), 3) + rocIsa.getInstance().setVgprMsb(-1) + self.assertEqual(_base.getVgprMsb(), -1) + + +# =========================================================================== +# Backwards-compatible private-field shims (test_container.py style usage) +# =========================================================================== + + +class TestBackwardCompatPrivateFields(_StateSaveRestore): + """``_vgpr_idx.clear()`` and ``_kernel_info = info`` must keep working. + + Test harnesses across the repo reach into these private fields to + reset state. The @property shim on ``rocIsa`` must transparently + delegate to ``base.*`` so existing tests survive. + """ + + def test_underscore_vgpr_idx_clear(self): + rocIsa.getInstance().setVgprIdx("ValuA", 100) + self.assertEqual(_base.getVgprIdx()["ValuA"], 100) + rocIsa.getInstance()._vgpr_idx.clear() + self.assertNotIn("ValuA", _base.getVgprIdx()) + # And it must clear the SAME dict as ``base.getVgprIdx()``. + self.assertEqual(len(_base.getVgprIdx()), 0) + + def test_underscore_vgpr_idx_is_live_reference(self): + live = rocIsa.getInstance()._vgpr_idx + live["ValuD"] = 8 + self.assertEqual(_base.getVgprIdx()["ValuD"], 8) + + def test_underscore_kernel_info_read(self): + custom = KernelInfo(isa=(12, 5, 0), wavefrontSize=64) + _base.setKernelInfo(custom) + self.assertIs(rocIsa.getInstance()._kernel_info, custom) + + def test_underscore_kernel_info_assignment_restore_unset(self): + # Mimic the test_container.py:1684 pattern -- restoring a saved + # KernelInfo where ``isa is None`` (the initial unset state). + unset = KernelInfo() # isa=None, wavefrontSize=0 + rocIsa.getInstance()._kernel_info = unset + self.assertIs(_base.getKernel(), unset) + self.assertIsNone(_base.getKernel().isa) + + +# =========================================================================== +# ParallelMap2 worker pattern (pickle round-trip of data + output options) +# =========================================================================== + + +class TestParallelMap2RoundTrip(_StateSaveRestore): + """Tensile spawns workers via ``ParallelMap2`` and re-hydrates state with + ``rocIsa.getInstance().setData(pickled_data)`` / + ``.setOutputOptions(pickled_opts)``. Verify both round-trip cleanly.""" + + def test_data_pickle_roundtrip(self): + rocIsa.getInstance().init((12, 5, 0)) + snapshot = rocIsa.getInstance().getData() + # Round-trip the snapshot via pickle (the actual worker boundary). + pickled = pickle.dumps(snapshot) + revived = pickle.loads(pickled) + # Wipe and re-hydrate from the pickle. + rocIsa.getInstance().setData({}) + rocIsa.getInstance().setData(revived) + # The active caps must come back identical. + info = rocIsa.getInstance().getIsaInfo((12, 5, 0)) + self.assertEqual(info.asmCaps, snapshot[(12, 5, 0)].asmCaps) + self.assertEqual(info.archCaps, snapshot[(12, 5, 0)].archCaps) + self.assertEqual(info.regCaps, snapshot[(12, 5, 0)].regCaps) + self.assertEqual(info.asmBugs, snapshot[(12, 5, 0)].asmBugs) + + def test_outputoptions_pickle_roundtrip(self): + opts = OutputOptions(outputNoComment=True) + revived = pickle.loads(pickle.dumps(opts)) + rocIsa.getInstance().setOutputOptions(revived) + self.assertTrue(_base.outputNoComment()) + # Round-tripping again preserves the flag bit-for-bit. + again = pickle.loads(pickle.dumps(_base.getOutputOptions())) + self.assertTrue(again.outputNoComment) + + def test_kernelinfo_pickle_roundtrip(self): + ki = KernelInfo(isa=(12, 5, 0), wavefrontSize=32) + revived = pickle.loads(pickle.dumps(ki)) + self.assertEqual(revived.isa, (12, 5, 0)) + self.assertEqual(revived.wavefrontSize, 32) + + def test_isainfo_pickle_roundtrip(self): + info = IsaInfo({"a": 1}, {"b": 2}, {"c": 3}, {"d": True}) + revived = pickle.loads(pickle.dumps(info)) + self.assertEqual(revived.asmCaps, info.asmCaps) + self.assertEqual(revived.archCaps, info.archCaps) + self.assertEqual(revived.regCaps, info.regCaps) + self.assertEqual(revived.asmBugs, info.asmBugs) + + +# =========================================================================== +# init() is idempotent for the same ISA +# =========================================================================== + + +class TestInitIdempotent(_StateSaveRestore): + """``init(arch)`` called twice with the same ISA must not re-populate.""" + + def test_init_same_isa_keeps_same_isainfo_object(self): + rocIsa.getInstance().init((12, 5, 0)) + first = _base.getData()[(12, 5, 0)] + rocIsa.getInstance().init((12, 5, 0), "/different/path") + second = _base.getData()[(12, 5, 0)] + # Same object identity -- ``init`` short-circuited the cap build. + self.assertIs(first, second) + # But the assembler path got updated regardless (mirrors C++, + # which assigns ``m_assemblerPath`` outside the cache-check + # branch... wait, actually C++ DOES update isainfo only inside + # the if-not-cached branch; assemblerPath isn't a field of + # rocIsa in C++. The Python adaptor's _assembler_path is updated + # unconditionally, which is fine.) + self.assertEqual(_base._assembler_path, "/different/path") + + +# =========================================================================== +# Wiring sanity: base accessors are the source of truth +# =========================================================================== + + +class TestBaseAccessorsAreSourceOfTruth(_StateSaveRestore): + """Verify mutations via ``base.*`` are visible via ``rocIsa.*`` and vice + versa -- they must operate on the same backing storage.""" + + def test_setvgpridx_via_base_visible_via_rocisa(self): + _base.setVgprIdx("X", 99) + self.assertEqual(rocIsa.getInstance().getVgprIdx()["X"], 99) + + def test_setvgpridx_via_rocisa_visible_via_base(self): + rocIsa.getInstance().setVgprIdx("Y", 77) + self.assertEqual(_base.getVgprIdx()["Y"], 77) + + def test_setkernel_via_base_visible_via_rocisa(self): + _base.setKernel((12, 5, 0), 32) + ki = rocIsa.getInstance().getKernel() + self.assertEqual(ki.isa, (12, 5, 0)) + self.assertEqual(ki.wavefrontSize, 32) + + def test_setkernel_via_rocisa_visible_via_base(self): + rocIsa.getInstance().setKernel((12, 5, 0), 64) + self.assertEqual(_base.getKernel().wavefrontSize, 64) + + def test_setvgprmsb_via_base_visible_via_rocisa(self): + _base.setVgprMsb(5) + self.assertEqual(rocIsa.getInstance().getVgprMsb(), 5) + + def test_setvgprmsb_via_rocisa_visible_via_base(self): + rocIsa.getInstance().setVgprMsb(7) + self.assertEqual(_base.getVgprMsb(), 7) + + +# =========================================================================== +# Item base class -- regular (concrete) class with default behaviour. +# =========================================================================== +# +# Mirror of ``rocisa::Item`` (base.hpp:220-297). The whole point of +# Commit Y is that this class exists as a real Python class so that +# Module / TextBlock / dummy nodes participate in a proper polymorphic +# hierarchy (``isinstance(x, Item)``), and that the default methods +# (toString / prettyPrint / countType / countExactType / clone / +# 7 cap-proxies) are inherited as-written rather than being silently +# overridden by the ``_dummy.py`` ``__getattr__`` no-op. + + +class TestItemConstruction(unittest.TestCase): + """Item is a regular (concrete) class -- the C++ ``Item`` is also + concrete (``Item("foo")`` compiles and ``.toString()`` returns + "foo"), so we mirror that. Only ``clone()`` is semi-abstract + (raises by default).""" + + def test_item_is_regular_class(self): + # Not an ABC -- ``Item()`` must succeed without an + # implementation of any "abstract" method. + it = Item() + self.assertIsInstance(it, Item) + self.assertEqual(it.name, "") + self.assertIsNone(it.parent) + + def test_item_named_construction(self): + it = Item("foo") + self.assertEqual(it.name, "foo") + + def test_item_has_slots(self): + # ``__slots__ = ("name", "parent")`` so no __dict__ -- catches + # accidental future regression where someone removes slots. + self.assertFalse(hasattr(Item(), "__dict__")) + + +class TestItemInheritanceShape(unittest.TestCase): + """``isinstance(x, Item)`` parity for ``code.py`` subclasses. + + Mirror of rocisa C++ ``TextBlock`` / ``Module`` inheriting from + ``Item`` (code.hpp:133 / code.hpp:330). Non-recursive + ``countType`` / ``countExactType`` on bare ``Item`` are tested + here; recursive ``Module`` overrides live in ``test_code.py``. + """ + + def test_textblock_isinstance_item(self): + self.assertIsInstance(TextBlock("x"), Item) + + def test_module_isinstance_item(self): + self.assertIsInstance(Module("k"), Item) + + def test_textblock_name_and_parent_inherited_from_item(self): + # ``__slots__`` for TextBlock are ``("text",)`` only -- name / + # parent live on Item. The class must NOT redeclare them or + # Python raises TypeError at class-creation time, so reaching + # this test at all means the slot composition is correct; + # the assertions below pin the runtime values. + tb = TextBlock("hello") + self.assertEqual(tb.name, "hello") + self.assertIsNone(tb.parent) + + def test_module_name_and_parent_inherited_from_item(self): + m = Module("kernel") + self.assertEqual(m.name, "kernel") + self.assertIsNone(m.parent) + + def test_module_slots_do_not_redeclare_item_slots(self): + # Catch accidental future regression -- if Module's __slots__ + # ever re-adds "name" or "parent" the class itself fails to + # build (Python raises TypeError on slot conflict). We make + # the test explicit by checking the declared slot tuple. + self.assertNotIn("name", Module.__slots__) + self.assertNotIn("parent", Module.__slots__) + + def test_textblock_slots_do_not_redeclare_item_slots(self): + self.assertNotIn("name", TextBlock.__slots__) + self.assertNotIn("parent", TextBlock.__slots__) + + +class TestItemDefaultMethods(unittest.TestCase): + """Default ``toString`` / ``prettyPrint`` / ``__str__`` / counters + on the bare ``Item`` -- subclasses (Module / TextBlock) override + them but the base behaviour is what dummies fall back to.""" + + def test_toString_returns_name(self): + # rocisa C++ ``Item::toString`` (base.hpp:282-285) returns + # the stored name verbatim. + self.assertEqual(Item("hello").toString(), "hello") + + def test_str_delegates_to_toString(self): + # ``__str__`` is bound to ``toString`` so subclass overrides + # propagate -- ``str(Module(...))`` calls ``Module.toString`` + # automatically because of this binding (mirror of nanobind's + # ``.def("__str__", &Item::toString)`` pattern). + self.assertEqual(str(Item("bar")), "bar") + + def test_prettyPrint_default_format(self): + # rocisa C++ format (base.hpp:287-293): + # ``{indent}{className} {toString()}`` -- with the trailing + # space and NO newline. The trailing space is intentional + # (matches the C++ ``<< " " << toString()``). + self.assertEqual(Item("x").prettyPrint(" "), " Item x") + self.assertEqual(Item().prettyPrint(), "Item ") + + def test_countType_self_match(self): + # ``isinstance(self, Item)`` is True for any Item, so the + # default ``countType(Item)`` returns 1. + self.assertEqual(Item().countType(Item), 1) + + def test_countType_unrelated_zero(self): + self.assertEqual(Item().countType(str), 0) + + def test_countExactType_match(self): + self.assertEqual(Item().countExactType(Item), 1) + + def test_countExactType_subclass_does_not_match(self): + # ``type(self) is type_obj`` is strict equality, so a + # DummyItem instance does NOT count as ``Item`` here even + # though it ``isinstance``-matches. + self.assertEqual(DummyItem().countExactType(Item), 0) + # ... but countType (isinstance) DOES match the base class. + self.assertEqual(DummyItem().countType(Item), 0) + # NB the second line above tests DummyItem's hardcoded 0 + # override (mirror of rocisa base.hpp:306-309), not the + # generic Item.countType. + + def test_clone_raises_by_default(self): + # Mirror of rocisa C++ ``Item::clone`` which throws + # ``std::runtime_error("clone() not implemented")``. Python + # callers wanting a copy use ``copy.deepcopy`` (which Module + # / TextBlock implement via ``__deepcopy__``). + with self.assertRaises(NotImplementedError): + Item().clone() + + +class TestItemCapabilityProxies(_StateSaveRestore): + """The seven cap-proxy methods on Item forward to the module-level + accessors in ``base.py``. This lets any Item subclass read caps + without knowing about the ``rocIsa`` singleton at all. + + Cap accessors (``getAsmCaps`` / ``getArchCaps`` / ``getRegCaps`` / + ``getAsmBugs``) need an active ISA to resolve, so the per-test + setUp pumps ``init((12, 5, 0), "", False)`` -- the same wakeup + sequence the package-level ``rocIsa.init`` uses in production. + The ``_StateSaveRestore`` superclass undoes any state mutation in + tearDown so the rest of the suite sees a clean slate.""" + + def setUp(self) -> None: + super().setUp() + # Pump ISA state so the four cap-table accessors stop raising + # the "init() or setKernel() must be called first" RuntimeError. + # gfx1250 is the only ISA the adapter has cap tables for today. + _base.init((12, 5, 0), "", False) + + def test_getAsmCaps_forwards_to_base(self): + # Both call paths must hit the same dict object so a mutation + # in either place is visible to the other. + self.assertIs(Item().getAsmCaps(), _base.getAsmCaps()) + + def test_getArchCaps_forwards_to_base(self): + self.assertIs(Item().getArchCaps(), _base.getArchCaps()) + + def test_getRegCaps_forwards_to_base(self): + self.assertIs(Item().getRegCaps(), _base.getRegCaps()) + + def test_getAsmBugs_forwards_to_base(self): + self.assertIs(Item().getAsmBugs(), _base.getAsmBugs()) + + def test_getVgprIdx_forwards_to_base(self): + # ``_base.getVgprIdx()`` returns the live dict; mutations + # propagate. + _base.setVgprIdx("CAP_PROXY_K", 42) + self.assertEqual(Item().getVgprIdx()["CAP_PROXY_K"], 42) + + def test_getVgprMsb_forwards_to_base(self): + _base.setVgprMsb(13) + self.assertEqual(Item().getVgprMsb(), 13) + + def test_kernel_forwards_to_base(self): + _base.setKernel((12, 5, 0), 32) + ki = Item().kernel() + self.assertEqual(ki.isa, (12, 5, 0)) + self.assertEqual(ki.wavefrontSize, 32) + + +class TestDummyItem(unittest.TestCase): + """``DummyItem`` is the inert sentinel KernelWriter sticks into + Module trees as a marker without polluting countType totals. + Mirror of rocisa C++ ``DummyItem`` (base.hpp:299-310).""" + + def test_dummyitem_isinstance_item(self): + self.assertIsInstance(DummyItem(), Item) + + def test_dummyitem_no_ctor_args(self): + # rocisa C++ ctor takes no args (just ``Item("")``); the + # Python adapter mirrors that. + d = DummyItem() + self.assertEqual(d.name, "") + self.assertIsNone(d.parent) + + def test_dummyitem_countType_always_zero(self): + # Hardcoded 0 regardless of queried type -- mirror of + # rocisa C++ override (base.hpp:306-309). Matters because + # KernelWriter uses DummyItem as a "neutral" marker that + # tree-walks must skip; counting it would corrupt + # instruction tallies. + self.assertEqual(DummyItem().countType(DummyItem), 0) + self.assertEqual(DummyItem().countType(Item), 0) + self.assertEqual(DummyItem().countType(object), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_caps.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_caps.py new file mode 100644 index 000000000000..dd928393b112 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_caps.py @@ -0,0 +1,95 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Tests for caps and StinkyTofu arch-probe forwarding.""" + +import os +import sys +import unittest + +_PKG_PARENT = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir) +) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +import rocisa_stinkytofu_adaptor as rocisa # noqa: E402 +from rocisa_stinkytofu_adaptor import caps # noqa: E402 + +_GFX1250 = (12, 5, 0) + + +def _stinkytofu_available() -> bool: + try: + import stinkytofu # noqa: F401 + return True + except ImportError: + return False + + +@unittest.skipUnless(_stinkytofu_available(), "stinkytofu binding not on PYTHONPATH") +class TestGetCapsDynamic(unittest.TestCase): + def test_gfx1250_shape(self): + asm, arch, reg, bugs = caps.getCaps(_GFX1250) + self.assertIsInstance(asm, dict) + self.assertIsInstance(arch, dict) + self.assertIsInstance(reg, dict) + self.assertIsInstance(bugs, dict) + self.assertIn("SupportedISA", asm) + self.assertIn("HasWave32", arch) + self.assertIn("MaxVgpr", reg) + + def test_gfx1250_modifier_caps_present(self): + asm, _, _, _ = caps.getCaps(_GFX1250) + for key in ("HasTHModifier", "HasNVModifier", "HasGlobalPrefetch", "HasXcnt"): + self.assertIn(key, asm) + self.assertIn(asm[key], (0, 1)) + + def test_matches_stinkytofu_api(self): + import stinkytofu + + direct = stinkytofu.getHardwareCaps(list(_GFX1250)) + via_caps = caps.getCaps(_GFX1250) + # getCaps() may inject supplemental caps (e.g. HasWMMA_AccImmZero) + # that the binding does not yet probe; verify the binding's own caps + # are a subset of the returned dict. + self.assertTrue( + dict(direct["asmCaps"]).items() <= via_caps[0].items(), + "getCaps asmCaps must be a superset of stinkytofu raw caps") + self.assertEqual(via_caps[1], dict(direct["archCaps"])) + self.assertTrue( + dict(direct["regCaps"]).items() <= via_caps[2].items(), + "getCaps regCaps must be a superset of stinkytofu raw caps") + self.assertEqual(via_caps[3], dict(direct["asmBugs"])) + + def test_returns_fresh_copies(self): + asm1, _, _, _ = caps.getCaps(_GFX1250) + asm2, _, _, _ = caps.getCaps(_GFX1250) + asm1["HasSCOPEModifier"] = 0 + self.assertNotEqual(asm1["HasSCOPEModifier"], asm2["HasSCOPEModifier"]) + + +@unittest.skipUnless(_stinkytofu_available(), "stinkytofu binding not on PYTHONPATH") +class TestStinkyTofuArchProbes(unittest.TestCase): + def test_has_backend_when_binding_present(self): + self.assertTrue(rocisa.hasStinkyTofuBackend()) + + def test_gfx1250_supported(self): + self.assertTrue(rocisa.isSupportedByStinkyTofu(_GFX1250)) + self.assertTrue(rocisa.isSupportedByStinkyTofu("gfx1250")) + + def test_matches_stinkytofu_api(self): + import stinkytofu + + self.assertEqual( + rocisa.isSupportedByStinkyTofu(_GFX1250), + stinkytofu.isSupportedByStinkyTofu(list(_GFX1250)), + ) + self.assertEqual( + rocisa.getRegisteredArchKeys(), + stinkytofu.getRegisteredArchKeys(), + ) + self.assertIn("gfx1250", rocisa.getRegisteredArchKeys()) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_code.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_code.py new file mode 100644 index 000000000000..c18605f1d4d0 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_code.py @@ -0,0 +1,3465 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Standalone tests for ``rocisa_stinkytofu_adaptor.code``. + +1:1 mirror of every export in ``code.py``: TextBlock, Module, +ValueIf/ElseIf/Endif, ValueSet, RegSet, Label, StructuredModule, +Macro, Signature*, KernelBody, BitfieldUnion, and ``to_stinky_asm``. + +``Item`` inheritance shape for TextBlock / Module lives in +``test_base.py``. Cross-backend emit parity lives in +``test_emission_consistency.py``. + +Run from any working directory: + + python3 projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_code.py + +Or via the wrapper: + + ./test.sh test_code + +The ``TestToStinkyAsm`` block is gated on a built ``stinkytofu`` +binding and exercises the full left-path lowering pipeline (Module tree +-> ``LogicalModule`` -> ``StinkyAsmModule.emitAssembly``). +""" + +from __future__ import annotations + +import copy +import os +import pickle +import sys +import unittest + +# --------------------------------------------------------------------------- +# Self-contained sys.path bootstrap (mirrors test_container.py). +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor.base import Item # noqa: E402 +from rocisa_stinkytofu_adaptor.enum import SignatureValueKind as SVK # noqa: E402 +from rocisa_stinkytofu_adaptor.code import ( # noqa: E402 + BitfieldUnion, + KernelBody, + Label, + Macro, + Module, + RegSet, + SignatureBase, + SignatureCodeMeta, + StructuredModule, + TextBlock, + ValueElseIf, + ValueEndif, + ValueIf, + ValueSet, +) + + +# --------------------------------------------------------------------------- +# Shared fixtures -- isolate rocIsa singleton side effects across tests. +# --------------------------------------------------------------------------- + + +class _VgprIdxIsolation: + """Snapshots / restores ``_vgpr_idx`` and ``HasVgprMSB`` for RegSet.""" + + def setUp(self) -> None: + from rocisa_stinkytofu_adaptor import base as _base # noqa: WPS433 + self._base = _base + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + rocIsa.getInstance().init((12, 5, 0)) + self._caps = _base.getAsmCaps() + self._saved_hasvgprmsb = self._caps.get("HasVgprMSB", 0) + self._saved_vgpr_idx = dict(_base.getVgprIdx()) + + def tearDown(self) -> None: + if self._saved_hasvgprmsb == 0: + self._caps.pop("HasVgprMSB", None) + else: + self._caps["HasVgprMSB"] = self._saved_hasvgprmsb + live = self._base.getVgprIdx() + live.clear() + live.update(self._saved_vgpr_idx) + + +class _VgprMsbIsolation: + """Snapshots / restores ``HasVgprMSB`` and ``_vgpr_msb`` for Label.""" + + def setUp(self) -> None: + from rocisa_stinkytofu_adaptor import base as _base # noqa: WPS433 + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + self._base = _base + rocIsa.getInstance().init((12, 5, 0)) + self._caps = _base.getAsmCaps() + self._saved_hasvgprmsb = self._caps.get("HasVgprMSB", 0) + self._saved_vgpr_msb = _base.getVgprMsb() + + def tearDown(self) -> None: + if self._saved_hasvgprmsb == 0: + self._caps.pop("HasVgprMSB", None) + else: + self._caps["HasVgprMSB"] = self._saved_hasvgprmsb + self._base.setVgprMsb(self._saved_vgpr_msb) + + +class _SignatureKernelSetup(unittest.TestCase): + """Pump ISA + wavefront so signature emitters can read kernel().""" + + def setUp(self) -> None: + from rocisa_stinkytofu_adaptor import base as _base # noqa: WPS433 + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + rocIsa.getInstance().init((12, 5, 0)) + _base.setKernel((12, 5, 0), 64) + + +# =========================================================================== +# TextBlock -- raw text leaf. +# =========================================================================== + + +class TestTextBlockConstruction(unittest.TestCase): + def test_default_text_empty(self): + tb = TextBlock() + self.assertEqual(tb.text, "") + # rocisa C++: ``TextBlock(text) : Item(text)`` -- default ``text=""`` + # produces ``name == ""`` because Item's ctor stores ``name = text``. + # The common ``addComment / addSpaceLine`` path goes through this + # branch via ``TextBlock(formatted_text)`` (non-empty), so ``name`` + # will normally equal the formatted string -- see + # ``test_name_mirrors_text`` below. + self.assertEqual(tb.name, "") + self.assertIsNone(tb.parent) + + def test_text_arg_stored(self): + tb = TextBlock("hello\n") + self.assertEqual(tb.text, "hello\n") + + def test_name_mirrors_text(self): + # rocisa parity (code.hpp:137-141): the Item(name) base ctor + # stores ``name = text``, so a non-empty TextBlock has its text + # as its name. KernelWriter does NOT rely on TextBlock names + # for ``removeItemsByName`` (it targets Module / Label names), + # but the adapter must still mirror the field exactly so any + # downstream tooling that inspects ``tb.name`` sees the same + # value across backends. + tb = TextBlock("// foo\n") + self.assertEqual(tb.name, "// foo\n") + + def test_name_mirrors_text_for_comment_textblocks(self): + # Module.addComment("hello") creates TextBlock("// hello\n"). + # After P1 (name == text), that TextBlock now has its full + # formatted text as its name. This is rocisa-faithful; documented + # here so reviewers can sanity-check the diff. + from rocisa_stinkytofu_adaptor.code import Module as _Module # noqa: WPS433 + m = _Module() + m.addComment("hello") + self.assertEqual(m.itemList[0].name, "// hello\n") + + def test_str_and_toString_match(self): + tb = TextBlock("// foo\n") + self.assertEqual(str(tb), "// foo\n") + self.assertEqual(tb.toString(), "// foo\n") + + def test_str_delegates_to_toString(self): + # ``__str__`` must route through ``toString`` so that any future + # gating on ``outputNoComment`` (or future production toggles) + # automatically applies to ``str(tb)`` too -- mirrors rocisa + # binding ``__str__ -> toString`` (code.cpp:142). + class _TBSub(TextBlock): + def toString(self): # noqa: D401 + return "<>" + + self.assertEqual(str(_TBSub("hi")), "<>") + + def test_repr_shows_text(self): + # Debug-only; exact format isn't pinned but ``text`` must appear. + r = repr(TextBlock("abc")) + self.assertIn("TextBlock", r) + self.assertIn("abc", r) + + def test_deepcopy_independent(self): + tb = TextBlock("a") + c = copy.deepcopy(tb) + c.text = "b" + self.assertEqual(tb.text, "a") + self.assertEqual(c.text, "b") + + def test_deepcopy_preserves_renamed_name(self): + # rocisa __deepcopy__ (code.cpp:143-148) rebuilds via the ctor + # AND patches ``name`` separately so renamed TextBlocks survive + # the clone. Verify the adapter matches. + tb = TextBlock("a") + tb.name = "renamed" + c = copy.deepcopy(tb) + self.assertEqual(c.name, "renamed") + self.assertEqual(c.text, "a") + + def test_prettyPrint_returns_string(self): + # Defensive: Module.prettyPrint joins children's prettyPrint output; + # a non-str return would explode the join. + self.assertIsInstance(TextBlock("x").prettyPrint(), str) + + +# =========================================================================== +# TextBlock prettyPrint -- text content visibility + rocisa-shape parity. +# =========================================================================== + + +class TestTextBlockPrettyPrint(unittest.TestCase): + """``TextBlock.prettyPrint`` inherits ``rocisa::Item::prettyPrint``:: + + return indent + className + " " + toString(); // base.hpp:287-293 + + The line carries the text content and has NO trailing newline. + """ + + def test_pretty_print_includes_text(self): + # Regression for P3 -- the old impl dropped ``text`` and just + # emitted "{indent}TextBlock\n", silently hiding contents in + # any debug dump that walked a Module tree. + self.assertEqual(TextBlock("abc").prettyPrint(), "TextBlock abc") + + def test_pretty_print_with_indent(self): + self.assertEqual(TextBlock("x").prettyPrint("|--"), "|--TextBlock x") + + def test_pretty_print_no_trailing_newline(self): + # Item::prettyPrint base does NOT append \n -- Module's prettyPrint + # concatenates children verbatim and is responsible for the line + # break (it adds \n only on its own header; child Modules add their + # own header \n; leaf items either include \n in toString() or + # don't, just like rocisa). + self.assertFalse(TextBlock("x").prettyPrint().endswith("\n")) + + def test_pretty_print_reflects_outputNoComment_suppression(self): + # Item::prettyPrint calls toString(); when ``outputNoComment`` is + # set, TextBlock.toString() returns "" -- so prettyPrint of a + # TextBlock degrades to ``"{indent}TextBlock "`` (trailing space + # is what Item::prettyPrint emits; matches rocisa C++). + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + opts = rocIsa.getInstance().getOutputOptions() + saved = opts.outputNoComment + try: + opts.outputNoComment = True + self.assertEqual(TextBlock("anything").prettyPrint(), "TextBlock ") + finally: + opts.outputNoComment = saved + + +# =========================================================================== +# TextBlock.toString -- outputNoComment production-build suppression. +# =========================================================================== + + +class TestTextBlockOutputNoComment(unittest.TestCase): + """``rocIsa.outputNoComment=True`` blanket-suppresses TextBlock text. + + Mirrors rocisa code.hpp:154-159 -- the flag suppresses EVERY TextBlock + payload (comments AND inline-asm) so production builds emit no human- + readable annotations. + """ + + def setUp(self): + # Snapshot the flag so we can restore after each test, regardless + # of pass/fail. Important because the singleton is process-wide. + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + self._opts = rocIsa.getInstance().getOutputOptions() + self._saved = self._opts.outputNoComment + + def tearDown(self): + self._opts.outputNoComment = self._saved + + def test_default_does_not_suppress(self): + self._opts.outputNoComment = False + self.assertEqual(TextBlock("// comment\n").toString(), "// comment\n") + self.assertEqual(str(TextBlock("// comment\n")), "// comment\n") + + def test_outputNoComment_blanks_text(self): + # Regression for P2 -- the old toString returned ``self.text`` + # unconditionally, so production-build kernels carried every + # comment & inline-asm fragment in the emitted output. + self._opts.outputNoComment = True + self.assertEqual(TextBlock("// comment\n").toString(), "") + self.assertEqual(str(TextBlock("// comment\n")), "") + + def test_outputNoComment_suppresses_inline_asm_too(self): + # Not just comments: rocisa's gate is "blanket suppress" -- inline + # asm fragments (which KernelWriter occasionally injects as + # TextBlocks) get stripped just the same. We must match. + self._opts.outputNoComment = True + self.assertEqual(TextBlock("v_mov_b32 v0, v1").toString(), "") + + def test_outputNoComment_module_str_concats_empty(self): + # Module.toString concatenates ``str(child)`` -- with the flag + # on, every TextBlock collapses to "" so the whole Module string + # contains only non-TextBlock items. + from rocisa_stinkytofu_adaptor.code import Module as _Module # noqa: WPS433 + m = _Module() + m.add(TextBlock("// a\n")) + m.add(TextBlock("// b\n")) + self._opts.outputNoComment = True + self.assertEqual(str(m), "") + + def test_outputNoComment_round_trip_via_setOutputOptions(self): + # rocIsa.setOutputOptions(opts) is how Tensile ships the flag to + # ParallelMap2 workers; verify the helper picks the new value up. + from rocisa_stinkytofu_adaptor import rocIsa, OutputOptions # noqa: WPS433 + rocIsa.getInstance().setOutputOptions(OutputOptions(outputNoComment=True)) + try: + self.assertEqual(TextBlock("x").toString(), "") + finally: + rocIsa.getInstance().setOutputOptions( + OutputOptions(outputNoComment=self._saved) + ) + # Re-snapshot because we just swapped the *instance*. + self._opts = rocIsa.getInstance().getOutputOptions() + + +# =========================================================================== +# TextBlock pickle -- (name, text) round-trip mirroring rocisa C++. +# =========================================================================== + + +class TestTextBlockPickle(unittest.TestCase): + """rocisa code.cpp:149-154 -- ``(name, text)`` tuple round-trip.""" + + def test_pickle_round_trip_default(self): + tb = TextBlock("hello\n") + # name defaults to text after P1, so both are "hello\n". + clone = pickle.loads(pickle.dumps(tb)) + self.assertEqual(clone.name, "hello\n") + self.assertEqual(clone.text, "hello\n") + self.assertIsNone(clone.parent) + self.assertIsNot(clone, tb) + + def test_pickle_round_trip_after_rename(self): + # rocisa __setstate__ rebuilds via TextBlock(text), then patches + # ``name``. Verify the two-step actually preserves a name that + # was changed post-construction. + tb = TextBlock("payload") + tb.name = "label-X" + clone = pickle.loads(pickle.dumps(tb)) + self.assertEqual(clone.name, "label-X") + self.assertEqual(clone.text, "payload") + + def test_pickle_protocol_2_and_5(self): + # Sanity: KernelWriter / ParallelMap2 don't pin a protocol, so + # cover the common ones (2 = py3 default-ish, 5 = py3.8+ buffer + # protocol used by multiprocessing fast path). + tb = TextBlock("v_mov_b32 v0, v1") + for proto in (2, pickle.HIGHEST_PROTOCOL): + clone = pickle.loads(pickle.dumps(tb, protocol=proto)) + self.assertEqual(clone.text, "v_mov_b32 v0, v1") + self.assertEqual(clone.name, "v_mov_b32 v0, v1") + + def test_getstate_returns_name_text_tuple(self): + # Pin the wire format so any future shape change is intentional. + tb = TextBlock("payload") + tb.name = "lbl" + self.assertEqual(tb.__getstate__(), ("lbl", "payload")) + + +# =========================================================================== +# Module construction & basic attributes. +# =========================================================================== + + +class TestModuleConstruction(unittest.TestCase): + def test_default_name_empty(self): + m = Module() + self.assertEqual(m.name, "") + self.assertEqual(m.itemList, []) + self.assertIsNone(m.parent) + self.assertIsNone(m.tempVgpr) + self.assertFalse(m.isNoOpt()) + + def test_named_ctor(self): + m = Module("TopModule") + self.assertEqual(m.name, "TopModule") + + def test_itemsSize_zero_on_construction(self): + self.assertEqual(Module().itemsSize(), 0) + self.assertEqual(Module().count(), 0) + + +# =========================================================================== +# add / addItems -- parent rebind, None tolerance, positional insert. +# =========================================================================== + + +class TestModuleAdd(unittest.TestCase): + def test_add_returns_item(self): + # rocisa returns the added item to enable one-liners + # like ``foo = mod.add(SomeInstr(...))``. + m = Module() + tb = TextBlock("x") + result = m.add(tb) + self.assertIs(result, tb) + self.assertEqual(m.itemList, [tb]) + + def test_add_None_silently_ignored(self): + # rocisa's ``if(item)`` guard; KernelWriter passes optional values + # directly to add() and expects None to drop on the floor. + m = Module() + result = m.add(None) + self.assertIsNone(result) + self.assertEqual(m.itemList, []) + + def test_add_sets_parent(self): + m = Module() + tb = TextBlock("a") + self.assertIsNone(tb.parent) + m.add(tb) + self.assertIs(tb.parent, m) + + def test_add_to_end_by_default(self): + m = Module() + a, b, c = TextBlock("a"), TextBlock("b"), TextBlock("c") + m.add(a) + m.add(b) + m.add(c) + self.assertEqual(m.itemList, [a, b, c]) + + def test_add_at_index(self): + # ``pos`` mirrors rocisa's ``itemList.insert(begin() + pos, item)``. + m = Module() + a, b, c = TextBlock("a"), TextBlock("b"), TextBlock("c") + m.add(a) + m.add(c) + m.add(b, pos=1) + self.assertEqual(m.itemList, [a, b, c]) + + def test_add_at_pos_zero(self): + m = Module() + a, b = TextBlock("a"), TextBlock("b") + m.add(a) + m.add(b, pos=0) + self.assertEqual(m.itemList, [b, a]) + + def test_addItems_extends(self): + m = Module() + items = [TextBlock("a"), TextBlock("b"), TextBlock("c")] + m.addItems(items) + self.assertEqual(m.itemList, items) + # All children reparented. + for it in items: + self.assertIs(it.parent, m) + + def test_addItems_iterable_supported(self): + # ``addItems`` takes any iterable, not just list (rocisa parity). + m = Module() + m.addItems(iter([TextBlock("a"), TextBlock("b")])) + self.assertEqual(m.itemsSize(), 2) + + def test_add_item_without_parent_attr_tolerated(self): + # Some leaf shims do not expose ``parent`` (e.g. immutable value + # objects). add() must not raise on them. + class _NoParent: + __slots__ = () + + def __str__(self): + return "" + + np = _NoParent() + m = Module() + m.add(np) # must not raise + self.assertEqual(m.itemList, [np]) + + +# =========================================================================== +# Comment / spacing helpers -- TextBlock content is what matters. +# =========================================================================== + + +class TestModuleCommentHelpers(unittest.TestCase): + def test_addSpaceLine_appends_newline_textblock(self): + m = Module() + m.addSpaceLine() + self.assertEqual(m.itemsSize(), 1) + self.assertIsInstance(m.itemList[0], TextBlock) + self.assertEqual(str(m), "\n") + + def test_addComment_single_slash_format(self): + # `// comment\n` is the bare minimum KernelWriter assumes. + m = Module() + m.addComment("hello") + self.assertEqual(str(m), "// hello\n") + + def test_addCommentAlign_pads_to_col_50(self): + # Aligned comments must end with the same `// comment\n` payload; + # the padding leading is what makes them align in instruction-rich + # blocks. We assert the suffix, not the exact column count, so the + # format can be retuned without breaking the test. + m = Module() + m.addCommentAlign("aligned") + text = str(m) + self.assertTrue(text.endswith("// aligned\n"), text) + self.assertIn(" ", text) + + def test_addComment0_block_format(self): + # addComment0 produces a single-line block comment matching native + # format.hpp::block() — ``/* COMMENT */\n``. + m = Module() + m.addComment0("section") + text = str(m) + self.assertEqual(text, "/* section */\n") + + def test_addComment1_includes_leading_blank_line(self): + m = Module() + m.addComment1("with blank") + text = str(m) + self.assertTrue(text.startswith("\n"), text) + self.assertIn("with blank", text) + + def test_addComment2_includes_trailing_blank_line(self): + m = Module() + m.addComment2("trailing") + text = str(m) + # _block_3line produces: \n + bar + \n + content + bar + \n + self.assertIn("trailing", text) + self.assertIn("/******", text) + self.assertTrue(text.endswith("/\n"), text) + + +# =========================================================================== +# Accessors -- items / itemsSize / count. +# =========================================================================== + + +class TestModuleAccessors(unittest.TestCase): + def test_items_returns_list_alias(self): + # rocisa returns ``const vector<...>&``; in Python we expose the + # underlying list (callers must NOT mutate it directly -- but the + # alias behaviour itself is rocisa-compat). + m = Module() + m.add(TextBlock("a")) + self.assertIs(m.items(), m.itemList) + + def test_itemsSize_grows_with_add(self): + m = Module() + for i in range(5): + m.add(TextBlock(str(i))) + self.assertEqual(m.itemsSize(), 5) + + def test_count_flat(self): + # Flat module: count == itemsSize for non-Module children. + m = Module() + m.add(TextBlock("a")) + m.add(TextBlock("b")) + self.assertEqual(m.count(), 2) + + def test_count_recurses_into_submodules(self): + # Sub-Module contributes its own recursive ``count()`` (not 1). + outer = Module("outer") + inner = Module("inner") + inner.add(TextBlock("a")) + inner.add(TextBlock("b")) + outer.add(inner) + outer.add(TextBlock("c")) + self.assertEqual(outer.count(), 3) # 2 from inner + 1 leaf + self.assertEqual(outer.itemsSize(), 2) # children of outer only + + def test_count_empty_submodule(self): + outer = Module() + outer.add(Module()) # empty inner + outer.add(TextBlock("x")) + self.assertEqual(outer.count(), 1) + + def test_count_deeply_nested(self): + # 4-deep tree of nested Modules, one leaf at the bottom. + leaf = TextBlock("L") + root = Module() + cur = root + for _ in range(4): + inner = Module() + cur.add(inner) + cur = inner + cur.add(leaf) + self.assertEqual(root.count(), 1) + + +# =========================================================================== +# getItem / setItem / setItems -- bounds + parent rebind. +# =========================================================================== + + +class TestModuleGetSetItem(unittest.TestCase): + def test_getItem_returns_child(self): + m = Module() + a, b = TextBlock("a"), TextBlock("b") + m.add(a) + m.add(b) + self.assertIs(m.getItem(0), a) + self.assertIs(m.getItem(1), b) + + def test_getItem_out_of_range_raises_runtime_error(self): + # rocisa throws std::runtime_error("index out of range") -> we + # raise RuntimeError with the same message so any caller's + # exception-text match keeps working. + m = Module() + m.add(TextBlock("a")) + with self.assertRaises(RuntimeError) as cm: + m.getItem(5) + self.assertEqual(str(cm.exception), "index out of range") + + def test_setItem_replaces_and_reparents(self): + m = Module() + a, b = TextBlock("a"), TextBlock("b") + m.add(a) + m.setItem(0, b) + self.assertIs(m.getItem(0), b) + self.assertIs(b.parent, m) + + def test_setItem_out_of_range_raises(self): + m = Module() + m.add(TextBlock("a")) + with self.assertRaises(RuntimeError): + m.setItem(5, TextBlock("z")) + + def test_setItems_replaces_entire_list(self): + m = Module() + m.add(TextBlock("old")) + new = [TextBlock("a"), TextBlock("b"), TextBlock("c")] + m.setItems(new) + self.assertEqual(m.itemList, new) + for it in new: + self.assertIs(it.parent, m) + + def test_setItems_copies_input(self): + # Mutating the source list after setItems must not bleed in. + m = Module() + src = [TextBlock("a")] + m.setItems(src) + src.append(TextBlock("b")) + self.assertEqual(m.itemsSize(), 1) + + +# =========================================================================== +# Find APIs. +# =========================================================================== + + +class TestModuleFind(unittest.TestCase): + def test_findNamedItem_returns_matching(self): + m = Module() + named = Module("target") + other = Module("other") + m.add(other) + m.add(named) + self.assertIs(m.findNamedItem("target"), named) + + def test_findNamedItem_returns_None_when_absent(self): + m = Module() + m.add(Module("foo")) + self.assertIsNone(m.findNamedItem("missing")) + + def test_findNamedItem_first_match_wins(self): + m = Module() + m.add(Module("dup")) + second = Module("dup") + m.add(second) + # rocisa uses ``std::find_if`` which returns the first match. + self.assertIsNot(m.findNamedItem("dup"), second) + + def test_findIndex_identity_match(self): + # rocisa's ``std::find`` on ``shared_ptr`` -> identity. + m = Module() + a, b, c = TextBlock("a"), TextBlock("b"), TextBlock("c") + m.add(a) + m.add(b) + m.add(c) + self.assertEqual(m.findIndex(b), 1) + + def test_findIndex_missing_returns_minus_one(self): + m = Module() + m.add(TextBlock("a")) + self.assertEqual(m.findIndex(TextBlock("a")), -1) + + def test_findIndexByType_returns_first_match(self): + m = Module() + m.add(TextBlock("t")) + m.add(Module("sub")) + m.add(TextBlock("u")) + self.assertEqual(m.findIndexByType(Module), 1) + self.assertEqual(m.findIndexByType(TextBlock), 0) + + def test_findIndexByType_missing_returns_minus_one(self): + m = Module() + m.add(TextBlock("t")) + self.assertEqual(m.findIndexByType(Module), -1) + + +# =========================================================================== +# Mutations -- replaceItem / removeItem / popFirstItem. +# =========================================================================== + + +class TestModuleReplace(unittest.TestCase): + def test_replaceItem_swaps_first_identity_match(self): + m = Module() + a, b, replacement = TextBlock("a"), TextBlock("b"), TextBlock("R") + m.add(a) + m.add(b) + m.replaceItem(a, replacement) + self.assertEqual(m.itemList, [replacement, b]) + self.assertIs(replacement.parent, m) + + def test_replaceItem_no_match_no_op(self): + m = Module() + m.add(TextBlock("a")) + m.replaceItem(TextBlock("missing"), TextBlock("R")) + self.assertEqual(m.itemsSize(), 1) + + def test_replaceItem_first_match_only(self): + # rocisa's loop breaks on the first match. + m = Module() + a1, a2 = TextBlock("a"), TextBlock("a") + m.add(a1) + m.add(a2) + replacement = TextBlock("R") + m.replaceItem(a1, replacement) + self.assertIs(m.itemList[0], replacement) + self.assertIs(m.itemList[1], a2) + + def test_replaceItemByIndex(self): + m = Module() + m.add(TextBlock("a")) + m.add(TextBlock("b")) + r = TextBlock("R") + m.replaceItemByIndex(1, r) + self.assertEqual(m.itemList[1].text, "R") + self.assertIs(r.parent, m) + + def test_replaceItemByIndex_out_of_range_silent_noop(self): + # rocisa: ``if(index >= itemList.size()) return;``. + m = Module() + m.add(TextBlock("a")) + m.replaceItemByIndex(99, TextBlock("R")) + self.assertEqual(m.itemsSize(), 1) + self.assertEqual(m.itemList[0].text, "a") + + +class TestModuleRemove(unittest.TestCase): + def test_removeItem_identity_match(self): + m = Module() + a, b, c = TextBlock("a"), TextBlock("b"), TextBlock("c") + m.addItems([a, b, c]) + m.removeItem(b) + self.assertEqual(m.itemList, [a, c]) + + def test_removeItem_missing_is_noop(self): + m = Module() + m.add(TextBlock("a")) + m.removeItem(TextBlock("a")) # equal text but different identity + self.assertEqual(m.itemsSize(), 1) + + def test_removeItem_removes_all_identity_matches(self): + m = Module() + a = TextBlock("a") + # Same identity twice -- ``[it for it in ... if it is not item]`` + # drops every copy. Matches rocisa's std::remove semantics for + # shared_ptr identity. + m.add(a) + m.add(TextBlock("b")) + m.itemList.append(a) # alias the same identity in twice + m.removeItem(a) + self.assertEqual(len(m.itemList), 1) + self.assertEqual(m.itemList[0].text, "b") + + def test_removeItemByIndex(self): + m = Module() + a, b, c = TextBlock("a"), TextBlock("b"), TextBlock("c") + m.addItems([a, b, c]) + m.removeItemByIndex(1) + self.assertEqual(m.itemList, [a, c]) + + def test_removeItemByIndex_clamps_overrange_to_last(self): + # rocisa: ``if(index >= size) index = size - 1`` then erase. + m = Module() + a, b = TextBlock("a"), TextBlock("b") + m.addItems([a, b]) + m.removeItemByIndex(99) + self.assertEqual(m.itemList, [a]) + + def test_removeItemByIndex_empty_is_noop(self): + m = Module() + m.removeItemByIndex(0) # must not raise + self.assertEqual(m.itemsSize(), 0) + + def test_removeItemsByName(self): + m = Module() + a, b, c = Module("foo"), Module("bar"), Module("foo") + m.addItems([a, b, c]) + m.removeItemsByName("foo") + self.assertEqual(m.itemList, [b]) + + +class TestModulePop(unittest.TestCase): + def test_popFirstItem(self): + m = Module() + a, b = TextBlock("a"), TextBlock("b") + m.addItems([a, b]) + self.assertIs(m.popFirstItem(), a) + self.assertEqual(m.itemList, [b]) + + def test_popFirstItem_empty_returns_None(self): + # rocisa returns ``nullptr``; Python equivalent is None. + self.assertIsNone(Module().popFirstItem()) + + def test_popFirstNItems_partial(self): + m = Module() + items = [TextBlock(str(i)) for i in range(5)] + m.addItems(items) + popped = m.popFirstNItems(2) + self.assertEqual(popped, items[:2]) + self.assertEqual(m.itemList, items[2:]) + + def test_popFirstNItems_whole_list(self): + # ``n >= size`` drains the list (rocisa: ``std::move``). + m = Module() + items = [TextBlock("a"), TextBlock("b")] + m.addItems(items) + popped = m.popFirstNItems(5) + self.assertEqual(popped, items) + self.assertEqual(m.itemList, []) + + def test_popFirstNItems_zero(self): + m = Module() + items = [TextBlock("a"), TextBlock("b")] + m.addItems(items) + popped = m.popFirstNItems(0) + self.assertEqual(popped, []) + self.assertEqual(m.itemList, items) + + +# =========================================================================== +# Tree ops -- appendModule / addModuleAsFlatItems / flatitems / setParent. +# =========================================================================== + + +class TestModuleTreeOps(unittest.TestCase): + def test_appendModule_copies_children(self): + # Each child of ``module`` is added to ``self`` (parent gets + # rewritten to ``self``). The donor module is returned. + target = Module("target") + donor = Module("donor") + a, b = TextBlock("a"), TextBlock("b") + donor.addItems([a, b]) + result = target.appendModule(donor) + self.assertIs(result, donor) + self.assertEqual(target.itemList, [a, b]) + self.assertIs(a.parent, target) + self.assertIs(b.parent, target) + + def test_addModuleAsFlatItems_flattens_subtree(self): + # Flattens transitively before adding -- nested Modules are + # gone in the target's itemList. + target = Module() + donor = Module() + sub = Module() + leaf_a, leaf_b = TextBlock("a"), TextBlock("b") + sub.add(leaf_a) + donor.add(sub) + donor.add(leaf_b) + target.addModuleAsFlatItems(donor) + self.assertEqual(target.itemList, [leaf_a, leaf_b]) + + def test_flatitems_flattens_all_levels(self): + m = Module() + leaf_a = TextBlock("a") + leaf_b = TextBlock("b") + leaf_c = TextBlock("c") + inner1 = Module() + inner1.add(leaf_a) + inner2 = Module() + inner2.add(leaf_b) + m.add(inner1) + m.add(inner2) + m.add(leaf_c) + self.assertEqual(m.flatitems(), [leaf_a, leaf_b, leaf_c]) + + def test_flatitems_empty_module(self): + self.assertEqual(Module().flatitems(), []) + + def test_setParent_recurses(self): + # setParent is called after deep-loaded trees to make sure every + # node points at its lexical parent. We simulate a broken parent + # chain and verify setParent fixes it top-down. + outer = Module() + inner = Module() + leaf = TextBlock("L") + # Bypass add() to avoid auto-reparent. + outer.itemList.append(inner) + inner.itemList.append(leaf) + outer.setParent() + self.assertIs(inner.parent, outer) + self.assertIs(leaf.parent, inner) + + +class TestModuleSetters(unittest.TestCase): + def test_setNoOpt_isNoOpt_roundtrip(self): + m = Module() + self.assertFalse(m.isNoOpt()) + m.setNoOpt(True) + self.assertTrue(m.isNoOpt()) + m.setNoOpt(False) + self.assertFalse(m.isNoOpt()) + + def test_addTempVgpr_stores(self): + m = Module() + sentinel = object() + m.addTempVgpr(sentinel) + self.assertIs(m.tempVgpr, sentinel) + + def test_setInlineAsmPrintMode_recurses_modules_and_calls_instructions(self): + # rocisa: walks itemList; sub-Module -> recurse, Instruction -> + # setInlineAsm(mode). We verify both branches with a fake Instr. + class _FakeInstr: + def __init__(self): + self.parent = None + self.mode = None + + def setInlineAsm(self, m): + self.mode = m + + def __str__(self): + return "" + + i_outer = _FakeInstr() + i_inner = _FakeInstr() + inner = Module() + inner.add(i_inner) + outer = Module() + outer.add(inner) + outer.add(i_outer) + outer.setInlineAsmPrintMode(True) + self.assertEqual(i_outer.mode, True) + self.assertEqual(i_inner.mode, True) + + +# =========================================================================== +# Rendering -- toString / str / prettyPrint. +# =========================================================================== + + +class TestModuleToString(unittest.TestCase): + def test_empty_module_empty_string(self): + self.assertEqual(str(Module()), "") + self.assertEqual(Module().toString(), "") + + def test_concatenates_children_str(self): + m = Module() + m.add(TextBlock("alpha ")) + m.add(TextBlock("beta\n")) + self.assertEqual(str(m), "alpha beta\n") + + def test_recursive_render(self): + outer = Module() + outer.add(TextBlock("# outer-start\n")) + inner = Module() + inner.add(TextBlock("# inner\n")) + outer.add(inner) + outer.add(TextBlock("# outer-end\n")) + self.assertEqual(str(outer), "# outer-start\n# inner\n# outer-end\n") + + def test_str_delegates_to_toString(self): + m = Module() + m.add(TextBlock("ABC")) + self.assertEqual(str(m), m.toString()) + + +class TestModulePrettyPrint(unittest.TestCase): + def test_empty_module(self): + out = Module("Foo").prettyPrint() + self.assertIn('Module "Foo"', out) + + def test_empty_module_exact_format(self): + # rocisa code.hpp:418-427 -- ``{indent}{ClassName} "{name}"\n`` and + # nothing else when the module is empty. Pin the exact bytes so + # any future format drift surfaces here, not in a downstream diff. + self.assertEqual(Module("Foo").prettyPrint(), 'Module "Foo"\n') + + def test_nested_structure(self): + outer = Module("Outer") + inner = Module("Inner") + inner.add(TextBlock("x")) + outer.add(inner) + out = outer.prettyPrint() + self.assertIn('Module "Outer"', out) + self.assertIn('Module "Inner"', out) + self.assertIn("TextBlock", out) + + def test_includes_textblock_payload(self): + # Regression for P3: the old TextBlock.prettyPrint dropped its + # text, so debugging a tree gave you no idea what the comments + # said. After the fix, each TextBlock line contains its content. + outer = Module("Outer") + outer.add(TextBlock("INTERESTING-PAYLOAD")) + out = outer.prettyPrint() + self.assertIn("INTERESTING-PAYLOAD", out) + + def test_nested_exact_concat_format(self): + # Byte-for-byte format check against rocisa C++: + # - Module header line ends with `"\n` + # - Each child's prettyPrint(indent + "|--") is concatenated + # verbatim (no extra separators / no rstrip / no joins) + # - Sub-Module brings its own trailing newline (its header) + # - Leaf TextBlock has NO trailing newline (Item base) + outer = Module("Outer") + inner = Module("Inner") + inner.add(TextBlock("leaf")) + outer.add(inner) + expected = ( + 'Module "Outer"\n' + '|--Module "Inner"\n' + '|--|--TextBlock leaf' + ) + self.assertEqual(outer.prettyPrint(), expected) + + def test_deep_nesting_indent_doubles(self): + # Each level adds "|--" to the indent. A 3-level nest has 3 |--. + root = Module("L0") + l1 = Module("L1") + l2 = Module("L2") + l2.add(TextBlock("x")) + l1.add(l2) + root.add(l1) + expected = ( + 'Module "L0"\n' + '|--Module "L1"\n' + '|--|--Module "L2"\n' + '|--|--|--TextBlock x' + ) + self.assertEqual(root.prettyPrint(), expected) + + def test_starting_indent_param_propagates(self): + # ``prettyPrint("> ")`` ships the indent through to children too. + m = Module("Root") + m.add(TextBlock("x")) + self.assertEqual(m.prettyPrint("> "), '> Module "Root"\n> |--TextBlock x') + + def test_multiple_siblings_concatenated_in_order(self): + # rocisa: ``for(const auto& i : itemList) ostream += i->prettyPrint(...)``. + # Two siblings emit two consecutive child lines, in itemList order. + m = Module("M") + m.add(TextBlock("first")) + m.add(TextBlock("second")) + # No newline between the two TextBlock lines because Item::prettyPrint + # has no trailing \n -- they fuse exactly as in rocisa. + self.assertEqual( + m.prettyPrint(), + 'Module "M"\n|--TextBlock first|--TextBlock second', + ) + + def test_indent_propagates(self): + # Inner items should be indented more than the outer header. + outer = Module("Outer") + outer.add(TextBlock("x")) + out = outer.prettyPrint() + lines = [ln for ln in out.split("\n") if ln] + self.assertTrue(any("|--" in ln for ln in lines)) + + def test_dummy_child_without_prettyPrint_falls_back(self): + # If a child returns a non-string from prettyPrint (e.g. dummy + # shim's noop __getattr__), Module emits a class-name fallback + # line so the tree dump stays usable during bring-up. + class _NoPretty: + def __str__(self): + return "" + + def prettyPrint(self, indent=""): + return None # simulate dummy ``_noop`` + + m = Module("M") + m.add(_NoPretty()) + out = m.prettyPrint() + self.assertIn("_NoPretty", out) + self.assertTrue(out.endswith("\n")) + + +# =========================================================================== +# Copy / pickle semantics. +# =========================================================================== + + +class TestModuleDeepCopy(unittest.TestCase): + def test_deepcopy_returns_independent_module(self): + m = Module("orig") + m.add(TextBlock("a")) + m.add(TextBlock("b")) + c = copy.deepcopy(m) + self.assertEqual(c.name, "orig") + self.assertEqual(len(c.itemList), 2) + self.assertIsNot(c, m) + # Children are deep-copied -- mutating the clone's TextBlock + # must not bleed back. + c.itemList[0].text = "MUTATED" + self.assertEqual(m.itemList[0].text, "a") + + def test_deepcopy_reparents_children_to_clone(self): + m = Module() + tb = TextBlock("x") + m.add(tb) + c = copy.deepcopy(m) + # Clone's child points at the clone (not the original). + self.assertIs(c.itemList[0].parent, c) + # Original is unchanged. + self.assertIs(m.itemList[0].parent, m) + + def test_deepcopy_preserves_noopt_flag(self): + m = Module() + m.setNoOpt(True) + c = copy.deepcopy(m) + self.assertTrue(c.isNoOpt()) + + def test_deepcopy_handles_nested_modules(self): + outer = Module("outer") + inner = Module("inner") + inner.add(TextBlock("leaf")) + outer.add(inner) + c = copy.deepcopy(outer) + self.assertEqual(str(c), str(outer)) + # Reparent walks the whole tree. + self.assertIs(c.itemList[0].parent, c) + self.assertIs(c.itemList[0].itemList[0].parent, c.itemList[0]) + + +class TestModulePickleRejected(unittest.TestCase): + def test_pickle_raises_runtime_error(self): + # rocisa explicitly raises ``Module is not picklable`` to keep + # ParallelMap2 workers from silently shipping malformed IR. + m = Module() + m.add(TextBlock("a")) + with self.assertRaises(RuntimeError) as cm: + pickle.dumps(m) + self.assertIn("not picklable", str(cm.exception)) + + + +# =========================================================================== +# logicalIR handoff -- _populate_logical_module walk semantics. +# =========================================================================== +# +# Module-side collector tests (fake leaves, traversal, dummy skip, ValueSet). +# Real ``VMovB32`` pickup when stinkytofu is built lives in +# ``test_instruction.TestCollectLogicalIntegration``. End-to-end lowering +# is in ``TestToStinkyAsm`` below and ``test_emission_consistency``. + + +class _FakeLogicalInst: + """Looks like a Step-3 instruction shim to ``_populate_logical_module``.""" + + def __init__(self, payload): + self.parent = None + self.payload = payload + self.lowered = False + + def to_stinky_logical(self): + self.lowered = True + return self.payload + + def __str__(self): + return "" + + +class _MockLogicalModule: + """Records add() and add_set_directive() calls for assertion.""" + + def __init__(self): + self.items = [] # list of ("inst", payload) or ("set", sym, val) + + def add(self, inst): + self.items.append(("inst", inst)) + + def add_set_directive(self, symbol, value): + self.items.append(("set", symbol, value)) + + def add_textblock(self, text): + self.items.append(("textblock", text)) + + def add_label(self, name, alignment, comment): + self.items.append(("label", name, alignment, comment)) + + +class TestPopulateLogicalModule(unittest.TestCase): + def _payloads(self, m): + """Helper: run _populate_logical_module and return recorded items.""" + mock = _MockLogicalModule() + m._populate_logical_module(mock) + return mock.items + + def test_empty_module(self): + self.assertEqual(self._payloads(Module()), []) + + def test_emits_textblock(self): + m = Module() + m.add(TextBlock("ignored")) + self.assertEqual(self._payloads(m), [("textblock", "ignored")]) + + def test_picks_up_logical_leaf(self): + m = Module() + fake = _FakeLogicalInst(payload="P1") + m.add(fake) + self.assertEqual(self._payloads(m), [("inst", "P1")]) + self.assertTrue(fake.lowered) + + def test_in_order_traversal_flat(self): + m = Module() + a, b, c = (_FakeLogicalInst("A"), _FakeLogicalInst("B"), _FakeLogicalInst("C")) + m.add(a) + m.add(b) + m.add(c) + self.assertEqual(self._payloads(m), [("inst", "A"), ("inst", "B"), ("inst", "C")]) + + def test_in_order_traversal_with_nested_modules(self): + outer = Module() + outer.add(_FakeLogicalInst("0")) + inner = Module() + inner.add(_FakeLogicalInst("1")) + inner.add(_FakeLogicalInst("2")) + outer.add(inner) + outer.add(_FakeLogicalInst("3")) + self.assertEqual( + self._payloads(outer), + [("inst", "0"), ("inst", "1"), ("inst", "2"), ("inst", "3")], + ) + + def test_skips_dummy_instruction_classes(self): + from rocisa_stinkytofu_adaptor.instruction import SAddPCI64_SIMM # noqa: WPS433 + m = Module() + m.add(SAddPCI64_SIMM()) + self.assertEqual(self._payloads(m), []) + + def test_textblocks_and_logical_mixed(self): + m = Module() + m.add(TextBlock("// header\n")) + m.add(_FakeLogicalInst("INST")) + m.add(TextBlock("// footer\n")) + items = self._payloads(m) + self.assertEqual(items[0], ("textblock", "// header\n")) + self.assertEqual(items[1], ("inst", "INST")) + self.assertEqual(items[2], ("textblock", "// footer\n")) + + def test_valueset_emitted_as_set_directive(self): + m = Module() + m.add(ValueSet("vgprBase", 516)) + m.add(_FakeLogicalInst("A")) + m.add(ValueSet("vgprFoo", "vgprBase", offset=0)) + m.add(_FakeLogicalInst("B")) + m.add(ValueSet("vgprBase", "UNDEF", format=-1)) + items = self._payloads(m) + self.assertEqual(items[0], ("set", "vgprBase", "516")) + self.assertEqual(items[1], ("inst", "A")) + self.assertEqual(items[2], ("set", "vgprFoo", "vgprBase+0")) + self.assertEqual(items[3], ("inst", "B")) + self.assertEqual(items[4], ("set", "vgprBase", "UNDEF")) + + +# =========================================================================== +# to_stinky_asm -- end-to-end binding call (gated on built stinkytofu). +# =========================================================================== + + +try: + import stinkytofu as _stinky # noqa: F401 + _STINKY_OK = ( + hasattr(_stinky, "LogicalModule") + and hasattr(_stinky, "lower_logical_module") + and hasattr(_stinky, "VMovB32") + ) +except ImportError: + _STINKY_OK = False + + +@unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built / missing left-path symbols") +class TestToStinkyAsm(unittest.TestCase): + """Run the full left-path lowering on a single-VMovB32 toy module. + + Uses a thin wrapper class that fabricates the ``_stinkytofu.VMovB32`` + on demand so this test does not depend on Step 3 (the + ``rocisa.instruction.VMovB32`` shim) landing first. + """ + + def _make_fake_vmovb32(self): + # Build a logical-IR VMovB32 the way Step 3 will, encapsulated + # in a shim that exposes ``to_stinky_logical()``. + import stinkytofu as _st + + dst = _st.vgpr(0, 1) + src = _st.vgpr(1, 1) + + class _ShimVMovB32: + def __init__(self): + self.parent = None + + def to_stinky_logical(self): + # ``VMovB32`` ctor signature in the bindings is + # (dest, src0, comment=""); see + # ``shared/stinkytofu/python_module/src/python_bindings.cpp``. + return _st.VMovB32(dst, src, "smoke") + + def __str__(self): + return "" + + return _ShimVMovB32() + + def test_empty_module_lowers_to_empty_asm(self): + # Empty LogicalModule -> StinkyAsmModule with no real instructions. + m = Module("kEmpty") + asm = m.to_stinky_asm([12, 5, 0]) + text = asm.emitAssembly() + # Empty kernel still emits a header / directives; the only hard + # invariant is that no v_mov_b32 leaks in. + self.assertNotIn("v_mov_b32", text) + + def test_single_vmovb32_lowers_to_assembly(self): + m = Module("kSingle") + m.add(self._make_fake_vmovb32()) + asm = m.to_stinky_asm([12, 5, 0]) + text = asm.emitAssembly() + # Byte-parity is for the right-path tests; here we just assert + # the lowering pipeline ran end-to-end and emitted the expected + # mnemonic. + self.assertIn("v_mov_b32", text) + + def test_nested_modules_are_flattened_for_lowering(self): + outer = Module("kNested") + inner = Module("kInner") + inner.add(self._make_fake_vmovb32()) + outer.add(inner) + outer.add(self._make_fake_vmovb32()) + asm = outer.to_stinky_asm([12, 5, 0]) + text = asm.emitAssembly() + # Two leaves were added, so two v_mov_b32 lines should emerge. + self.assertEqual(text.count("v_mov_b32"), 2) + + def test_textblock_items_appear_in_output(self): + # TextBlock items are emitted via add_textblock and appear in + # the final assembly output as standalone comments. + m = Module("kWithComments") + m.add(TextBlock("// a header comment\n")) + m.add(self._make_fake_vmovb32()) + m.add(TextBlock("// a footer comment\n")) + asm = m.to_stinky_asm([12, 5, 0]) + text = asm.emitAssembly() + self.assertIn("v_mov_b32", text) + self.assertIn("a header comment", text) + self.assertIn("a footer comment", text) + + def test_arch_accepts_sequence_not_just_list(self): + # Tuples / arrays are common in KernelWriter (kernel["ISA"] is + # often a tuple). Accept anything sequence-like. + m = Module() + m.add(self._make_fake_vmovb32()) + asm = m.to_stinky_asm((12, 5, 0)) + self.assertIn("v_mov_b32", asm.emitAssembly()) + + +# =========================================================================== +# KernelWriter-shaped integration -- the actual usage patterns from +# tensilelite KernelWriter that the adapter has to support 1:1. +# =========================================================================== + + +class TestKernelWriterModuleUsage(unittest.TestCase): + """Pin the Module/TextBlock interactions KernelWriter relies on. + + KernelWriter constructs a kernel as a Module containing named child + Modules (one per logical phase: SetupVgpr, LoopHeader, ...), interleaved + with comment / spacing TextBlocks. It then uses ``findNamedItem`` to + splice extra code into a specific phase, and ``removeItemsByName`` to + drop a section entirely. Cover the combinations that matter. + """ + + def _make_kernel_skeleton(self): + """Build a realistic mini-Module: comments + named sub-Modules.""" + kernel = Module("MyKernel") + kernel.addComment0("Kernel preamble") + kernel.add(Module("SetupVgpr")) + kernel.addComment("BetaCheck setup") + kernel.add(Module("BetaCheck")) + kernel.addSpaceLine() + kernel.add(Module("LoopBody")) + kernel.addComment("Cleanup") + kernel.add(Module("Cleanup")) + return kernel + + def test_findNamedItem_skips_comment_textblocks(self): + # After P1 TextBlock.name == text, so a TextBlock from addComment + # has name == "// BetaCheck setup\n" -- which must NOT collide + # with the named ``Module("BetaCheck")`` lookup. + kernel = self._make_kernel_skeleton() + beta = kernel.findNamedItem("BetaCheck") + self.assertIsNotNone(beta) + self.assertIsInstance(beta, Module) + self.assertEqual(beta.name, "BetaCheck") + + def test_findNamedItem_returns_None_for_partial_match(self): + # ``BetaCheck setup`` is a comment's name (well, the formatted + # ``// BetaCheck setup\n`` is). ``findNamedItem`` is exact-match + # so the substring ``"BetaCheck"`` of the comment does NOT + # short-circuit the real ``Module("BetaCheck")`` -- otherwise + # KernelWriter splicing logic would target the wrong node. + kernel = self._make_kernel_skeleton() + # The comment's exact name (the formatted text), not just substring: + comment_name = "// BetaCheck setup\n" + found = kernel.findNamedItem(comment_name) + # This DOES match the TextBlock by exact name -- demonstrating + # the rocisa-faithful exact-equality semantic. KernelWriter + # never calls findNamedItem with such a string, so no harm done. + self.assertIsNotNone(found) + self.assertNotIsInstance(found, Module) + + def test_removeItemsByName_targets_module_not_textblock(self): + # KernelWriter calls e.g. ``module.removeItemsByName("LoopBody")``; + # only the Module with that exact name is removed, comments stay. + kernel = self._make_kernel_skeleton() + before = kernel.itemsSize() + kernel.removeItemsByName("LoopBody") + self.assertEqual(kernel.itemsSize(), before - 1) + # Comments untouched: + self.assertTrue( + any( + isinstance(it, TextBlock) and "Cleanup" in it.text + for it in kernel.itemList + ) + ) + + def test_findNamedItem_returns_first_among_modules_only(self): + # A subtle KernelWriter assumption: ``findNamedItem`` is identity- + # free, name-based, first-match. We covered the first-match case + # in TestModuleFind; here pin the cross-Module-and-TextBlock + # variant for a more realistic kernel skeleton. + kernel = self._make_kernel_skeleton() + setup = kernel.findNamedItem("SetupVgpr") + self.assertEqual(setup.name, "SetupVgpr") + + def test_emitted_asm_contains_comments_by_default(self): + # Default OutputOptions(outputNoComment=False): every comment makes + # it into the rendered string. This is the development-build path. + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + opts = rocIsa.getInstance().getOutputOptions() + saved = opts.outputNoComment + try: + opts.outputNoComment = False + kernel = self._make_kernel_skeleton() + text = str(kernel) + self.assertIn("// BetaCheck setup", text) + self.assertIn("// Cleanup", text) + finally: + opts.outputNoComment = saved + + def test_emitted_asm_strips_comments_when_outputNoComment(self): + # Production build path (P2 regression). With the flag on, every + # TextBlock in the kernel -- including header / divider / spacing + # / per-section comments -- collapses to "". Named sub-Modules + # still render normally (they have no text payload). + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + opts = rocIsa.getInstance().getOutputOptions() + saved = opts.outputNoComment + try: + opts.outputNoComment = True + kernel = self._make_kernel_skeleton() + text = str(kernel) + self.assertNotIn("BetaCheck setup", text) + self.assertNotIn("Cleanup", text) + self.assertNotIn("//", text) + self.assertNotIn("/*", text) + finally: + opts.outputNoComment = saved + + def test_replaceItem_swap_preserves_outer_parent(self): + # KernelWriter occasionally swaps a phase Module wholesale + # (e.g. choosing between two LoopBody implementations). The + # replacement must inherit the outer Module as its parent, or + # any subsequent tree walk that ascends would break. + kernel = Module("kernel") + old = Module("LoopBody") + new = Module("LoopBody") + kernel.add(old) + kernel.replaceItem(old, new) + self.assertIs(new.parent, kernel) + self.assertIs(kernel.findNamedItem("LoopBody"), new) + + +# =========================================================================== +# Import-time absence of stinkytofu does NOT break Module. +# =========================================================================== + + +class TestStinkytofuOptional(unittest.TestCase): + def test_module_works_without_stinkytofu(self): + # to_stinky_asm imports stinkytofu *lazily*; constructing / + # editing Modules must work even when the binding is missing + # (matches the SrdUpperValue soft-import pattern elsewhere + # in code.py). + m = Module() + m.add(TextBlock("x")) + m.add(Module()) # noqa: WPS441 -- intentional nest + # str / count / items still work without ever touching stinkytofu. + self.assertEqual(str(m), "x") + self.assertEqual(m.itemsSize(), 2) + + +# =========================================================================== +# Item hierarchy -- BitfieldUnion exception + Module recursion. +# =========================================================================== +# +# ``TextBlock`` / ``Module`` isinstance-Item shape is in ``test_base``. +# Here: ``BitfieldUnion`` stays outside Item (code.hpp:928); Module's +# recursive ``countType`` / ``countExactType`` (code.hpp:441-459). + + +class TestDummyClassesInheritItem(unittest.TestCase): + """``BitfieldUnion`` is the only code export outside the Item tree.""" + + def test_bitfieldunion_is_not_item(self): + # Intentionally NOT in the Item hierarchy -- mirror of + # rocisa C++ where ``BitfieldUnion`` (code.hpp:928) is its + # own standalone polymorphic root for the SrdUpperValue + # family. Counting it as an Item would incorrectly inflate + # ``Module.countType(Item)`` for any tree that contains + # a BitfieldUnion sibling. + self.assertNotIsInstance(BitfieldUnion(), Item) + + +class TestKernelBodyItemDefaults(unittest.TestCase): + """``KernelBody`` inherits ``Item.countType`` and raises on empty + ``toString`` when no body is attached (rocisa parity).""" + + def test_toString_raises_when_body_missing(self): + kb = KernelBody("kb") + with self.assertRaises(RuntimeError): + kb.toString() + + def test_str_raises_when_body_missing(self): + kb = KernelBody("kb") + with self.assertRaises(RuntimeError): + str(kb) + + def test_countType_is_one_for_itself(self): + kb = KernelBody("kb") + self.assertEqual(kb.countType(KernelBody), 1) + self.assertEqual(kb.countType(Item), 1) + self.assertEqual(kb.countType(Module), 0) + + +class TestModuleCountTypeRecursion(unittest.TestCase): + """``Module.countType`` / ``countExactType`` override Item's default + to recurse through ``itemList``. Mirror of rocisa C++ code.hpp: + 441-459.""" + + def test_countType_recurses_through_children(self): + # ``Module(Item)`` so countType(Item) on a module with two + # TextBlocks (also Items) yields 1 (self) + 2 (children) = 3. + m = Module("k") + m.add(TextBlock("a")) + m.add(TextBlock("b")) + self.assertEqual(m.countType(Item), 3) + + def test_countType_recurses_into_submodules(self): + # Two-level tree: 1 (outer Module) + 1 (TextBlock) + + # 1 (inner Module) + 1 (TextBlock inside inner) = 4 Items. + outer = Module("o") + outer.add(TextBlock("a")) + inner = Module("i") + inner.add(TextBlock("b")) + outer.add(inner) + self.assertEqual(outer.countType(Item), 4) + + def test_countType_TextBlock_only(self): + # TextBlock is the target, not Item -- Module counts as 0, + # TextBlocks count as 1 each. + m = Module() + m.add(TextBlock("a")) + m.add(TextBlock("b")) + m.add(TextBlock("c")) + self.assertEqual(m.countType(TextBlock), 3) + + def test_countType_includes_dummy_descendants(self): + # Real and dummy Items both inherit from Item and MUST be + # visible to ``countType(Item)`` walks (the very reason for + # ``make_dummy_class(..., base=Item)`` for the still-dummy + # nodes, and for ``class Label(Item)`` for the real ones). + m = Module() + m.add(Label(0, "")) + m.add(KernelBody("kb")) + m.add(TextBlock("x")) + # 1 (Module) + 1 (Label) + 1 (KernelBody) + 1 (TextBlock) = 4. + self.assertEqual(m.countType(Item), 4) + + def test_countExactType_is_strict_identity(self): + # ``type(self) is Module`` is True for Module but False for + # any subclass. We don't have a real Module subclass to + # contrast with yet (StructuredModule is dummy + base=Module), + # so verify with TextBlock as the comparison. + m = Module() + m.add(TextBlock("a")) + m.add(TextBlock("b")) + # countExactType(Module): only ``m`` itself counts (1). + self.assertEqual(m.countExactType(Module), 1) + # countExactType(TextBlock): only the two leaves count (2). + self.assertEqual(m.countExactType(TextBlock), 2) + + def test_countExactType_subclass_does_not_match_module(self): + # StructuredModule is a Module subclass; countExactType(Module) + # on a StructuredModule itself does NOT count it (the exact + # type check is ``typeid(*this) == targetType``, so the + # derived StructuredModule doesn't match Module). But the + # 3 auto-added sub-modules (header / middle / footer) ARE + # exact Modules, so countExactType recurses into itemList + # and counts them = 3. + sm = StructuredModule() + self.assertEqual(sm.countExactType(Module), 3) + # ... StructuredModule itself matches its own exact type. + # The 3 sub-modules are Modules (not StructuredModules) so + # they DON'T count here -- just self. + self.assertEqual(sm.countExactType(StructuredModule), 1) + # ... and isinstance-based countType matches BOTH self + # (StructuredModule is-a Module) AND the 3 sub-modules, + # giving 1 + 3 = 4. + self.assertEqual(sm.countType(Module), 4) + + +class TestStructuredModuleConstruction(unittest.TestCase): + """``StructuredModule`` is a real ``Module`` subclass that + auto-adds three named sub-modules ``(header, middle, footer)`` to + ``itemList`` at construction. Mirror of ``rocisa::StructuredModule`` + (code.hpp:763-791).""" + + def test_inherits_module(self): + sm = StructuredModule() + self.assertIsInstance(sm, Module) + # ``isinstance(sm, Item)`` follows from Module; see + # ``test_base.TestItemInheritanceShape``. + + def test_default_name_empty(self): + # rocisa ctor default: ``StructuredModule(name="")``. + self.assertEqual(StructuredModule().name, "") + + def test_custom_name_set(self): + # KernelWriter call sites pass things like + # ``StructuredModule("globalReadDoA_0")`` -- the name must + # propagate to ``Item.name``. + self.assertEqual(StructuredModule("globalReadA").name, "globalReadA") + + def test_three_submodules_present(self): + sm = StructuredModule() + # All three are real Module instances (NOT None, NOT dummy). + self.assertIsInstance(sm.header, Module) + self.assertIsInstance(sm.middle, Module) + self.assertIsInstance(sm.footer, Module) + + def test_submodule_names_match_rocisa(self): + # Names are the literal strings rocisa uses + # (``make_shared("header")`` / etc.) so that + # ``findIndexByName("middle")`` works against either side. + sm = StructuredModule() + self.assertEqual(sm.header.name, "header") + self.assertEqual(sm.middle.name, "middle") + self.assertEqual(sm.footer.name, "footer") + + def test_itemList_has_three_entries(self): + # itemList starts at 3 (NOT 0) -- this is a behaviour change + # vs. the dummy version, where ``StructuredModule()`` was an + # empty Module. Any caller that previously asserted + # ``itemsSize() == 0`` post-construction must be updated. + self.assertEqual(StructuredModule().itemsSize(), 3) + + def test_submodules_aliased_into_itemList(self): + # ``add(header)`` / ``add(middle)`` / ``add(footer)`` push the + # SAME shared_ptr (Python: same object) into itemList. This + # aliasing is what makes ``sm.middle.add(...)`` show up in + # ``str(sm)`` -- toString iterates itemList. + sm = StructuredModule() + self.assertIs(sm.itemList[0], sm.header) + self.assertIs(sm.itemList[1], sm.middle) + self.assertIs(sm.itemList[2], sm.footer) + + def test_submodule_parent_set_to_owner(self): + # ``Module.add`` reparents the child; the 3 sub-modules must + # have their ``.parent`` set to the owning StructuredModule. + sm = StructuredModule() + self.assertIs(sm.header.parent, sm) + self.assertIs(sm.middle.parent, sm) + self.assertIs(sm.footer.parent, sm) + + +class TestStructuredModuleEmission(unittest.TestCase): + """``StructuredModule.toString`` is inherited from ``Module`` and + just concatenates ``str(item)`` over itemList -- so the emission + is the concatenation of header + middle + footer (in order).""" + + def test_empty_emits_empty_string(self): + # All three sub-modules are empty Modules; each emits "" so + # the StructuredModule emits "" overall. + self.assertEqual(str(StructuredModule()), "") + + def test_middle_mutation_visible_in_str(self): + # The whole point of the aliasing -- mutating sm.middle.add + # MUST round-trip through ``str(sm)`` because itemList[1] + # IS sm.middle. + sm = StructuredModule("globalReadA") + sm.middle.add(TextBlock("load_a_0\n")) + sm.middle.add(TextBlock("load_a_1\n")) + self.assertEqual(str(sm), "load_a_0\nload_a_1\n") + + def test_header_middle_footer_concatenated_in_order(self): + # Emission order is fixed: header content first, then + # middle, then footer. Any reordering would silently break + # SIA-scheduled kernels. + sm = StructuredModule() + sm.header.add(TextBlock("H\n")) + sm.middle.add(TextBlock("M\n")) + sm.footer.add(TextBlock("F\n")) + self.assertEqual(str(sm), "H\nM\nF\n") + + def test_external_add_appends_after_footer(self): + # ``StructuredModule`` is still a Module -- raw ``.add(...)`` + # appends to itemList AFTER the 3 sub-modules. The new + # entry's content shows up AFTER footer in the emission. + sm = StructuredModule() + sm.middle.add(TextBlock("M\n")) + sm.add(TextBlock("X\n")) # itemList[3] + self.assertEqual(sm.itemsSize(), 4) + self.assertEqual(str(sm), "M\nX\n") + + +class TestStructuredModuleAttributeSwap(unittest.TestCase): + """The ``header / middle / footer`` attributes are read-write + (``def_rw`` in rocisa). A caller may swap one out for a freshly + built Module -- emission must reflect the swap, but ONLY if the + caller also patches itemList (the aliasing is positional).""" + + def test_assigning_new_module_does_not_auto_resync_itemList(self): + # ``sm.middle = Module("replacement")`` breaks the aliasing + # with itemList[1] -- a caller that wants the new middle to + # show up in ``str(sm)`` MUST also patch ``sm.itemList[1]``. + # We pin this behavior so anyone tempted to add magic + # syncing later thinks twice (rocisa doesn't do it either). + sm = StructuredModule() + replacement = Module("replacement") + sm.middle = replacement + # Aliasing broken: itemList[1] still points to the original. + self.assertIsNot(sm.itemList[1], replacement) + # Emission still reflects the OLD middle (empty). + replacement.add(TextBlock("X\n")) + self.assertEqual(str(sm), "") + + +class TestStructuredModuleFind(unittest.TestCase): + """``findIndexByName`` / ``findIndexByType`` inherited from Module + must locate the 3 sub-modules at their fixed positions.""" + + def test_findNamedItem_locates_sub_modules(self): + sm = StructuredModule() + # ``findNamedItem`` returns the matching Item (the aliased + # sub-module), not an index. Mirror of rocisa code.hpp:216. + self.assertIs(sm.findNamedItem("header"), sm.header) + self.assertIs(sm.findNamedItem("middle"), sm.middle) + self.assertIs(sm.findNamedItem("footer"), sm.footer) + + def test_findIndexByType_Module_returns_zero(self): + # ``findIndexByType(Module)`` returns the FIRST itemList + # entry that ``isinstance(..., Module)`` -- which is + # ``sm.header`` at position 0. + self.assertEqual(StructuredModule().findIndexByType(Module), 0) + + def test_findIndexByType_TextBlock_skips_sub_modules(self): + # Sub-modules are Modules, not TextBlocks; the first + # TextBlock-typed child added afterwards lives at position 3. + sm = StructuredModule() + sm.add(TextBlock("x")) + self.assertEqual(sm.findIndexByType(TextBlock), 3) + + +class TestStructuredModuleDeepCopy(unittest.TestCase): + """``copy.deepcopy(sm)`` returns an isolated clone that PRESERVES + the construction-time aliasing between ``header / middle / + footer`` and ``itemList[0..2]``. + + This is a CONSCIOUS DIVERGENCE from rocisa's C++ copy ctor + (code.hpp:781-790), which accidentally re-clones the 3 sub- + modules a second time and ends up with attrs that point to + objects NOT in itemList. See ``StructuredModule.__deepcopy__``'s + long docstring for the full rationale -- in short, breaking the + aliasing on deepcopy makes ``cloned_sm.middle.add(...)`` + silently no-op (the mutation lands on a Module nobody reaches + via toString), which is a sleeper bug. We use Python's standard + ``memo`` mechanism to keep attrs aliased to itemList entries + while still fully isolating clone from original.""" + + def test_deepcopy_returns_distinct_instance(self): + original = StructuredModule("foo") + clone = copy.deepcopy(original) + self.assertIsNot(clone, original) + self.assertIsInstance(clone, StructuredModule) + self.assertEqual(clone.name, "foo") + + def test_deepcopy_clones_sub_modules(self): + original = StructuredModule() + clone = copy.deepcopy(original) + # Each sub-module attr on the clone is a different object + # from the original's same-named attr. + self.assertIsNot(clone.header, original.header) + self.assertIsNot(clone.middle, original.middle) + self.assertIsNot(clone.footer, original.footer) + # But their identity-content matches. + self.assertEqual(clone.header.name, "header") + self.assertEqual(clone.middle.name, "middle") + self.assertEqual(clone.footer.name, "footer") + + def test_deepcopy_preserves_aliasing_with_itemList(self): + # The construction-time aliasing invariant (``sm.header is + # sm.itemList[0]``) MUST survive deepcopy -- this is the + # whole point of our divergence from rocisa C++ here. + # Achieved via Python's standard deepcopy ``memo`` cache: + # Step 1 deepcopies itemList entries (registering them in + # memo); Step 2 deepcopies ``self.header / middle / footer`` + # with the SAME memo, hitting the cache and rebinding to + # the already-cloned itemList entries. + original = StructuredModule() + original.middle.add(TextBlock("M\n")) + clone = copy.deepcopy(original) + self.assertIs(clone.header, clone.itemList[0]) + self.assertIs(clone.middle, clone.itemList[1]) + self.assertIs(clone.footer, clone.itemList[2]) + + def test_deepcopy_emission_matches_original(self): + # toString iterates itemList. With aliasing preserved (see + # test_deepcopy_preserves_aliasing_with_itemList), the clone's + # ``itemList[0..2]`` ARE ``clone.header/middle/footer`` -- + # so emission round-trips trivially. + original = StructuredModule() + original.header.add(TextBlock("H\n")) + original.middle.add(TextBlock("M\n")) + original.footer.add(TextBlock("F\n")) + clone = copy.deepcopy(original) + self.assertEqual(str(clone), str(original)) + self.assertEqual(str(clone), "H\nM\nF\n") + + def test_deepcopy_mutation_isolation_via_itemList(self): + # Mutating original.header propagates into original's + # itemList[0] (aliased), but the clone's itemList[0] is a + # fresh deepcopy and must be untouched. + original = StructuredModule() + clone = copy.deepcopy(original) + original.header.add(TextBlock("new_in_original\n")) + self.assertEqual(str(original), "new_in_original\n") + self.assertEqual(str(clone), "") + + def test_deepcopy_mutation_via_clone_middle_does_show_up(self): + # Direct consequence of preserved aliasing: mutating + # ``clone.middle.add(...)`` MUST appear in ``str(clone)`` + # because clone.middle IS clone.itemList[1] (toString + # iterates itemList). This is the "principle of least + # surprise" behavior that motivated diverging from rocisa + # C++ here -- a fresh-ctor instance and a deepcopy'd + # instance behave identically for the same API call. + clone = copy.deepcopy(StructuredModule()) + clone.middle.add(TextBlock("yes\n")) + self.assertEqual(str(clone), "yes\n") + + +class TestStructuredModulePickleRejected(unittest.TestCase): + """``StructuredModule`` is explicitly NOT picklable -- rocisa's + nanobind binding installs a ``__reduce__`` that raises with a + class-specific message (distinct from Module's own + ``"Module is not picklable"``).""" + + def test_pickle_raises_runtime_error(self): + # ``pickle.dumps`` triggers ``__reduce__`` -- must raise. + with self.assertRaises(RuntimeError) as cm: + pickle.dumps(StructuredModule()) + self.assertEqual(str(cm.exception), "StructuredModule is not picklable") + + def test_pickle_message_distinct_from_module(self): + # Verify the message is the class-specific text, NOT the + # parent Module's ("Module is not picklable"). Any consumer + # that string-matches on the exception text relies on this + # distinction. + sm_msg = "" + try: + pickle.dumps(StructuredModule()) + except RuntimeError as e: + sm_msg = str(e) + m_msg = "" + try: + pickle.dumps(Module()) + except RuntimeError as e: + m_msg = str(e) + self.assertNotEqual(sm_msg, m_msg) + self.assertIn("StructuredModule", sm_msg) + self.assertIn("Module is not picklable", m_msg) + + +class TestStructuredModuleCountType(unittest.TestCase): + """``countType`` inherited from Module recurses through itemList, + so a fresh StructuredModule counts as 4 Modules (self + 3 sub- + modules) and 4 Items.""" + + def test_countType_Module_recurses_through_sub_modules(self): + sm = StructuredModule() + # 1 (sm itself) + 3 (header/middle/footer) = 4. + self.assertEqual(sm.countType(Module), 4) + + def test_countType_Item_includes_nested_content(self): + sm = StructuredModule() + sm.middle.add(TextBlock("x")) + # 1 (sm) + 3 (3 sub-modules) + 1 (TextBlock in middle) = 5. + self.assertEqual(sm.countType(Item), 5) + + +# =========================================================================== +# Preprocessor conditional blocks -- ValueIf / ValueElseIf / ValueEndif. +# =========================================================================== +# +# Mirror of rocisa's ``ValueIf`` / ``ValueElseIf`` / ``ValueEndif``. +# KernelWriter uses these to gate macro / kernel-text sections at +# assemble time -- byte-for-byte parity matters because the GNU +# assembler is strict about ``.if`` / ``.elseif`` / ``.endif`` +# placement, and the ``.endif`` comment alignment is the only +# difference between a clean diff and a noisy one when comparing +# adapter output to the rocisa baseline. + + +class TestValueIfConstruction(unittest.TestCase): + """``ValueIf`` ctor + toString format + Item integration.""" + + def test_construction_positional(self): + vi = ValueIf("foo == 1") + self.assertEqual(vi.value, "foo == 1") + + def test_construction_keyword(self): + # KernelWriterAssembly.py:1855 -- ``ValueIf(value="0")``. + vi = ValueIf(value="0") + self.assertEqual(vi.value, "0") + + def test_name_is_class_name_not_value(self): + # ``Item.name`` is the literal class name, NOT the condition + # expression -- KernelWriter's ``findNamedItem`` searches by + # name; this parity guarantees those searches match between + # the two backends. + self.assertEqual(ValueIf("foo == 1").name, "ValueIf") + + def test_parent_starts_none(self): + self.assertIsNone(ValueIf("x").parent) + + def test_isinstance_item(self): + # The whole point of inheriting Item in Commit Y: standard + # type-walks see ValueIf as a code-composition node. + self.assertIsInstance(ValueIf("x"), Item) + + def test_toString_format(self): + # ``".if " + value + "\\n"``. The trailing newline matters + # because Module.toString concatenates child toString() + # outputs verbatim. + self.assertEqual(ValueIf("a == b").toString(), ".if a == b\n") + + def test_toString_empty_value(self): + # C++ doesn't reject empty value; produces ".if \n". + # KernelWriter never does this in practice but parity is + # cheap so we keep it. + self.assertEqual(ValueIf("").toString(), ".if \n") + + def test_str_delegates_to_toString(self): + # Inherited Item.__str__ -> self.toString(). + self.assertEqual(str(ValueIf("k > 0")), ".if k > 0\n") + + +class TestValueElseIfConstruction(unittest.TestCase): + """``ValueElseIf`` -- mirror of ``ValueIf`` with ``.elseif`` prefix.""" + + def test_construction(self): + vei = ValueElseIf("y == 2") + self.assertEqual(vei.value, "y == 2") + self.assertEqual(vei.name, "ValueElseIf") + self.assertIsNone(vei.parent) + + def test_isinstance_item(self): + self.assertIsInstance(ValueElseIf("x"), Item) + + def test_toString_format(self): + # ``".elseif " + value + "\\n"``. + self.assertEqual( + ValueElseIf("\\useGR == 0").toString(), + ".elseif \\useGR == 0\n", + ) + + def test_str_delegates_to_toString(self): + self.assertEqual(str(ValueElseIf("a")), ".elseif a\n") + + +class TestValueEndifConstruction(unittest.TestCase): + """``ValueEndif`` -- ``.endif`` with optional trailing comment.""" + + def test_construction_default_comment(self): + # Default ``comment=""`` matches the most common KernelWriter + # call site (a bare ``ValueEndif()`` closing an .if block). + ve = ValueEndif() + self.assertEqual(ve.comment, "") + self.assertEqual(ve.name, "ValueEndif") + + def test_construction_positional_comment(self): + # KernelWriterAssembly.py:1827 -- ``ValueEndif("overflowed + # resources")``. + ve = ValueEndif("overflowed resources") + self.assertEqual(ve.comment, "overflowed resources") + + def test_construction_keyword_comment(self): + # CustomSchedule.py:493 -- ``ValueEndif(comment="EndIf %s" + # % macroGuard)``. + ve = ValueEndif(comment="EndIf foo") + self.assertEqual(ve.comment, "EndIf foo") + + def test_isinstance_item(self): + self.assertIsInstance(ValueEndif(), Item) + + +class TestValueEndifToStringFormatting(unittest.TestCase): + """ValueEndif's ``toString`` mirrors rocisa's ``formatStr`` + byte-for-byte. The padding-to-column-50 behaviour is the only + non-trivial bit in this batch; we pin it explicitly because + production-build diffs against the rocisa baseline would + otherwise show as spurious whitespace changes.""" + + def test_empty_comment_no_padding(self): + # rocisa formatStr: empty comment -> ".endif\n" with no + # padding (avoids trailing-whitespace lines). + self.assertEqual(ValueEndif().toString(), ".endif\n") + self.assertEqual(ValueEndif("").toString(), ".endif\n") + + def test_nonempty_comment_padded_to_column_50(self): + # ``.endif`` is 6 chars, so 44 spaces are appended to reach + # column 50, then ``" // closing\n"``. Total line length: + # 6 + 44 + 4 + 7 + 1 = 62 chars. + out = ValueEndif("closing").toString() + expected = ".endif" + " " * 44 + " // closing\n" + self.assertEqual(out, expected) + self.assertEqual(len(out), 62) + # The ``//`` must land at exactly column 51 (0-indexed), + # the same column rocisa instruction lines target. + self.assertEqual(out.index("//"), 51) + + def test_long_instr_no_negative_padding(self): + # The ``max(0, 50 - len)`` guard in _format_endif_str + # protects against the unlikely future case where the + # instruction string itself exceeds width 50. We exercise + # it via the private helper directly since ValueEndif's + # instr is always ``.endif`` (6 chars). + from rocisa_stinkytofu_adaptor.code import _format_endif_str + out = _format_endif_str("X" * 55, "tail") + # No padding (negative clamped to 0), so the comment is + # appended immediately after the long instr. + self.assertEqual(out, "X" * 55 + " // tail\n") + + def test_outputNoComment_suppresses_comment(self): + # When the rocIsa output-options flag is set, ValueEndif + # drops the comment AND the padding -- matches rocisa's + # ``formatStr`` ``noComment=True`` branch (falls through to + # ``formattedStr + "\n"``). + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + opts = rocIsa.getInstance().getOutputOptions() + saved = opts.outputNoComment + try: + opts.outputNoComment = True + self.assertEqual( + ValueEndif("would be suppressed").toString(), + ".endif\n", + ) + finally: + opts.outputNoComment = saved + + +class TestValueConditionalPickle(unittest.TestCase): + """Pickle round-trip preserves the single string field on each of + the three classes. Mirrors rocisa's pickle hooks which serialise + just the value/comment string.""" + + def test_valueif_pickle_round_trip(self): + original = ValueIf("count > 0") + restored = pickle.loads(pickle.dumps(original)) + self.assertIsInstance(restored, ValueIf) + self.assertEqual(restored.value, "count > 0") + self.assertEqual(restored.name, "ValueIf") + self.assertIsNone(restored.parent) + self.assertEqual(restored.toString(), original.toString()) + + def test_valueelseif_pickle_round_trip(self): + original = ValueElseIf("y == 2") + restored = pickle.loads(pickle.dumps(original)) + self.assertIsInstance(restored, ValueElseIf) + self.assertEqual(restored.value, "y == 2") + self.assertEqual(restored.toString(), original.toString()) + + def test_valueendif_pickle_round_trip_with_comment(self): + original = ValueEndif("EndIf guard") + restored = pickle.loads(pickle.dumps(original)) + self.assertIsInstance(restored, ValueEndif) + self.assertEqual(restored.comment, "EndIf guard") + self.assertEqual(restored.toString(), original.toString()) + + def test_valueendif_pickle_round_trip_default(self): + # The bare ``ValueEndif()`` case picks up the default "". + restored = pickle.loads(pickle.dumps(ValueEndif())) + self.assertEqual(restored.comment, "") + + +class TestValueConditionalDeepCopy(unittest.TestCase): + """deepcopy yields a fresh instance with the same string payload + and no shared mutable state, matching rocisa's copy ctor.""" + + def test_valueif_deepcopy(self): + original = ValueIf("x") + clone = copy.deepcopy(original) + self.assertIsNot(clone, original) + self.assertIsInstance(clone, ValueIf) + self.assertEqual(clone.value, "x") + + def test_valueelseif_deepcopy(self): + original = ValueElseIf("y") + clone = copy.deepcopy(original) + self.assertIsNot(clone, original) + self.assertEqual(clone.value, "y") + + def test_valueendif_deepcopy(self): + original = ValueEndif("c") + clone = copy.deepcopy(original) + self.assertIsNot(clone, original) + self.assertEqual(clone.comment, "c") + + +class TestValueConditionalModuleIntegration(unittest.TestCase): + """A full ``.if`` / ``.elseif`` / ``.endif`` block built inside a + Module reproduces the CustomSchedule.py:448-466 pattern. The + emitted string must concatenate the three children verbatim + (each child supplies its own trailing newline).""" + + def _build_if_elseif_endif_module(self) -> Module: + m = Module("conditional") + m.add(ValueIf("\\useGR == 1")) + m.add(ValueElseIf("\\useGR == 0")) + m.add(ValueEndif("EndIf useGR")) + return m + + def test_module_toString_concatenates_block(self): + out = str(self._build_if_elseif_endif_module()) + expected = ( + ".if \\useGR == 1\n" + ".elseif \\useGR == 0\n" + ".endif" + " " * 44 + " // EndIf useGR\n" + ) + self.assertEqual(out, expected) + + def test_reparented_on_add(self): + # Item.parent must be set to the containing Module on add() + # -- the same parent-rebind invariant exercised for TextBlock + # and sub-Modules elsewhere in this file. + m = Module() + vi = ValueIf("x") + m.add(vi) + self.assertIs(vi.parent, m) + + def test_countType_recurses_into_conditionals(self): + m = self._build_if_elseif_endif_module() + # 3 children, none of them Modules; countType(Item) on m: + # 1 (Module) + 3 (children) = 4. + self.assertEqual(m.countType(Item), 4) + # Targeting individual conditional types: + self.assertEqual(m.countType(ValueIf), 1) + self.assertEqual(m.countType(ValueElseIf), 1) + self.assertEqual(m.countType(ValueEndif), 1) + + def test_deepcopy_module_with_conditionals_preserves_block(self): + # Cloning a Module containing ValueIf/ElseIf/Endif must + # round-trip the emitted block exactly -- ParallelMap2-style + # workers rely on this if they ever decide to deepcopy a + # Module subtree (rare but legal). + m = self._build_if_elseif_endif_module() + clone = copy.deepcopy(m) + self.assertIsNot(clone, m) + self.assertEqual(str(clone), str(m)) + + +# =========================================================================== +# Symbol-emission leaves -- ValueSet / RegSet. +# =========================================================================== +# +# Mirror of rocisa's ``ValueSet`` (assembler ``.set`` directive) and +# ``RegSet`` (a ValueSet subclass that also tracks VGPR allocation +# in the rocIsa singleton for MSB-aware archs). +# matters because KernelWriter sprinkles ``.set`` lines throughout +# every kernel and any divergence (whitespace, decimal-vs-hex, +# +offset literal preservation) shows up as a noisy diff against the +# rocisa baseline. + + +class TestValueSetCtorIntPath(unittest.TestCase): + """Single Python ``__init__`` dispatches to the int-payload branch + when ``value`` is not a string. Mirrors the ``int`` and + ``uint32_t`` C++ ctors which both store the integer in ``value`` + and leave ``ref`` unset.""" + + def test_minimal_construction(self): + vs = ValueSet("foo", 42) + self.assertEqual(vs.name, "foo") + self.assertEqual(vs.value, 42) + self.assertIsNone(vs.ref) + self.assertEqual(vs.offset, 0) + self.assertEqual(vs.format, 0) + self.assertIsNone(vs.parent) + + def test_with_offset_and_format(self): + vs = ValueSet("bar", 7, 3, 1) + self.assertEqual(vs.value, 7) + self.assertEqual(vs.offset, 3) + self.assertEqual(vs.format, 1) + + def test_keyword_args_match_rocisa_names(self): + vs = ValueSet(name="baz", value=5, offset=1, format=-1) + self.assertEqual(vs.name, "baz") + self.assertEqual(vs.value, 5) + self.assertEqual(vs.offset, 1) + self.assertEqual(vs.format, -1) + + +class TestValueSetCtorRefPath(unittest.TestCase): + """``isinstance(value, str)`` discriminator routes string payloads + into the ref-payload branch (mirror of the third C++ ctor).""" + + def test_string_value_stored_as_ref(self): + vs = ValueSet("alias", "other_sym") + self.assertEqual(vs.ref, "other_sym") + self.assertIsNone(vs.value) + + def test_string_value_with_offset_and_format(self): + vs = ValueSet("alias", "other_sym", 4, 0) + self.assertEqual(vs.ref, "other_sym") + self.assertIsNone(vs.value) + self.assertEqual(vs.offset, 4) + self.assertEqual(vs.format, 0) + + +class TestValueSetToStringValuePath(unittest.TestCase): + """``.set , `` rendering for the three + ``format`` codes. Byte-for-byte match required.""" + + def test_format_default_zero_emits_value_plus_offset(self): + # ``format == 0`` -> decimal ``str(value + offset)``. + self.assertEqual(ValueSet("a", 5).toString(), ".set a, 5\n") + self.assertEqual(ValueSet("a", 5, 3).toString(), ".set a, 8\n") + + def test_format_minus_one_emits_value_alone(self): + # ``format == -1`` -> raw ``str(value)``; offset is NOT applied + # on the value path (mirror of C++ which calls + # ``std::to_string(value.value())`` directly in that branch). + self.assertEqual( + ValueSet("a", 100, 5, -1).toString(), + ".set a, 100\n", + ) + + def test_format_one_emits_hex_with_offset(self): + # ``format == 1`` -> ``"0x" + hex(value + offset)``, lowercase, + # no padding. + self.assertEqual( + ValueSet("a", 0xFF, 0, 1).toString(), + ".set a, 0xff\n", + ) + self.assertEqual( + ValueSet("a", 0x10, 0x20, 1).toString(), + ".set a, 0x30\n", + ) + self.assertEqual( + ValueSet("a", 0, 0, 1).toString(), + ".set a, 0x0\n", + ) + + def test_format_one_negative_uses_64bit_two_complement(self): + # rocisa's ``std::hex`` over int64_t prints two's-complement + # bits for negatives. ``-1`` -> ``0xffffffffffffffff``. + self.assertEqual( + ValueSet("a", -1, 0, 1).toString(), + ".set a, 0xffffffffffffffff\n", + ) + # ``-2 + 1 = -1`` after offset arithmetic. + self.assertEqual( + ValueSet("a", -2, 1, 1).toString(), + ".set a, 0xffffffffffffffff\n", + ) + + +class TestValueSetToStringRefPath(unittest.TestCase): + """``.set , `` rendering. ``format == -1`` emits + the ref alone; any other format suffixes ``+`` (including + the literal ``+0`` -- not short-circuited).""" + + def test_format_default_appends_plus_offset(self): + self.assertEqual( + ValueSet("a", "other", 7).toString(), + ".set a, other+7\n", + ) + + def test_format_zero_offset_preserves_plus_zero_literal(self): + # rocisa does NOT short-circuit ``offset == 0`` to ``ref`` + # alone -- the literal ``+0`` is preserved for byte parity. + self.assertEqual( + ValueSet("a", "other", 0).toString(), + ".set a, other+0\n", + ) + + def test_format_minus_one_emits_ref_alone(self): + self.assertEqual( + ValueSet("a", "other", 7, -1).toString(), + ".set a, other\n", + ) + + def test_format_one_on_ref_path_still_appends_offset(self): + # ``format != -1`` -> ``ref + "+" + str(offset)``. The hex + # branch is value-only; for ref+format==1 rocisa just does + # the same plain ``+offset`` decimal output. + self.assertEqual( + ValueSet("a", "other", 3, 1).toString(), + ".set a, other+3\n", + ) + + +class TestValueSetInheritance(unittest.TestCase): + """``ValueSet`` is an ``Item`` subclass so Module type-walks / + cap proxies / Item defaults all apply.""" + + def test_isinstance_item(self): + self.assertIsInstance(ValueSet("a", 1), Item) + + def test_str_goes_through_item_toString(self): + # ``str(vs)`` must equal ``vs.toString()`` (via Item.__str__). + vs = ValueSet("a", 5) + self.assertEqual(str(vs), ".set a, 5\n") + + +class TestValueSetPickle(unittest.TestCase): + """5-tuple round-trip: ``(name, ref, value, offset, format)``. + Both ref and value branches must survive the round-trip with + identical ``toString`` output.""" + + def test_int_payload_round_trip(self): + original = ValueSet("foo", 42, 3, 1) + restored = pickle.loads(pickle.dumps(original)) + self.assertEqual(restored.name, "foo") + self.assertEqual(restored.value, 42) + self.assertIsNone(restored.ref) + self.assertEqual(restored.offset, 3) + self.assertEqual(restored.format, 1) + self.assertEqual(restored.toString(), original.toString()) + + def test_ref_payload_round_trip(self): + original = ValueSet("alias", "other", 5, 0) + restored = pickle.loads(pickle.dumps(original)) + self.assertEqual(restored.ref, "other") + self.assertIsNone(restored.value) + self.assertEqual(restored.offset, 5) + self.assertEqual(restored.toString(), original.toString()) + + +class TestValueSetDeepCopy(unittest.TestCase): + def test_int_payload_independent(self): + original = ValueSet("foo", 42, 3, 1) + clone = copy.deepcopy(original) + self.assertIsNot(clone, original) + self.assertEqual(clone.value, 42) + self.assertEqual(clone.toString(), original.toString()) + + def test_ref_payload_independent(self): + original = ValueSet("foo", "other", 3) + clone = copy.deepcopy(original) + self.assertIsNot(clone, original) + self.assertEqual(clone.ref, "other") + self.assertEqual(clone.toString(), original.toString()) + + +class TestValueSetModuleIntegration(unittest.TestCase): + """Module operations (add, str, countType) treat ValueSet leaves + correctly -- parent rebind, recursive counting, concatenation.""" + + def test_reparented_on_add(self): + m = Module() + vs = ValueSet("a", 1) + m.add(vs) + self.assertIs(vs.parent, m) + + def test_module_str_concatenates_toString(self): + m = Module() + m.add(ValueSet("a", 1)) + m.add(ValueSet("b", "other", 2)) + self.assertEqual(str(m), ".set a, 1\n.set b, other+2\n") + + def test_module_countType_finds_valueset(self): + m = Module() + m.add(ValueSet("a", 1)) + m.add(ValueSet("b", 2)) + self.assertEqual(m.countType(ValueSet), 2) + # 1 (Module) + 2 (children) = 3 Items. + self.assertEqual(m.countType(Item), 3) + + +# =========================================================================== +# RegSet -- ValueSet subclass with VGPR-index side effect. +# =========================================================================== + + +class TestRegSetCtor(_VgprIdxIsolation, unittest.TestCase): + """RegSet ctor accepts ``(regType, name, int_or_str, offset=0)`` + and stores ``regType`` on top of ValueSet's fields.""" + + def test_int_payload(self): + self._caps["HasVgprMSB"] = 0 # disable side effect + rs = RegSet("s", "sgprFoo", 5) + self.assertEqual(rs.regType, "s") + self.assertEqual(rs.name, "sgprFoo") + self.assertEqual(rs.value, 5) + self.assertIsNone(rs.ref) + self.assertEqual(rs.offset, 0) + self.assertEqual(rs.format, 0) + + def test_string_payload(self): + self._caps["HasVgprMSB"] = 0 + rs = RegSet("s", "sgprFoo", "sgprOther", 3) + self.assertEqual(rs.ref, "sgprOther") + self.assertIsNone(rs.value) + self.assertEqual(rs.offset, 3) + + def test_isinstance_valueset_and_item(self): + self._caps["HasVgprMSB"] = 0 + rs = RegSet("s", "sgprFoo", 5) + self.assertIsInstance(rs, ValueSet) + self.assertIsInstance(rs, Item) + + +class TestRegSetVgprIdxSideEffect(_VgprIdxIsolation, unittest.TestCase): + """When ``regType == "v"`` AND ``HasVgprMSB == 1``, both ``__init__`` + and ``toString`` MUST refresh ``getVgprIdx()`` with the latest + binding (stripping the ``"vgpr"`` prefix from the name).""" + + def test_ctor_sets_index_int_payload(self): + self._caps["HasVgprMSB"] = 1 + RegSet("v", "vgprFoo", 5, 2) + self.assertEqual(self._base.getVgprIdx()["Foo"], 7) + + def test_ctor_sets_index_string_payload(self): + self._caps["HasVgprMSB"] = 1 + self._base.getVgprIdx()["Existing"] = 10 + # ``RegSet("v", "vgprBar", "vgprExisting", 3)`` -> looks up + # ``Existing`` (= 10) and registers ``Bar`` = 13. + RegSet("v", "vgprBar", "vgprExisting", 3) + self.assertEqual(self._base.getVgprIdx()["Bar"], 13) + + def test_ctor_string_payload_missing_key_uses_zero(self): + # Mirror of ``std::map::operator[]`` which value- + # initialises missing keys to 0 instead of throwing. + # KernelWriter's ``macroAndSet`` deliberately establishes + # ``RegSet("v", "vgprG2LA", "vgprG2LA_BASE", 0)`` BEFORE + # ``vgprG2LA_BASE`` has been registered -- rocisa silently + # treats the missing key as 0 so the alias resolves to 0+0=0. + # If we raise here, KernelWriter crashes mid-kernel. + self._caps["HasVgprMSB"] = 1 + self.assertNotIn("G2LA_BASE", self._base.getVgprIdx()) + RegSet("v", "vgprG2LA", "vgprG2LA_BASE", 0) + self.assertEqual(self._base.getVgprIdx()["G2LA"], 0) + # Lookup of a missing key MUST NOT auto-insert into the live + # map (rocisa looks up in a copy of the map, so the singleton + # is unaffected). + self.assertNotIn("G2LA_BASE", self._base.getVgprIdx()) + + def test_ctor_string_payload_missing_key_with_offset(self): + # Same parity rule but with a non-zero offset -- missing key + # contributes 0 to the sum, offset survives. + self._caps["HasVgprMSB"] = 1 + RegSet("v", "vgprTarget", "vgprMissing", 7) + self.assertEqual(self._base.getVgprIdx()["Target"], 7) + + def test_toString_re_triggers_setIdx(self): + # Construct RegSet, mutate the in-memory map, call toString, + # and verify the map was refreshed back to the RegSet's view + # of the world. + self._caps["HasVgprMSB"] = 1 + rs = RegSet("v", "vgprFoo", 5, 2) + self.assertEqual(self._base.getVgprIdx()["Foo"], 7) + # External mutation -- simulate a stale snapshot. + self._base.getVgprIdx()["Foo"] = 999 + # toString must re-set to 7 (mirror of C++ which calls setIdx + # at the top of toString). + rs.toString() + self.assertEqual(self._base.getVgprIdx()["Foo"], 7) + + def test_toString_output_does_not_include_regType(self): + # The side-effecting toString delegates string formatting to + # ValueSet.toString -- regType must NOT appear in the output. + self._caps["HasVgprMSB"] = 1 + rs = RegSet("v", "vgprFoo", 5) + self.assertEqual(rs.toString(), ".set vgprFoo, 5\n") + + +class TestRegSetNoSideEffectWhenDisabled(_VgprIdxIsolation, unittest.TestCase): + """The side effect is gated on BOTH ``regType == "v"`` AND + ``HasVgprMSB``; missing either skips the index update.""" + + def test_sgpr_does_not_set_index(self): + self._caps["HasVgprMSB"] = 1 + before = dict(self._base.getVgprIdx()) + rs = RegSet("s", "sgprFoo", 5) + rs.toString() + self.assertEqual(self._base.getVgprIdx(), before) + + def test_vgpr_without_HasVgprMSB_does_not_set_index(self): + self._caps["HasVgprMSB"] = 0 + before = dict(self._base.getVgprIdx()) + rs = RegSet("v", "vgprFoo", 5) + rs.toString() + self.assertEqual(self._base.getVgprIdx(), before) + + def test_missing_HasVgprMSB_key_is_safe(self): + # Missing key must behave the same as ``0`` (matches C++ + # ``std::map[]`` value-initialisation). + self._caps.pop("HasVgprMSB", None) + before = dict(self._base.getVgprIdx()) + rs = RegSet("v", "vgprFoo", 5) + rs.toString() + self.assertEqual(self._base.getVgprIdx(), before) + + +class TestRegSetPickle(_VgprIdxIsolation, unittest.TestCase): + """6-tuple round-trip ``(regType, name, ref, value, offset, format)``. + ``format`` is preserved even though the ctor does not accept it + (mirror of C++ which does ``self.format = std::get<5>(t)`` after + placement-new).""" + + def test_int_payload_round_trip_preserves_format(self): + self._caps["HasVgprMSB"] = 0 + original = RegSet("s", "sgprFoo", 5, 2) + original.format = 1 # mutate post-ctor + restored = pickle.loads(pickle.dumps(original)) + self.assertEqual(restored.regType, "s") + self.assertEqual(restored.name, "sgprFoo") + self.assertEqual(restored.value, 5) + self.assertIsNone(restored.ref) + self.assertEqual(restored.offset, 2) + self.assertEqual(restored.format, 1) + self.assertEqual(restored.toString(), original.toString()) + + def test_ref_payload_round_trip(self): + self._caps["HasVgprMSB"] = 0 + original = RegSet("s", "sgprFoo", "sgprOther", 3) + restored = pickle.loads(pickle.dumps(original)) + self.assertEqual(restored.regType, "s") + self.assertEqual(restored.ref, "sgprOther") + self.assertIsNone(restored.value) + self.assertEqual(restored.toString(), original.toString()) + + def test_round_trip_re_fires_setIdx_on_HasVgprMSB(self): + # ``__setstate__`` MUST re-fire the side effect (matches C++ + # which restores via placement-new of the RegSet ctor). + self._caps["HasVgprMSB"] = 1 + original = RegSet("v", "vgprFoo", 5, 2) + # Clear the map so we can observe the restore re-populating it. + self._base.getVgprIdx().clear() + pickle.loads(pickle.dumps(original)) + self.assertEqual(self._base.getVgprIdx()["Foo"], 7) + + +class TestRegSetDeepCopy(_VgprIdxIsolation, unittest.TestCase): + def test_int_payload_independent_with_format_preserved(self): + self._caps["HasVgprMSB"] = 0 + original = RegSet("s", "sgprFoo", 5, 2) + original.format = 1 + clone = copy.deepcopy(original) + self.assertIsNot(clone, original) + self.assertEqual(clone.format, 1) + self.assertEqual(clone.toString(), original.toString()) + + def test_ref_payload_independent(self): + self._caps["HasVgprMSB"] = 0 + original = RegSet("v", "vgprFoo", "vgprOther", 1) + clone = copy.deepcopy(original) + self.assertEqual(clone.regType, "v") + self.assertEqual(clone.ref, "vgprOther") + + +class TestRegSetModuleIntegration(_VgprIdxIsolation, unittest.TestCase): + """Mix RegSet leaves into a Module tree and verify str / countType / + parent rebind behave like any other Item subclass.""" + + def test_reparented_on_add(self): + self._caps["HasVgprMSB"] = 0 + m = Module() + rs = RegSet("s", "sgprFoo", 5) + m.add(rs) + self.assertIs(rs.parent, m) + + def test_module_str_concatenates_regset_lines(self): + self._caps["HasVgprMSB"] = 0 + m = Module() + m.add(RegSet("v", "vgprA", 0)) + m.add(RegSet("v", "vgprB", "vgprA", 1)) + self.assertEqual( + str(m), + ".set vgprA, 0\n.set vgprB, vgprA+1\n", + ) + + def test_countType_distinguishes_regset_from_valueset(self): + # ``countType`` matches by isinstance; RegSet IS a ValueSet so + # counting ValueSet must include RegSets too. Counting RegSet + # alone must NOT include the plain ValueSet. + self._caps["HasVgprMSB"] = 0 + m = Module() + m.add(ValueSet("a", 1)) + m.add(RegSet("s", "sgprB", 2)) + m.add(RegSet("v", "vgprC", 3)) + self.assertEqual(m.countType(ValueSet), 3) + self.assertEqual(m.countType(RegSet), 2) + # ``countExactType`` must distinguish by exact class -- only + # the plain ValueSet matches when targeting ValueSet exactly. + self.assertEqual(m.countExactType(ValueSet), 1) + self.assertEqual(m.countExactType(RegSet), 2) + + +# =========================================================================== +# Label -- branch / loop target leaf (real implementation). +# =========================================================================== +# +# Mirror of ``rocisa::Label``. Tests cover the int / str payload +# variants, the static ``getFormatting`` helper, the ``getLabelName`` +# accessor, ``toString`` formatting (alignment prefix, comment +# suffix, ``outputNoComment`` gating), the ``HasVgprMSB`` side +# effect (resets ``setVgprMsb(-1)`` after emission), and the +# pickle / deepcopy / Module-integration round-trips. + + +class TestLabelConstruction(unittest.TestCase): + """``Label(label, comment, alignment=1)`` stores the three fields + verbatim and leaves ``Item.name`` empty (rocisa's ``Item("")``).""" + + def test_int_label(self): + lbl = Label(5, "") + self.assertEqual(lbl.label, 5) + self.assertEqual(lbl.comment, "") + self.assertEqual(lbl.alignment, 1) + # Item.name is the EMPTY STRING -- the textual identity comes + # from getLabelName(), not from Item.name. Critical for + # rocisa's ``findNamedItem`` semantics: a Label is NEVER + # findable by its label payload through findNamedItem. + self.assertEqual(lbl.name, "") + + def test_string_label(self): + lbl = Label("foo", "bar") + self.assertEqual(lbl.label, "foo") + self.assertEqual(lbl.comment, "bar") + self.assertEqual(lbl.alignment, 1) + + def test_explicit_alignment(self): + lbl = Label(5, "x", 8) + self.assertEqual(lbl.alignment, 8) + + def test_keyword_args(self): + # rocisa's nanobind binding (code.cpp:108-116) names the args + # ``label`` / ``comment`` / ``alignment`` -- KernelWriter + # (KernelWriterAssembly.py:2139) uses keyword form, so we + # must accept it. + lbl = Label(label="foo", comment="bar", alignment=4) + self.assertEqual(lbl.label, "foo") + self.assertEqual(lbl.comment, "bar") + self.assertEqual(lbl.alignment, 4) + + def test_is_item_subclass(self): + self.assertIsInstance(Label(0, ""), Item) + + +class TestLabelGetFormatting(unittest.TestCase): + """Static ``Label.getFormatting`` formats an ``int | str`` payload + into ``label_``. Mirror of code.hpp:87-103.""" + + def test_int_payload(self): + self.assertEqual(Label.getFormatting(5), "label_5") + + def test_string_payload(self): + self.assertEqual(Label.getFormatting("foo"), "label_foo") + + def test_negative_int_payload(self): + # Negative int must NOT lose its sign -- the format string + # uses the default ``__format__`` which preserves it. + self.assertEqual(Label.getFormatting(-1), "label_-1") + + +class TestLabelGetLabelName(unittest.TestCase): + """``Label.getLabelName`` is the public accessor branch + instructions reference -- forwards to ``getFormatting(self.label)``.""" + + def test_int_payload(self): + self.assertEqual(Label(5, "").getLabelName(), "label_5") + + def test_string_payload(self): + self.assertEqual(Label("foo", "").getLabelName(), "label_foo") + + +class TestLabelToString(_VgprMsbIsolation, unittest.TestCase): + """``Label.toString`` produces ``[.align \\n]label_:[ /// ]\\n``. + + Mirror of code.hpp:110-127. Caps init is required because the + method reads ``getAsmCaps()["HasVgprMSB"]``; we suppress the + side effect for these formatting-focused tests by forcing the + cap to 0. + """ + + def setUp(self) -> None: + super().setUp() + # Disable the MSB side effect for pure formatting tests -- + # it's covered separately in TestLabelMsbSideEffect. + self._caps["HasVgprMSB"] = 0 + + def test_basic_int(self): + self.assertEqual(Label(5, "").toString(), "label_5:\n") + + def test_basic_string(self): + self.assertEqual(Label("L", "").toString(), "label_L:\n") + + def test_with_comment(self): + # Two-space indent + ``///`` + single space + comment text, + # no padding (unlike ValueEndif which width-pads to col 50). + self.assertEqual(Label(5, "hi").toString(), "label_5: /// hi\n") + + def test_alignment_one_emits_no_prefix(self): + # ``alignment == 1`` is the default and is treated as "no + # prefix" -- the ``.align 1\n`` line is suppressed because + # alignment-of-1 is a no-op assembler directive. + self.assertEqual(Label(5, "", 1).toString(), "label_5:\n") + + def test_alignment_only(self): + # ``alignment > 1`` prepends ``.align \n`` to the label + # line. KernelWriter uses this for cache-line-aligned loop + # entries. + self.assertEqual(Label(5, "", 8).toString(), ".align 8\nlabel_5:\n") + + def test_alignment_and_comment(self): + self.assertEqual( + Label(5, "hi", 8).toString(), + ".align 8\nlabel_5: /// hi\n", + ) + + def test_outputNoComment_suppresses_comment(self): + # ``rocIsa.outputNoComment=True`` blanket-suppresses the + # `` /// `` suffix while keeping the label line + # itself. Production builds rely on this to scrub + # human-readable annotations from the emitted asm. + from rocisa_stinkytofu_adaptor import rocIsa # noqa: WPS433 + opts = rocIsa.getInstance().getOutputOptions() + saved = opts.outputNoComment + try: + opts.outputNoComment = True + self.assertEqual(Label(5, "hi").toString(), "label_5:\n") + # Alignment is NOT a comment, must still be emitted. + self.assertEqual( + Label(5, "hi", 8).toString(), + ".align 8\nlabel_5:\n", + ) + finally: + opts.outputNoComment = saved + + def test_str_dunder_equals_toString(self): + # ``Module.toString`` concatenates ``str(it)``; ``__str__`` + # must therefore go through Item.__str__ -> toString to match + # rocisa's binding (code.cpp:122). + lbl = Label(5, "hi") + self.assertEqual(str(lbl), lbl.toString()) + + +class TestLabelMsbSideEffect(_VgprMsbIsolation, unittest.TestCase): + """``Label.toString`` resets ``setVgprMsb(-1)`` on a HasVgprMSB + arch -- mirror of code.hpp:122-125. The semantic is "emitting a + label means we're entering a new basic block whose entry MSB is + not knowable", so callers must NOT rely on the pre-toString MSB + surviving the call.""" + + def test_msb_resets_when_HasVgprMSB(self): + self._caps["HasVgprMSB"] = 1 + self._base.setVgprMsb(7) + Label(5, "").toString() + self.assertEqual(self._base.getVgprMsb(), -1) + + def test_msb_unchanged_when_no_HasVgprMSB(self): + # Cap absent / zero ⇒ no side effect. The pre-toString MSB + # value MUST round-trip unchanged. + self._caps["HasVgprMSB"] = 0 + self._base.setVgprMsb(7) + Label(5, "").toString() + self.assertEqual(self._base.getVgprMsb(), 7) + + def test_msb_unchanged_before_toString(self): + # Reading ``label.label`` / ``label.comment`` / etc. must NOT + # trigger the side effect -- only ``toString`` does. + self._caps["HasVgprMSB"] = 1 + self._base.setVgprMsb(7) + lbl = Label(5, "") + # Touch attributes; verify MSB still 7. + _ = (lbl.label, lbl.comment, lbl.alignment, lbl.getLabelName()) + self.assertEqual(self._base.getVgprMsb(), 7) + + +class TestLabelDeepCopy(unittest.TestCase): + """``copy.deepcopy(label)`` produces an isolated clone with every + field preserved. Mirror of code.cpp:123-127.""" + + def test_deepcopy_preserves_fields(self): + original = Label("foo", "bar", 8) + clone = copy.deepcopy(original) + self.assertEqual(clone.label, "foo") + self.assertEqual(clone.comment, "bar") + self.assertEqual(clone.alignment, 8) + + def test_deepcopy_independent(self): + # Mutating the clone must NOT bleed back into the original. + original = Label(5, "hi", 4) + clone = copy.deepcopy(original) + clone.label = 99 + clone.comment = "changed" + clone.alignment = 16 + self.assertEqual(original.label, 5) + self.assertEqual(original.comment, "hi") + self.assertEqual(original.alignment, 4) + + def test_deepcopy_preserves_patched_name(self): + # Item.name defaults to "" but a caller may patch it post- + # construction; deepcopy must round-trip the patched value. + original = Label(5, "hi") + original.name = "custom_name" + clone = copy.deepcopy(original) + self.assertEqual(clone.name, "custom_name") + + +class TestLabelPickle(unittest.TestCase): + """Pickle round-trip uses the 4-tuple ``(name, label, comment, + alignment)`` shape from rocisa's ``__getstate__`` / + ``__setstate__`` (code.cpp:128-138).""" + + def test_pickle_int_payload(self): + original = Label(5, "hi", 4) + clone = pickle.loads(pickle.dumps(original)) + self.assertEqual(clone.label, 5) + self.assertEqual(clone.comment, "hi") + self.assertEqual(clone.alignment, 4) + self.assertEqual(clone.name, "") + + def test_pickle_string_payload(self): + original = Label("foo", "bar", 1) + clone = pickle.loads(pickle.dumps(original)) + self.assertEqual(clone.label, "foo") + self.assertEqual(clone.comment, "bar") + self.assertEqual(clone.alignment, 1) + + def test_pickle_preserves_patched_name(self): + # ``__setstate__`` calls ``__init__`` (which resets name="") + # then patches name -- mirrors rocisa's placement-new + + # post-patch pattern. The tuple's name field must round-trip. + original = Label(5, "hi") + original.name = "custom" + clone = pickle.loads(pickle.dumps(original)) + self.assertEqual(clone.name, "custom") + + +class TestLabelModuleIntegration(_VgprMsbIsolation, unittest.TestCase): + """Real Label inside a real Module -- emission, parent linkage, + and findIndexByType all behave like any other Item.""" + + def setUp(self) -> None: + super().setUp() + self._caps["HasVgprMSB"] = 0 # disable side effect + + def test_module_str_concatenates_label_lines(self): + m = Module() + m.add(Label("loop_top", "")) + m.add(TextBlock("v_mov_b32 v0, 0\n")) + m.add(Label(2, "exit", 8)) + self.assertEqual( + str(m), + "label_loop_top:\nv_mov_b32 v0, 0\n.align 8\nlabel_2: /// exit\n", + ) + + def test_label_parent_set_after_module_add(self): + # Module.add patches child.parent. Critical for Item walks + # that rely on upward navigation (e.g. tree-prefixed + # prettyPrint). + m = Module() + lbl = Label(5, "") + m.add(lbl) + self.assertIs(lbl.parent, m) + + def test_findIndexByType_finds_label(self): + # ``findIndexByType(Label)`` must locate the real Label and + # NOT confuse it with siblings of a different concrete type. + m = Module() + m.add(TextBlock("a")) + m.add(Label("L", "")) + m.add(TextBlock("b")) + self.assertEqual(m.findIndexByType(Label), 1) + + +# =========================================================================== +# Macro -- ``.macro args ... .endm`` block. +# =========================================================================== +# +# KernelWriter constructs 4 macros (KWA ``GLOBAL_OFFSET_*``, ``MAC_*``, +# ``MAC_*_OneIUI``; Components/CustomSchedule.py ``MAINLOOP``). Each +# is filled with CommonInstruction / Module / TextBlock children, then +# attached to a parent Module via ``module.add(macro)``. + + +class TestMacroConstruction(unittest.TestCase): + def test_basic_construction(self): + mc = Macro("GLOBAL_OFFSET_A", ["vgprAddr:req", "vgprTmp:req"]) + self.assertEqual(mc.name, "GLOBAL_OFFSET_A") + self.assertEqual(mc.itemList, []) + # Internal header object type; field parity is in + # ``test_instruction.TestMacroInstruction*``. + from rocisa_stinkytofu_adaptor.instruction import MacroInstruction + self.assertIsInstance(mc.macro, MacroInstruction) + + def test_empty_args(self): + # KWA:1774 pattern: ``Macro("MAC_...", [])``. + mc = Macro("MAC_4x4_X0", []) + self.assertEqual(mc.macro.args, []) + + def test_inherits_Item(self): + mc = Macro("X", []) + self.assertIsInstance(mc, Item) + + def test_is_NOT_Module_subclass(self): + # rocisa::Macro inherits from Item directly, NOT from Module. + # KernelWriter type-walks rely on this distinction so e.g. + # ``Macro`` doesn't get picked up by ``findIndexByType(Module)``. + mc = Macro("X", []) + self.assertNotIsInstance(mc, Module) + + +class TestMacroAdd(unittest.TestCase): + def test_add_TextBlock(self): + mc = Macro("X", []) + tb = TextBlock("inst\n") + ret = mc.add(tb) + self.assertIs(ret, tb) + self.assertEqual(mc.itemList, [tb]) + self.assertIs(tb.parent, mc) + + def test_add_Module(self): + mc = Macro("X", []) + inner = Module("inner") + mc.add(inner) + self.assertIs(inner.parent, mc) + + def test_add_ValueIf_ValueElseIf_ValueEndif(self): + # Whitelist includes the value-conditional family. + mc = Macro("X", []) + for it in [ValueIf("1"), ValueElseIf("2"), ValueEndif()]: + mc.add(it) + self.assertEqual(len(mc.itemList), 3) + + def test_add_Instruction_subclass(self): + # CommonInstruction / MacroInstruction subclasses must be + # accepted -- this is the common case (KernelWriter feeds + # macros V* / S* instructions). + from rocisa_stinkytofu_adaptor.instruction import ( + MacroInstruction, VMovB32, + ) + from rocisa_stinkytofu_adaptor.container import vgpr + mc = Macro("X", []) + mc.add(VMovB32(vgpr(0), vgpr(1))) + mc.add(MacroInstruction("Y", [1])) + self.assertEqual(len(mc.itemList), 2) + + def test_add_unknown_type_raises(self): + # rocisa throws "unknown item type for Macro.add: ...". + mc = Macro("X", []) + with self.assertRaises(RuntimeError) as ctx: + mc.add("just a string") + self.assertIn("unknown item type for Macro.add", str(ctx.exception)) + + def test_add_int_raises(self): + mc = Macro("X", []) + with self.assertRaises(RuntimeError): + mc.add(42) + + +class TestMacroAddComment0(unittest.TestCase): + """Single-line ``/* ... */\\n``, distinct from Module's 3-line banner.""" + + def test_addComment0_single_line(self): + mc = Macro("X", []) + mc.addComment0("hello") + self.assertEqual(len(mc.itemList), 1) + tb = mc.itemList[0] + self.assertIsInstance(tb, TextBlock) + self.assertEqual(tb.text, "/* hello */\n") + + def test_addComment0_same_as_Module_addComment0(self): + # Both Module and Macro addComment0 now produce single-line + # block comments matching native format.hpp::block(). + mc = Macro("X", []) + mc.addComment0("hi") + mod = Module() + mod.addComment0("hi") + self.assertEqual(mc.itemList[0].text, mod.itemList[0].text) + + +class TestMacroSetItems(unittest.TestCase): + def test_setItems_replaces_list(self): + mc = Macro("X", []) + mc.add(TextBlock("a")) + new_items = [TextBlock("b"), TextBlock("c")] + mc.setItems(new_items) + self.assertEqual(mc.itemList, new_items) + + def test_setItems_does_NOT_reparent(self): + # rocisa C++ does plain assignment; no type check and no parent + # update. Mirror exactly so SetItems is a fast no-frills swap + # (callers that care must reparent manually). + mc = Macro("X", []) + tb = TextBlock("a") + mc.setItems([tb]) + self.assertIsNone(tb.parent) # NOT reparented + + +class TestMacroToString(unittest.TestCase): + """Byte-parity with ``rocisa::Macro::toString``.""" + + def test_empty_macro(self): + mc = Macro("X", []) + self.assertEqual(str(mc), ".macro X\n.endm\n") + + def test_macro_with_args_only(self): + mc = Macro("GLOBAL_OFFSET_A", ["vgprAddr:req", "vgprTmp:req"]) + self.assertEqual( + str(mc), + ".macro GLOBAL_OFFSET_A vgprAddr:req, vgprTmp:req\n.endm\n", + ) + + def test_macro_with_text_children_indented(self): + # Each child line is prefixed with 4 spaces. + mc = Macro("X", []) + mc.add(TextBlock("v_add v0, v1, v2\n")) + mc.add(TextBlock("v_mul v3, v4, v5\n")) + expected = ( + ".macro X\n" + " v_add v0, v1, v2\n" + " v_mul v3, v4, v5\n" + ".endm\n" + ) + self.assertEqual(str(mc), expected) + + def test_s_set_vgpr_msb_quirk_extra_indent(self): + # rocisa C++ hack: any child whose toString contains + # ``s_set_vgpr_msb`` gets an EXTRA 4-space indent inserted + # right after the first newline (so subsequent body lines + # stay visually aligned with auto-inserted MSB toggles). + mc = Macro("foo", []) + mc.add(TextBlock("s_set_vgpr_msb 1\nactual_inst v0, v1\n")) + expected = ( + ".macro foo\n" + " s_set_vgpr_msb 1\n" + " actual_inst v0, v1\n" + ".endm\n" + ) + self.assertEqual(str(mc), expected) + + def test_addComment0_renders_inside_macro_body(self): + mc = Macro("X", []) + mc.addComment0("note") + expected = ".macro X\n /* note */\n.endm\n" + self.assertEqual(str(mc), expected) + + +class TestMacroPrettyPrint(unittest.TestCase): + def test_prettyPrint_header_and_children(self): + mc = Macro("MyMacro", []) + mc.add(TextBlock("inst\n")) + out = mc.prettyPrint() + # Header: `Macro "MyMacro"\n`, then child via `|--` prefix. + self.assertIn('Macro "MyMacro"', out) + self.assertIn("|--", out) + + +class TestMacroDeepCopy(unittest.TestCase): + def test_deepcopy_independent(self): + mc = Macro("X", ["a:req"]) + mc.add(TextBlock("inst\n")) + c = copy.deepcopy(mc) + # Mutating clone must not affect original. + c.add(TextBlock("extra\n")) + self.assertEqual(len(mc.itemList), 1) + self.assertEqual(len(c.itemList), 2) + + def test_deepcopy_clones_header_macro(self): + # The internal ``self.macro`` (a MacroInstruction) must be deep- + # cloned -- mutating clone's macro args must not bleed in. + mc = Macro("X", [1, 2]) + c = copy.deepcopy(mc) + c.macro.args.append(99) + self.assertEqual(mc.macro.args, [1, 2]) + self.assertEqual(c.macro.args, [1, 2, 99]) + + def test_deepcopy_reparents_children(self): + # Cloned children's parent must point to the clone, not the + # original (matches rocisa C++ parent-fixup). + mc = Macro("X", []) + mc.add(TextBlock("inst\n")) + c = copy.deepcopy(mc) + self.assertIs(c.itemList[0].parent, c) + self.assertIsNot(c.itemList[0].parent, mc) + + def test_deepcopy_preserves_subclass(self): + mc = Macro("X", []) + c = copy.deepcopy(mc) + self.assertIs(type(c), Macro) + + +class TestMacroPickleRejected(unittest.TestCase): + def test_pickle_raises(self): + import pickle + mc = Macro("X", []) + with self.assertRaises(RuntimeError): + pickle.dumps(mc) + + +class TestMacroModuleIntegration(unittest.TestCase): + """KernelWriter pattern: ``module.add(macro)`` -- the Macro itself + is an Item, so a parent Module can carry it like any other child.""" + + def test_macro_added_to_module(self): + mod = Module("kernel") + mc = Macro("GLOBAL_OFFSET_A", ["vgprAddr:req"]) + mc.add(TextBlock("v_add v0, v1, v2\n")) + mod.add(mc) + # Module parented the macro; rendering nests the .macro block + # inside the module's flat join. + self.assertIs(mc.parent, mod) + s = str(mod) + self.assertIn(".macro GLOBAL_OFFSET_A", s) + self.assertIn(".endm", s) + + def test_macro_survives_module_deepcopy(self): + # When a parent Module is deepcopied, the nested Macro must + # come along (with its own internal header MacroInstruction). + mod = Module() + mod.add(Macro("X", [1])) + c = copy.deepcopy(mod) + self.assertIsInstance(c.itemList[0], Macro) + self.assertEqual(c.itemList[0].macro.args, [1]) + + +# =========================================================================== +# SignatureCodeMeta / SignatureBase +# =========================================================================== + + +def _make_signature_code_meta() -> SignatureCodeMeta: + return SignatureCodeMeta( + "k", + kernArgsVersion=1, + groupSegSize=256, + flatWgSize=64, + codeObjectVersion="4", + ) + + +def _make_signature_base(**kwargs) -> SignatureBase: + defaults = dict( + kernelName="k", + kernArgsVersion=1, + codeObjectVersion="4", + groupSegmentSize=256, + sgprWorkGroup=(1, 1, 0), + vgprWorkItem=0, + flatWorkGroupSize=64, + ) + defaults.update(kwargs) + return SignatureBase(**defaults) + + +class TestSignatureCodeMetaConstruction(unittest.TestCase): + def test_is_item_subclass(self): + meta = _make_signature_code_meta() + self.assertIsInstance(meta, Item) + + def test_ctor_fields(self): + meta = _make_signature_code_meta() + self.assertEqual(meta.name, "k") + self.assertEqual(meta.kernArgsVersion, 1) + self.assertEqual(meta.groupSegSize, 256) + self.assertEqual(meta.flatWgSize, 64) + self.assertEqual(meta.codeObjectVersion, "4") + self.assertEqual(meta.offset, 0) + self.assertEqual(meta.argList, []) + + +class TestSignatureCodeMetaAddArg(_SignatureKernelSetup, unittest.TestCase): + def test_add_arg_accumulates_offset(self): + meta = _make_signature_code_meta() + meta.addArg("alpha", SVK.SIG_VALUE, "f32") + meta.addArg("D", SVK.SIG_GLOBALBUFFER, "f32", "generic") + self.assertEqual(len(meta.argList), 2) + self.assertEqual(meta.argList[0].offset, 0) + self.assertEqual(meta.argList[0].size, 4) + self.assertEqual(meta.argList[1].offset, 4) + self.assertEqual(meta.argList[1].size, 8) + self.assertEqual(meta.offset, 12) + + def test_unknown_value_type_raises(self): + meta = _make_signature_code_meta() + with self.assertRaises(RuntimeError): + meta.addArg("x", SVK.SIG_VALUE, "not_a_type") + + +class TestSignatureCodeMetaSetGprs(_SignatureKernelSetup, unittest.TestCase): + def test_set_gprs_updates_counts(self): + meta = _make_signature_code_meta() + meta.setGprs(40, 20) + s = meta.toString() + self.assertIn(".vgpr_count: 40", s) + self.assertIn(".sgpr_count: 20", s) + + +class TestSignatureCodeMetaToString(_SignatureKernelSetup, unittest.TestCase): + def test_metadata_header_and_kernarg_align(self): + meta = _make_signature_code_meta() + meta.addArg("numWG", SVK.SIG_VALUE, "u32") + s = meta.toString() + self.assertTrue(s.startswith(".amdgpu_metadata\n")) + self.assertIn("KernArgsVersion: 1", s) + self.assertIn("amdhsa.version:\n - 1\n - 1\n", s) + self.assertIn(".kernarg_segment_size: 8\n", s) + self.assertIn(".wavefront_size: 64\n", s) + self.assertTrue(s.endswith("k:\n")) + + def test_code_object_version_five(self): + meta = SignatureCodeMeta("k", 1, 0, 64, "5") + s = meta.toString() + self.assertIn("amdhsa.version:\n - 1\n - 2\n", s) + + +class TestSignatureCodeMetaCopyRejected(unittest.TestCase): + def test_deepcopy_raises(self): + meta = _make_signature_code_meta() + with self.assertRaises(RuntimeError): + copy.deepcopy(meta) + + def test_pickle_raises(self): + meta = _make_signature_code_meta() + with self.assertRaises(RuntimeError): + pickle.dumps(meta) + + +class TestSignatureBaseConstruction(unittest.TestCase): + def test_is_item_subclass(self): + sig = _make_signature_base() + self.assertIsInstance(sig, Item) + + def test_composes_descriptor_and_meta(self): + sig = _make_signature_base(kernelName="my_k") + self.assertEqual(sig.name, "my_k") + self.assertEqual(sig.kernelDescriptor.name, "my_k") + self.assertEqual(sig.codeMeta.name, "my_k") + + +class TestSignatureBaseSetGprs(_SignatureKernelSetup, unittest.TestCase): + def test_set_gprs_syncs_both_children(self): + sig = _make_signature_base() + sig.setGprs(32, 4, 16) + self.assertEqual(sig.getNextFreeVgpr(), 32) + self.assertEqual(sig.getNextFreeSgpr(), 16) + s = sig.toString() + self.assertIn(".amdhsa_next_free_vgpr 32 // vgprs", s) + self.assertIn(".vgpr_count: 32", s) + + +class TestSignatureBaseAddArg(_SignatureKernelSetup, unittest.TestCase): + def test_add_arg_delegates_to_code_meta(self): + sig = _make_signature_base() + sig.addArg("A", SVK.SIG_GLOBALBUFFER, "f32", "generic") + self.assertEqual(len(sig.codeMeta.argList), 1) + self.assertIn("- .name: A", sig.toString()) + + +class TestSignatureBaseDescriptions(_SignatureKernelSetup, unittest.TestCase): + def test_description_helpers_emit_in_order(self): + sig = _make_signature_base() + sig.addDescriptionTopic("Optimizations and Config:") + sig.addDescriptionBlock("ThreadTile= 8 x 8") + s = sig.toString() + self.assertIn("/* Optimizations and Config:", s) + self.assertIn("/* ThreadTile= 8 x 8 */", s) + + sig.addDescription("tail note") + sig.clearDescription() + sig.addDescriptionBlock("after clear") + s2 = sig.toString() + self.assertNotIn("tail note", s2) + self.assertNotIn("ThreadTile= 8 x 8", s2) + self.assertIn("/* after clear */", s2) + self.assertIn("Optimizations and Config:", s2) + + +class TestSignatureBaseToString(_SignatureKernelSetup, unittest.TestCase): + def test_smoke_matches_rocisa_test_shape(self): + sig = SignatureBase( + kernelName="123", + kernArgsVersion=1, + codeObjectVersion="4", + groupSegmentSize=256, + sgprWorkGroup=(1, 1, 100), + vgprWorkItem=1, + flatWorkGroupSize=256, + numSgprPreload=16, + ) + s = str(sig) + self.assertIn('.amdgcn_target "amdgcn-amd-amdhsa--gfx1250"', s) + self.assertIn(".protected 123", s) + self.assertIn(".amdhsa_user_sgpr_count 18\n", s) + self.assertIn(".amdhsa_user_sgpr_kernarg_preload_length 16\n", s) + self.assertIn(".amdhsa_user_sgpr_kernarg_preload_offset 0\n", s) + self.assertIn(".amdgpu_metadata", s) + self.assertIn("123:\n", s) + + def test_num_sgpr_preload_zero_omits_preload_lines(self): + sig = _make_signature_base(numSgprPreload=0) + s = str(sig) + self.assertNotIn(".amdhsa_user_sgpr_kernarg_preload_length", s) + self.assertNotIn(".amdhsa_user_sgpr_count", s) + + +class TestSignatureBaseCopyRejected(unittest.TestCase): + def test_deepcopy_raises(self): + sig = _make_signature_base() + with self.assertRaises(RuntimeError): + copy.deepcopy(sig) + + def test_pickle_raises(self): + sig = _make_signature_base() + with self.assertRaises(RuntimeError): + pickle.dumps(sig) + + +# =========================================================================== +# KernelBody +# =========================================================================== + + +def _make_kernel_body(**kwargs) -> KernelBody: + name = kwargs.pop("name", "kernelBody") + return KernelBody(name) + + +class TestKernelBodyConstruction(unittest.TestCase): + def test_is_item_subclass(self): + kb = _make_kernel_body() + self.assertIsInstance(kb, Item) + + def test_ctor_sets_name_and_defaults(self): + kb = KernelBody("kernelBody") + self.assertEqual(kb.name, "kernelBody") + self.assertIsNone(kb.signature) + self.assertIsNone(kb.body) + self.assertEqual(kb.totalVgprs, 0) + self.assertEqual(kb.totalAgprs, 0) + self.assertEqual(kb.totalSgprs, 0) + + +class TestKernelBodyAddSignatureAndBody(_SignatureKernelSetup, unittest.TestCase): + def test_add_signature_and_body(self): + kb = _make_kernel_body() + sig = _make_signature_base() + body = Module("body") + body.add(TextBlock("s_nop 0\n")) + kb.addSignature(sig) + kb.addBody(body) + self.assertIs(kb.signature, sig) + self.assertIs(kb.body, body) + + def test_body_rw_attribute(self): + kb = _make_kernel_body() + body = Module("body") + kb.body = body + self.assertIs(kb.body, body) + + +class TestKernelBodySetGprs(_SignatureKernelSetup, unittest.TestCase): + def test_set_gprs_updates_fields_and_signature(self): + kb = _make_kernel_body() + kb.addSignature(_make_signature_base()) + kb.setGprs(48, 4, 20) + self.assertEqual(kb.totalVgprs, 48) + self.assertEqual(kb.totalAgprs, 4) + self.assertEqual(kb.totalSgprs, 20) + self.assertEqual(kb.getNextFreeVgpr(), 48) + self.assertEqual(kb.getNextFreeSgpr(), 20) + + def test_get_next_free_without_signature_returns_zero(self): + kb = _make_kernel_body() + self.assertEqual(kb.getNextFreeVgpr(), 0) + self.assertEqual(kb.getNextFreeSgpr(), 0) + + +class TestKernelBodyToString(_SignatureKernelSetup, unittest.TestCase): + def test_emits_banner_signature_and_body(self): + kb = _make_kernel_body() + kb.addSignature(_make_signature_base(kernelName="my_k")) + body = Module("body") + body.add(TextBlock("s_nop 0\n")) + kb.addBody(body) + s = str(kb) + self.assertIn("Begin Kernel", s) + self.assertIn('.amdgcn_target "amdgcn-amd-amdhsa--gfx1250"', s) + self.assertIn("s_nop 0", s) + + def test_signature_only_still_requires_body(self): + kb = _make_kernel_body() + kb.addSignature(_make_signature_base()) + with self.assertRaises(RuntimeError): + kb.toString() + + +class TestKernelBodyCheckResourcesPattern(_SignatureKernelSetup, unittest.TestCase): + def test_body_add_after_set_gprs(self): + """Mirror ``KernelWriterAssembly.checkResources`` overflow patch.""" + kb = _make_kernel_body() + kb.addSignature(_make_signature_base()) + kb.addBody(Module("body")) + kb.setGprs(totalVgprs=32, totalAgprs=0, totalSgprs=16) + kb.body.add(TextBlock("/* overflow patch */\n")) + s = str(kb) + self.assertIn("/* overflow patch */", s) + self.assertIn(".amdhsa_next_free_vgpr 32 // vgprs", s) + + +class TestKernelBodyCopyRejected(unittest.TestCase): + def test_deepcopy_raises(self): + kb = _make_kernel_body() + with self.assertRaises(RuntimeError): + copy.deepcopy(kb) + + def test_pickle_raises(self): + kb = _make_kernel_body() + with self.assertRaises(RuntimeError): + pickle.dumps(kb) + + +class TestKernelBodyModuleIntegration(_SignatureKernelSetup, unittest.TestCase): + def test_kernel_body_counted_in_module_tree(self): + outer = Module("outer") + kb = _make_kernel_body() + kb.addSignature(_make_signature_base()) + kb.addBody(Module("inner")) + outer.add(kb) + self.assertEqual(outer.countType(Item), 2) + self.assertEqual(outer.countType(KernelBody), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_container.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_container.py new file mode 100644 index 000000000000..8012f7f3e6c3 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_container.py @@ -0,0 +1,1942 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Standalone tests for ``rocisa_stinkytofu_adaptor.container``. + +Run from any working directory: + + python3 projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_container.py + +Or with pytest: + + pytest projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_container.py + +Treat any failure here as a regression that will silently corrupt +generated asm: KernelWriter compares ``RegisterContainer`` byte-for-byte +against ``toString`` output on every emitted line. +""" + +from __future__ import annotations + +import copy +import os +import pickle +import sys +import unittest + +# --------------------------------------------------------------------------- +# Self-contained sys.path bootstrap (see test_register.py for rationale). +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor import rocIsa # noqa: E402 +from rocisa_stinkytofu_adaptor import getGlcBitName, getSlcBitName # noqa: E402 +from rocisa_stinkytofu_adaptor.container import ( # noqa: E402 + Container, + ContinuousRegister, + DSModifiers, + DPPModifiers, + EXEC, + EXECLO, + EXECHI, + FLATModifiers, + GLOBALModifiers, + HWRegContainer, + MemTokenData, + MUBUFModifiers, + SDWAModifiers, + SMEMModifiers, + True16Modifiers, + VOP3PModifiers, + Holder, + HolderContainer, + RegisterContainer, + RegName, + VCC, + accvgpr, + mgpr, + replaceHolder, + sgpr, + vgpr, +) +from rocisa_stinkytofu_adaptor.enum import CacheScope, HighBitSel, NonVolatile, SelectBit, TemporalHint # noqa: E402 + + +# =========================================================================== +# RegName +# =========================================================================== + + +class TestRegNameConstruction(unittest.TestCase): + def test_default_ctor_empty(self): + rn = RegName() + self.assertEqual(rn.name, "") + self.assertEqual(rn.offsets, []) + + def test_name_only_ctor(self): + rn = RegName("ValuA") + self.assertEqual(rn.name, "ValuA") + self.assertEqual(rn.offsets, []) + + def test_name_and_offsets_ctor(self): + rn = RegName("ValuA", [2, 4]) + self.assertEqual(rn.name, "ValuA") + self.assertEqual(rn.offsets, [2, 4]) + + def test_offsets_are_copied(self): + # Ctor copies the input list so post-construction mutation of + # the source does not bleed into the RegName. + src = [1, 2, 3] + rn = RegName("X", src) + src.append(99) + self.assertEqual(rn.offsets, [1, 2, 3]) + + +class TestRegNameOffsets(unittest.TestCase): + def test_getOffsets_returns_list(self): + rn = RegName("V", [1, 2, 3]) + self.assertEqual(rn.getOffsets(), [1, 2, 3]) + + def test_setOffset_in_range(self): + rn = RegName("V", [1, 2, 3]) + rn.setOffset(1, 99) + self.assertEqual(rn.offsets, [1, 99, 3]) + + def test_setOffset_out_of_range_raises(self): + rn = RegName("V", [1, 2]) + with self.assertRaises(IndexError): + rn.setOffset(2, 0) + + def test_addOffset_appends(self): + rn = RegName("V", [1]) + rn.addOffset(5) + rn.addOffset(7) + self.assertEqual(rn.offsets, [1, 5, 7]) + + def test_getTotalOffsets_sum(self): + self.assertEqual(RegName("V", []).getTotalOffsets(), 0) + self.assertEqual(RegName("V", [3, 4, 5]).getTotalOffsets(), 12) + self.assertEqual(RegName("V", [-2, 7]).getTotalOffsets(), 5) + + +class TestRegNameTotalIdx(unittest.TestCase): + """``getTotalIdx`` resolves ``name`` against ``rocIsa.getVgprIdx``.""" + + def setUp(self): + # Wipe the shared symbol table to keep tests order-independent. + rocIsa.getInstance()._vgpr_idx.clear() + + def tearDown(self): + rocIsa.getInstance()._vgpr_idx.clear() + + def test_unknown_name_resolves_to_zero(self): + # Missing symbol defaults to 0, so total = sum(offsets). + rn = RegName("MissingSym", [3]) + self.assertEqual(rn.getTotalIdx(), 3) + + def test_known_name_plus_offsets(self): + rocIsa.getInstance().setVgprIdx("ValuA", 100) + rn = RegName("ValuA", [4, 2]) + self.assertEqual(rn.getTotalIdx(), 106) # 100 + 4 + 2 + self.assertEqual(rn.nameIdx, 100) + + def test_setNameIdx_re_resolves_after_remap(self): + rocIsa.getInstance().setVgprIdx("ValuA", 100) + rn = RegName("ValuA") + rn.setNameIdx() + self.assertEqual(rn.nameIdx, 100) + # KernelWriter rebinds the symbol; getTotalIdx must reflect it. + rocIsa.getInstance().setVgprIdx("ValuA", 200) + self.assertEqual(rn.getTotalIdx(), 200) + + +class TestRegNameStringify(unittest.TestCase): + """``__str__`` -> ``"name+off1+off2+..."``.""" + + def test_name_only(self): + self.assertEqual(str(RegName("ValuA")), "ValuA") + + def test_name_with_one_offset(self): + self.assertEqual(str(RegName("ValuA", [3])), "ValuA+3") + + def test_name_with_many_offsets(self): + self.assertEqual(str(RegName("ValuA", [1, 2, 3])), "ValuA+1+2+3") + + def test_negative_offset(self): + self.assertEqual(str(RegName("X", [-5])), "X+-5") + + +class TestRegNameEquality(unittest.TestCase): + def test_eq_same_name_and_offsets(self): + self.assertEqual(RegName("A", [1, 2]), RegName("A", [1, 2])) + + def test_ne_different_name(self): + self.assertNotEqual(RegName("A", [1]), RegName("B", [1])) + + def test_ne_different_offsets(self): + self.assertNotEqual(RegName("A", [1, 2]), RegName("A", [1, 3])) + + def test_eq_with_non_regname_returns_NotImplemented(self): + # NotImplemented bubbles up to Python's default ``False`` for !=. + self.assertFalse(RegName("A") == "A") + self.assertTrue(RegName("A") != "A") + + def test_hash_equal_for_equal_regnames(self): + self.assertEqual(hash(RegName("A", [1, 2])), hash(RegName("A", [1, 2]))) + + def test_usable_as_dict_key(self): + d = {RegName("A", [1]): "first"} + self.assertEqual(d[RegName("A", [1])], "first") + + +class TestRegNameCopy(unittest.TestCase): + def test_deepcopy_independence(self): + rn = RegName("A", [1, 2]) + clone = copy.deepcopy(rn) + clone.addOffset(99) + self.assertEqual(rn.offsets, [1, 2]) + self.assertEqual(clone.offsets, [1, 2, 99]) + + def test_copy_independence(self): + rn = RegName("A", [1, 2]) + clone = copy.copy(rn) + clone.setOffset(0, 99) + self.assertEqual(rn.offsets, [1, 2]) + self.assertEqual(clone.offsets, [99, 2]) + + +class TestRegNamePickle(unittest.TestCase): + def test_pickle_round_trip(self): + rn = RegName("ValuA", [3, 4, 5]) + rt = pickle.loads(pickle.dumps(rn)) + self.assertEqual(rt, rn) + self.assertEqual(str(rt), "ValuA+3+4+5") + + +# =========================================================================== +# RegisterContainer -- construction & basic attributes +# =========================================================================== + + +class TestRegisterContainerConstruction(unittest.TestCase): + def test_unnamed_default(self): + rc = RegisterContainer("v", None, 0, 1) + self.assertEqual(rc.regType, "v") + self.assertIsNone(rc.regName) + self.assertEqual(rc.regIdx, 0) + self.assertEqual(rc.regNum, 1) + self.assertEqual(rc.msb, 0) + self.assertFalse(rc.isInlineAsm) + self.assertFalse(rc.isMinus) + self.assertFalse(rc.isAbs) + self.assertFalse(rc.isMacro) + self.assertFalse(rc.isOff) + + def test_named(self): + rn = RegName("ValuA") + rc = RegisterContainer("v", rn, 0, 2) + self.assertIs(rc.regName, rn) + self.assertEqual(rc.regNum, 2) + + def test_regNum_ceil(self): + # regNum is rounded up: 1.5 -> 2, 0.1 -> 1. + self.assertEqual(RegisterContainer("v", None, 0, 1.5).regNum, 2) + self.assertEqual(RegisterContainer("v", None, 0, 0.1).regNum, 1) + self.assertEqual(RegisterContainer("v", None, 0, 3.0).regNum, 3) + + def test_seven_arg_kwargs(self): + # Extended ctor: isAbs/isMacro/isOff as keyword-only overrides. + rc = RegisterContainer("v", None, 0, 1, isAbs=True, isMacro=True, isOff=True) + self.assertTrue(rc.isAbs) + self.assertTrue(rc.isMacro) + self.assertTrue(rc.isOff) + + +class TestRegisterContainerSetters(unittest.TestCase): + def test_setInlineAsm(self): + rc = RegisterContainer("v", None, 0, 1) + rc.setInlineAsm(True) + self.assertTrue(rc.isInlineAsm) + + def test_setMinus_and_setAbs(self): + rc = RegisterContainer("v", None, 0, 1) + rc.setMinus(True) + rc.setAbs(True) + self.assertTrue(rc.isMinus) + self.assertTrue(rc.isAbs) + + def test_getMinus_returns_copy(self): + rc = RegisterContainer("v", None, 5, 1) + minus_rc = rc.getMinus() + self.assertTrue(minus_rc.isMinus) + self.assertFalse(rc.isMinus) # original unchanged + self.assertIsNot(minus_rc, rc) + + +# =========================================================================== +# replaceRegName +# =========================================================================== + + +class TestReplaceRegName(unittest.TestCase): + def test_no_regName_is_noop(self): + rc = RegisterContainer("v", None, 5, 1) + rc.replaceRegName("X", 9) + self.assertIsNone(rc.regName) + self.assertEqual(rc.regIdx, 5) + + def test_exact_match_int_collapses_to_idx(self): + # Exact int match collapses symbolic name into + # ``regIdx = dst + sum(offsets)`` and clears regName. + rc = RegisterContainer("v", RegName("ValuA", [3]), 0, 1) + rc.replaceRegName("ValuA", 100) + self.assertIsNone(rc.regName) + self.assertEqual(rc.regIdx, 103) + + def test_exact_match_int_no_offsets(self): + rc = RegisterContainer("v", RegName("ValuA"), 0, 1) + rc.replaceRegName("ValuA", 7) + self.assertIsNone(rc.regName) + self.assertEqual(rc.regIdx, 7) + + def test_partial_match_int_substring_replaces(self): + # ``ValuA+Sub`` with src="Sub", dst=42 -> name becomes "ValuA+42". + rc = RegisterContainer("v", RegName("ValuA+Sub"), 0, 1) + rc.replaceRegName("Sub", 42) + self.assertIsNotNone(rc.regName) + self.assertEqual(rc.regName.name, "ValuA+42") + + def test_str_dst_substring_replace(self): + rc = RegisterContainer("v", RegName("Foo+Bar"), 0, 1) + rc.replaceRegName("Bar", "Baz") + self.assertEqual(rc.regName.name, "Foo+Baz") + + def test_str_dst_no_match_noop(self): + rc = RegisterContainer("v", RegName("Foo"), 0, 1) + rc.replaceRegName("Bar", "Baz") + self.assertEqual(rc.regName.name, "Foo") + + def test_bool_rejected(self): + # bool is an int subclass; rejected so callers don't drift into + # the int overload by accident. + rc = RegisterContainer("v", RegName("X"), 0, 1) + with self.assertRaises(TypeError): + rc.replaceRegName("X", True) + + +# =========================================================================== +# Composite name accessors / splitRegContainer / setMsb +# =========================================================================== + + +class TestRegNameAccessors(unittest.TestCase): + def test_getRegNameWithType(self): + rc = RegisterContainer("v", RegName("ValuA", [3]), 0, 1) + # Bare ``name``, no offsets baked in. + self.assertEqual(rc.getRegNameWithType(), "vgprValuA") + + def test_getCompleteRegNameWithType(self): + rc = RegisterContainer("v", RegName("ValuA", [3]), 0, 1) + # Includes the offset suffix. + self.assertEqual(rc.getCompleteRegNameWithType(), "vgprValuA+3") + + def test_getCompleteRegName(self): + rc = RegisterContainer("v", RegName("ValuA", [3]), 0, 1) + self.assertEqual(rc.getCompleteRegName(), "ValuA+3") + + +class TestSplitRegContainer(unittest.TestCase): + def test_unnamed_even_split(self): + rc = RegisterContainer("v", None, 4, 2) + r1, r2 = rc.splitRegContainer() + self.assertEqual(r1.regIdx, 4) + self.assertEqual(r1.regNum, 1) + self.assertEqual(r2.regIdx, 5) + self.assertEqual(r2.regNum, 1) + + def test_unnamed_odd_split(self): + # ``new_reg_num = ceil(regNum/2)`` so r1 keeps the larger half + # (3 -> 2,1). + rc = RegisterContainer("v", None, 4, 3) + r1, r2 = rc.splitRegContainer() + self.assertEqual(r1.regNum, 2) + self.assertEqual(r2.regNum, 1) + self.assertEqual(r2.regIdx, 5) + + def test_named_appends_offset_one_to_right_half(self): + rc = RegisterContainer("v", RegName("ValuA"), 0, 2) + r1, r2 = rc.splitRegContainer() + # Left half keeps the original RegName. + self.assertEqual(r1.regName.offsets, []) + # Right half gets ``+1`` appended to its offset chain. + self.assertEqual(r2.regName.offsets, [1]) + + def test_split_results_independent(self): + # Mutating r2.regName.offsets must NOT bleed back into the source + # rc.regName.offsets; split deep-clones the RegName field. + rc = RegisterContainer("v", RegName("V", [3]), 0, 2) + _r1, r2 = rc.splitRegContainer() + r2.regName.addOffset(99) + self.assertEqual(rc.regName.offsets, [3]) + + def test_split_preserves_unrelated_state(self): + rc = RegisterContainer("v", RegName("V"), 0, 2) + rc.setMinus(True) + rc.isMacro = True + r1, r2 = rc.splitRegContainer() + # Decorations propagate to both halves. + self.assertTrue(r1.isMinus and r2.isMinus) + self.assertTrue(r1.isMacro and r2.isMacro) + + +class TestSetMsb(unittest.TestCase): + def setUp(self): + rocIsa.getInstance()._vgpr_idx.clear() + + def tearDown(self): + rocIsa.getInstance()._vgpr_idx.clear() + + def test_named_uses_totalIdx_div_256(self): + rocIsa.getInstance().setVgprIdx("ValuA", 512) + rc = RegisterContainer("v", RegName("ValuA", [1]), 0, 1) + rc.setMsb() + self.assertEqual(rc.msb, 2) # (512+1)//256 + + def test_unnamed_uses_regIdx_div_256(self): + rc = RegisterContainer("v", None, 600, 1) + rc.setMsb() + self.assertEqual(rc.msb, 2) # 600//256 + + def test_msb_zero_for_low_index(self): + rc = RegisterContainer("v", None, 5, 1) + rc.setMsb() + self.assertEqual(rc.msb, 0) + + +# =========================================================================== +# Hash / equality +# =========================================================================== + + +class TestRegisterContainerEquality(unittest.TestCase): + def test_eq_same_fields(self): + a = RegisterContainer("v", RegName("X"), 5, 2) + b = RegisterContainer("v", RegName("X"), 5, 2) + self.assertEqual(a, b) + self.assertEqual(hash(a), hash(b)) + + def test_eq_ignores_decorations(self): + # Equality only checks (regType, regIdx, regNum, regName); the + # isMinus/isAbs/isInlineAsm decorations don't participate. + a = RegisterContainer("v", None, 5, 1) + b = RegisterContainer("v", None, 5, 1) + b.setMinus(True) + b.setAbs(True) + self.assertEqual(a, b) + + def test_ne_regType(self): + self.assertNotEqual( + RegisterContainer("v", None, 0, 1), + RegisterContainer("s", None, 0, 1), + ) + + def test_ne_regIdx(self): + self.assertNotEqual( + RegisterContainer("v", None, 0, 1), + RegisterContainer("v", None, 1, 1), + ) + + def test_ne_regNum(self): + self.assertNotEqual( + RegisterContainer("v", None, 0, 1), + RegisterContainer("v", None, 0, 2), + ) + + def test_ne_regName(self): + self.assertNotEqual( + RegisterContainer("v", RegName("X"), 0, 1), + RegisterContainer("v", RegName("Y"), 0, 1), + ) + + def test_eq_with_non_container_returns_false(self): + # ``__eq__`` returns False for non-RC objects (does not raise). + self.assertFalse(RegisterContainer("v", None, 0, 1) == "v0") + self.assertFalse(RegisterContainer("v", None, 0, 1) == None) # noqa: E711 + + def test_usable_as_dict_key(self): + d = {RegisterContainer("v", None, 0, 1): "first"} + self.assertEqual(d[RegisterContainer("v", None, 0, 1)], "first") + + +# =========================================================================== +# Aliasing (sameRegBaseAddr / __and__) +# =========================================================================== + + +class TestAliasing(unittest.TestCase): + def test_sameRegBaseAddr_both_named_same(self): + a = RegisterContainer("v", RegName("X"), 0, 1) + b = RegisterContainer("v", RegName("X", [3]), 0, 1) + # Compares ``regName.name`` only, ignoring offsets. + self.assertTrue(a.sameRegBaseAddr(b)) + + def test_sameRegBaseAddr_both_named_diff(self): + a = RegisterContainer("v", RegName("X"), 0, 1) + b = RegisterContainer("v", RegName("Y"), 0, 1) + self.assertFalse(a.sameRegBaseAddr(b)) + + def test_sameRegBaseAddr_both_unnamed_same_idx(self): + a = RegisterContainer("v", None, 5, 1) + b = RegisterContainer("v", None, 5, 3) + self.assertTrue(a.sameRegBaseAddr(b)) + + def test_sameRegBaseAddr_mixed_returns_false(self): + a = RegisterContainer("v", RegName("X"), 0, 1) + b = RegisterContainer("v", None, 0, 1) + self.assertFalse(a.sameRegBaseAddr(b)) + + def test_and_self_overlap(self): + rc = RegisterContainer("v", None, 5, 4) + # range [0, regNum) vs itself -> True. + self.assertTrue(rc & rc) + + def test_and_disjoint_offsets(self): + a = RegisterContainer("v", RegName("X", [0]), 0, 2) + b = RegisterContainer("v", RegName("X", [4]), 0, 2) + # ranges [0,2) and [4,6) -- no overlap. + self.assertFalse(a & b) + + def test_and_adjacent_no_overlap(self): + a = RegisterContainer("v", RegName("X", [0]), 0, 2) + b = RegisterContainer("v", RegName("X", [2]), 0, 2) + # ranges [0,2) and [2,4) -- touching, not overlapping. + self.assertFalse(a & b) + + def test_and_overlapping_offsets(self): + a = RegisterContainer("v", RegName("X", [0]), 0, 3) + b = RegisterContainer("v", RegName("X", [2]), 0, 2) + # ranges [0,3) and [2,4) -- overlap. + self.assertTrue(a & b) + + def test_and_different_bases_disjoint(self): + a = RegisterContainer("v", RegName("X"), 0, 1) + b = RegisterContainer("v", RegName("Y"), 0, 1) + self.assertFalse(a & b) + + +# =========================================================================== +# toString (byte-for-byte parity required for KernelWriter emit) +# =========================================================================== + + +class TestToString(unittest.TestCase): + """The KernelWriter pipes ``str(rc)`` directly into the emitted asm. + + Byte-for-byte parity is a hard requirement -- any drift will break a + downstream assembler / disassembler diff. + """ + + def setUp(self): + # toString reads rocIsa.getAsmCaps() for the HasVgprMSB path. + # Tests that don't care about MSB stay in the uninitialised-rocIsa + # default ("no MSB"), so we do NOT init here. + rocIsa._instance = None # noqa: SLF001 + + def test_isOff(self): + rc = RegisterContainer("v", None, 0, 1, isOff=True) + self.assertEqual(str(rc), "off") + + def test_inlineAsm_single(self): + rc = RegisterContainer("v", None, 3, 1) + rc.setInlineAsm(True) + self.assertEqual(str(rc), "%3") + + def test_inlineAsm_minus(self): + rc = RegisterContainer("v", None, 3, 1) + rc.setInlineAsm(True) + rc.setMinus(True) + self.assertEqual(str(rc), "-%3") + + def test_inlineAsm_abs(self): + rc = RegisterContainer("v", None, 3, 1) + rc.setInlineAsm(True) + rc.setAbs(True) + self.assertEqual(str(rc), "abs(%3)") + + def test_unnamed_single_vgpr(self): + self.assertEqual(str(RegisterContainer("v", None, 5, 1)), "v5") + + def test_unnamed_single_sgpr(self): + self.assertEqual(str(RegisterContainer("s", None, 7, 1)), "s7") + + def test_unnamed_range(self): + self.assertEqual(str(RegisterContainer("v", None, 0, 4)), "v[0:3]") + self.assertEqual(str(RegisterContainer("s", None, 8, 2)), "s[8:9]") + + def test_unnamed_single_minus(self): + rc = RegisterContainer("v", None, 5, 1) + rc.setMinus(True) + self.assertEqual(str(rc), "-v5") + + def test_unnamed_single_abs(self): + rc = RegisterContainer("v", None, 5, 1) + rc.setAbs(True) + self.assertEqual(str(rc), "abs(v5)") + + def test_unnamed_single_minus_and_abs(self): + # When both flags are set the abs prefix wraps the minus. + rc = RegisterContainer("v", None, 5, 1) + rc.setMinus(True) + rc.setAbs(True) + self.assertEqual(str(rc), "abs(-v5)") + + def test_named_single(self): + rc = RegisterContainer("v", RegName("ValuA"), 0, 1) + self.assertEqual(str(rc), "v[vgprValuA]") + + def test_named_range(self): + rc = RegisterContainer("v", RegName("ValuA"), 0, 4) + self.assertEqual(str(rc), "v[vgprValuA:vgprValuA+3]") + + def test_named_with_offset(self): + rc = RegisterContainer("v", RegName("ValuA", [3]), 0, 1) + self.assertEqual(str(rc), "v[vgprValuA+3]") + + def test_named_macro(self): + rc = RegisterContainer("v", RegName("ValuA"), 0, 1, isMacro=True) + # Macro path adds a ``\`` between ``[`` and ``vgpr``. + self.assertEqual(str(rc), "v[\\vgprValuA]") + + def test_named_minus_abs(self): + rc = RegisterContainer("v", RegName("X"), 0, 1) + rc.setMinus(True) + rc.setAbs(True) + self.assertEqual(str(rc), "abs(-v[vgprX])") + + +# =========================================================================== +# Copy semantics +# =========================================================================== + + +class TestRegisterContainerCopy(unittest.TestCase): + def test_deepcopy_independent_regName(self): + rc = RegisterContainer("v", RegName("X", [1, 2]), 0, 1) + clone = copy.deepcopy(rc) + clone.regName.addOffset(99) + # Mutation must not leak. + self.assertEqual(rc.regName.offsets, [1, 2]) + self.assertEqual(clone.regName.offsets, [1, 2, 99]) + + def test_copy_preserves_flags(self): + rc = RegisterContainer("v", None, 5, 2) + rc.setMinus(True) + rc.setAbs(True) + rc.setInlineAsm(True) + clone = copy.copy(rc) + self.assertTrue(clone.isMinus) + self.assertTrue(clone.isAbs) + self.assertTrue(clone.isInlineAsm) + + def test_getMinus_does_not_share_regName(self): + # getMinus is the hot path KernelWriter uses for "-v0"; the clone + # must not alias the source's RegName. + rc = RegisterContainer("v", RegName("X", [1]), 0, 1) + m = rc.getMinus() + m.regName.addOffset(7) + self.assertEqual(rc.regName.offsets, [1]) + + def test_pickle_round_trip(self): + rc = RegisterContainer("v", RegName("Y", [1, 2]), 5, 3) + rc.setMinus(True) + rc.isMacro = True + rt = pickle.loads(pickle.dumps(rc)) + self.assertEqual(rt, rc) + self.assertTrue(rt.isMinus) + self.assertTrue(rt.isMacro) + # toString quirk: the macro ``\`` is emitted only on the left + # end of a range expression, never on the right. + self.assertEqual(str(rt), "-v[\\vgprY+1+2:vgprY+1+2+2]") + + +# =========================================================================== +# Stinkytofu handoff (to_stinky) +# =========================================================================== +# +# These tests can be skipped if stinkytofu is not available in the test +# environment (the wrapper itself remains a usable rocisa-shape). + + +try: + import stinkytofu as _stinky # noqa: F401 + + _STINKY_OK = True +except ImportError: + _STINKY_OK = False + + +@unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built in this env") +class TestToStinky(unittest.TestCase): + """``to_stinky`` builds a fresh stinky.Register from the wrapper state. + + Symbolic name carries the ``gpr`` prefix; physical idx is + resolved through ``rocIsa.getVgprIdx()`` for named V registers. + """ + + def setUp(self): + rocIsa.getInstance()._vgpr_idx.clear() + + def tearDown(self): + rocIsa.getInstance()._vgpr_idx.clear() + + def test_basic_vgpr(self): + rc = RegisterContainer("v", None, 5, 2) + reg = rc.to_stinky() + self.assertTrue(reg.is_register) + self.assertEqual(reg.index, 5) + self.assertEqual(reg.count, 2) + + def test_named_includes_vgpr_prefix(self): + # When regIdx is unresolved (-1), symbolic name is attached so + # stinkytofu can do its own scheduling/analysis. + rc = RegisterContainer("v", RegName("ValuA", [1, 2]), -1, 1) + reg = rc.to_stinky() + self.assertTrue(reg.has_reg_name()) + name, offsets = reg.get_reg_name() + self.assertEqual(name, "vgprValuA") + self.assertEqual(offsets, [1, 2]) + + def test_named_resolved_preserves_symbolic(self): + # Resolved registers still carry symbolic names for readable asm. + rc = RegisterContainer("v", RegName("ValuA", [1, 2]), 0, 1) + reg = rc.to_stinky() + self.assertTrue(reg.has_reg_name()) + name, offsets = reg.get_reg_name() + self.assertEqual(name, "vgprValuA") + self.assertEqual(offsets, [1, 2]) + + def test_named_sgpr_prefix(self): + rc = RegisterContainer("s", RegName("KArg"), -1, 1) + reg = rc.to_stinky() + name, _ = reg.get_reg_name() + self.assertEqual(name, "sgprKArg") + + def test_macro_injects_backslash_prefix(self): + # Macro context renders as ``v[\vgprAddr+0]``; the ``\`` is + # encoded into the symbolic name for byte-parity. + rc = RegisterContainer("v", RegName("Addr", [0]), -1, 1, isMacro=True) + reg = rc.to_stinky() + name, offsets = reg.get_reg_name() + self.assertEqual(name, "\\vgprAddr") + self.assertEqual(offsets, [0]) + + def test_inlineAsm_raises(self): + # ``%idx`` format is inline-asm-in-cpp only; stinky emit targets + # .s. Module layer (T6) must catch this before to_stinky. + rc = RegisterContainer("v", None, 3, 1) + rc.setInlineAsm(True) + with self.assertRaises(NotImplementedError): + rc.to_stinky() + + def test_named_idx_resolves_through_rocIsa(self): + # Physical idx for named V registers = regName.getTotalIdx(). + rocIsa.getInstance().setVgprIdx("ValuA", 64) + rc = RegisterContainer("v", RegName("ValuA", [2]), 0, 1) + reg = rc.to_stinky() + self.assertEqual(reg.index, 66) # 64 + 2 + + def test_named_idx_unresolved_falls_back_to_zero(self): + # Symbol missing from getVgprIdx() defaults to 0+sum(offsets). + rc = RegisterContainer("v", RegName("UnknownSym", [3]), 0, 1) + reg = rc.to_stinky() + self.assertEqual(reg.index, 3) + + def test_minus_propagates(self): + rc = RegisterContainer("v", None, 5, 1) + rc.setMinus(True) + reg = rc.to_stinky() + self.assertTrue(reg.is_minus) + + def test_abs_propagates(self): + rc = RegisterContainer("v", None, 5, 1) + rc.setAbs(True) + reg = rc.to_stinky() + self.assertTrue(reg.is_abs) + + def test_isOff_returns_literal_off(self): + rc = RegisterContainer("v", None, 0, 1, isOff=True) + reg = rc.to_stinky() + self.assertTrue(reg.is_literal_string) + self.assertEqual(reg.literal_string, "off") + + def test_fresh_each_call(self): + # to_stinky is documented as "build fresh each time" -- mutations + # between calls must be reflected. + rc = RegisterContainer("v", None, 5, 1) + first = rc.to_stinky() + rc.setMinus(True) + second = rc.to_stinky() + self.assertFalse(first.is_minus) + self.assertTrue(second.is_minus) + + +# =========================================================================== +# KernelWriter scenario coverage +# =========================================================================== +# +# Walks through the exact call patterns KernelWriter uses (grepped from +# KernelWriterAssembly / Components). + + +class TestKernelWriterScenarios(unittest.TestCase): + def test_vgpr_factory_pattern(self): + # Real factory: vgpr("ValuA", 1) -> str-form RegisterContainer. + rc = vgpr("ValuA", 1) + self.assertEqual(str(rc), "v[vgprValuA]") + # Used as a dict key in RegisterPool tracking. Two vgpr() calls + # with matching args must hash/compare equal for this to work. + d = {rc: "tracked"} + self.assertEqual(d[vgpr("ValuA", 1)], "tracked") + + def test_split_then_emit(self): + rc = RegisterContainer("v", RegName("Acc"), 0, 4) + r1, r2 = rc.splitRegContainer() + # KernelWriter emits both halves into different MFMAs. + self.assertEqual(str(r1), "v[vgprAcc:vgprAcc+1]") + self.assertEqual(str(r2), "v[vgprAcc+1:vgprAcc+1+1]") + + def test_replaceRegName_to_int_collapse(self): + # Mirrors KernelWriter's post-allocation pass that collapses + # symbolic refs to numeric indices. + rc = RegisterContainer("v", RegName("ValuA"), 0, 2) + rc.replaceRegName("ValuA", 32) + self.assertEqual(str(rc), "v[32:33]") + + def test_replaceRegName_partial_int_substitution(self): + # Mirrors prefix-stripping pattern (e.g. "vgprValuA+x" -> "vgprValuA+5"). + rc = RegisterContainer("v", RegName("ValuA+Tmp"), 0, 1) + rc.replaceRegName("Tmp", 5) + self.assertEqual(str(rc), "v[vgprValuA+5]") + + def test_minus_then_str(self): + # KernelWriter uses ``getMinus()`` for ``v_sub_x dest, -src``. + src = RegisterContainer("v", None, 7, 1) + neg = src.getMinus() + self.assertEqual(str(neg), "-v7") + self.assertEqual(str(src), "v7") + + def test_overlap_check_for_aliasing_warning(self): + # KernelWriter uses ``a && b`` to detect aliased register operands. + dest = RegisterContainer("v", RegName("Acc", [0]), 0, 4) + src = RegisterContainer("v", RegName("Acc", [2]), 0, 4) + self.assertTrue(dest & src) # ranges [0,4) and [2,6) overlap + + def test_inline_asm_emit(self): + # ``%0``, ``%1`` style operands for inline asm blocks. + rc = RegisterContainer("v", None, 0, 1) + rc.setInlineAsm(True) + self.assertEqual(str(rc), "%0") + + +# =========================================================================== +# Holder +# =========================================================================== + + +class TestHolderConstruction(unittest.TestCase): + def test_int_ctor(self): + # ``Holder(int idx)`` sets ``name = None``. + h = Holder(5) + self.assertEqual(h.idx, 5) + self.assertIsNone(h.name) + + def test_string_ctor_no_offsets(self): + # generateRegName("Foo") -> RegName("Foo", []). + h = Holder("ValuC") + self.assertEqual(h.idx, -1) + self.assertIsNotNone(h.name) + self.assertEqual(h.name.name, "ValuC") + self.assertEqual(h.name.offsets, []) + + def test_string_ctor_single_offset(self): + # "Foo+5" -> RegName("Foo", [5]). + h = Holder("ValuC+5") + self.assertEqual(h.idx, -1) + self.assertEqual(h.name.name, "ValuC") + self.assertEqual(h.name.offsets, [5]) + + def test_string_ctor_multi_offset(self): + # "Foo+1+2+3" -> RegName("Foo", [1, 2, 3]). + h = Holder("ValuC+1+2+3") + self.assertEqual(h.name.offsets, [1, 2, 3]) + + def test_int_kwarg(self): + h = Holder(idx=7) + self.assertEqual(h.idx, 7) + self.assertIsNone(h.name) + + def test_name_kwarg(self): + h = Holder(name="Foo+2") + self.assertEqual(h.idx, -1) + self.assertEqual(h.name.offsets, [2]) + + def test_bool_rejected(self): + # bool is an int subclass but would silently become idx=0/1; + # caller almost certainly meant something else. + with self.assertRaises(TypeError): + Holder(True) + with self.assertRaises(TypeError): + Holder(idx=False) + + def test_float_rejected(self): + with self.assertRaises(TypeError): + Holder(3.0) + + def test_both_kwargs_rejected(self): + with self.assertRaises(TypeError): + Holder(idx=1, name="Foo") + + def test_no_args_rejected(self): + with self.assertRaises(TypeError): + Holder() + + def test_multi_pos_rejected(self): + with self.assertRaises(TypeError): + Holder(1, 2) + + +class TestHolderSemantics(unittest.TestCase): + def test_idx_mutable(self): + # idx is a writable field. + h = Holder(3) + h.idx = 10 + self.assertEqual(h.idx, 10) + + def test_name_mutable(self): + # name is a writable field. + h = Holder(3) + h.name = RegName("X", [1]) + self.assertEqual(h.name.name, "X") + + def test_equality(self): + self.assertEqual(Holder(5), Holder(5)) + self.assertEqual(Holder("Foo+1"), Holder("Foo+1")) + self.assertNotEqual(Holder(5), Holder(6)) + self.assertNotEqual(Holder(5), Holder("Foo")) + self.assertNotEqual(Holder("Foo"), Holder("Bar")) + + def test_hashable(self): + # Hash parity with equality lets KernelWriter use Holders as + # dict keys / set members (e.g. dedup loops). + s = {Holder(5), Holder(5), Holder("Foo")} + self.assertEqual(len(s), 2) + + def test_repr_contains_state(self): + # Debug-printability; exact format is not pinned. + r = repr(Holder("Foo+2")) + self.assertIn("Holder", r) + self.assertIn("Foo", r) + + def test_eq_vs_other_type(self): + # Cross-type comparison must not throw. + self.assertFalse(Holder(5) == 5) + self.assertTrue(Holder(5) != "Foo") + + def test_shallow_copy_independent(self): + # Mutating the copy must not bleed into the original. + h = Holder("ValuC+1") + h2 = copy.copy(h) + h2.name.offsets.append(99) + self.assertEqual(h.name.offsets, [1]) + self.assertEqual(h2.name.offsets, [1, 99]) + + def test_deep_copy_independent(self): + h = Holder("ValuC+1+2") + h2 = copy.deepcopy(h) + h2.idx = 99 + h2.name.offsets[0] = 7 + self.assertEqual(h.idx, -1) + self.assertEqual(h.name.offsets, [1, 2]) + + def test_pickle_roundtrip_int(self): + h = Holder(42) + h2 = pickle.loads(pickle.dumps(h)) + self.assertEqual(h2.idx, 42) + self.assertIsNone(h2.name) + + def test_pickle_roundtrip_name(self): + h = Holder("Foo+3+4") + h2 = pickle.loads(pickle.dumps(h)) + self.assertEqual(h2.idx, -1) + self.assertEqual(h2.name.name, "Foo") + self.assertEqual(h2.name.offsets, [3, 4]) + + +# =========================================================================== +# HolderContainer +# =========================================================================== +# +# Subclass of RegisterContainer; resolution happens lazily via setRegNum(). + + +class TestHolderContainerConstruction(unittest.TestCase): + def test_string_ctor_named(self): + # String ctor: type=1, holderName=str, parent regName = + # RegName(holderName) with offsets=[]. + hc = HolderContainer("v", "ValuC", 2) + self.assertEqual(hc.regType, "v") + self.assertEqual(hc.regIdx, 0) + self.assertEqual(hc.regNum, 2) + self.assertIsNotNone(hc.regName) + self.assertEqual(hc.regName.name, "ValuC") + self.assertEqual(hc.regName.offsets, []) + self.assertEqual(hc.holderName, "ValuC") + self.assertEqual(hc.holderIdx, 0) + self.assertEqual(hc.holderType, 1) + self.assertEqual(hc.holderOffsets, []) + + def test_regname_ctor_named(self): + # RegName ctor: holderOffsets seeded from RegName.offsets so + # pre-existing offsets survive the resolution boundary. + rn = RegName("ValuC", [3]) + hc = HolderContainer("v", rn, 2) + self.assertEqual(hc.holderName, "ValuC") + self.assertEqual(hc.holderType, 1) + self.assertEqual(hc.holderOffsets, [3]) + # Parent regName carries source offsets verbatim. + self.assertEqual(hc.regName.offsets, [3]) + + def test_int_ctor_numeric(self): + # Int ctor: type=0, regName=None, regIdx=holderIdx. + hc = HolderContainer("v", 10, 4) + self.assertEqual(hc.regType, "v") + self.assertEqual(hc.regIdx, 10) + self.assertEqual(hc.regNum, 4) + self.assertIsNone(hc.regName) + self.assertEqual(hc.holderName, "") + self.assertEqual(hc.holderIdx, 10) + self.assertEqual(hc.holderType, 0) + self.assertEqual(hc.holderOffsets, []) + + def test_bool_rejected(self): + # bool falls through to int otherwise; ensures intent isn't lost. + with self.assertRaises(TypeError): + HolderContainer("v", True, 1) + + def test_bad_type_rejected(self): + with self.assertRaises(TypeError): + HolderContainer("v", 3.0, 1) + with self.assertRaises(TypeError): + HolderContainer("v", None, 1) + + def test_inheritance(self): + # KernelWriter checks `isinstance(p, RegisterContainer)` in + # several places; HolderContainer must satisfy that. + hc = HolderContainer("v", "Foo", 1) + self.assertIsInstance(hc, RegisterContainer) + self.assertIsInstance(hc, HolderContainer) + + +class TestHolderContainerSetRegNum(unittest.TestCase): + def test_numeric_resolution(self): + # type 0 sets ``regIdx = holderIdx + num``; holderIdx is + # preserved so setRegNum is idempotent on rerun. + hc = HolderContainer("v", 4, 2) + hc.setRegNum(10) + self.assertEqual(hc.regIdx, 14) + self.assertEqual(hc.holderIdx, 4) + + def test_numeric_resolution_with_zero(self): + # dst=0 is the canonical call site (replaceHolder(module, 0)). + hc = HolderContainer("v", 8, 1) + hc.setRegNum(0) + self.assertEqual(hc.regIdx, 8) + + def test_named_resolution_simple(self): + # type 1 rebuilds regName = RegName(holderName) and prepends num. + hc = HolderContainer("v", "ValuC", 2) + hc.setRegNum(5) + self.assertEqual(hc.regName.name, "ValuC") + self.assertEqual(hc.regName.offsets, [5]) + + def test_named_resolution_with_holder_offsets(self): + # If the RegName-form ctor was used with non-empty offsets, + # setRegNum prepends num and re-appends the saved offsets. + rn = RegName("ValuC", [7, 9]) + hc = HolderContainer("v", rn, 1) + hc.setRegNum(3) + self.assertEqual(hc.regName.name, "ValuC") + self.assertEqual(hc.regName.offsets, [3, 7, 9]) + + def test_named_resolution_idempotent_on_replay(self): + # setRegNum unconditionally re-seeds regName from holderName, so + # calling it twice yields the same result. + hc = HolderContainer("v", "ValuC", 1) + hc.setRegNum(5) + hc.setRegNum(5) + self.assertEqual(hc.regName.offsets, [5]) + + def test_named_resolution_idempotent_with_offsets(self): + rn = RegName("ValuC", [1]) + hc = HolderContainer("v", rn, 1) + hc.setRegNum(2) + hc.setRegNum(2) + self.assertEqual(hc.regName.offsets, [2, 1]) + + +class TestHolderContainerGetCopiedRC(unittest.TestCase): + def test_numeric_snapshot(self): + # type 0 -> RC(regType, None, regIdx, regNum). + hc = HolderContainer("v", 4, 2) + hc.setRegNum(10) + rc = hc.getCopiedRC() + self.assertNotIsInstance(rc, HolderContainer) + self.assertIsInstance(rc, RegisterContainer) + self.assertEqual(rc.regType, "v") + self.assertIsNone(rc.regName) + self.assertEqual(rc.regIdx, 14) + self.assertEqual(rc.regNum, 2) + + def test_named_snapshot(self): + # type 1 -> RC(regType, regName, regIdx, regNum). regIdx is 0 + # for the named flavour (the symbolic name carries the offset). + hc = HolderContainer("v", "ValuC", 2) + hc.setRegNum(5) + rc = hc.getCopiedRC() + self.assertNotIsInstance(rc, HolderContainer) + self.assertEqual(rc.regName.name, "ValuC") + self.assertEqual(rc.regName.offsets, [5]) + self.assertEqual(rc.regIdx, 0) + self.assertEqual(rc.regNum, 2) + + def test_snapshot_is_independent(self): + # Snapshot is a value, not a ref; mutating either side must not + # bleed into the other. + hc = HolderContainer("v", "ValuC", 2) + hc.setRegNum(5) + rc = hc.getCopiedRC() + rc.regName.offsets.append(99) + self.assertEqual(hc.regName.offsets, [5]) + + def test_snapshot_renders_correctly(self): + # End-to-end: the snapshot must emit the same string a directly- + # constructed RC would. + hc = HolderContainer("v", "ValuC", 2) + hc.setRegNum(5) + rc = hc.getCopiedRC() + self.assertEqual(str(rc), "v[vgprValuC+5:vgprValuC+5+1]") + + +class TestHolderContainerSplit(unittest.TestCase): + def test_split_numeric(self): + # Numeric branch: r2.holderIdx bumps by 1; both halves stay as + # HolderContainer. + hc = HolderContainer("v", 4, 2) + r1, r2 = hc.splitRegContainer() + self.assertIsInstance(r1, HolderContainer) + self.assertIsInstance(r2, HolderContainer) + self.assertEqual(r1.holderIdx, 4) + self.assertEqual(r2.holderIdx, 5) + # regNum is halved (ceil) for r1, remainder for r2. + self.assertEqual(r1.regNum, 1) + self.assertEqual(r2.regNum, 1) + + def test_split_named(self): + # Named branch: pushes 1 onto r2.regName.offsets. The RegName- + # form ctor's saved offsets are preserved on the parent's + # regName (used by addOffset). + hc = HolderContainer("v", "ValuC", 2) + r1, r2 = hc.splitRegContainer() + self.assertIsInstance(r1, HolderContainer) + self.assertIsInstance(r2, HolderContainer) + self.assertEqual(r1.regName.offsets, []) + self.assertEqual(r2.regName.offsets, [1]) + self.assertEqual(r1.holderName, "ValuC") + self.assertEqual(r2.holderName, "ValuC") + self.assertEqual(r1.regNum, 1) + self.assertEqual(r2.regNum, 1) + + def test_split_independent(self): + # Mutating halves must not bleed into the source. + hc = HolderContainer("v", "ValuC", 2) + r1, r2 = hc.splitRegContainer() + r2.regName.offsets.append(99) + self.assertEqual(hc.regName.offsets, []) + + +class TestHolderContainerCopy(unittest.TestCase): + def test_shallow_copy_independent(self): + hc = HolderContainer("v", "ValuC", 2) + hc.setRegNum(5) + c = copy.copy(hc) + c.regName.offsets.append(99) + c.holderOffsets.append(99) + self.assertEqual(hc.regName.offsets, [5]) + self.assertEqual(hc.holderOffsets, []) + + def test_deep_copy_independent(self): + hc = HolderContainer("v", "ValuC", 2) + c = copy.deepcopy(hc) + self.assertEqual(c.holderName, "ValuC") + self.assertEqual(c.holderType, 1) + c.holderName = "Other" + self.assertEqual(hc.holderName, "ValuC") + + def test_copy_preserves_subclass(self): + hc = HolderContainer("v", 5, 1) + self.assertIsInstance(copy.copy(hc), HolderContainer) + self.assertIsInstance(copy.deepcopy(hc), HolderContainer) + + +class TestHolderContainerPickle(unittest.TestCase): + def test_pickle_roundtrip_named(self): + hc = HolderContainer("v", "ValuC", 2) + hc.setRegNum(5) + c = pickle.loads(pickle.dumps(hc)) + self.assertIsInstance(c, HolderContainer) + self.assertEqual(c.holderName, "ValuC") + self.assertEqual(c.holderType, 1) + self.assertEqual(c.regName.offsets, [5]) + self.assertEqual(c.regType, "v") + self.assertEqual(c.regNum, 2) + + def test_pickle_roundtrip_numeric(self): + hc = HolderContainer("v", 8, 1) + hc.setRegNum(2) + c = pickle.loads(pickle.dumps(hc)) + self.assertEqual(c.holderType, 0) + self.assertEqual(c.holderIdx, 8) + self.assertEqual(c.regIdx, 10) + + +# =========================================================================== +# replaceHolder +# =========================================================================== +# +# Adapter duck-types on .items() / .getParams() to detect Module / +# Instruction. TODO(T6): lights up once real Module/Instruction land. + + +class _MockInstr: + """Minimal Instruction-like: exposes a mutable .getParams() list.""" + + def __init__(self, params): + self._params = list(params) + + def getParams(self): + return self._params + + +class _MockModule: + """Minimal Module-like: exposes .items() returning children.""" + + def __init__(self, items): + self._items = list(items) + + def items(self): + return self._items + + +class _MockSWaitCnt: + """Class-name-only marker for the SWaitCnt detection branch.""" + + pass + + +_MockSWaitCnt.__name__ = "SWaitCnt" + + +class TestReplaceHolderLeaf(unittest.TestCase): + def test_scalar_passthrough(self): + # Unknown types are returned unchanged. + self.assertEqual(replaceHolder(42, 5), 42) + self.assertEqual(replaceHolder("hello", 5), "hello") + self.assertIsNone(replaceHolder(None, 5)) + + def test_holder_alone_unchanged(self): + # A bare HolderContainer is neither Module nor Instruction so it + # falls through unmodified. + hc = HolderContainer("v", 4, 1) + result = replaceHolder(hc, 7) + self.assertIs(result, hc) + self.assertEqual(hc.regIdx, 4) + + +class TestReplaceHolderInstruction(unittest.TestCase): + def test_resolves_numeric_holder_in_params(self): + # Instruction branch: walks getParams() and swaps each + # HolderContainer for its resolved snapshot. + hc = HolderContainer("v", 4, 1) + inst = _MockInstr([hc, "literal"]) + replaceHolder(inst, 10) + # Param 0 is now a plain RC, not a HolderContainer. + self.assertNotIsInstance(inst.getParams()[0], HolderContainer) + self.assertIsInstance(inst.getParams()[0], RegisterContainer) + self.assertEqual(inst.getParams()[0].regIdx, 14) + # Non-HolderContainer params are untouched. + self.assertEqual(inst.getParams()[1], "literal") + + def test_resolves_named_holder_in_params(self): + hc = HolderContainer("v", "ValuC", 2) + inst = _MockInstr([hc]) + replaceHolder(inst, 5) + rc = inst.getParams()[0] + self.assertNotIsInstance(rc, HolderContainer) + self.assertEqual(rc.regName.name, "ValuC") + self.assertEqual(rc.regName.offsets, [5]) + self.assertEqual(rc.regNum, 2) + + def test_multiple_holders_resolved(self): + # Same dst applied to every holder in the param list. + h1 = HolderContainer("v", 4, 1) + h2 = HolderContainer("v", "ValuC", 1) + inst = _MockInstr([h1, h2]) + replaceHolder(inst, 3) + self.assertEqual(inst.getParams()[0].regIdx, 7) + self.assertEqual(inst.getParams()[1].regName.offsets, [3]) + + def test_non_holder_register_left_intact(self): + # Plain RegisterContainer params must pass through untouched + # (only HolderContainer instances are resolved). + rc = RegisterContainer("v", None, 5, 1) + inst = _MockInstr([rc]) + replaceHolder(inst, 99) + self.assertIs(inst.getParams()[0], rc) + self.assertEqual(rc.regIdx, 5) + + +class TestReplaceHolderModule(unittest.TestCase): + def test_recurses_into_module_items(self): + # Module branch recurses into each child. + hc = HolderContainer("v", 4, 1) + inst = _MockInstr([hc]) + mod = _MockModule([inst]) + result = replaceHolder(mod, 10) + self.assertIs(result, mod) + self.assertEqual(inst.getParams()[0].regIdx, 14) + + def test_nested_modules(self): + # Module-of-Modules: must recurse fully. + hc = HolderContainer("v", 0, 1) + inner_inst = _MockInstr([hc]) + inner_mod = _MockModule([inner_inst]) + outer_mod = _MockModule([inner_mod]) + replaceHolder(outer_mod, 7) + self.assertEqual(inner_inst.getParams()[0].regIdx, 7) + + def test_mixed_children(self): + # A Module containing instructions AND other modules. + h1 = HolderContainer("v", 1, 1) + h2 = HolderContainer("v", 2, 1) + i1 = _MockInstr([h1]) + i2 = _MockInstr([h2]) + sub = _MockModule([i2]) + root = _MockModule([i1, sub]) + replaceHolder(root, 100) + self.assertEqual(i1.getParams()[0].regIdx, 101) + self.assertEqual(i2.getParams()[0].regIdx, 102) + + +class TestReplaceHolderSWaitCnt(unittest.TestCase): + def test_raises(self): + # SWaitCnt branch raises explicitly (intentional gap). + with self.assertRaises(RuntimeError): + replaceHolder(_MockSWaitCnt(), 0) + + +# =========================================================================== +# Factory functions: vgpr / sgpr / accvgpr / mgpr +# =========================================================================== +# +# Per rocisa::createGPR + the type-specific wrappers in container.cpp. +# Three dispatch arms each (Holder / int / str); plus type-tagging, +# regNum rounding (delegated to RegisterContainer), and modifier-kwarg +# acceptance matching the C++ signatures. + + +class TestVgprFactory(unittest.TestCase): + def test_int_arg_makes_numeric_register(self): + rc = vgpr(5) + self.assertIsInstance(rc, RegisterContainer) + self.assertNotIsInstance(rc, HolderContainer) + self.assertEqual(rc.regType, "v") + self.assertIsNone(rc.regName) + self.assertEqual(rc.regIdx, 5) + self.assertEqual(rc.regNum, 1) + + def test_int_arg_with_regnum(self): + rc = vgpr(8, 4) + self.assertEqual(rc.regIdx, 8) + self.assertEqual(rc.regNum, 4) + + def test_str_arg_makes_symbolic_register(self): + rc = vgpr("ValuA") + self.assertEqual(rc.regType, "v") + self.assertEqual(rc.regIdx, -1) + self.assertIsNotNone(rc.regName) + self.assertEqual(rc.regName.name, "ValuA") + self.assertEqual(rc.regName.offsets, []) + + def test_str_arg_with_offsets_parsed(self): + rc = vgpr("Foo+1+2", 1) + self.assertEqual(rc.regName.name, "Foo") + self.assertEqual(rc.regName.offsets, [1, 2]) + + def test_str_arg_passes_modifier_kwargs(self): + rc = vgpr("Foo", 1, isMacro=True, isAbs=True, isOff=True) + self.assertTrue(rc.isMacro) + self.assertTrue(rc.isAbs) + self.assertTrue(rc.isOff) + + def test_holder_int_makes_typed_holdercontainer(self): + h = Holder(idx=3) + hc = vgpr(h, 2) + self.assertIsInstance(hc, HolderContainer) + self.assertEqual(hc.regType, "v") + self.assertEqual(hc.holderIdx, 3) + self.assertEqual(hc.holderType, 0) + self.assertEqual(hc.regNum, 2) + + def test_holder_str_makes_named_holdercontainer(self): + h = Holder(name="ValuC") + hc = vgpr(h, 1) + self.assertIsInstance(hc, HolderContainer) + self.assertEqual(hc.regType, "v") + self.assertEqual(hc.holderName, "ValuC") + self.assertEqual(hc.holderType, 1) + + def test_bool_rejected(self): + with self.assertRaises(TypeError): + vgpr(True) + with self.assertRaises(TypeError): + vgpr(False, 1) + + def test_unsupported_type_rejected(self): + with self.assertRaises(TypeError): + vgpr(3.14) + with self.assertRaises(TypeError): + vgpr(None) + + def test_regnum_rounded_up_by_container(self): + # vgpr delegates rounding semantics to RegisterContainer.__init__. + rc = vgpr(0, 1.5) + self.assertEqual(rc.regNum, 2) + + +class TestSgprFactory(unittest.TestCase): + def test_int_arg(self): + rc = sgpr(2) + self.assertEqual(rc.regType, "s") + self.assertIsNone(rc.regName) + self.assertEqual(rc.regIdx, 2) + + def test_str_arg_default(self): + rc = sgpr("AddressA") + self.assertEqual(rc.regType, "s") + self.assertEqual(rc.regName.name, "AddressA") + self.assertFalse(rc.isMacro) + self.assertFalse(rc.isAbs) + self.assertFalse(rc.isOff) + + def test_str_arg_isMacro_only(self): + rc = sgpr("Foo", 1, isMacro=True) + self.assertTrue(rc.isMacro) + self.assertFalse(rc.isAbs) + self.assertFalse(rc.isOff) + + def test_str_arg_rejects_isAbs_isOff(self): + # sgpr has no isAbs / isOff in the rocisa signature. + with self.assertRaises(TypeError): + sgpr("Foo", 1, isAbs=True) + with self.assertRaises(TypeError): + sgpr("Foo", 1, isOff=True) + + def test_holder_dispatch(self): + hc = sgpr(Holder(idx=4), 1) + self.assertIsInstance(hc, HolderContainer) + self.assertEqual(hc.regType, "s") + self.assertEqual(hc.holderIdx, 4) + + +class TestAccvgprFactory(unittest.TestCase): + def test_int_arg(self): + rc = accvgpr(7) + self.assertEqual(rc.regType, "acc") + self.assertEqual(rc.regIdx, 7) + + def test_str_arg(self): + rc = accvgpr("Acc") + self.assertEqual(rc.regType, "acc") + self.assertEqual(rc.regName.name, "Acc") + + def test_holder_dispatch(self): + hc = accvgpr(Holder(name="Acc"), 1) + self.assertIsInstance(hc, HolderContainer) + self.assertEqual(hc.regType, "acc") + self.assertEqual(hc.holderName, "Acc") + + def test_rejects_modifier_kwargs(self): + with self.assertRaises(TypeError): + accvgpr("Acc", 1, isMacro=True) + + +class TestMgprFactory(unittest.TestCase): + def test_int_arg(self): + rc = mgpr(0) + self.assertEqual(rc.regType, "m") + self.assertEqual(rc.regIdx, 0) + + def test_str_arg(self): + rc = mgpr("Mdesc") + self.assertEqual(rc.regType, "m") + self.assertEqual(rc.regName.name, "Mdesc") + + def test_holder_dispatch(self): + hc = mgpr(Holder(idx=1), 4) + self.assertIsInstance(hc, HolderContainer) + self.assertEqual(hc.regType, "m") + self.assertEqual(hc.holderIdx, 1) + self.assertEqual(hc.regNum, 4) + + def test_rejects_modifier_kwargs(self): + with self.assertRaises(TypeError): + mgpr("Mdesc", 1, isMacro=True) + + +class TestFactoryEquivalentToDirectConstruction(unittest.TestCase): + """Factories must produce containers indistinguishable from + ``RegisterContainer(...)`` / ``HolderContainer(...)`` direct calls.""" + + def test_vgpr_int_matches_manual_construction(self): + from_factory = vgpr(5, 2) + manual = RegisterContainer("v", None, 5, 2) + self.assertEqual(from_factory.regType, manual.regType) + self.assertEqual(from_factory.regIdx, manual.regIdx) + self.assertEqual(from_factory.regNum, manual.regNum) + self.assertEqual(from_factory.regName, manual.regName) + + def test_vgpr_holder_matches_manual_construction(self): + h = Holder(idx=3) + from_factory = vgpr(h, 1) + manual = HolderContainer("v", 3, 1) + self.assertEqual(from_factory.regType, manual.regType) + self.assertEqual(from_factory.holderIdx, manual.holderIdx) + self.assertEqual(from_factory.holderType, manual.holderType) + + +# =========================================================================== +# ContinuousRegister +# =========================================================================== +# +# rocisa::ContinuousRegister is a POD with read-only {idx, size}. +# The Python shim mirrors that surface; no equality or hashing in the +# C++ binding either. + + +class TestContinuousRegisterConstruction(unittest.TestCase): + def test_positional_ctor(self): + cr = ContinuousRegister(4, 2) + self.assertEqual(cr.idx, 4) + self.assertEqual(cr.size, 2) + + def test_keyword_ctor(self): + cr = ContinuousRegister(idx=8, size=4) + self.assertEqual(cr.idx, 8) + self.assertEqual(cr.size, 4) + + def test_zero_ok(self): + cr = ContinuousRegister(0, 0) + self.assertEqual(cr.idx, 0) + self.assertEqual(cr.size, 0) + + def test_negative_ok(self): + # rocisa stores raw ints; -1 is a common sentinel. + cr = ContinuousRegister(-1, 0) + self.assertEqual(cr.idx, -1) + + def test_rejects_bool(self): + with self.assertRaises(TypeError): + ContinuousRegister(True, 1) + with self.assertRaises(TypeError): + ContinuousRegister(1, False) + + def test_rejects_non_int(self): + with self.assertRaises(TypeError): + ContinuousRegister(1.0, 2) + with self.assertRaises(TypeError): + ContinuousRegister("a", 2) + + +class TestContinuousRegisterReadOnly(unittest.TestCase): + def test_idx_is_read_only(self): + cr = ContinuousRegister(4, 2) + with self.assertRaises(AttributeError): + cr.idx = 5 # type: ignore[misc] + + def test_size_is_read_only(self): + cr = ContinuousRegister(4, 2) + with self.assertRaises(AttributeError): + cr.size = 9 # type: ignore[misc] + + def test_extra_attribute_blocked(self): + cr = ContinuousRegister(4, 2) + with self.assertRaises(AttributeError): + cr.something = 1 # type: ignore[attr-defined] + + +class TestContinuousRegisterRepr(unittest.TestCase): + def test_repr_contains_fields(self): + cr = ContinuousRegister(4, 2) + r = repr(cr) + self.assertIn("4", r) + self.assertIn("2", r) + self.assertIn("ContinuousRegister", r) + + +class TestContinuousRegisterCopyPickle(unittest.TestCase): + def test_copy_returns_equal_state(self): + cr = ContinuousRegister(4, 2) + c = copy.copy(cr) + self.assertIs(type(c), ContinuousRegister) + self.assertEqual(c.idx, 4) + self.assertEqual(c.size, 2) + self.assertIsNot(c, cr) + + def test_deepcopy_returns_equal_state(self): + cr = ContinuousRegister(4, 2) + c = copy.deepcopy(cr) + self.assertIs(type(c), ContinuousRegister) + self.assertEqual(c.idx, 4) + self.assertEqual(c.size, 2) + + def test_pickle_roundtrip(self): + cr = ContinuousRegister(4, 2) + c = pickle.loads(pickle.dumps(cr)) + self.assertEqual(c.idx, 4) + self.assertEqual(c.size, 2) + # Pickled copy is still read-only. + with self.assertRaises(AttributeError): + c.idx = 0 # type: ignore[misc] + + +class TestContinuousRegisterNoEqualityOrHash(unittest.TestCase): + """Mirror rocisa C++: ContinuousRegister has no eq/hash binding.""" + + def test_eq_is_identity(self): + a = ContinuousRegister(4, 2) + b = ContinuousRegister(4, 2) + self.assertNotEqual(a, b) # identity-only equality + self.assertEqual(a, a) + + def test_hashable_via_identity(self): + # Default object.__hash__ is identity-based; we only assert that + # hashing does not raise (rocisa exposes no __hash__ override). + a = ContinuousRegister(4, 2) + hash(a) + self.assertEqual(hash(a), hash(a)) + + +# =========================================================================== +# Container ABC + hardware tokens (T4) +# =========================================================================== + + +class _WavefrontTestCase(unittest.TestCase): + """Pin ``rocIsa`` kernel wavefront for EXEC/VCC ``toString`` branches.""" + + def setUp(self): + self._saved = rocIsa.getInstance().getKernel() + rocIsa.getInstance().setKernel((12, 5, 0), 64) + + def tearDown(self): + info = self._saved + if info.isa is not None: + rocIsa.getInstance().setKernel(info.isa, info.wavefrontSize) + else: + rocIsa.getInstance()._kernel_info = info + + +class TestContainerABC(unittest.TestCase): + def test_not_instantiable(self): + with self.assertRaises(TypeError): + Container() + + def test_subclasses_are_container(self): + self.assertIsInstance(EXEC(), Container) + self.assertIsInstance(VCC(), Container) + self.assertIsInstance(EXECLO(), Container) + self.assertIsInstance(EXECHI(), Container) + self.assertIsInstance(HWRegContainer("r", [1]), Container) + + def test_register_container_is_container(self): + rc = RegisterContainer("v", None, 0, 1) + self.assertIsInstance(rc, Container) + hc = HolderContainer("v", 3, 1) + self.assertIsInstance(hc, Container) + self.assertIsInstance(hc, RegisterContainer) + + +class TestHWRegContainer(_WavefrontTestCase): + def test_to_string(self): + self.assertEqual(str(HWRegContainer("reg", [1, 1])), "hwreg(reg,1,1)") + self.assertEqual( + HWRegContainer("26", [4, 1]).toString(), "hwreg(26,4,1)" + ) + + def test_value_list_copied(self): + raw = [1, 2] + h = HWRegContainer("r", raw) + raw.append(3) + self.assertEqual(h.value, [1, 2]) + + def test_clone_and_pickle(self): + h = HWRegContainer("reg", [1, 1]) + self.assertEqual(str(h.clone()), str(h)) + self.assertEqual(str(copy.deepcopy(h)), str(h)) + self.assertEqual(str(pickle.loads(pickle.dumps(h))), str(h)) + + +class TestEXECLOEXECHI(unittest.TestCase): + def test_exec_lo_hi_tokens(self): + self.assertEqual(str(EXECLO()), "exec_lo") + self.assertEqual(str(EXECHI()), "exec_hi") + self.assertIsInstance(EXECLO(), Container) + + def test_copy_pickle(self): + self.assertEqual(str(copy.deepcopy(EXECLO())), "exec_lo") + self.assertEqual(str(pickle.loads(pickle.dumps(EXECHI()))), "exec_hi") + + +class TestEXECWavefront(_WavefrontTestCase): + def test_wavefront_64(self): + self.assertEqual(str(EXEC()), "exec") + self.assertEqual(str(EXEC(True)), "exec") + + def test_wavefront_32(self): + rocIsa.getInstance().setKernel((12, 5, 0), 32) + self.assertEqual(str(EXEC()), "exec_lo") + self.assertEqual(str(EXEC(True)), "exec_lo") + + def test_clone_matches_str(self): + e = EXEC(True) + self.assertEqual(str(e.clone()), str(e)) + self.assertEqual(str(pickle.loads(pickle.dumps(e))), "exec") + + +class TestVCCWavefront(_WavefrontTestCase): + def test_wavefront_64(self): + self.assertEqual(str(VCC()), "vcc") + self.assertEqual(str(VCC(True)), "vcc") + + def test_wavefront_32(self): + rocIsa.getInstance().setKernel((12, 5, 0), 32) + self.assertEqual(str(VCC()), "vcc_lo") + self.assertEqual(str(VCC(True)), "vcc_hi") + + def test_to_string_alias(self): + self.assertEqual(VCC().toString(), str(VCC())) + + +# =========================================================================== +# MemTokenData (T5) +# =========================================================================== + + +class _Gfx1250CapsTestCase(unittest.TestCase): + """Exercise modifier ``toString`` under gfx1250 caps from ``getHardwareCaps``.""" + + def setUp(self): + self._saved = rocIsa.getInstance().getKernel() + rocIsa.getInstance().setKernel((12, 5, 0), 64) + + def tearDown(self): + info = self._saved + if info.isa is not None: + rocIsa.getInstance().setKernel(info.isa, info.wavefrontSize) + else: + rocIsa.getInstance()._kernel_info = info + + +class TestModifiersGfx1250(_Gfx1250CapsTestCase): + """``toString`` parity with ``container.hpp`` under gfx1250 caps.""" + + def test_ds_modifiers(self): + self.assertEqual(str(DSModifiers(1, 2, gds=True)), " offset:2 gds") + self.assertEqual(str(DSModifiers(2, offset0=3, offset1=4)), " offset0:3 offset1:4") + self.assertEqual(str(DSModifiers(gds=True)), " offset:0 gds") + + def test_global_modifiers(self): + self.assertEqual(str(GLOBALModifiers()), "") + self.assertEqual(str(GLOBALModifiers(16)), " offset:16") + gl2 = GLOBALModifiers( + th=TemporalHint.TH_NT, scope=CacheScope.SCOPE_SE, + ) + self.assertEqual(str(gl2), " th:TH_LOAD_NT scope:SCOPE_SE") + self.assertEqual( + str(GLOBALModifiers(16, TemporalHint.TH_NT, CacheScope.SCOPE_SE)), + " offset:16 th:TH_LOAD_NT scope:SCOPE_SE", + ) + + def test_flat_modifiers_gfx1250(self): + # gfx1250: HasGLCModifier=0, HasSC0Modifier=0 -> glc/slc bits are silent. + flat = FLATModifiers( + offset12=8, glc=True, slc=False, dlc=False, + lds=True, isStore=False, scope=CacheScope.SCOPE_NONE, + ) + self.assertEqual(str(flat), " offset:8 lds") + + def test_flat_modifiers_th_nv(self): + flat = FLATModifiers( + offset12=4, th=TemporalHint.TH_NT, nv=NonVolatile.NV, + ) + self.assertEqual(str(flat), " offset:4 th:TH_LOAD_NT nv") + + def test_mubuf_modifiers_gfx1250(self): + mubuf = MUBUFModifiers( + offen=True, offset12=12, glc=True, slc=False, + dlc=False, scope=CacheScope.SCOPE_NONE, + nt=True, lds=False, isStore=True, + ) + self.assertEqual(str(mubuf), " offen offset:12 ") + + def test_mubuf_modifiers_th_over_nt(self): + mubuf = MUBUFModifiers( + offen=True, offset12=12, nt=True, + th=TemporalHint.TH_NT, isStore=False, + ) + self.assertEqual(str(mubuf), " offen offset:12 th:TH_LOAD_NT") + + def test_mubuf_modifiers_store_th_lu(self): + mubuf = MUBUFModifiers( + offen=True, offset12=0, th=TemporalHint.TH_LU, isStore=True, + ) + self.assertEqual(str(mubuf), " offen offset:0 th:TH_STORE_WB") + + def test_smem_modifiers_gfx1250(self): + # HasSCOPEModifier=1 -> literal "glc" is suppressed (C++ SMEM path). + smem = SMEMModifiers( + glc=True, dlc=False, scope=CacheScope.SCOPE_NONE, + nv=NonVolatile.NV_NONE, offset=8, + ) + self.assertEqual(str(smem), " offset:8") + + def test_smem_modifiers_th_nv(self): + smem = SMEMModifiers( + offset=0, th=TemporalHint.TH_RT, nv=NonVolatile.NV, + ) + self.assertEqual(str(smem), " th:TH_LOAD_RT nv") + + def test_smem_scope_when_set(self): + smem = SMEMModifiers( + glc=False, scope=CacheScope.SCOPE_DEV, offset=0, + ) + self.assertEqual(str(smem), " scope:SCOPE_DEV") + + def test_sdwa_modifiers(self): + sdwa = SDWAModifiers( + dst_sel=SelectBit.WORD_0, + src0_sel=SelectBit.WORD_0, + src1_sel=SelectBit.WORD_1, + ) + self.assertEqual( + str(sdwa), " dst_sel:WORD_0 src0_sel:WORD_0 src1_sel:WORD_1", + ) + + def test_vop3p_modifiers(self): + vop3p = VOP3PModifiers([0, 0], [0, 1], [0, 0]) + self.assertEqual( + str(vop3p), " op_sel:[0,0] op_sel_hi:[0,1] byte_sel:[0,0]", + ) + + def test_dpp_modifiers(self): + dpp = DPPModifiers(row_shr=1, quad_perm=[0, 1, 2, 3]) + self.assertEqual(str(dpp), " row_shr:1 quad_perm:[0,1,2,3]") + + def test_true16_modifiers(self): + self.assertEqual(str(True16Modifiers()), "") + self.assertEqual(str(True16Modifiers(HighBitSel.HIGH)), ".h") + self.assertEqual(str(True16Modifiers(HighBitSel.LOW)), ".l") + self.assertEqual(str(True16Modifiers(2)), ".h") + + def test_is_container(self): + self.assertIsInstance(DSModifiers(), Container) + self.assertIsInstance(FLATModifiers(), Container) + + def test_clone_and_pickle(self): + for obj in ( + DSModifiers(gds=True), + FLATModifiers(lds=True, th=TemporalHint.TH_NT, nv=NonVolatile.NV), + GLOBALModifiers(th=TemporalHint.TH_NT, scope=CacheScope.SCOPE_SE), + MUBUFModifiers(offen=True, offset12=12, glc=True, nt=True, th=TemporalHint.TH_NT), + SMEMModifiers(offset=8, th=TemporalHint.TH_RT, nv=NonVolatile.NV), + SDWAModifiers(dst_sel=SelectBit.WORD_0), + VOP3PModifiers([1], [2], [3]), + DPPModifiers(row_bcast=2), + True16Modifiers(HighBitSel.LOW), + ): + self.assertEqual(str(obj.clone()), str(obj)) + self.assertEqual(str(copy.deepcopy(obj)), str(obj)) + self.assertEqual(str(pickle.loads(pickle.dumps(obj))), str(obj)) + + +class TestGlcSlcBitNames(_Gfx1250CapsTestCase): + def test_gfx1250_defaults_empty(self): + self.assertEqual(getGlcBitName(), "") + self.assertEqual(getSlcBitName(), "") + + def test_has_glc_modifier_branch(self): + caps = dict(rocIsa.getInstance().getAsmCaps()) + caps["HasGLCModifier"] = 1 + caps["HasSC0Modifier"] = 0 + from rocisa_stinkytofu_adaptor.caps import ( # noqa: WPS433 + glc_bit_name_from_caps, + slc_bit_name_from_caps, + ) + + self.assertEqual(glc_bit_name_from_caps(caps), "glc") + self.assertEqual(slc_bit_name_from_caps(caps), "slc") + + +class TestMemTokenData(unittest.TestCase): + def test_empty_tokens(self): + self.assertEqual(str(MemTokenData()), "mem_token:") + self.assertEqual(MemTokenData().toString(), "mem_token:") + + def test_single_and_multiple_tokens(self): + self.assertEqual(str(MemTokenData([7])), "mem_token: 7") + self.assertEqual(str(MemTokenData([1, 2, 3])), "mem_token: 1, 2, 3") + + def test_default_arg_none(self): + m = MemTokenData(None) + self.assertEqual(m.tokens, []) + + def test_tokens_list_copied(self): + raw = [1, 2] + m = MemTokenData(raw) + raw.append(3) + self.assertEqual(m.tokens, [1, 2]) + + def test_tokens_mutable_like_cpp_binding(self): + m = MemTokenData([1]) + m.tokens.append(2) + self.assertEqual(str(m), "mem_token: 1, 2") + + def test_is_container(self): + self.assertIsInstance(MemTokenData([0]), Container) + + def test_clone_and_pickle(self): + m = MemTokenData([4, 5]) + self.assertEqual(str(m.clone()), str(m)) + self.assertEqual(str(copy.deepcopy(m)), str(m)) + self.assertEqual(str(pickle.loads(pickle.dumps(m))), str(m)) + + def test_kernel_writer_style_single_meta(self): + # KernelWriterAssembly passes a one-element list from memTokenLdsBufferMeta. + m = MemTokenData([42]) + self.assertEqual(str(m), "mem_token: 42") + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_emission_consistency.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_emission_consistency.py new file mode 100644 index 000000000000..b7f6af7cae9b --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_emission_consistency.py @@ -0,0 +1,1576 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Three-path emission consistency tests. + +A rocisa-shape ``Module`` built from KernelWriter-style code can be turned +into gfx1250 assembly text via three distinct production code paths -- this +module verifies they all produce byte-identical kernel body for the same +input. + +The three paths (drawn from the IR architecture diagram): + + (1) default rocisa -> ``str(module)`` [right path] + KernelWriter --(rocisa native C++)--> toString --> asm + + (2) default rocisa -> ``toStinkyTofuModule`` -> ``emitAssembly`` + KernelWriter --(rocisa native C++)--> stinkytofu asm IR --> emitAssembly --> asm + + (3) stinkytofu adapter -> ``module.to_stinky_asm`` -> ``emitAssembly`` [left path] + KernelWriter --(rocisa_stinkytofu_adaptor)--> stinkytofu python_binding + --> logical IR --> lowering pass --> stinkytofu asm IR + --> emitAssembly --> asm + +Why all three should match exactly: + - (1) ↔ (3) is the actual Phase 3 acceptance criterion ("the new + stinkytofu logical-IR pipeline is observationally equivalent to the + native rocisa right path for any single instruction we promote"). + - (1) ↔ (2) catches regressions in the rocisa->stinky asm-IR bridge. + - (2) ↔ (3) catches regressions on either side of the asm-IR layer. + +Adding coverage for a new instruction shim: + Subclass ``_ThreePathEqualityCase`` and set ``BUILD_MODULE_SNIPPET`` to + a snippet that constructs a variable named ``module`` (using only + ``rocisa.*`` imports -- they resolve to the right backend in each + subprocess). Three equality tests are generated automatically. + +Running: + Use the ``test.sh`` wrapper in this directory -- it auto-detects the + built ``stinkytofu`` / ``rocisa`` binding directory and exports + ``PYTHONPATH`` for both the parent runner and the per-path subprocesses: + + ./test.sh test_emission_consistency # this file + ./test.sh test_emission_consistency -v + ./test.sh # discover all tests + + If you'd rather invoke ``python3`` directly, set ``PYTHONPATH`` to + the build's ``tensilelite/rocisa`` directory first, e.g.:: + + PYTHONPATH=/.../tensilelite//tensilelite/rocisa \\ + python3 test_emission_consistency.py -v + +Notes: + - Each test method spawns 2 or 3 fresh Python subprocesses (one per + path). They inherit ``PYTHONPATH`` from the parent runner. Backend + selection is via the ``ROCISA_BACKEND`` env var only. + - We always call ``rocIsa.getInstance().init(arch, "")`` then + ``setKernel(arch, 64)`` in the preamble. ``init`` alone registers ISA + metadata (caps); ``setKernel`` installs the per-thread ``KernelInfo`` + whose ``isaVersion`` ``ReadWriteInstruction::typeConvert()`` uses for + gfx11+ mnemonic suffixes (e.g. ``s_load_b64`` vs legacy ``s_load_dwordx2``). + Path (2) still needs caps for ``getAsmCaps()``; paths (1) and (3) need + the active ISA for byte-identical ``toString`` / lowering. + - ``s_set_vgpr_msb`` (gfx1250 VGPR-MSB workaround) is *not* triggered + under the current gfx1250 caps snapshot for VGPR indices < 256. + Once we promote tests that touch VGPRs >= 256 OR enable the + ``HasVgprMSB`` cap, the adapter shim's ``CommonInstruction.__str__`` + will need to grow ``setMsb`` to keep path (1) == (3); the left-path + side is already covered by ``InsertVgprMsbPass`` in stinkytofu. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import textwrap +import unittest + + +# =========================================================================== +# Environment probes +# =========================================================================== +# +# We gate path (2) and path (3) on the stinkytofu binding being importable +# in the current process. Path (1) only needs native rocisa (i.e. +# _rocisa.so), which is always built alongside the adapter anyway. +# +# When invoked via the ``test.sh`` wrapper in this directory, PYTHONPATH +# is auto-set to a built ``tensilelite//tensilelite/rocisa`` and +# the gate passes. Otherwise, the user must set PYTHONPATH themselves -- +# all cases will skip otherwise. + +try: + import stinkytofu as _stinky # noqa: F401 + _STINKY_OK = True +except ImportError: + _STINKY_OK = False + + +# =========================================================================== +# Subprocess runner +# =========================================================================== + + +# Sentinels framing the asm payload in each subprocess's stdout. Anything +# stinkytofu / rocisa prints during import or init (e.g. the +# ``IntrinsicRegistry: Loaded N intrinsics`` banner) lands outside the +# sentinels and gets dropped by the extractor. +_BEGIN = "<<>>" +_END = "<<>>" + + +def _run_in_subproc(script: str, *, backend, timeout: float = 30) -> str: + """Run @p script in a fresh Python process and return the emitted asm. + + @p backend == None -> default rocisa (no ROCISA_BACKEND env var) + @p backend == "stinkytofu" -> our adapter (via env var) + + The script is wrapped so that the asm payload is written between + sentinels; banner prints from third-party imports are stripped. + PYTHONPATH and other env vars are inherited from the parent process so + that ``import rocisa`` / ``import stinkytofu`` resolve to the built + .so files that the parent test runner could already see. + """ + env = os.environ.copy() + env.pop("ROCISA_BACKEND", None) + if backend is not None: + env["ROCISA_BACKEND"] = backend + # PYTHONPATH is inherited from the parent runner (the test.sh wrapper + # in this directory sets it; manual ``python3`` invocations need to + # set it themselves). + proc = subprocess.run( + [sys.executable, "-c", script], + env=env, + capture_output=True, + text=True, + timeout=timeout, + ) + if proc.returncode != 0: + raise AssertionError( + f"emission subprocess (backend={backend!r}) failed " + f"with exit {proc.returncode}\n" + f"--- stderr ---\n{proc.stderr}\n" + f"--- stdout ---\n{proc.stdout}\n" + f"--- script ---\n{script}" + ) + start = proc.stdout.find(_BEGIN) + end = proc.stdout.find(_END) + if start < 0 or end < 0 or end < start: + raise AssertionError( + f"emission subprocess (backend={backend!r}) returned 0 but " + f"sentinels were not found in stdout.\n" + f"--- stdout ---\n{proc.stdout}\n" + f"--- stderr ---\n{proc.stderr}\n" + f"--- script ---\n{script}" + ) + return proc.stdout[start + len(_BEGIN):end] + + +# Shared preamble: ``init`` loads caps for ``arch_tuple``; ``setKernel`` binds +# that ISA to this thread so ``Item::kernel().isaVersion`` (used by native +# ``toString`` type suffixes) matches ``arch_tuple`` — same as real KernelWriter +# flows (see ``KernelWriter`` / unit tests calling ``setKernel`` after ``init``). +_INIT_PREAMBLE = textwrap.dedent("""\ + import rocisa + _ri = rocisa.rocIsa.getInstance() + _ri.init({arch_tuple}, "", False) + _ri.setKernel({arch_tuple}, 64) +""") + + +# =========================================================================== +# Path emitters +# =========================================================================== +# +# Each takes a snippet that constructs a variable named ``module`` and +# returns the asm text emitted by that specific code path. Snippets must +# import via ``rocisa.*`` (not ``rocisa_stinkytofu_adaptor.*``) so the +# backend dispatch in ``rocisa/__init__.py`` can swap implementations. + + +def _strip_kernel_descriptor(asm: str, kernel_name: str) -> str: + """Strip path-2's kernel header / metadata wrapper. + + ``toStinkyTofuModule`` produces a full kernel object: ``.amdgcn_target`` + directive, ``.amdhsa_kernel`` block, ``.amdgpu_metadata`` YAML, then + the kernel symbol label, then the instruction body. We extract only + the instruction body to compare against paths (1) and (3), which emit + just the body. + + Strategy: split on the kernel symbol label (``\\n:\\n``). + The last occurrence is the body marker; everything before it is + descriptor / metadata. + """ + marker = "\n" + kernel_name + ":\n" + idx = asm.rfind(marker) + if idx < 0: + raise AssertionError( + f"path-2 output is missing kernel-symbol marker {marker!r}; " + f"got:\n{asm[:500]}..." + ) + return asm[idx + len(marker):] + + +_EMIT_TAIL = textwrap.dedent(f"""\ + + import sys + sys.stdout.write({_BEGIN!r}) + sys.stdout.write(_payload) + sys.stdout.write({_END!r}) + sys.stdout.flush() +""") + + +def emit_path1_rocisa_tostring(build_snippet: str, *, + arch_tuple=(12, 5, 0)) -> str: + """Path 1 -- default rocisa native + ``str(module)``.""" + script = ( + _INIT_PREAMBLE.format(arch_tuple=arch_tuple) + + build_snippet + + "\n_payload = str(module)\n" + + _EMIT_TAIL + ) + return _run_in_subproc(script, backend=None) + + +def emit_path2_rocisa_stinkyasm(build_snippet: str, *, + arch_tuple=(12, 5, 0), + kernel_name: str = "k") -> str: + """Path 2 -- default rocisa native + ``toStinkyTofuModule`` + ``emitAssembly``. + + Wraps the module in a minimal kernel signature, calls the C++ bridge + that converts rocisa Module to stinkytofu asm IR, emits assembly, + then strips the kernel descriptor wrapper so only the body is + compared. + """ + script = ( + _INIT_PREAMBLE.format(arch_tuple=arch_tuple) + + build_snippet + + textwrap.dedent(f"""\ + + import rocisa + from rocisa.code import SignatureBase + sig = SignatureBase( + kernelName="{kernel_name}", + kernArgsVersion=1, + codeObjectVersion="4", + groupSegmentSize=0, + sgprWorkGroup=(1, 1, 0), + vgprWorkItem=0, + flatWorkGroupSize=64, + numSgprPreload=0, + ) + module.setParent() + st = rocisa.toStinkyTofuModule( + module, {arch_tuple}, "{kernel_name}", + signature=sig, options={{"OptLevel": 0}}, + ) + _payload = st.emitAssembly() + """) + + _EMIT_TAIL + ) + raw = _run_in_subproc(script, backend=None) + return _strip_kernel_descriptor(raw, kernel_name) + + +def emit_path3_adapter_logical(build_snippet: str, *, + arch_tuple=(12, 5, 0)) -> str: + """Path 3 -- stinkytofu adapter + logical IR pipeline + ``emitAssembly``. + + ``ROCISA_BACKEND=stinkytofu`` swaps ``rocisa.*`` for our adapter, so + the same build snippet now constructs adapter ``Module`` / + ``VMovB32`` / ``vgpr`` objects. ``to_stinky_asm(list(arch))`` runs + the C++ ``CompositeInstructionLoweringPass`` + ``ToStinkyAsmPass`` + via ``lower_logical_module``. + """ + script = ( + _INIT_PREAMBLE.format(arch_tuple=arch_tuple) + + build_snippet + + textwrap.dedent(f"""\ + + _asm_mod = module.to_stinky_asm(list({arch_tuple})) + _payload = _asm_mod.emitAssembly() + """) + + _EMIT_TAIL + ) + return _run_in_subproc(script, backend="stinkytofu") + + +# =========================================================================== +# Mixin: subclass + set BUILD_MODULE_SNIPPET = automatic 3 equality tests +# =========================================================================== + + +class _ThreePathEqualityCase: + """Mixin generating three byte-equality tests from a single snippet. + + Subclasses must set ``BUILD_MODULE_SNIPPET`` to a Python snippet that: + * Imports only via ``rocisa.*`` (so backend dispatch can rewire it). + * Builds a top-level variable named ``module``. + * Does NOT print or terminate the process. + + Optional overrides: + * ``ARCH_TUPLE``: target arch (default gfx1250 = ``(12, 5, 0)``). + * ``KERNEL_NAME``: kernel name used for path-2 SignatureBase + (default ``"k"``). + """ + + BUILD_MODULE_SNIPPET: str = "" # override + ARCH_TUPLE: tuple = (12, 5, 0) + KERNEL_NAME: str = "k" + + @unittest.skipUnless(_STINKY_OK, + "path-3 needs the stinkytofu Python binding") + def test_path1_equals_path3(self): + """Native ``toString`` == adapter logical-IR pipeline emit. + + This is the Phase-3 acceptance criterion: the new stinkytofu + left-path is observationally equivalent to the rocisa right-path. + """ + a = emit_path1_rocisa_tostring( + self.BUILD_MODULE_SNIPPET, arch_tuple=self.ARCH_TUPLE) + b = emit_path3_adapter_logical( + self.BUILD_MODULE_SNIPPET, arch_tuple=self.ARCH_TUPLE) + self.assertEqual( + a, b, + f"\n[path-1 toString ] {a!r}" + f"\n[path-3 adapter ] {b!r}", + ) + + @unittest.skipUnless(_STINKY_OK, + "path-2 needs stinkytofu compiled into rocisa") + def test_path1_equals_path2(self): + """Native ``toString`` == native ``toStinkyTofuModule`` body. + + Catches regressions in the rocisa->stinkytofu asm-IR bridge. + """ + a = emit_path1_rocisa_tostring( + self.BUILD_MODULE_SNIPPET, arch_tuple=self.ARCH_TUPLE) + b = emit_path2_rocisa_stinkyasm( + self.BUILD_MODULE_SNIPPET, arch_tuple=self.ARCH_TUPLE, + kernel_name=self.KERNEL_NAME) + self.assertEqual( + a, b, + f"\n[path-1 toString ] {a!r}" + f"\n[path-2 stinky-asm] {b!r}", + ) + + @unittest.skipUnless(_STINKY_OK, + "paths 2 and 3 need the stinkytofu binding") + def test_path2_equals_path3(self): + """Native ``toStinkyTofuModule`` body == adapter logical-IR emit. + + Both ultimately hit ``emitAssembly`` on a stinkytofu asm-IR; this + verifies the rocisa->asm bridge and the adapter->logical->asm + pipeline land in the same asm-IR state for the same input. + """ + a = emit_path2_rocisa_stinkyasm( + self.BUILD_MODULE_SNIPPET, arch_tuple=self.ARCH_TUPLE, + kernel_name=self.KERNEL_NAME) + b = emit_path3_adapter_logical( + self.BUILD_MODULE_SNIPPET, arch_tuple=self.ARCH_TUPLE) + self.assertEqual( + a, b, + f"\n[path-2 stinky-asm] {a!r}" + f"\n[path-3 adapter ] {b!r}", + ) + + +# =========================================================================== +# VMovB32 scenarios +# =========================================================================== +# +# Each class below is one input scenario. Three equality tests are +# auto-generated. Add a new instruction = add a new class. + + +class TestVMovB32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_mov_b32 v0, v1`` -- the most basic VGPR-to-VGPR move.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMovB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMovB32(dst=vgpr(0), src=vgpr(1), comment="probe")) + """) + + +class TestVMovB32_VgprToVgprNoComment(unittest.TestCase, + _ThreePathEqualityCase): + """Empty-comment branch in ``formatStr`` (no padding, no '//').""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMovB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMovB32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVMovB32_HexImmediate(unittest.TestCase, _ThreePathEqualityCase): + """``v_mov_b32 v0, 0x0`` -- string-immediate src (KernelWriter's + typical ``hex(N)`` pattern).""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMovB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMovB32(dst=vgpr(0), src="0x0", comment="init zero")) + """) + + +class TestVMovB32_IntImmediate(unittest.TestCase, _ThreePathEqualityCase): + """``v_mov_b32 v0, 42`` -- int-immediate src.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMovB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMovB32(dst=vgpr(0), src=42, comment="int imm")) + """) + + +class TestVMovB32_MultipleSequential(unittest.TestCase, + _ThreePathEqualityCase): + """Three sequential ``v_mov_b32`` -- verifies Module child order + survives all three paths.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMovB32 + from rocisa.container import vgpr + module = Module("k") + for i in range(3): + module.add(VMovB32(dst=vgpr(i), src=vgpr(i + 10), + comment=f"move {i}")) + """) + + +class TestVMovB32_NestedModules(unittest.TestCase, _ThreePathEqualityCase): + """Inner Module inside outer Module -- depth-first traversal must + preserve emit order across all three paths.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMovB32 + from rocisa.container import vgpr + module = Module("k") + inner = Module("sub") + inner.add(VMovB32(dst=vgpr(0), src=vgpr(1), comment="inner")) + module.add(inner) + module.add(VMovB32(dst=vgpr(2), src=vgpr(3), comment="outer")) + """) + + +class TestVMovB32_NamedRegister(unittest.TestCase, _ThreePathEqualityCase): + """``v_mov_b32 vgprValuA, vgprValuB`` -- symbolic-name VGPR via + ``vgpr("Name")``. Verifies the RegName round-trip survives the + rocisa-shape -> logical-IR -> asm-IR pipeline.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMovB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMovB32(dst=vgpr("ValuA"), src=vgpr("ValuB"), + comment="symbolic")) + """) + + +# =========================================================================== +# SMovB32 / SMovB64 scenarios (Phase A scalar moves) +# =========================================================================== + + +class TestSMovB32_SgprToSgpr(unittest.TestCase, _ThreePathEqualityCase): + """``s_mov_b32 s0, s1`` -- basic SGPR-to-SGPR move.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SMovB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SMovB32(dst=sgpr(0), src=sgpr(1), comment="probe")) + """) + + +class TestSMovB32_SgprToSgprNoComment(unittest.TestCase, _ThreePathEqualityCase): + """Empty-comment branch for ``s_mov_b32`` (no ``//`` suffix).""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SMovB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SMovB32(dst=sgpr(0), src=sgpr(1))) + """) + + +class TestSMovB32_HexImmediate(unittest.TestCase, _ThreePathEqualityCase): + """``s_mov_b32 s0, 0x0`` -- string-immediate src.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SMovB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SMovB32(dst=sgpr(0), src="0x0", comment="init zero")) + """) + + +class TestSMovB64_PairToPair(unittest.TestCase, _ThreePathEqualityCase): + """``s_mov_b64 s[0:1], s[4:5]`` -- 64-bit pair operands.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SMovB64 + from rocisa.container import sgpr + module = Module("k") + module.add(SMovB64(dst=sgpr(0, 2), src=sgpr(4, 2), comment="wide")) + """) + + +class TestSLoadB32_BaseImmOffset(unittest.TestCase, _ThreePathEqualityCase): + """``s_load_b32 s0, s[2:3], 0`` -- minimal SMEM load (no modifiers).""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SLoadB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SLoadB32(dst=sgpr(0), base=sgpr(2, 2), soffset=0, + comment="smem")) + """) + + +class TestSLoadB64_WideDst(unittest.TestCase, _ThreePathEqualityCase): + """``s_load_b64 s[0:1], s[4:5], 0``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SLoadB64 + from rocisa.container import sgpr + module = Module("k") + module.add(SLoadB64(dst=sgpr(0, 2), base=sgpr(4, 2), soffset=0, + comment="wide load")) + """) + + +class TestSNop_Wait0(unittest.TestCase, _ThreePathEqualityCase): + """``s_nop 0`` -- minimal scalar NOP (wait=0).""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SNop + module = Module("k") + module.add(SNop(waitState=0, comment="align")) + """) + + +# =========================================================================== +# Scalar ALU -- arithmetic, shift, bitwise (Phase 6 Step 1) +# =========================================================================== + + +class TestSAddU32_SgprToSgpr(unittest.TestCase, _ThreePathEqualityCase): + """``s_add_u32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SAddU32 + from rocisa.container import sgpr + module = Module("k") + module.add(SAddU32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2), comment="add")) + """) + + +class TestSAddU32_Immediate(unittest.TestCase, _ThreePathEqualityCase): + """``s_add_u32 s0, s1, 4`` -- immediate operand.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SAddU32 + from rocisa.container import sgpr + module = Module("k") + module.add(SAddU32(dst=sgpr(0), src0=sgpr(1), src1=4)) + """) + + +class TestSSubU32_SgprToSgpr(unittest.TestCase, _ThreePathEqualityCase): + """``s_sub_u32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SSubU32 + from rocisa.container import sgpr + module = Module("k") + module.add(SSubU32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2), comment="sub")) + """) + + +class TestSMulI32_SgprToSgpr(unittest.TestCase, _ThreePathEqualityCase): + """``s_mul_i32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SMulI32 + from rocisa.container import sgpr + module = Module("k") + module.add(SMulI32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2), comment="mul")) + """) + + +class TestSMulHII32_SgprToSgpr(unittest.TestCase, _ThreePathEqualityCase): + """``s_mul_hi_i32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SMulHII32 + from rocisa.container import sgpr + module = Module("k") + module.add(SMulHII32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2))) + """) + + +class TestSLShiftLeftB32(unittest.TestCase, _ThreePathEqualityCase): + """``s_lshl_b32 s0, s1, s2`` -- value=s1, shift=s2.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SLShiftLeftB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SLShiftLeftB32(dst=sgpr(0), shiftHex=sgpr(2), src=sgpr(1), comment="shl")) + """) + + +class TestSLShiftRightB32(unittest.TestCase, _ThreePathEqualityCase): + """``s_lshr_b32 s0, s1, s2`` -- value=s1, shift=s2.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SLShiftRightB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SLShiftRightB32(dst=sgpr(0), shiftHex=sgpr(2), src=sgpr(1))) + """) + + +class TestSAShiftRightI32(unittest.TestCase, _ThreePathEqualityCase): + """``s_ashr_i32 s0, s1, s2`` -- value=s1, shift=s2.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SAShiftRightI32 + from rocisa.container import sgpr + module = Module("k") + module.add(SAShiftRightI32(dst=sgpr(0), shiftHex=sgpr(2), src=sgpr(1))) + """) + + +class TestSAndB32(unittest.TestCase, _ThreePathEqualityCase): + """``s_and_b32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SAndB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SAndB32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2), comment="mask")) + """) + + +class TestSOrB32(unittest.TestCase, _ThreePathEqualityCase): + """``s_or_b32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SOrB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SOrB32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2))) + """) + + +class TestSXorB32(unittest.TestCase, _ThreePathEqualityCase): + """``s_xor_b32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SXorB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SXorB32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2))) + """) + + +class TestSAndSaveExecB32(unittest.TestCase, _ThreePathEqualityCase): + """``s_and_saveexec_b32 s0, s1`` -- unary (1 src).""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SAndSaveExecB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SAndSaveExecB32(dst=sgpr(0), src=sgpr(1), comment="exec")) + """) + + +class TestSLShiftLeft1AddU32(unittest.TestCase, _ThreePathEqualityCase): + """``s_lshl1_add_u32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SLShiftLeft1AddU32 + from rocisa.container import sgpr + module = Module("k") + module.add(SLShiftLeft1AddU32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2))) + """) + + +# =========================================================================== +# Scalar Control (Phase 6 Step 2) -- SGetRegB32 +# =========================================================================== + + +class TestSGetRegB32(unittest.TestCase, _ThreePathEqualityCase): + """``s_getreg_b32 s0, s1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SGetRegB32 + from rocisa.container import sgpr + module = Module("k") + module.add(SGetRegB32(dst=sgpr(0), src=sgpr(1), comment="hwid")) + """) + + +# =========================================================================== +# Vector ALU (Phase 6 Step 4) +# =========================================================================== + + +## VAddU32 emission consistency is skipped: native rocisa uses arch caps to +## choose between ``v_add_u32`` (+ VCC dst1) and ``v_add_nc_u32`` (no dst1) +## depending on ExplicitNC/ExplicitCO flags. The stinkytofu logical IR +## lowering always emits ``v_add_nc_u32`` for gfx1250, but the native rocisa +## build in the test environment may not have ExplicitNC configured, causing +## path1/path2 to emit ``v_add_u32 dst, vcc, src0, src1``. This is a known +## configuration gap between the native right-path and the stinkytofu +## left-path; the adaptor correctly matches the logical IR lowering output. +## Same applies to VSubI32 / VSubU32 (ExplicitNC variants). + + +class TestVAddF32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_add_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VAddF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VAddF32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), comment="addf")) + """) + + +class TestVSubF32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_sub_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VSubF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VSubF32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVMulF32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_mul_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMulF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMulF32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), comment="mul")) + """) + + +class TestVMulLOU32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_mul_lo_u32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMulLOU32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMulLOU32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVMulHIU32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_mul_hi_u32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMulHIU32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMulHIU32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVAndB32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_and_b32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VAndB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VAndB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVOrB32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_or_b32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VOrB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VOrB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVXorB32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_xor_b32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VXorB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VXorB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVLShiftLeftB32(unittest.TestCase, _ThreePathEqualityCase): + """``v_lshlrev_b32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VLShiftLeftB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VLShiftLeftB32(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2), comment="shl")) + """) + + +class TestVLShiftRightB32(unittest.TestCase, _ThreePathEqualityCase): + """``v_lshrrev_b32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VLShiftRightB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VLShiftRightB32(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2))) + """) + + +class TestVFmaF32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_fma_f32 v0, v1, v2, v3``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VFmaF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VFmaF32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3), comment="fma")) + """) + + +class TestVAndOrB32_VgprToVgpr(unittest.TestCase, _ThreePathEqualityCase): + """``v_and_or_b32 v0, v1, v2, v3``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VAndOrB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VAndOrB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3))) + """) + + +## VCndMaskB32 emission consistency is skipped: native rocisa always appends +## VCC as src2 (``VCndMaskB32(dst, src0, src1, src2=VCC())``), while the +## logical IR lowering path uses only 2 sources (VCC mask is implicit in the +## architecture). This is an intentional design divergence, analogous to +## SBarrier's signal/wait expansion. The adaptor's to_stinky_logical correctly +## passes 2 srcs and the instruction unit tests (test_instruction.py) cover +## the adaptor's own rendering and deepcopy. + + +class TestVReadfirstlaneB32(unittest.TestCase, _ThreePathEqualityCase): + """``v_readfirstlane_b32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VReadfirstlaneB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VReadfirstlaneB32(dst=vgpr(0), src=vgpr(1), comment="lane0")) + """) + + +# =========================================================================== +# Compare instructions (Phase 6 Step 5) +# =========================================================================== + + +class TestSCmpEQI32(unittest.TestCase, _ThreePathEqualityCase): + """``s_cmp_eq_i32 s0, s1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SCmpEQI32 + from rocisa.container import sgpr + module = Module("k") + module.add(SCmpEQI32(src0=sgpr(0), src1=sgpr(1), comment="eq")) + """) + + +class TestSCmpGtU32(unittest.TestCase, _ThreePathEqualityCase): + """``s_cmp_gt_u32 s0, s1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SCmpGtU32 + from rocisa.container import sgpr + module = Module("k") + module.add(SCmpGtU32(src0=sgpr(0), src1=sgpr(1))) + """) + + +class TestSCmpLgI32(unittest.TestCase, _ThreePathEqualityCase): + """``s_cmp_lg_i32 s0, s1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SCmpLgI32 + from rocisa.container import sgpr + module = Module("k") + module.add(SCmpLgI32(src0=sgpr(0), src1=sgpr(1))) + """) + + +class TestSBitcmp1B32(unittest.TestCase, _ThreePathEqualityCase): + """``s_bitcmp1_b32 s0, s1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SBitcmp1B32 + from rocisa.container import sgpr + module = Module("k") + module.add(SBitcmp1B32(src0=sgpr(0), src1=sgpr(1))) + """) + + +class TestVCmpEQF32(unittest.TestCase, _ThreePathEqualityCase): + """``v_cmp_eq_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCmpEQF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCmpEQF32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), comment="eq")) + """) + + +class TestVCmpGEI32(unittest.TestCase, _ThreePathEqualityCase): + """``v_cmp_ge_i32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCmpGEI32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCmpGEI32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVCmpNeU32(unittest.TestCase, _ThreePathEqualityCase): + """``v_cmp_ne_u32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCmpNeU32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCmpNeU32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVCmpClassF32(unittest.TestCase, _ThreePathEqualityCase): + """``v_cmp_class_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCmpClassF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCmpClassF32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +## VCmpX* emission consistency is skipped: the native rocisa VCmpXInstruction +## toString() has arch-dependent behaviour — when CMPXWritesSGPR is false (test +## env default), it replaces "v_cmpx_" with "v_cmp_" and appends an +## "s_mov_b64 exec, dst" (wavefront64) or "s_mov_b32 exec_lo, dst" (wavefront32) +## instruction. The logical IR path on gfx1250 (which has CMPXWritesSGPR=true) +## correctly emits the real "v_cmpx_*" mnemonic directly. This is an intentional +## arch-specific divergence in the native C++ toString; the adaptor correctly +## forwards to the logical IR lowering which matches the hardware behaviour. + + +# =========================================================================== +# Step 6: Scalar Min/Max/Abs + Vector Unary/Misc +# =========================================================================== + + +class TestSAbsI32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``s_abs_i32 s0, s1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SAbsI32 + from rocisa.container import sgpr + module = Module("k") + module.add(SAbsI32(dst=sgpr(0), src=sgpr(1))) + """) + + +class TestSMaxI32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``s_max_i32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SMaxI32 + from rocisa.container import sgpr + module = Module("k") + module.add(SMaxI32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2))) + """) + + +class TestSMinU32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``s_min_u32 s0, s1, s2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import SMinU32 + from rocisa.container import sgpr + module = Module("k") + module.add(SMinU32(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2))) + """) + + +class TestVExpF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_exp_f32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VExpF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VExpF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVRcpF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_rcp_f32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VRcpF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VRcpF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVRsqF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_rsq_f32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VRsqF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VRsqF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVNotB32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_not_b32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VNotB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VNotB32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVRndneF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_rndne_f32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VRndneF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VRndneF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVMaxF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_max_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMaxF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMaxF32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVMinF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_min_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMinF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMinF32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVMaxI32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_max_i32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMaxI32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMaxI32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVMed3I32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_med3_i32 v0, v1, v2, v3``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMed3I32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMed3I32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3))) + """) + + +class TestVMed3F32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_med3_f32 v0, v1, v2, v3``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VMed3F32 + from rocisa.container import vgpr + module = Module("k") + module.add(VMed3F32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3))) + """) + + +class TestVAShiftRightI32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_ashrrev_i32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VAShiftRightI32 + from rocisa.container import vgpr + module = Module("k") + module.add(VAShiftRightI32(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2))) + """) + + +class TestVPackF16toB32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_pack_b32_f16 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VPackF16toB32 + from rocisa.container import vgpr + module = Module("k") + module.add(VPackF16toB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +## VLShiftLeftOrB32 emission consistency is skipped: the native rocisa class is a +## CompositeInstruction that expands to two sub-instructions +## ("v_lshlrev_b32 dst, shift, src" + "v_or_b32 dst, dst, src2"), while the +## logical IR correctly emits the fused "v_lshl_or_b32 dst, shift, src, src2". +## This is an intentional arch-level optimisation in the logical IR pipeline. + + +## VPrngB32 emission consistency is skipped: "v_prng_b32" is only available +## on architectures with hardware PRNG support. The instruction may not +## assemble successfully in all test environments. + + +# =========================================================================== +# Step 7 — Conversion (VCvt*) emission consistency tests +# =========================================================================== + + +class TestVCvtF16toF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_f32_f16 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtF16toF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtF16toF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtF32toF16Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_f16_f32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtF32toF16 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtF32toF16(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtF32toU32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_u32_f32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtF32toU32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtF32toU32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtU32toF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_f32_u32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtU32toF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtU32toF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtI32toF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_f32_i32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtI32toF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtI32toF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtF32toI32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_i32_f32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtF32toI32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtF32toI32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtFP8toF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_f32_fp8 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtFP8toF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtFP8toF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtBF8toF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_f32_bf8 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtBF8toF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtBF8toF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtPkFP8toF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_pk_f32_fp8 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtPkFP8toF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtPkFP8toF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtPkBF8toF32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_pk_f32_bf8 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtPkBF8toF32 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtPkBF8toF32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestVCvtPkF32toBF8Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_pk_bf8_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtPkF32toBF8 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtPkF32toBF8(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVCvtSRF32toFP8Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_sr_fp8_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtSRF32toFP8 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtSRF32toFP8(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVCvtSRF32toBF8Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_sr_bf8_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtSRF32toBF8 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtSRF32toBF8(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVCvtPkF32toFP8Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_pk_fp8_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtPkF32toFP8 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtPkF32toFP8(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +class TestVCvtPkF32toBF16Emission(unittest.TestCase, _ThreePathEqualityCase): + """``v_cvt_pk_bf16_f32 v0, v1, v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import VCvtPkF32toBF16 + from rocisa.container import vgpr + module = Module("k") + module.add(VCvtPkF32toBF16(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + """) + + +## VCvtScale* emission consistency tests are skipped: these instructions are in +## SKIP_LOWERING in LogicalToAsmMultiArchTest.cpp because the standard lowering +## pass cannot handle them (they require scale-specific patterns not yet in the +## lowering pipeline). The adaptor correctly constructs them with to_stinky_logical() +## but the asm emission cannot be compared end-to-end yet. + + +# =========================================================================== +# Memory instructions -- DS Load/Store (Step 8) +# =========================================================================== + + +class TestDSLoadB32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_load_b32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSLoadB32 + from rocisa.container import vgpr + module = Module("k") + module.add(DSLoadB32(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestDSLoadB64Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_load_b64 v[0:1], v2``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSLoadB64 + from rocisa.container import vgpr + module = Module("k") + module.add(DSLoadB64(dst=vgpr(0, 2), src=vgpr(2))) + """) + + +class TestDSLoadB128Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_load_b128 v[0:3], v4``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSLoadB128 + from rocisa.container import vgpr + module = Module("k") + module.add(DSLoadB128(dst=vgpr(0, 4), src=vgpr(4))) + """) + + +class TestDSLoadU8Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_load_u8 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSLoadU8 + from rocisa.container import vgpr + module = Module("k") + module.add(DSLoadU8(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestDSLoadU16Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_load_u16 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSLoadU16 + from rocisa.container import vgpr + module = Module("k") + module.add(DSLoadU16(dst=vgpr(0), src=vgpr(1))) + """) + + +class TestDSStoreB32Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_store_b32 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSStoreB32 + from rocisa.container import vgpr + module = Module("k") + module.add(DSStoreB32(dstAddr=vgpr(0), src=vgpr(1))) + """) + + +class TestDSStoreB64Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_store_b64 v0, v[1:2]``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSStoreB64 + from rocisa.container import vgpr + module = Module("k") + module.add(DSStoreB64(dstAddr=vgpr(0), src=vgpr(1, 2))) + """) + + +class TestDSStoreB128Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_store_b128 v0, v[1:4]``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSStoreB128 + from rocisa.container import vgpr + module = Module("k") + module.add(DSStoreB128(dstAddr=vgpr(0), src=vgpr(1, 4))) + """) + + +class TestDSStoreB8Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_store_b8 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSStoreB8 + from rocisa.container import vgpr + module = Module("k") + module.add(DSStoreB8(dstAddr=vgpr(0), src=vgpr(1))) + """) + + +class TestDSStoreB16Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_store_b16 v0, v1``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSStoreB16 + from rocisa.container import vgpr + module = Module("k") + module.add(DSStoreB16(dstAddr=vgpr(0), src=vgpr(1))) + """) + + +class TestDSStoreB96Emission(unittest.TestCase, _ThreePathEqualityCase): + """``ds_store_b96 v0, v[1:3]``.""" + + BUILD_MODULE_SNIPPET = textwrap.dedent("""\ + from rocisa.code import Module + from rocisa.instruction import DSStoreB96 + from rocisa.container import vgpr + module = Module("k") + module.add(DSStoreB96(dstAddr=vgpr(0), src=vgpr(1, 3))) + """) + + +## DSBPermuteB32 emission consistency is skipped: native rocisa path-1 toString() +## and path-2 toStinkyTofuModule produce different asm for this instruction +## (mismatch unrelated to our adaptor), so three-path equality is not achievable. +## The adaptor's to_stinky_logical() is validated by unit tests above. + + +## Buffer/Flat emission consistency tests are skipped: the logicalIR lowering +## for buffer/flat memory instructions uses a simplified operand model (2-3 +## register args) while the native rocisa toString() emits full asm with buffer +## descriptors, soffset, and MUBUF/FLAT modifiers. Matching all three paths +## end-to-end requires the lowering pass to reconstruct these modifiers from +## the abstract operand set, which is architecture-specific and not yet +## validated for the adaptor path. + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_enum_values.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_enum_values.py new file mode 100644 index 000000000000..2c024cb39630 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_enum_values.py @@ -0,0 +1,42 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Verify develop-gap enum members expose correct C++ integral values.""" + +import os +import sys +import unittest + +_PKG_PARENT = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir) +) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor.enum import ( # noqa: E402 + InstType, + NonVolatile, + TemporalHint, +) + + +class TestDevelopGapEnumValues(unittest.TestCase): + def test_temporal_hint_values(self): + self.assertEqual(TemporalHint.TH_NONE, -1) + self.assertEqual(TemporalHint.TH_RT, 0) + self.assertEqual(TemporalHint.TH_NT, 1) + self.assertEqual(TemporalHint.TH_LU, 3) + self.assertEqual(TemporalHint.TH_WB, 3) + self.assertEqual(TemporalHint.TH_RESERVED, 7) + self.assertEqual(TemporalHint.TH_NT_WB, 7) + + def test_non_volatile_values(self): + self.assertEqual(NonVolatile.NV_NONE, 0) + self.assertEqual(NonVolatile.NV, 1) + + def test_inst_type_b192_value(self): + self.assertEqual(InstType.INST_B192, 22) + self.assertEqual(InstType.INST_NOTYPE, 68) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_functions.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_functions.py new file mode 100644 index 000000000000..6364ba4363d4 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_functions.py @@ -0,0 +1,551 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Standalone tests for ``rocisa_stinkytofu_adaptor.functions``. + +Run from any working directory: + + python3 projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_functions.py + +Or with pytest if available: + + pytest projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_functions.py + +Section A — ``ArgumentLoader`` (real): offset-bookkeeping contract that +Tensile's KernelWriterAssembly relies on (``self.argLoader.getOffset()`` +is read directly to compute ``s_load_b*`` immediates). Instruction +emission is stubbed; only byte advancement is checked here. + +Section B — dummy exports (structural): every ``make_dummy_func`` symbol +in ``functions.py`` must import, be callable, and return ``None``. +""" + +from __future__ import annotations + +import os +import sys +import unittest +from unittest import mock + +# --------------------------------------------------------------------------- +# Self-contained sys.path bootstrap so the test runs without any install / +# editable-mode setup. The ``rocisa_stinkytofu_adaptor`` Python package +# lives at: +# projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/ +# This file lives at: +# projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_functions.py +# So the package's parent directory (where ``import +# rocisa_stinkytofu_adaptor`` resolves) is one level up. +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor import functions as _functions # noqa: E402 +from rocisa_stinkytofu_adaptor.functions import ArgumentLoader # noqa: E402 + + +# --------------------------------------------------------------------------- +# Registry of dummy function exports in ``functions.py``. +# --------------------------------------------------------------------------- + +FUNCTIONS_DUMMY_EXPORTS: tuple[str, ...] = ( + # DS init + "DSInit", +) + + +# =========================================================================== +# Section A — ArgumentLoader (real implementation) +# =========================================================================== + + +class TestArgumentLoaderConstruction(unittest.TestCase): + def test_initial_offset_is_zero(self): + # Mirrors ``ArgumentLoader() : kernArgOffset(0)`` in argument.hpp:34. + loader = ArgumentLoader() + self.assertEqual(loader.getOffset(), 0) + + def test_returns_int_not_dummy(self): + # The whole point of this workaround: ``getOffset()`` must be a real + # ``int`` because Tensile does ``getOffset() - numSgprPreload * 4``. + loader = ArgumentLoader() + self.assertIsInstance(loader.getOffset(), int) + + +class TestArgumentLoaderSetGetReset(unittest.TestCase): + def test_setOffset_then_getOffset(self): + loader = ArgumentLoader() + loader.setOffset(64) + self.assertEqual(loader.getOffset(), 64) + + def test_setOffset_overwrites(self): + loader = ArgumentLoader() + loader.setOffset(64) + loader.setOffset(128) + self.assertEqual(loader.getOffset(), 128) + + def test_setOffset_coerces_to_int(self): + loader = ArgumentLoader() + loader.setOffset(48) + self.assertIsInstance(loader.getOffset(), int) + + def test_resetOffset_zeros(self): + loader = ArgumentLoader() + loader.setOffset(96) + loader.resetOffset() + self.assertEqual(loader.getOffset(), 0) + + def test_resetOffset_after_loads(self): + loader = ArgumentLoader() + loader.loadKernArg("AddressDbg", "KernArgAddress", dword=2) + loader.resetOffset() + self.assertEqual(loader.getOffset(), 0) + + +class TestArgumentLoaderLoadKernArg(unittest.TestCase): + def test_default_dword_advances_4_bytes(self): + loader = ArgumentLoader() + loader.loadKernArg("AddressDbg", "KernArgAddress") + self.assertEqual(loader.getOffset(), 4) + + def test_dword_2_advances_8_bytes(self): + loader = ArgumentLoader() + loader.loadKernArg("AddressDbg", "KernArgAddress", dword=2) + self.assertEqual(loader.getOffset(), 8) + + def test_dword_4_advances_16_bytes(self): + loader = ArgumentLoader() + loader.loadKernArg("AddressDbg", "KernArgAddress", dword=4) + self.assertEqual(loader.getOffset(), 16) + + def test_repeated_calls_accumulate(self): + loader = ArgumentLoader() + loader.loadKernArg("a", "KernArgAddress", dword=1) + loader.loadKernArg("b", "KernArgAddress", dword=2) + loader.loadKernArg("c", "KernArgAddress", dword=4) + self.assertEqual(loader.getOffset(), 4 + 8 + 16) + + def test_with_sgprOffset_does_not_advance(self): + # ``kernArgOffset += sgprOffset ? 0 : size`` — explicit sgprOffset + # means the caller is providing its own offset, so the loader must + # NOT bump kernArgOffset (argument.hpp:119). + loader = ArgumentLoader() + loader.loadKernArg("AddressDbg", "KernArgAddress", + sgprOffset=hex(64), dword=2) + self.assertEqual(loader.getOffset(), 0) + + def test_with_sgprOffset_int_does_not_advance(self): + # InstructionInput in C++ accepts both int and shared_ptr; + # in Python both surface as just "non-None" — covered identically. + loader = ArgumentLoader() + loader.loadKernArg("AddressDbg", "KernArgAddress", + sgprOffset=64, dword=2) + self.assertEqual(loader.getOffset(), 0) + + def test_writeSgpr_false_still_advances(self): + # The C++ advances outside the ``if(writeSgpr)`` block; this case + # corresponds to "skip unused parm" (argument.hpp:57-58). + loader = ArgumentLoader() + loader.loadKernArg("UnusedParm", "KernArgAddress", + dword=2, writeSgpr=False) + self.assertEqual(loader.getOffset(), 8) + + def test_returns_sload_instruction(self): + from rocisa_stinkytofu_adaptor.instruction import SLoadB32, SLoadB64 + loader = ArgumentLoader() + item = loader.loadKernArg("a", "KernArgAddress", dword=1) + self.assertIsInstance(item, SLoadB32) + item2 = loader.loadKernArg("b", "KernArgAddress", dword=2) + self.assertIsInstance(item2, SLoadB64) + + def test_writeSgpr_false_returns_textblock(self): + from rocisa_stinkytofu_adaptor.code import TextBlock + loader = ArgumentLoader() + item = loader.loadKernArg("x", "y", dword=2, writeSgpr=False) + self.assertIsInstance(item, TextBlock) + + +class TestArgumentLoaderLoadAllKernArg(unittest.TestCase): + def test_basic_total_advance(self): + loader = ArgumentLoader() + loader.loadAllKernArg(sgprStartIndex=0, srcAddr="KernArgAddress", + numSgprToLoad=8) + self.assertEqual(loader.getOffset(), 8 * 4) + + def test_with_preload_total_advance_is_full(self): + # numSgprPreload * 4 (initial) + actualLoad * 4 (chunked) = numSgprToLoad * 4 + loader = ArgumentLoader() + loader.loadAllKernArg(sgprStartIndex=0, srcAddr="KernArgAddress", + numSgprToLoad=20, numSgprPreload=4) + self.assertEqual(loader.getOffset(), 20 * 4) + + def test_preload_equals_total_still_advances_full(self): + # Edge: every sgpr is preloaded (actualLoad == 0); only the initial + # ``kernArgOffset += numSgprPreload * 4`` runs. + loader = ArgumentLoader() + loader.loadAllKernArg(sgprStartIndex=0, srcAddr="KernArgAddress", + numSgprToLoad=8, numSgprPreload=8) + self.assertEqual(loader.getOffset(), 8 * 4) + + def test_zero_load_is_noop(self): + loader = ArgumentLoader() + loader.loadAllKernArg(sgprStartIndex=0, srcAddr="KernArgAddress", + numSgprToLoad=0) + self.assertEqual(loader.getOffset(), 0) + + def test_chained_with_loadKernArg(self): + loader = ArgumentLoader() + loader.loadAllKernArg(sgprStartIndex=0, srcAddr="KernArgAddress", + numSgprToLoad=4) # +16 + loader.loadKernArg("Foo", "KernArgAddress", dword=2) # +8 + loader.loadKernArg("Bar", "KernArgAddress", + sgprOffset=0, dword=2) # +0 + self.assertEqual(loader.getOffset(), 16 + 8) + + def test_returns_module(self): + from rocisa_stinkytofu_adaptor.code import Module + loader = ArgumentLoader() + mod = loader.loadAllKernArg(0, "KernArgAddress", 4) + self.assertIsInstance(mod, Module) + + def test_module_contains_sload_instructions(self): + from rocisa_stinkytofu_adaptor.instruction import SLoadB128 + loader = ArgumentLoader() + mod = loader.loadAllKernArg(0, "KernArgAddress", 4) + self.assertEqual(len(mod.itemList), 1) + self.assertIsInstance(mod.itemList[0], SLoadB128) + + def test_greedy_packing(self): + from rocisa_stinkytofu_adaptor.instruction import ( + SLoadB32, SLoadB64, SLoadB128, SLoadB512, + ) + loader = ArgumentLoader() + mod = loader.loadAllKernArg(0, "KernArgAddress", 20) + self.assertEqual(len(mod.itemList), 2) + self.assertIsInstance(mod.itemList[0], SLoadB512) + self.assertIsInstance(mod.itemList[1], SLoadB128) + + def test_unaligned_start(self): + from rocisa_stinkytofu_adaptor.instruction import ( + SLoadB32, SLoadB64, SLoadB128, + ) + loader = ArgumentLoader() + mod = loader.loadAllKernArg(1, "KernArgAddress", 7) + self.assertEqual(len(mod.itemList), 3) + self.assertIsInstance(mod.itemList[0], SLoadB32) + self.assertIsInstance(mod.itemList[1], SLoadB64) + self.assertIsInstance(mod.itemList[2], SLoadB128) + + def test_countSMemLoad_on_output(self): + from rocisa_stinkytofu_adaptor import countSMemLoad + loader = ArgumentLoader() + mod = loader.loadAllKernArg(0, "KernArgAddress", 20) + self.assertEqual(countSMemLoad(mod), 2) + + +class TestArgumentLoaderTensileRegression(unittest.TestCase): + def test_kernarg_wait_arithmetic_does_not_raise(self): + # Reproduce the L1909-1916 + L2351 pattern: reset, loadAllKernArg, + # then read getOffset() and subtract numSgprPreload*4. + loader = ArgumentLoader() + numSgprPreload = 4 + numsOfLoad = 24 + + loader.resetOffset() + loader.loadAllKernArg(sgprStartIndex=0, srcAddr="KernArgAddress", + numSgprToLoad=numsOfLoad, + numSgprPreload=numSgprPreload) + + # The expression that crashed pre-workaround: + kernArgBytes = loader.getOffset() - numSgprPreload * 4 + self.assertEqual(kernArgBytes, (numsOfLoad - numSgprPreload) * 4) + + def test_two_loaders_independent(self): + # KernelWriterAssembly creates ``argLoader`` and ``externalArgLoader`` + # (KernelWriterAssembly.py:2104-2105); state must not leak between + # instances. + a = ArgumentLoader() + b = ArgumentLoader() + a.loadKernArg("x", "KernArgAddress", dword=4) + self.assertEqual(a.getOffset(), 16) + self.assertEqual(b.getOffset(), 0) + + +# =========================================================================== +# Section B — dummy function exports (structural smoke) +# =========================================================================== + + +class TestFunctionsModuleExports(unittest.TestCase): + def test_argument_loader_is_real_class(self): + self.assertTrue(callable(ArgumentLoader)) + self.assertIsInstance(ArgumentLoader(), ArgumentLoader) + + def test_all_dummy_symbols_exported(self): + for name in FUNCTIONS_DUMMY_EXPORTS: + with self.subTest(name=name): + self.assertTrue(hasattr(_functions, name), + f"functions.{name} missing") + + def test_dummy_registry_matches_module(self): + # Guard against adding a dummy to functions.py without updating tests. + _skip = frozenset({ + "annotations", "make_dummy_func", "math", + "ArgumentLoader", + "Module", "TextBlock", "sgpr", "vgpr", "Any", + "ContinuousRegister", "EXEC", "VCC", + "DataTypeEnum", + # Real branch helpers (no longer dummies) + "BranchIfZero", "BranchIfNotZero", + "SLoadB32", "SLoadB64", "SLoadB128", "SLoadB256", "SLoadB512", + "SAddCU32", "SAddU32", "SAndB32", "SAndB64", + "SCBranchSCC0", "SCBranchSCC1", "SCBranchVCCNZ", "SCBranchVCCZ", + "SCmpEQU32", "SCmpEQU64", "SCmpLgU32", + "SLShiftLeftB32", "SLShiftLeftB64", "SLShiftRightB32", + "SLShiftRightB64", "SMulHIU32", "SMulI32", "SMovB32", "SMovB64", + "SNop", "SSubU32", + "VAddCCOU32", "VAddLShiftLeftU32", "VAddU32", "VAndB32", + "VCmpEQF32", "VCmpEQF64", + "VCmpNeU32", "VCmpXEqU32", "VCmpXGeU32", "VCmpXGtU32", + "VCvtF32toU32", "VCvtF64toU32", "VCvtU32toF32", "VCvtU32toF64", + "VLShiftLeftAddU32", "VLShiftLeftB32", "VLShiftLeftB64", + "VLShiftRightB32", "VLShiftRightB64", "VMadU32U24", "VMovB32", + "VMulF32", "VMulF64", "VMulHIU32", "VMulLOU32", "VMulU32U24", + "VRcpF64", "VRcpIFlagF32", "VReadfirstlaneB32", "VSubU32", + # Real math functions (no longer dummies) + "vectorStaticDivideAndRemainder", "vectorStaticDivide", + "vectorUInt32DivideAndRemainder", + "vectorUInt32CeilDivideAndRemainder", + "vectorStaticRemainder", "vectorStaticMultiply", + "vectorStaticMultiplyAdd", "vectorAddMultiplyBpe", + "vectorMultiplyBpe", "vectorMultiply64Bpe", + "scalarStaticDivideAndRemainder", "scalarStaticCeilDivide", + "scalarStaticRemainder", "scalarUInt24DivideAndRemainder", + "scalarUInt32DivideAndRemainder", "scalarStaticMultiply64", + "scalarMultiplyBpe", "scalarMultiply64Bpe", + "sMagicDiv", "sMagicDiv2", + # Real cast helper (no longer dummy) + "VSaturateCastInt", + "SMovkI32", "VMed3I32", "VMinI32", "VMaxI32", + "SaturateCastType", + }) + module_dummies = { + name for name in dir(_functions) + if not name.startswith("_") + and name not in _skip + and callable(getattr(_functions, name)) + } + self.assertEqual(set(FUNCTIONS_DUMMY_EXPORTS), module_dummies) + + +class TestFunctionsDummyCallables(unittest.TestCase): + def test_each_dummy_callable_returns_none(self): + for name in FUNCTIONS_DUMMY_EXPORTS: + with self.subTest(name=name): + fn = getattr(_functions, name) + self.assertTrue(callable(fn)) + with mock.patch("builtins.print"): + self.assertIsNone(fn()) + + +# ========================================================================== +# Section C — Counting / analysis functions (real, in __init__.py) +# ========================================================================== + +from rocisa_stinkytofu_adaptor import ( # noqa: E402 + countType, countInstruction, countGlobalRead, countSMemLoad, + countLocalRead, countLocalWrite, countWeightedLocalRead, + countWeightedLocalWrite, countDSStoreB128, countDSStoreB192, + countDSStoreB256, countVMovB32, countMFMA, getMFMAs, findInstCount, +) +from rocisa_stinkytofu_adaptor.code import Module, TextBlock # noqa: E402 +from rocisa_stinkytofu_adaptor.instruction import ( # noqa: E402 + Instruction, BufferLoadB128, BufferLoadB32, FlatLoadB64, + DSLoadB32, DSLoadB64, DSLoadB192, DSLoad2B32, + DSStoreB32, DSStoreB64, DSStoreB128, DSStoreB192, DSStoreB256, + DSStore2B32, VMovB32, SLoadB32, SLoadB128, + GlobalLoadTR8B64, MFMAInstruction, SMFMAInstruction, MXMFMAInstruction, + SAddU32, SNop, +) + + +class TestCountInstruction(unittest.TestCase): + def test_empty_module(self): + self.assertEqual(countInstruction(Module()), 0) + + def test_flat_module(self): + m = Module() + m.add(BufferLoadB128()) + m.add(DSLoadB32()) + m.add(DSStoreB32()) + self.assertEqual(countInstruction(m), 3) + + def test_nested_module(self): + m = Module() + m.add(BufferLoadB128()) + sub = Module() + sub.add(DSLoadB32()) + sub.add(DSStoreB32()) + m.add(sub) + self.assertEqual(countInstruction(m), 3) + + def test_skips_textblock(self): + m = Module() + m.add(TextBlock("// comment\n")) + m.add(BufferLoadB128()) + self.assertEqual(countInstruction(m), 1) + + +class TestCountGlobalRead(unittest.TestCase): + def test_buffer_loads(self): + m = Module() + m.add(BufferLoadB128()) + m.add(BufferLoadB32()) + m.add(DSLoadB32()) + self.assertEqual(countGlobalRead(m), 2) + + def test_flat_loads(self): + m = Module() + m.add(FlatLoadB64()) + self.assertEqual(countGlobalRead(m), 1) + + def test_global_load_tr(self): + m = Module() + m.add(GlobalLoadTR8B64()) + self.assertEqual(countGlobalRead(m), 1) + + def test_no_false_positives(self): + m = Module() + m.add(DSLoadB32()) + m.add(SLoadB32()) + self.assertEqual(countGlobalRead(m), 0) + + +class TestCountSMemLoad(unittest.TestCase): + def test_sloads(self): + m = Module() + m.add(SLoadB32()) + m.add(SLoadB128()) + m.add(BufferLoadB128()) + self.assertEqual(countSMemLoad(m), 2) + + +class TestCountLocalRead(unittest.TestCase): + def test_ds_loads(self): + m = Module() + m.add(DSLoadB32()) + m.add(DSLoadB64()) + m.add(DSLoad2B32()) + m.add(DSStoreB32()) + self.assertEqual(countLocalRead(m), 3) + + def test_nested(self): + m = Module() + sub = Module() + sub.add(DSLoadB32()) + m.add(sub) + m.add(DSLoadB64()) + self.assertEqual(countLocalRead(m), 2) + + +class TestCountLocalWrite(unittest.TestCase): + def test_ds_stores(self): + m = Module() + m.add(DSStoreB32()) + m.add(DSStoreB64()) + m.add(DSStore2B32()) + m.add(DSLoadB32()) + self.assertEqual(countLocalWrite(m), 3) + + +class TestCountWeighted(unittest.TestCase): + def test_weighted_local_read(self): + m = Module() + m.add(DSLoadB32()) + m.add(DSLoadB192()) + self.assertEqual(countWeightedLocalRead(m), 3) # 1 + 2 + + def test_weighted_local_write(self): + m = Module() + m.add(DSStoreB128()) + m.add(DSStoreB192()) + m.add(DSStoreB256()) + self.assertEqual(countWeightedLocalWrite(m), 5) # 1 + 2 + 2 + + +class TestCountExactType(unittest.TestCase): + def test_ds_store_b128(self): + m = Module() + m.add(DSStoreB128()) + m.add(DSStoreB192()) + self.assertEqual(countDSStoreB128(m), 1) + self.assertEqual(countDSStoreB192(m), 1) + self.assertEqual(countDSStoreB256(m), 0) + + def test_vmov_b32(self): + m = Module() + m.add(VMovB32(dst="v0", src="v1")) + m.add(VMovB32(dst="v2", src="v3")) + self.assertEqual(countVMovB32(m), 2) + + +class TestCountMFMA(unittest.TestCase): + def test_no_mfma(self): + m = Module() + m.add(BufferLoadB128()) + self.assertEqual(countMFMA(m), 0) + self.assertEqual(getMFMAs(m), []) + + +class TestFindInstCount(unittest.TestCase): + def test_found(self): + target = DSLoadB32() + m = Module() + m.add(BufferLoadB128()) + m.add(target) + cnt, found = findInstCount(m, target) + self.assertTrue(found) + self.assertEqual(cnt, 1) + + def test_not_found(self): + target = DSLoadB32() + m = Module() + m.add(BufferLoadB128()) + cnt, found = findInstCount(m, target) + self.assertFalse(found) + + def test_skips_textblock(self): + target = DSStoreB32() + m = Module() + m.add(BufferLoadB128()) + m.add(TextBlock("// skip me\n")) + m.add(DSLoadB32()) + m.add(target) + cnt, found = findInstCount(m, target) + self.assertTrue(found) + self.assertEqual(cnt, 2) + + def test_nested_module(self): + target = DSLoadB32() + m = Module() + sub = Module() + sub.add(BufferLoadB128()) + sub.add(target) + m.add(sub) + cnt, found = findInstCount(m, target) + self.assertTrue(found) + self.assertEqual(cnt, 1) + + +class TestCountType(unittest.TestCase): + def test_generic_isinstance(self): + m = Module() + m.add(BufferLoadB128()) + m.add(DSLoadB32()) + m.add(TextBlock("// x\n")) + self.assertEqual(countType(m, Instruction), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_instruction.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_instruction.py new file mode 100644 index 000000000000..2caed6bd07ad --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_instruction.py @@ -0,0 +1,2762 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Standalone tests for ``rocisa_stinkytofu_adaptor.instruction`` -- Step 3 +(``Instruction`` / ``CommonInstruction`` bases + ``VMovB32`` / ``SMovB32`` / +``SMovB64`` / ``SMemLoadInstruction`` / ``SLoadB*`` shims). + +Run from any working directory: + + python3 projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_instruction.py + +Or with pytest: + + pytest projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_instruction.py + +These tests pin: + * Base-class shape (``isinstance`` / fields / mutability) -- KernelWriter + does ``isinstance(x, CommonInstruction) and ... and x.dst.regType == 'm'`` + and ``codeAccVgprReadInst.dst = vgpr(...)``; the shim must support both. + * ``__str__`` byte-parity with ``rocisa::CommonInstruction::toString`` + (padding to col 50 + ``" // "`` + comment + newline). A drift here will + silently corrupt diff-based regression tests downstream. + * ``to_stinky_logical()`` and the ``_to_stinky_register`` coercion + table -- both register operands (via ``RegisterContainer.to_stinky``) + and the int / float / str literal paths. + * Step-3 collector contract: ``VMovB32`` / ``SMovB32`` ``to_stinky_logical`` + is callable and, when stinkytofu is built, is picked up by + ``Module._collect_logical_insts``. + Dummy-only skip behaviour (e.g. ``SBarrier``) lives in + ``test_code.TestCollectLogicalInsts``. + +End-to-end byte-equality of the emitted asm against the rocisa right-path +is covered in ``tests/test_emission_consistency.py`` (a stronger check +than the old substring assertions). +""" + +from __future__ import annotations + +import copy +import os +import sys +import unittest + +# --------------------------------------------------------------------------- +# Self-contained sys.path bootstrap (matches test_container / test_code). +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor.code import Module # noqa: E402 +from rocisa_stinkytofu_adaptor.container import ( # noqa: E402 + RegisterContainer, + SMEMModifiers, + sgpr, + vgpr, +) +from rocisa_stinkytofu_adaptor.enum import InstType # noqa: E402 +from rocisa_stinkytofu_adaptor.instruction import ( # noqa: E402 + CommonInstruction, + Instruction, + MacroInstruction, + SAddI32, + SAddU32, + SAddCU32, + SAndB32, + SAndB64, + SAndN2B32, + SAndSaveExecB32, + SAndSaveExecB64, + SAShiftRightI32, + SBarrier, + SBitcmp1B32, + SCmpEQI32, + SCmpEQU32, + SCmpEQU64, + SCmpGeI32, + SCmpGeU32, + SCmpGtI32, + SCmpGtU32, + SCmpLeI32, + SCmpLeU32, + SCmpLgI32, + SCmpLgU32, + SCmpLgU64, + SCmpLtI32, + SCmpLtU32, + SGetRegB32, + SLShiftLeftB32, + SLShiftRightB32, + SLShiftLeftB64, + SLShiftRightB64, + SLShiftLeft1AddU32, + SLShiftLeft2AddU32, + SLShiftLeft3AddU32, + SLShiftLeft4AddU32, + SLoadB32, + SLoadB64, + SMemLoadInstruction, + SMovB32, + SMovB64, + SMulHII32, + SMulHIU32, + SMulI32, + SMulLOU32, + SNop, + SOrB32, + SOrB64, + SOrSaveExecB32, + SOrSaveExecB64, + SSubBU32, + SSubI32, + SSubU32, + SXorB32, + VAddF32, + VAddU32, + VAndB32, + VAndOrB32, + VCmpClassF32, + VCmpEQF32, + VCmpEQF64, + VCmpEQI32, + VCmpEQU32, + VCmpGEF16, + VCmpGEF32, + VCmpGEF64, + VCmpGEI32, + VCmpGEU32, + VCmpGTF16, + VCmpGTF32, + VCmpGTF64, + VCmpGTI32, + VCmpGtU32, + VCmpLeI32, + VCmpLeU32, + VCmpLtI32, + VCmpLtU32, + VCmpNeI32, + VCmpNeU32, + VCmpNeU64, + VCmpUF32, + VCmpXClassF32, + VCmpXEqU32, + VCmpXGeU32, + VCmpXGtU32, + VCmpXLeI32, + VCmpXLeU32, + VCmpXLtF32, + VCmpXLtI32, + VCmpXLtU32, + VCmpXLtU64, + VCmpXNeU16, + VCmpXNeU32, + VCndMaskB32, + VFmaF32, + VFmaMixF32, + VLShiftLeftB32, + VLShiftLeftB64, + VLShiftRightB32, + VLShiftRightB64, + VMovB32, + VMulF32, + VMulHII32, + VMulHIU32, + VMulI32I24, + VMulLOU32, + VMulU32U24, + VOrB32, + VReadfirstlaneB32, + VSubF32, + VSubI32, + VSubU32, + VXorB32, + SAbsI32, + SMaxI32, + SMaxU32, + SMinI32, + SMinU32, + VExpF16, + VExpF32, + VRcpF16, + VRcpF32, + VRcpIFlagF32, + VRsqF16, + VRsqF32, + VRsqIFlagF32, + VMaxF16, + VMaxF32, + VMaxF64, + VMaxI32, + VMaxPKF16, + VMinF16, + VMinF32, + VMinF64, + VMinI32, + VMed3I32, + VMed3F32, + VNotB32, + VPrngB32, + VRndneF32, + VAShiftRightI32, + VPackF16toB32, + VLShiftLeftOrB32, + VCvtF16toF32, + VCvtF32toF16, + VCvtF32toU32, + VCvtU32toF32, + VCvtI32toF32, + VCvtF32toI32, + VCvtFP8toF32, + VCvtBF8toF32, + VCvtPkFP8toF32, + VCvtPkBF8toF32, + VCvtPkF32toBF8, + VCvtSRF32toFP8, + VCvtSRF32toBF8, + VCvtPkF32toFP8, + VCvtPkF32toBF16, + VCvtScalePkFP8toF16, + VCvtScalePkBF8toF16, + VCvtScaleFP8toF16, + VCvtScalePkF16toFP8, + VCvtScalePkF16toBF8, + VCvtScaleSRF16toFP8, + VCvtScaleSRF16toBF8, + BufferLoadU8, + BufferLoadD16HIU8, + BufferLoadD16U8, + BufferLoadD16HIB16, + BufferLoadD16B16, + BufferLoadB32, + BufferLoadB64, + BufferLoadB96, + BufferLoadB128, + BufferStoreB8, + BufferStoreD16HIU8, + BufferStoreD16HIB16, + BufferStoreB16, + BufferStoreB32, + BufferStoreB64, + BufferStoreB128, + BufferAtomicAddF32, + BufferAtomicCmpswapB32, + BufferAtomicCmpswapB64, + FlatLoadD16HIU8, + FlatLoadD16U8, + FlatLoadD16HIB16, + FlatLoadD16B16, + FlatLoadB32, + FlatLoadB64, + FlatLoadB128, + FlatStoreD16HIB16, + FlatStoreB32, + FlatStoreB64, + FlatStoreB128, + FlatAtomicCmpswapB32, + DSLoadU8, + DSLoadU16, + DSLoadB32, + DSLoadB64, + DSLoadB128, + DSLoad2B32, + DSLoad2B64, + DSStoreB8, + DSStoreB16, + DSStoreB32, + DSStoreB64, + DSStoreB96, + DSStoreB128, + DSStore2B32, + DSStore2B64, + DSBPermuteB32, + TensorLoadToLds, + BranchInstruction, + SBranch, + SCBranchSCC0, + SCBranchSCC1, + SCBranchVCCNZ, + SCBranchVCCZ, + SCBranchExecZ, + SCBranchExecNZ, + SEndpgm, + _SWaitCnt, + SWaitCnt, + SWaitXCnt, + SWaitTensorcnt, + SWaitAlu, + SSchedulingFence, + _to_stinky_register, +) + + +# =========================================================================== +# Instruction base class -- shape & abstract surface. +# =========================================================================== + + +class TestInstructionBase(unittest.TestCase): + def test_cannot_use_toString_directly(self): + # rocisa::Instruction::toString throws; we mirror that so any + # accidental ``str(Instruction(...))`` is loud, not silent. + inst = Instruction(InstType.INST_B32, "x") + with self.assertRaises(NotImplementedError): + inst.toString() + with self.assertRaises(NotImplementedError): + inst.getParams() + with self.assertRaises(NotImplementedError): + inst.getDstParams() + with self.assertRaises(NotImplementedError): + inst.getSrcParams() + + def test_default_fields(self): + inst = Instruction(InstType.INST_B32, "foo") + # Mirrors rocisa::Item("") + Instruction ctor. + self.assertEqual(inst.name, "") + self.assertIsNone(inst.parent) + self.assertEqual(inst.comment, "foo") + self.assertEqual(inst.instStr, "") + self.assertFalse(inst.outputInlineAsm) + self.assertIsNone(inst.m_memToken) + + def test_setInst_records_str(self): + inst = Instruction(InstType.INST_B32) + inst.setInst("v_mov_b32") + self.assertEqual(inst.instStr, "v_mov_b32") + # preStr() returns instStr (subclasses override). + self.assertEqual(inst.preStr(), "v_mov_b32") + + def test_setInlineAsm_toggles_outputInlineAsm(self): + inst = Instruction(InstType.INST_B32) + inst.setInlineAsm(True) + self.assertTrue(inst.outputInlineAsm) + inst.setInlineAsm(False) + self.assertFalse(inst.outputInlineAsm) + + def test_mem_token_roundtrip(self): + inst = Instruction(InstType.INST_B32) + token = object() # opaque sentinel matches "any shared_ptr" + inst.setMemToken(token) + self.assertIs(inst.getMemToken(), token) + + def test_default_issue_latency_and_cycles(self): + # rocisa::Instruction defaults; subclasses override. + inst = Instruction(InstType.INST_B32) + self.assertEqual(inst.getIssueLatency(), 1) + self.assertEqual(inst.getIssueCycles(), 1) + + def test_deepcopy_raises(self): + # rocisa binding: throws "Deepcopy not supported for Instruction". + with self.assertRaises(RuntimeError): + copy.deepcopy(Instruction(InstType.INST_B32)) + + def test_pickle_raises(self): + # rocisa binding: throws "Pickling not supported for Instruction". + import pickle + with self.assertRaises(RuntimeError): + pickle.dumps(Instruction(InstType.INST_B32)) + + +# =========================================================================== +# CommonInstruction -- minimal shape that other shims will inherit from. +# =========================================================================== + + +class TestCommonInstructionFields(unittest.TestCase): + def test_construction_sets_all_fields(self): + d = vgpr(0) + s0, s1 = vgpr(1), vgpr(2) + ci = CommonInstruction(InstType.INST_B32, dst=d, srcs=[s0, s1], + comment="hi") + self.assertIs(ci.dst, d) + self.assertIsNone(ci.dst1) + self.assertEqual(ci.srcs, [s0, s1]) + self.assertIsNone(ci.dpp) + self.assertIsNone(ci.sdwa) + self.assertIsNone(ci.vop3) + self.assertEqual(ci.comment, "hi") + + def test_srcs_is_copied(self): + # rocisa stores the vector by value; mutating the caller's list + # after construction must not leak in. + src_list = [vgpr(0)] + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(1), srcs=src_list) + src_list.append(vgpr(99)) + self.assertEqual(len(ci.srcs), 1) + + def test_setSrc_swaps_in_place(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)]) + new_src = vgpr(7) + ci.setSrc(0, new_src) + self.assertIs(ci.srcs[0], new_src) + + def test_dst_mutable(self): + # KernelWriter pattern: ``codeAccVgprReadInst.dst = vgpr(N)``. + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)]) + new_dst = vgpr(5) + ci.dst = new_dst + self.assertIs(ci.dst, new_dst) + + def test_comment_mutable(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)], + comment="old") + ci.comment = "new" + self.assertEqual(ci.comment, "new") + + +class TestCommonInstructionParams(unittest.TestCase): + def test_getParams_returns_dst_then_srcs(self): + d, s0, s1 = vgpr(0), vgpr(1), vgpr(2) + ci = CommonInstruction(InstType.INST_B32, dst=d, srcs=[s0, s1]) + self.assertEqual(ci.getParams(), [d, s0, s1]) + + def test_getDstParams(self): + d = vgpr(0) + ci = CommonInstruction(InstType.INST_B32, dst=d, srcs=[vgpr(1)]) + self.assertEqual(ci.getDstParams(), [d]) + + def test_getDstParams_includes_dst1_when_set(self): + # rocisa::CommonInstruction has a ``dst1`` slot used by a handful + # of instructions (e.g. ``V*MulHIU32`` returning two outputs). + d, d1 = vgpr(0), vgpr(1) + ci = CommonInstruction(InstType.INST_B32, dst=d, srcs=[vgpr(2)]) + ci.dst1 = d1 + self.assertEqual(ci.getDstParams(), [d, d1]) + self.assertEqual(ci.getParams(), [d, d1, vgpr(2)]) + + def test_getSrcParams_returns_independent_copy(self): + srcs = [vgpr(1), vgpr(2)] + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=srcs) + out = ci.getSrcParams() + out.append("trash") + # Mutating the returned list must not bleed in. + self.assertEqual(len(ci.srcs), 2) + + def test_getParams_skips_None_dst(self): + # Some patterns construct with no dst (e.g. control-flow insts). + # rocisa C++ guards with ``if(dst)``; we do too. + ci = CommonInstruction(InstType.INST_B32, dst=None, srcs=[vgpr(0)]) + self.assertEqual(ci.getParams(), [vgpr(0)]) + + +class TestCommonInstructionToString(unittest.TestCase): + """Byte-parity with ``rocisa::CommonInstruction::toString``. + + Format: ``" , , ... // \\n"`` + with padding to column 50 before ``" // "`` when a comment is set + (see ``format.hpp:54-75``). + """ + + def test_dst_and_one_src(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)]) + ci.setInst("v_mov_b32") + self.assertEqual(str(ci), "v_mov_b32 v0, v1\n") + + def test_with_comment_padded_to_col_50(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)], + comment="copy") + ci.setInst("v_mov_b32") + # ``v_mov_b32 v0, v1`` is 16 chars; pad to col 50 with (50 - 16) + # = 34 spaces, then `" // copy\n"`. Locking exact length here + # because emitted asm diffs against rocisa native rely on this + # being byte-identical (see format.hpp:64-69). + head = "v_mov_b32 v0, v1" + self.assertEqual(len(head), 16) + expected = head + (" " * 34) + " // copy\n" + self.assertEqual(str(ci), expected) + + def test_long_inst_no_extra_padding(self): + # When inst is longer than 50 chars, max(0, ...) clamps the pad + # to zero -- no negative padding, no truncation. + ci = CommonInstruction(InstType.INST_B32, + dst=vgpr("VeryLongRegisterName"), + srcs=[vgpr("AnotherVeryLongOne"), vgpr("third")], + comment="x") + ci.setInst("v_long_instruction_name") + text = str(ci) + self.assertTrue(text.endswith(" // x\n")) + # No spaces immediately before `" // x"` -- the format is just + # `` // x\n`` since pad clamped to 0. + self.assertIn(" // x\n", text) + + def test_int_src_renders_decimal(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[42]) + ci.setInst("v_mov_b32") + self.assertEqual(str(ci).split("//", 1)[0].rstrip(), "v_mov_b32 v0, 42") + + def test_str_src_renders_verbatim(self): + # KernelWriter passes ``hex(value)`` -> "0x..." for inline imms. + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=["0x0"]) + ci.setInst("v_mov_b32") + self.assertEqual(str(ci), "v_mov_b32 v0, 0x0\n") + + def test_float_src_double_format(self): + # rocisa formats doubles with %.17g + trailing ``.0`` when + # there's no decimal / exponent in the printed form. + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[1.0]) + ci.setInst("v_mov_b32") + text = str(ci).split("//", 1)[0].rstrip() + self.assertTrue(text.endswith("1.0"), text) + + def test_no_comment_just_newline(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)]) + ci.setInst("v_mov_b32") + # No padding when comment is empty. + self.assertEqual(str(ci), "v_mov_b32 v0, v1\n") + + def test_inline_asm_wraps_inst(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)], + comment="c") + ci.setInst("v_mov_b32") + ci.setInlineAsm(True) + # Inline-asm path: ``"\n\t"`` literal + padding + comment. + self.assertTrue(str(ci).startswith('"v_mov_b32 v0, v1\\n\\t"'), + repr(str(ci))) + + +class TestCommonInstructionDeepcopy(unittest.TestCase): + def test_deepcopy_independent_dst(self): + d = vgpr(0) + ci = CommonInstruction(InstType.INST_B32, dst=d, srcs=[vgpr(1)], + comment="x") + ci.setInst("v_mov_b32") + c = copy.deepcopy(ci) + c.dst.setMinus(True) + # Mutating clone's dst must not leak into original. + self.assertFalse(ci.dst.isMinus) + self.assertTrue(c.dst.isMinus) + # Identity preserved across deepcopy (same subclass). + self.assertIs(type(c), type(ci)) + + def test_deepcopy_independent_srcs(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)]) + c = copy.deepcopy(ci) + c.srcs.append(vgpr(99)) + self.assertEqual(len(ci.srcs), 1) + + def test_deepcopy_preserves_instStr_and_comment(self): + ci = CommonInstruction(InstType.INST_B32, dst=vgpr(0), srcs=[vgpr(1)], + comment="orig") + ci.setInst("v_mov_b32") + c = copy.deepcopy(ci) + self.assertEqual(c.instStr, "v_mov_b32") + self.assertEqual(c.comment, "orig") + + +# =========================================================================== +# MacroInstruction -- macro-call leaf (V_MAGIC_DIV / MAINLOOP / etc.). +# =========================================================================== +# +# KernelWriter constructs these directly via +# ``MacroInstruction(name=..., args=[...], comment=...)``. The emitted +# text becomes a single ``NAME arg0, arg1, ...`` line, optionally +# right-padded to col 50 followed by ``// comment``. + + +class TestMacroInstructionFields(unittest.TestCase): + def test_default_comment_empty(self): + mi = MacroInstruction("MY_MACRO", [1, 2, 3]) + self.assertEqual(mi.name, "MY_MACRO") + self.assertEqual(mi.args, [1, 2, 3]) + self.assertEqual(mi.comment, "") + self.assertEqual(mi.instType, InstType.INST_MACRO) + + def test_args_is_copied(self): + # rocisa stores ``args`` by value; mutating the caller's list + # afterwards must not bleed in. + args = [1, 2] + mi = MacroInstruction("X", args) + args.append(99) + self.assertEqual(len(mi.args), 2) + + def test_args_mutable_post_construction(self): + # rocisa binding exposes ``args`` as def_rw. + mi = MacroInstruction("X", [1]) + mi.args.append(2) + self.assertEqual(mi.args, [1, 2]) + + def test_setSrc_in_place(self): + mi = MacroInstruction("X", [1, 2, 3]) + mi.setSrc(1, 99) + self.assertEqual(mi.args, [1, 99, 3]) + + def test_inherits_Instruction(self): + mi = MacroInstruction("X", []) + self.assertIsInstance(mi, Instruction) + + +class TestMacroInstructionParams(unittest.TestCase): + def test_getParams_returns_args(self): + mi = MacroInstruction("X", [1, "a", 3.0]) + self.assertEqual(mi.getParams(), [1, "a", 3.0]) + + def test_getDstParams_raises(self): + # rocisa throws "MacroInstruction does not have destination + # parameters". Crashing loudly is the contract. + mi = MacroInstruction("X", [1]) + with self.assertRaises(RuntimeError) as ctx: + mi.getDstParams() + self.assertIn("destination parameters", str(ctx.exception)) + + def test_getSrcParams_raises(self): + # rocisa throws "MacroInstruction does not have source + # parameters". Same intentional crash policy. + mi = MacroInstruction("X", [1]) + with self.assertRaises(RuntimeError) as ctx: + mi.getSrcParams() + self.assertIn("source parameters", str(ctx.exception)) + + +class TestMacroInstructionToString(unittest.TestCase): + """Byte-parity with ``rocisa::MacroInstruction::toString``.""" + + def test_empty_args(self): + mi = MacroInstruction("BARRIER", []) + # Empty args -> no leading space, just "NAME\n". + self.assertEqual(str(mi), "BARRIER\n") + + def test_single_int_arg(self): + # KWA:16977 pattern: ``MacroInstruction(name="MAINLOOP", args=[0])``. + mi = MacroInstruction("MAINLOOP", [0]) + self.assertEqual(str(mi), "MAINLOOP 0\n") + + def test_multiple_mixed_args(self): + mi = MacroInstruction("X", [1, "lit", 2]) + self.assertEqual(str(mi), "X 1, lit, 2\n") + + def test_register_container_args(self): + # KWA:2784 pattern: register operands routed via _input_to_str + # which calls ``.toString()``. + mi = MacroInstruction("V_MAGIC_DIV", [vgpr(0), sgpr("Foo")]) + self.assertEqual(str(mi), "V_MAGIC_DIV v0, s[sgprFoo]\n") + + def test_with_comment_padded_to_col_50(self): + mi = MacroInstruction("X", [1], comment="hi") + head = "X 1" + pad = " " * (50 - len(head)) + self.assertEqual(str(mi), head + pad + " // hi\n") + + def test_name_with_space_emitted_verbatim(self): + # Components/StreamK.py:152 pattern: ``MacroInstruction(name= + # "s_wait_xcnt 0", args=[])``. Spaces inside name must be + # preserved untouched. + mi = MacroInstruction("s_wait_xcnt 0", []) + self.assertEqual(str(mi), "s_wait_xcnt 0\n") + + def test_getArgStr_format(self): + # Helper directly: leading space + comma-join. + mi = MacroInstruction("X", [1, 2]) + self.assertEqual(mi.getArgStr(), " 1, 2") + mi2 = MacroInstruction("X", []) + self.assertEqual(mi2.getArgStr(), "") + + +class TestMacroInstructionDeepcopy(unittest.TestCase): + def test_deepcopy_independent_args_list(self): + mi = MacroInstruction("X", [1, 2, 3], comment="c") + c = copy.deepcopy(mi) + c.args.append(99) + self.assertEqual(len(mi.args), 3) + self.assertEqual(len(c.args), 4) + + def test_deepcopy_preserves_name_and_comment(self): + mi = MacroInstruction("PRND", [1], comment="orig") + c = copy.deepcopy(mi) + self.assertEqual(c.name, "PRND") + self.assertEqual(c.comment, "orig") + + def test_deepcopy_clones_container_args(self): + # Container args must be deep-cloned -- mutating the clone's + # Container should not bleed into the original. + rc = vgpr(0) + mi = MacroInstruction("X", [rc]) + c = copy.deepcopy(mi) + c.args[0].setMinus(True) + self.assertFalse(mi.args[0].isMinus) + self.assertTrue(c.args[0].isMinus) + + def test_clone_method_returns_independent_copy(self): + # rocisa Instruction::clone() returns a new shared_ptr; our + # ``clone()`` returns a deepcopy with a fresh memo. + mi = MacroInstruction("X", [1, 2]) + c = mi.clone() + self.assertIsInstance(c, MacroInstruction) + self.assertIsNot(c, mi) + self.assertEqual(c.args, mi.args) + + +# =========================================================================== +# _to_stinky_register coercion table. +# =========================================================================== + + +try: + import stinkytofu as _stinky + _STINKY_OK = all( + hasattr(_stinky, name) + for name in ("Register", "vgpr", "VMovB32", "SNop", + "SAddU32", "SAndB32", "SLShiftLeftB32", + "SBarrier", "SGetRegB32", + "LogicalModule", "lower_logical_module") + ) +except ImportError: + _STINKY_OK = False + + +@unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") +class TestToStinkyRegister(unittest.TestCase): + def test_register_container_delegates_to_to_stinky(self): + rc = vgpr(5) + reg = _to_stinky_register(rc) + # Numeric VGPR -> Register with index/count. + self.assertTrue(reg.is_register) + self.assertEqual(reg.index, 5) + self.assertEqual(reg.count, 1) + + def test_named_register_container_via_to_stinky(self): + rc = vgpr("ValuA") + reg = _to_stinky_register(rc) + self.assertTrue(reg.has_reg_name) + name, offsets = reg.get_reg_name() + self.assertEqual(name, "vgprValuA") + self.assertEqual(offsets, []) + + def test_int_literal(self): + reg = _to_stinky_register(42) + self.assertTrue(reg.is_literal) + + def test_bool_routed_through_int(self): + # bool is int subclass; ensure we don't crash on it. + reg = _to_stinky_register(True) + self.assertTrue(reg.is_literal) + + def test_float_literal(self): + reg = _to_stinky_register(1.5) + self.assertTrue(reg.is_literal) + + def test_str_literal_verbatim(self): + # KernelWriter passes ``hex(N)`` strings; stinky emits them + # verbatim as literal-string Register. + reg = _to_stinky_register("0x42") + self.assertTrue(reg.is_literal_string) + self.assertEqual(reg.literal_string, "0x42") + + def test_unsupported_type_raises(self): + with self.assertRaises(TypeError): + _to_stinky_register([1, 2, 3]) + with self.assertRaises(TypeError): + _to_stinky_register(None) + + +# =========================================================================== +# VMovB32 -- rocisa-shape construction + isinstance behaviour. +# =========================================================================== + + +class TestVMovB32Construction(unittest.TestCase): + def test_positional_dst_src(self): + d, s = vgpr(0), vgpr(1) + v = VMovB32(d, s) + self.assertIs(v.dst, d) + self.assertEqual(v.srcs, [s]) + self.assertEqual(v.comment, "") + self.assertIsNone(v.sdwa) + self.assertIsNone(v.dpp) + # ``setInst`` was called in __init__. + self.assertEqual(v.instStr, "v_mov_b32") + self.assertEqual(v.instType, InstType.INST_B32) + + def test_keyword_ctor_rocisa_shape(self): + # rocisa accepts (dst, src, sdwa=None, comment="", dpp=None). + v = VMovB32(dst=vgpr(0), src="0x0", comment="init zero") + self.assertEqual(v.srcs, ["0x0"]) + self.assertEqual(v.comment, "init zero") + + def test_inherits_from_CommonInstruction_and_Instruction(self): + v = VMovB32(vgpr(0), vgpr(1)) + # KernelWriter:1105 / SubtileBasedInstructionScheduler:151 rely + # on these isinstance checks -- duck typing isn't enough. + self.assertIsInstance(v, Instruction) + self.assertIsInstance(v, CommonInstruction) + self.assertIsInstance(v, VMovB32) + + def test_dst_regType_accessor_used_by_subtile_scheduler(self): + # Components/SubtileBasedInstructionScheduler.py:151 does: + # isinstance(x, CommonInstruction) and hasattr(x.dst, 'regType') + # and x.dst.regType == 'm' + v = VMovB32(vgpr(0), vgpr(1)) + self.assertTrue(hasattr(v.dst, "regType")) + self.assertEqual(v.dst.regType, "v") + + def test_str_uses_v_mov_b32_format(self): + v = VMovB32(vgpr(0), vgpr(1), comment="rename") + text = str(v) + self.assertTrue(text.startswith("v_mov_b32 v0, v1"), text) + self.assertTrue(text.endswith(" // rename\n"), text) + + def test_str_with_immediate_src(self): + v = VMovB32(vgpr(0), "0x0", comment="zero") + self.assertEqual(str(v).split("//", 1)[0].rstrip(), + "v_mov_b32 v0, 0x0") + + def test_dst_mutation_post_construction(self): + # GSU.py pattern: write to .dst after construction. + v = VMovB32(vgpr(0), vgpr(1)) + v.dst = vgpr(42) + self.assertEqual(str(v).split("//", 1)[0].rstrip(), + "v_mov_b32 v42, v1") + + def test_comment_mutation_post_construction(self): + v = VMovB32(vgpr(0), vgpr(1)) + v.comment = "later" + self.assertIn("// later", str(v)) + + def test_getParams_dst_then_src(self): + d, s = vgpr(0), vgpr(1) + v = VMovB32(d, s) + self.assertEqual(v.getParams(), [d, s]) + + def test_deepcopy_preserves_subclass(self): + v = VMovB32(vgpr(0), vgpr(1), comment="x") + c = copy.deepcopy(v) + self.assertIsInstance(c, VMovB32) + self.assertEqual(str(c), str(v)) + + +# =========================================================================== +# SMovB32 / SMovB64 -- scalar moves (Phase A). +# =========================================================================== + + +class TestSMovB32Construction(unittest.TestCase): + def test_positional_dst_src(self): + d, s = sgpr(0), sgpr(1) + m = SMovB32(d, s) + self.assertIs(m.dst, d) + self.assertEqual(m.srcs, [s]) + self.assertEqual(m.comment, "") + self.assertEqual(m.instStr, "s_mov_b32") + self.assertEqual(m.instType, InstType.INST_B32) + + def test_str_sgpr_to_sgpr(self): + m = SMovB32(sgpr(0), sgpr(1), comment="probe") + text = str(m) + self.assertTrue(text.startswith("s_mov_b32 s0, s1"), text) + self.assertTrue(text.endswith(" // probe\n"), text) + + def test_inherits_common_instruction(self): + m = SMovB32(sgpr(0), sgpr(1)) + self.assertIsInstance(m, Instruction) + self.assertIsInstance(m, CommonInstruction) + self.assertIsInstance(m, SMovB32) + + def test_deepcopy(self): + m = SMovB32(sgpr(0), sgpr(1), comment="x") + c = copy.deepcopy(m) + self.assertIsInstance(c, SMovB32) + self.assertEqual(str(c), str(m)) + + +class TestSMovB64Construction(unittest.TestCase): + def test_inst_type_b64(self): + m = SMovB64(sgpr(0, 2), sgpr(4, 2), comment="wide") + self.assertEqual(m.instStr, "s_mov_b64") + self.assertEqual(m.instType, InstType.INST_B64) + + def test_str_contains_mnemonic(self): + m = SMovB64(sgpr(0, 2), sgpr(4, 2)) + self.assertIn("s_mov_b64", str(m)) + + +# =========================================================================== +# SNop +# =========================================================================== + + +class TestSNopConstruction(unittest.TestCase): + def test_inst_type_notype(self): + m = SNop(waitState=0) + self.assertEqual(m.instStr, "s_nop") + self.assertEqual(m.instType, InstType.INST_NOTYPE) + + def test_str_wait_and_comment(self): + m = SNop(waitState=3, comment="pad") + text = str(m) + self.assertIn("s_nop", text) + self.assertIn("3", text) + self.assertIn("pad", text) + + def test_get_params(self): + m = SNop(waitState=7) + self.assertEqual(m.getParams(), [7]) + self.assertEqual(m.getDstParams(), []) + self.assertEqual(m.getSrcParams(), [7]) + + def test_to_stinky_logical(self): + m = SNop(waitState=2, comment="c") + if hasattr(m, "to_stinky_logical"): + name_fn = getattr(m, "getOpcodeName", None) + if name_fn: + self.assertEqual(name_fn(), "SNop") + + def test_deepcopy(self): + m = SNop(waitState=1, comment="x") + c = copy.deepcopy(m) + self.assertIsInstance(c, SNop) + self.assertIsNot(c, m) + self.assertEqual(c.wait_state, 1) + self.assertEqual(c.comment, "x") + + def test_positional_arg(self): + """Supports SNop(3, 'comment') positional style used in Tensile.""" + m = SNop(3, "delay") + self.assertEqual(m.wait_state, 3) + self.assertEqual(m.comment, "delay") + + +# =========================================================================== +# SMemLoadInstruction / SLoadB32 / SLoadB64 +# =========================================================================== + + +class TestSLoadB32Construction(unittest.TestCase): + def test_is_smem_load_not_common(self): + m = SLoadB32(sgpr(0), sgpr(2, 2), 0, comment="k") + self.assertIsInstance(m, Instruction) + self.assertIsInstance(m, SMemLoadInstruction) + self.assertIsInstance(m, SLoadB32) + self.assertNotIsInstance(m, CommonInstruction) + + def test_str_basic(self): + m = SLoadB32(sgpr(0), sgpr(2, 2), 0, comment="probe") + text = str(m) + self.assertTrue(text.startswith("s_load_b32 s0, s[2:3], 0"), text) + self.assertTrue(text.endswith(" // probe\n"), text) + + def test_get_params_order(self): + d, b = sgpr(0), sgpr(2, 2) + m = SLoadB32(d, b, 4) + p = m.getParams() + self.assertEqual(len(p), 3) + self.assertIs(p[0], d) + self.assertIs(p[1], b) + self.assertEqual(p[2], 4) + + def test_smem_modifier_raises_on_logical_bridge(self): + m = SLoadB32(sgpr(0), sgpr(2, 2), 0, smem=SMEMModifiers(offset=4)) + with self.assertRaises(NotImplementedError): + m.to_stinky_logical() + + def test_deepcopy(self): + m = SLoadB32(sgpr(0), sgpr(2, 2), 0, comment="x") + c = copy.deepcopy(m) + self.assertIsInstance(c, SLoadB32) + self.assertEqual(str(c), str(m)) + + +class TestSLoadB64Construction(unittest.TestCase): + def test_prestr_b64(self): + m = SLoadB64(sgpr(0, 2), sgpr(4, 2), 0) + self.assertIn("s_load_b64", str(m)) + + +# =========================================================================== +# Step-3 contract: VMovB32 is picked up by Module._collect_logical_insts. +# =========================================================================== + + +class TestCollectLogicalIntegration(unittest.TestCase): + """``VMovB32`` / ``SMovB32`` instruction-side contract for + ``_collect_logical_insts``. + + Collector walk semantics (fake leaves, TextBlock filter, dummy skip) + are owned by ``test_code.TestCollectLogicalInsts`` -- this class only + pins that the real shims expose ``to_stinky_logical`` and are actually + collected when the stinkytofu binding is importable. + """ + + def test_vmovb32_exposes_to_stinky_logical(self): + v = VMovB32(vgpr(0), vgpr(1)) + self.assertTrue(callable(getattr(v, "to_stinky_logical", None))) + + def test_smovb32_exposes_to_stinky_logical(self): + s = SMovB32(sgpr(0), sgpr(1)) + self.assertTrue(callable(getattr(s, "to_stinky_logical", None))) + + def test_smovb64_exposes_to_stinky_logical(self): + s = SMovB64(sgpr(0, 2), sgpr(4, 2)) + self.assertTrue(callable(getattr(s, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_vmovb32_collected_by_module(self): + m = Module() + m.add(VMovB32(vgpr(0), vgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_smovb32_collected_by_module(self): + m = Module() + m.add(SMovB32(sgpr(0), sgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + def test_snop_exposes_to_stinky_logical(self): + n = SNop(waitState=0) + self.assertTrue(callable(getattr(n, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_sloadb32_collected_by_module(self): + m = Module() + m.add(SLoadB32(sgpr(0), sgpr(2, 2), 0)) + self.assertEqual(len(m._collect_logical_insts()), 1) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_snop_collected_by_module(self): + m = Module() + m.add(SNop(waitState=0)) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +# =========================================================================== +# Scalar ALU instructions (Phase 6 Step 1) +# =========================================================================== + +_SCALAR_ALU_BINARY = [ + # (class, mnemonic, InstType) + (SAddI32, "s_add_i32", InstType.INST_I32), + (SAddU32, "s_add_u32", InstType.INST_U32), + (SAddCU32, "s_addc_u32", InstType.INST_U32), + (SMulI32, "s_mul_i32", InstType.INST_I32), + (SMulHII32, "s_mul_hi_i32", InstType.INST_I32), + (SMulHIU32, "s_mul_hi_u32", InstType.INST_U32), + (SMulLOU32, "s_mul_lo_u32", InstType.INST_U32), + (SSubI32, "s_sub_i32", InstType.INST_I32), + (SSubU32, "s_sub_u32", InstType.INST_U32), + (SSubBU32, "s_subb_u32", InstType.INST_U32), + (SLShiftLeftB32, "s_lshl_b32", InstType.INST_B32), + (SLShiftRightB32, "s_lshr_b32", InstType.INST_B32), + (SLShiftLeftB64, "s_lshl_b64", InstType.INST_B64), + (SLShiftRightB64, "s_lshr_b64", InstType.INST_B64), + (SAShiftRightI32, "s_ashr_i32", InstType.INST_I32), + (SLShiftLeft1AddU32, "s_lshl1_add_u32", InstType.INST_U32), + (SLShiftLeft2AddU32, "s_lshl2_add_u32", InstType.INST_U32), + (SLShiftLeft3AddU32, "s_lshl3_add_u32", InstType.INST_U32), + (SLShiftLeft4AddU32, "s_lshl4_add_u32", InstType.INST_U32), + (SAndB32, "s_and_b32", InstType.INST_B32), + (SAndB64, "s_and_b64", InstType.INST_B64), + (SAndN2B32, "s_andn2_b32", InstType.INST_B32), + (SOrB32, "s_or_b32", InstType.INST_B32), + (SOrB64, "s_or_b64", InstType.INST_B64), + (SXorB32, "s_xor_b32", InstType.INST_B32), +] + +_SCALAR_ALU_UNARY = [ + (SAndSaveExecB32, "s_and_saveexec_b32", InstType.INST_B32), + (SAndSaveExecB64, "s_and_saveexec_b64", InstType.INST_B64), + (SOrSaveExecB32, "s_or_saveexec_b32", InstType.INST_B32), + (SOrSaveExecB64, "s_or_saveexec_b64", InstType.INST_B64), +] + + +# =========================================================================== +# SBarrier / SGetRegB32 / SSetRegB32 / SSetRegIMM32B32 (Phase 6 Step 2) +# =========================================================================== + + +class TestSBarrierConstruction(unittest.TestCase): + """SBarrier is a zero-operand instruction with stinkytofu bridge.""" + + def test_default_construction(self): + b = SBarrier() + self.assertIsInstance(b, Instruction) + self.assertEqual(b.instStr, "s_barrier") + self.assertEqual(b.instType, InstType.INST_NOTYPE) + + def test_with_comment(self): + b = SBarrier(comment="sync") + text = str(b) + self.assertIn("s_barrier", text) + self.assertIn("sync", text) + + def test_deepcopy(self): + b = SBarrier(comment="x") + c = copy.deepcopy(b) + self.assertIsInstance(c, SBarrier) + self.assertEqual(str(c), str(b)) + + def test_has_to_stinky_logical(self): + b = SBarrier() + self.assertTrue(callable(getattr(b, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(SBarrier()) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestSGetRegB32Construction(unittest.TestCase): + """SGetRegB32 is a unary scalar control instruction.""" + + def test_construction(self): + inst = SGetRegB32(dst=sgpr(0), src=sgpr(1)) + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, "s_getreg_b32") + + def test_str(self): + inst = SGetRegB32(dst=sgpr(0), src=sgpr(1), comment="hwid") + text = str(inst) + self.assertIn("s_getreg_b32", text) + self.assertIn("s0", text) + + def test_has_to_stinky_logical(self): + inst = SGetRegB32(dst=sgpr(0), src=sgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(SGetRegB32(dst=sgpr(0), src=sgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestScalarALUBinaryConstruction(unittest.TestCase): + """All binary scalar ALU shims inherit CommonInstruction and emit correct asm.""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _SCALAR_ALU_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2), comment="t") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + text = str(inst) + self.assertTrue(text.startswith(mnemonic), text) + self.assertIn("s0", text) + self.assertIn("s1", text) + self.assertIn("s2", text) + + def test_deepcopy(self): + for cls, _, _ in _SCALAR_ALU_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=sgpr(4), src0=sgpr(5), src1=sgpr(6), comment="cp") + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _SCALAR_ALU_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _SCALAR_ALU_BINARY: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + def test_immediate_src(self): + inst = SAddU32(dst=sgpr(0), src0=sgpr(1), src1=42) + text = str(inst) + self.assertIn("42", text) + + +class TestScalarALUUnaryConstruction(unittest.TestCase): + """Unary scalar ALU shims (dst, src) inherit CommonInstruction.""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _SCALAR_ALU_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=sgpr(0), src=sgpr(1), comment="u") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + text = str(inst) + self.assertTrue(text.startswith(mnemonic), text) + + def test_deepcopy(self): + for cls, _, _ in _SCALAR_ALU_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=sgpr(0), src=sgpr(1), comment="x") + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _SCALAR_ALU_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=sgpr(0), src=sgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _SCALAR_ALU_UNARY: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=sgpr(0), src=sgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestDevelopInstructionExports(unittest.TestCase): + """Develop-added rocisa instructions must resolve (dummy phase). + + Tensile imports these by name during module load; missing exports + fail before any kernel is generated. Real ``toString`` / logicalIR + parity is deferred to the instruction vertical-slice batch. + """ + + _NAMES = ( + "SMemAtomicIncInstruction", + "SMemAtomicDecInstruction", + "SMemStoreInstruction", + ) + + def test_all_develop_exports_importable(self): + import rocisa_stinkytofu_adaptor.instruction as inst_mod + + for name in self._NAMES: + with self.subTest(name=name): + cls = getattr(inst_mod, name) + obj = cls() + self.assertIn("DummyShim", repr(obj)) + + +# =========================================================================== +# Vector ALU instructions (Phase 6 Step 4) +# =========================================================================== + +_VECTOR_ALU_BINARY = [ + # (class, mnemonic, InstType) + (VAddU32, "v_add_nc_u32", InstType.INST_U32), + (VAddF32, "v_add_f32", InstType.INST_F32), + (VSubF32, "v_sub_f32", InstType.INST_F32), + (VSubI32, "v_sub_nc_i32", InstType.INST_I32), + (VSubU32, "v_sub_nc_u32", InstType.INST_U32), + (VMulF32, "v_mul_f32", InstType.INST_F32), + (VMulLOU32, "v_mul_lo_u32", InstType.INST_LO_U32), + (VMulHIU32, "v_mul_hi_u32", InstType.INST_HI_U32), + (VMulHII32, "v_mul_hi_i32", InstType.INST_HI_I32), + (VMulI32I24, "v_mul_i32_i24", InstType.INST_I32), + (VMulU32U24, "v_mul_u32_u24", InstType.INST_U32), + (VAndB32, "v_and_b32", InstType.INST_B32), + (VOrB32, "v_or_b32", InstType.INST_B32), + (VXorB32, "v_xor_b32", InstType.INST_B32), +] + +_VECTOR_SHIFT = [ + (VLShiftLeftB32, "v_lshlrev_b32", InstType.INST_B32), + (VLShiftRightB32, "v_lshrrev_b32", InstType.INST_B32), + (VLShiftLeftB64, "v_lshlrev_b64", InstType.INST_B64), + (VLShiftRightB64, "v_lshrrev_b64", InstType.INST_B64), +] + +_VECTOR_TERNARY = [ + (VFmaF32, "v_fma_f32", InstType.INST_F32), + (VFmaMixF32, "v_fma_mix_f32", InstType.INST_F32), + (VAndOrB32, "v_and_or_b32", InstType.INST_B32), +] + + +class TestVectorALUBinaryConstruction(unittest.TestCase): + """Binary vector ALU shims (dst, src0, src1) inherit CommonInstruction.""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _VECTOR_ALU_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), comment="t") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + text = str(inst) + self.assertTrue(text.startswith(mnemonic), text) + self.assertIn("v0", text) + self.assertIn("v1", text) + self.assertIn("v2", text) + + def test_deepcopy(self): + for cls, _, _ in _VECTOR_ALU_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(4), src0=vgpr(5), src1=vgpr(6), comment="cp") + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _VECTOR_ALU_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _VECTOR_ALU_BINARY: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + def test_immediate_src(self): + inst = VAddU32(dst=vgpr(0), src0=vgpr(1), src1=42) + text = str(inst) + self.assertIn("42", text) + + +class TestVectorShiftConstruction(unittest.TestCase): + """Vector shift shims (dst, shiftHex, src) with rev mnemonics.""" + + def test_construction_native_api(self): + for cls, mnemonic, itype in _VECTOR_SHIFT: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2), comment="sh") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + text = str(inst) + self.assertTrue(text.startswith(mnemonic), text) + self.assertIn("v0", text) + self.assertIn("v1", text) + self.assertIn("v2", text) + + def test_construction_generic_api(self): + for cls, mnemonic, _ in _VECTOR_SHIFT: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertEqual(inst.instStr, mnemonic) + text = str(inst) + self.assertIn("v1", text) + self.assertIn("v2", text) + + def test_deepcopy(self): + for cls, _, _ in _VECTOR_SHIFT: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _VECTOR_SHIFT: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _VECTOR_SHIFT: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestVectorTernaryConstruction(unittest.TestCase): + """Ternary vector ALU shims (dst, src0, src1, src2).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _VECTOR_TERNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), + src2=vgpr(3), comment="ter") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + text = str(inst) + self.assertTrue(text.startswith(mnemonic), text) + self.assertIn("v0", text) + self.assertIn("v1", text) + self.assertIn("v2", text) + self.assertIn("v3", text) + + def test_deepcopy(self): + for cls, _, _ in _VECTOR_TERNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _VECTOR_TERNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _VECTOR_TERNARY: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestVCndMaskB32Construction(unittest.TestCase): + """VCndMaskB32 (native: dst, src0, src1, src2=VCC; logical IR: 2 srcs).""" + + def test_construction_with_src2(self): + inst = VCndMaskB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), + src2=vgpr(3), comment="mask") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, "v_cndmask_b32") + text = str(inst) + self.assertIn("v0", text) + self.assertIn("v1", text) + self.assertIn("v2", text) + self.assertIn("v3", text) + + def test_construction_without_src2(self): + inst = VCndMaskB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertEqual(len(inst.srcs), 2) + text = str(inst) + self.assertIn("v_cndmask_b32", text) + + def test_deepcopy(self): + inst = VCndMaskB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, VCndMaskB32) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + inst = VCndMaskB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(VCndMaskB32(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestVReadfirstlaneB32Construction(unittest.TestCase): + """VReadfirstlaneB32 (unary: dst, src).""" + + def test_construction_and_str(self): + inst = VReadfirstlaneB32(dst=vgpr(0), src=vgpr(1), comment="lane0") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, "v_readfirstlane_b32") + text = str(inst) + self.assertIn("v_readfirstlane_b32", text) + self.assertIn("v0", text) + self.assertIn("v1", text) + + def test_deepcopy(self): + inst = VReadfirstlaneB32(dst=vgpr(0), src=vgpr(1)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, VReadfirstlaneB32) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + inst = VReadfirstlaneB32(dst=vgpr(0), src=vgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(VReadfirstlaneB32(dst=vgpr(0), src=vgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +# =========================================================================== +# Compare instructions (Phase 6 Step 5) +# =========================================================================== + +_SCALAR_CMP = [ + # (class, mnemonic, InstType) + (SCmpEQI32, "s_cmp_eq_i32", InstType.INST_I32), + (SCmpEQU32, "s_cmp_eq_u32", InstType.INST_U32), + (SCmpEQU64, "s_cmp_eq_u64", InstType.INST_U64), + (SCmpGeI32, "s_cmp_ge_i32", InstType.INST_I32), + (SCmpGeU32, "s_cmp_ge_u32", InstType.INST_U32), + (SCmpGtI32, "s_cmp_gt_i32", InstType.INST_I32), + (SCmpGtU32, "s_cmp_gt_u32", InstType.INST_U32), + (SCmpLeI32, "s_cmp_le_i32", InstType.INST_I32), + (SCmpLeU32, "s_cmp_le_u32", InstType.INST_U32), + (SCmpLgU32, "s_cmp_lg_u32", InstType.INST_U32), + (SCmpLgI32, "s_cmp_lg_i32", InstType.INST_I32), + (SCmpLgU64, "s_cmp_lg_u64", InstType.INST_U64), + (SCmpLtI32, "s_cmp_lt_i32", InstType.INST_I32), + (SCmpLtU32, "s_cmp_lt_u32", InstType.INST_U32), + (SBitcmp1B32, "s_bitcmp1_b32", InstType.INST_B32), +] + +_VECTOR_CMP = [ + (VCmpEQF32, "v_cmp_eq_f32", InstType.INST_F32), + (VCmpEQF64, "v_cmp_eq_f64", InstType.INST_F64), + (VCmpEQU32, "v_cmp_eq_u32", InstType.INST_U32), + (VCmpEQI32, "v_cmp_eq_i32", InstType.INST_I32), + (VCmpGEF16, "v_cmp_ge_f16", InstType.INST_F16), + (VCmpGTF16, "v_cmp_gt_f16", InstType.INST_F16), + (VCmpGEF32, "v_cmp_ge_f32", InstType.INST_F32), + (VCmpGTF32, "v_cmp_gt_f32", InstType.INST_F32), + (VCmpGEF64, "v_cmp_ge_f64", InstType.INST_F64), + (VCmpGTF64, "v_cmp_gt_f64", InstType.INST_F64), + (VCmpGEI32, "v_cmp_ge_i32", InstType.INST_I32), + (VCmpGTI32, "v_cmp_gt_i32", InstType.INST_I32), + (VCmpGEU32, "v_cmp_ge_u32", InstType.INST_U32), + (VCmpGtU32, "v_cmp_gt_u32", InstType.INST_U32), + (VCmpLeU32, "v_cmp_le_u32", InstType.INST_U32), + (VCmpLeI32, "v_cmp_le_i32", InstType.INST_I32), + (VCmpLtI32, "v_cmp_lt_i32", InstType.INST_I32), + (VCmpLtU32, "v_cmp_lt_u32", InstType.INST_U32), + (VCmpUF32, "v_cmp_u_f32", InstType.INST_F32), + (VCmpNeI32, "v_cmp_ne_i32", InstType.INST_I32), + (VCmpNeU32, "v_cmp_ne_u32", InstType.INST_U32), + (VCmpNeU64, "v_cmp_ne_u64", InstType.INST_U64), + (VCmpClassF32, "v_cmp_class_f32", InstType.INST_F32), +] + +_VECTOR_CMPX = [ + (VCmpXClassF32, "v_cmpx_class_f32", InstType.INST_F32), + (VCmpXEqU32, "v_cmpx_eq_u32", InstType.INST_U32), + (VCmpXGeU32, "v_cmpx_ge_u32", InstType.INST_U32), + (VCmpXGtU32, "v_cmpx_gt_u32", InstType.INST_U32), + (VCmpXLeU32, "v_cmpx_le_u32", InstType.INST_U32), + (VCmpXLeI32, "v_cmpx_le_i32", InstType.INST_I32), + (VCmpXLtF32, "v_cmpx_lt_f32", InstType.INST_F32), + (VCmpXLtI32, "v_cmpx_lt_i32", InstType.INST_I32), + (VCmpXLtU32, "v_cmpx_lt_u32", InstType.INST_U32), + (VCmpXLtU64, "v_cmpx_lt_u64", InstType.INST_U64), + (VCmpXNeU16, "v_cmpx_ne_u16", InstType.INST_U16), + (VCmpXNeU32, "v_cmpx_ne_u32", InstType.INST_U32), +] + + +class TestScalarCmpConstruction(unittest.TestCase): + """Scalar compare shims (no dst, src0, src1).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _SCALAR_CMP: + with self.subTest(cls=cls.__name__): + inst = cls(src0=sgpr(0), src1=sgpr(1), comment="cmp") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertIsNone(inst.dst) + self.assertEqual(len(inst.srcs), 2) + text = str(inst) + self.assertIn(mnemonic, text) + self.assertIn("s0", text) + self.assertIn("s1", text) + + def test_deepcopy(self): + for cls, _, _ in _SCALAR_CMP: + with self.subTest(cls=cls.__name__): + inst = cls(src0=sgpr(2), src1=sgpr(3), comment="cp") + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _SCALAR_CMP: + with self.subTest(cls=cls.__name__): + inst = cls(src0=sgpr(0), src1=sgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _SCALAR_CMP: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(src0=sgpr(0), src1=sgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + def test_immediate_operand(self): + inst = SCmpEQI32(src0=sgpr(0), src1=42) + text = str(inst) + self.assertIn("42", text) + + +class TestVectorCmpConstruction(unittest.TestCase): + """Vector compare shims (dst, src0, src1).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _VECTOR_CMP: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), comment="vcmp") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertIsNotNone(inst.dst) + self.assertEqual(len(inst.srcs), 2) + text = str(inst) + self.assertIn(mnemonic, text) + self.assertIn("v0", text) + self.assertIn("v1", text) + self.assertIn("v2", text) + + def test_deepcopy(self): + for cls, _, _ in _VECTOR_CMP: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(4), src0=vgpr(5), src1=vgpr(6), comment="cp") + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _VECTOR_CMP: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _VECTOR_CMP: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestVectorCmpXConstruction(unittest.TestCase): + """Vector compareX shims (dst, src0, src1).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _VECTOR_CMPX: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), comment="cmpx") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + text = str(inst) + self.assertIn(mnemonic, text) + self.assertIn("v0", text) + self.assertIn("v1", text) + self.assertIn("v2", text) + + def test_deepcopy(self): + for cls, _, _ in _VECTOR_CMPX: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(4), src0=vgpr(5), src1=vgpr(6)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _VECTOR_CMPX: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _VECTOR_CMPX: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +# =========================================================================== +# Step 6: Scalar Min/Max/Abs + Vector Unary/Misc tests +# =========================================================================== + +_SCALAR_MINMAX = [ + (SAbsI32, "s_abs_i32", InstType.INST_I32), + (SMaxI32, "s_max_i32", InstType.INST_I32), + (SMaxU32, "s_max_u32", InstType.INST_U32), + (SMinI32, "s_min_i32", InstType.INST_I32), + (SMinU32, "s_min_u32", InstType.INST_U32), +] + +_VECTOR_UNARY = [ + (VExpF16, "v_exp_f16", InstType.INST_F16), + (VExpF32, "v_exp_f32", InstType.INST_F32), + (VRcpF16, "v_rcp_f16", InstType.INST_F16), + (VRcpF32, "v_rcp_f32", InstType.INST_F32), + (VRcpIFlagF32, "v_rcp_iflag_f32", InstType.INST_F32), + (VRsqF16, "v_rsq_f16", InstType.INST_F16), + (VRsqF32, "v_rsq_f32", InstType.INST_F32), + (VRsqIFlagF32, "v_rsq_iflag_f32", InstType.INST_F32), + (VNotB32, "v_not_b32", InstType.INST_B32), + (VPrngB32, "v_prng_b32", InstType.INST_B32), + (VRndneF32, "v_rndne_f32", InstType.INST_F32), +] + +_VECTOR_MINMAX = [ + (VMaxF16, "v_max_f16", InstType.INST_F16), + (VMaxF32, "v_max_f32", InstType.INST_F32), + (VMaxF64, "v_max_f64", InstType.INST_F64), + (VMaxI32, "v_max_i32", InstType.INST_I32), + (VMaxPKF16, "v_pk_max_f16", InstType.INST_F16), + (VMinF16, "v_min_f16", InstType.INST_F16), + (VMinF32, "v_min_f32", InstType.INST_F32), + (VMinF64, "v_min_f64", InstType.INST_F64), + (VMinI32, "v_min_i32", InstType.INST_I32), + (VPackF16toB32, "v_pack_b32_f16", InstType.INST_B32), +] + +_VECTOR_TERNARY_MISC = [ + (VMed3I32, "v_med3_i32", InstType.INST_I32), + (VMed3F32, "v_med3_f32", InstType.INST_F32), + (VLShiftLeftOrB32, "v_lshl_or_b32", InstType.INST_B32), +] + + +class TestScalarMinMaxAbsConstruction(unittest.TestCase): + """SAbsI32 (unary), SMaxI32/SMaxU32/SMinI32/SMinU32 (binary).""" + + def test_sabsi32_construction(self): + inst = SAbsI32(dst=sgpr(0), src=sgpr(1), comment="abs") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, "s_abs_i32") + self.assertEqual(inst.instType, InstType.INST_I32) + self.assertEqual(len(inst.srcs), 1) + text = str(inst) + self.assertIn("s_abs_i32", text) + self.assertIn("s0", text) + self.assertIn("s1", text) + + def test_binary_construction(self): + for cls, mnemonic, itype in _SCALAR_MINMAX[1:]: + with self.subTest(cls=cls.__name__): + inst = cls(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2), comment="mm") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertEqual(len(inst.srcs), 2) + text = str(inst) + self.assertIn(mnemonic, text) + self.assertIn("s0", text) + self.assertIn("s1", text) + self.assertIn("s2", text) + + def test_deepcopy(self): + inst = SAbsI32(dst=sgpr(0), src=sgpr(1)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, type(inst)) + self.assertEqual(str(c), str(inst)) + for cls, _, _ in _SCALAR_MINMAX[1:]: + with self.subTest(cls=cls.__name__): + inst = cls(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _SCALAR_MINMAX: + with self.subTest(cls=cls.__name__): + if cls == SAbsI32: + inst = cls(dst=sgpr(0), src=sgpr(1)) + else: + inst = cls(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _SCALAR_MINMAX: + with self.subTest(cls=cls.__name__): + m = Module() + if cls == SAbsI32: + m.add(cls(dst=sgpr(0), src=sgpr(1))) + else: + m.add(cls(dst=sgpr(0), src0=sgpr(1), src1=sgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestVectorUnaryConstruction(unittest.TestCase): + """Vector unary shims (dst, src).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _VECTOR_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1), comment="vu") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertEqual(len(inst.srcs), 1) + text = str(inst) + self.assertIn(mnemonic, text) + self.assertIn("v0", text) + self.assertIn("v1", text) + + def test_deepcopy(self): + for cls, _, _ in _VECTOR_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(4), src=vgpr(5)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _VECTOR_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _VECTOR_UNARY: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src=vgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestVectorMinMaxConstruction(unittest.TestCase): + """Vector min/max binary shims (dst, src0, src1).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _VECTOR_MINMAX: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), comment="vmm") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertEqual(len(inst.srcs), 2) + text = str(inst) + self.assertIn(mnemonic, text) + self.assertIn("v0", text) + self.assertIn("v1", text) + self.assertIn("v2", text) + + def test_deepcopy(self): + for cls, _, _ in _VECTOR_MINMAX: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _VECTOR_MINMAX: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _VECTOR_MINMAX: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestVectorTernaryMiscConstruction(unittest.TestCase): + """VMed3I32, VMed3F32, VLShiftLeftOrB32 (dst, src0, src1, src2).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _VECTOR_TERNARY_MISC: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), + src2=vgpr(3), comment="vtern") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertEqual(len(inst.srcs), 3) + text = str(inst) + self.assertIn(mnemonic, text) + self.assertIn("v0", text) + self.assertIn("v1", text) + self.assertIn("v2", text) + self.assertIn("v3", text) + + def test_deepcopy(self): + for cls, _, _ in _VECTOR_TERNARY_MISC: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _VECTOR_TERNARY_MISC: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _VECTOR_TERNARY_MISC: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), src2=vgpr(3))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestVAShiftRightI32Construction(unittest.TestCase): + """VAShiftRightI32 — vector shift (dst, shiftHex, src).""" + + def test_construction_and_str(self): + inst = VAShiftRightI32(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2), + comment="ashr") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, "v_ashrrev_i32") + self.assertEqual(inst.instType, InstType.INST_I32) + self.assertEqual(len(inst.srcs), 2) + text = str(inst) + self.assertIn("v_ashrrev_i32", text) + self.assertIn("v0", text) + + def test_deepcopy(self): + inst = VAShiftRightI32(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, type(inst)) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + inst = VAShiftRightI32(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(VAShiftRightI32(dst=vgpr(0), shiftHex=vgpr(1), src=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +# =========================================================================== +# Step 7 — Conversion (VCvt*) instructions +# =========================================================================== + +_CVT_UNARY = [ + (VCvtF16toF32, "v_cvt_f32_f16", InstType.INST_NOTYPE), + (VCvtF32toF16, "v_cvt_f16_f32", InstType.INST_NOTYPE), + (VCvtF32toU32, "v_cvt_u32_f32", InstType.INST_NOTYPE), + (VCvtU32toF32, "v_cvt_f32_u32", InstType.INST_NOTYPE), + (VCvtI32toF32, "v_cvt_f32_i32", InstType.INST_NOTYPE), + (VCvtF32toI32, "v_cvt_i32_f32", InstType.INST_NOTYPE), + (VCvtFP8toF32, "v_cvt_f32_fp8", InstType.INST_NOTYPE), + (VCvtBF8toF32, "v_cvt_f32_bf8", InstType.INST_NOTYPE), + (VCvtPkFP8toF32, "v_cvt_pk_f32_fp8", InstType.INST_NOTYPE), + (VCvtPkBF8toF32, "v_cvt_pk_f32_bf8", InstType.INST_NOTYPE), +] + +_CVT_BINARY = [ + (VCvtPkF32toBF8, "v_cvt_pk_bf8_f32", InstType.INST_NOTYPE), + (VCvtSRF32toFP8, "v_cvt_sr_fp8_f32", InstType.INST_NOTYPE), + (VCvtSRF32toBF8, "v_cvt_sr_bf8_f32", InstType.INST_NOTYPE), + (VCvtPkF32toFP8, "v_cvt_pk_fp8_f32", InstType.INST_NOTYPE), + (VCvtPkF32toBF16, "v_cvt_pk_bf16_f32", InstType.INST_NOTYPE), +] + +_CVT_SCALE = [ + (VCvtScalePkFP8toF16, "v_cvt_scalef32_pk_f16_fp8", InstType.INST_NOTYPE), + (VCvtScalePkBF8toF16, "v_cvt_scalef32_pk_f16_bf8", InstType.INST_NOTYPE), + (VCvtScaleFP8toF16, "v_cvt_scalef32_f16_fp8", InstType.INST_NOTYPE), + (VCvtScalePkF16toFP8, "v_cvt_scalef32_pk_fp8_f16", InstType.INST_NOTYPE), + (VCvtScalePkF16toBF8, "v_cvt_scalef32_pk_bf8_f16", InstType.INST_NOTYPE), + (VCvtScaleSRF16toFP8, "v_cvt_scalef32_sr_fp8_f16", InstType.INST_NOTYPE), + (VCvtScaleSRF16toBF8, "v_cvt_scalef32_sr_bf8_f16", InstType.INST_NOTYPE), +] + + +class TestCvtUnaryConstruction(unittest.TestCase): + """Unary conversion shims (dst, src).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _CVT_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1), comment="cvt") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertEqual(len(inst.srcs), 1) + text = str(inst) + self.assertIn(mnemonic, text) + + def test_deepcopy(self): + for cls, _, _ in _CVT_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(4), src=vgpr(5)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _CVT_UNARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _CVT_UNARY: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src=vgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestCvtBinaryConstruction(unittest.TestCase): + """Binary conversion shims (dst, src0, src1).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _CVT_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2), comment="cvt2") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertEqual(len(inst.srcs), 2) + text = str(inst) + self.assertIn(mnemonic, text) + + def test_deepcopy(self): + for cls, _, _ in _CVT_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(4), src0=vgpr(5), src1=vgpr(6)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _CVT_BINARY: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _CVT_BINARY: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestCvtScaleConstruction(unittest.TestCase): + """Scale conversion shims (dst, src, scale).""" + + def test_construction_and_str(self): + for cls, mnemonic, itype in _CVT_SCALE: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1), scale=vgpr(2), comment="sc") + self.assertIsInstance(inst, CommonInstruction) + self.assertEqual(inst.instStr, mnemonic) + self.assertEqual(inst.instType, itype) + self.assertEqual(len(inst.srcs), 2) + text = str(inst) + self.assertIn(mnemonic, text) + + def test_deepcopy(self): + for cls, _, _ in _CVT_SCALE: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(4), src=vgpr(5), scale=vgpr(6)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _, _ in _CVT_SCALE: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1), scale=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _, _ in _CVT_SCALE: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src=vgpr(1), scale=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +# =========================================================================== +# Memory instructions -- Step 8 (Buffer/Flat/DS/Tensor). +# =========================================================================== + + +_BUFFER_LOAD = [ + (BufferLoadU8, "buffer_load_u8"), + (BufferLoadD16HIU8, "buffer_load_d16_hi_u8"), + (BufferLoadD16U8, "buffer_load_d16_u8"), + (BufferLoadD16HIB16, "buffer_load_d16_hi_b16"), + (BufferLoadD16B16, "buffer_load_d16_b16"), + (BufferLoadB32, "buffer_load_b32"), + (BufferLoadB64, "buffer_load_b64"), + (BufferLoadB96, "buffer_load_b96"), + (BufferLoadB128, "buffer_load_b128"), +] + +_BUFFER_STORE = [ + (BufferStoreB8, "buffer_store_b8"), + (BufferStoreD16HIU8, "buffer_store_d16_hi_b8"), + (BufferStoreD16HIB16, "buffer_store_d16_hi_b16"), + (BufferStoreB16, "buffer_store_b16"), + (BufferStoreB32, "buffer_store_b32"), + (BufferStoreB64, "buffer_store_b64"), + (BufferStoreB128, "buffer_store_b128"), + (BufferAtomicCmpswapB32, "buffer_atomic_cmpswap_b32"), + (BufferAtomicCmpswapB64, "buffer_atomic_cmpswap_b64"), +] + +_FLAT_LOAD = [ + (FlatLoadD16HIU8, "flat_load_d16_hi_u8"), + (FlatLoadD16U8, "flat_load_d16_u8"), + (FlatLoadD16HIB16, "flat_load_d16_hi_b16"), + (FlatLoadD16B16, "flat_load_d16_b16"), + (FlatLoadB32, "flat_load_b32"), + (FlatLoadB64, "flat_load_b64"), + (FlatLoadB128, "flat_load_b128"), +] + +_FLAT_STORE = [ + (FlatStoreD16HIB16, "flat_store_d16_hi_b16"), + (FlatStoreB32, "flat_store_b32"), + (FlatStoreB64, "flat_store_b64"), + (FlatStoreB128, "flat_store_b128"), +] + +_DS_LOAD = [ + (DSLoadU8, "ds_load_u8"), + (DSLoadU16, "ds_load_u16"), + (DSLoadB32, "ds_load_b32"), + (DSLoadB64, "ds_load_b64"), + (DSLoadB128, "ds_load_b128"), +] + +_DS_LOAD2 = [ + (DSLoad2B32, "ds_load2_b32"), + (DSLoad2B64, "ds_load2_b64"), +] + +_DS_STORE = [ + (DSStoreB8, "ds_store_b8"), + (DSStoreB16, "ds_store_b16"), + (DSStoreB32, "ds_store_b32"), + (DSStoreB64, "ds_store_b64"), + (DSStoreB96, "ds_store_b96"), + (DSStoreB128, "ds_store_b128"), +] + +_DS_STORE2 = [ + (DSStore2B32, "ds_store2_b32"), + (DSStore2B64, "ds_store2_b64"), +] + + +class TestBufferLoadInstructions(unittest.TestCase): + def test_construction_and_mnemonic(self): + for cls, mnemonic in _BUFFER_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), vaddr=vgpr(1), saddr=sgpr(4, 4), + soffset=0, comment="load") + self.assertIn(mnemonic, str(inst)) + + def test_deepcopy(self): + for cls, _ in _BUFFER_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), vaddr=vgpr(1), saddr=sgpr(4, 4), soffset=0) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _ in _BUFFER_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), vaddr=vgpr(1), saddr=sgpr(4, 4), soffset=0) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _ in _BUFFER_LOAD: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), vaddr=vgpr(1), saddr=sgpr(4, 4), soffset=0)) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestBufferAtomicAddF32(unittest.TestCase): + def test_construction(self): + inst = BufferAtomicAddF32(dst=vgpr(2), vaddr=vgpr(1), + saddr=sgpr(4, 4), soffset=0, comment="at") + self.assertIn("buffer_atomic_add_f32", str(inst)) + + def test_has_to_stinky_logical(self): + inst = BufferAtomicAddF32(dst=vgpr(0), vaddr=vgpr(1), + saddr=sgpr(4, 4), soffset=0) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + +class TestBufferStoreInstructions(unittest.TestCase): + def test_construction_and_mnemonic(self): + for cls, mnemonic in _BUFFER_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(src=vgpr(0), vaddr=vgpr(1), saddr=sgpr(4, 4), + soffset=0, comment="store") + self.assertIn(mnemonic, str(inst)) + + def test_deepcopy(self): + for cls, _ in _BUFFER_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(src=vgpr(0), vaddr=vgpr(1), saddr=sgpr(4, 4), soffset=0) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _ in _BUFFER_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(src=vgpr(0), vaddr=vgpr(1), saddr=sgpr(4, 4), soffset=0) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _ in _BUFFER_STORE: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(src=vgpr(0), vaddr=vgpr(1), saddr=sgpr(4, 4), soffset=0)) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestFlatLoadInstructions(unittest.TestCase): + def test_construction_and_mnemonic(self): + for cls, mnemonic in _FLAT_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), vaddr=vgpr(2), comment="fload") + self.assertIn(mnemonic, str(inst)) + + def test_deepcopy(self): + for cls, _ in _FLAT_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), vaddr=vgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _ in _FLAT_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), vaddr=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _ in _FLAT_LOAD: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), vaddr=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestFlatStoreInstructions(unittest.TestCase): + def test_construction_and_mnemonic(self): + for cls, mnemonic in _FLAT_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(src=vgpr(0), vaddr=vgpr(2), comment="fstore") + self.assertIn(mnemonic, str(inst)) + + def test_deepcopy(self): + for cls, _ in _FLAT_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(src=vgpr(0), vaddr=vgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _ in _FLAT_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(src=vgpr(0), vaddr=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _ in _FLAT_STORE: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(src=vgpr(0), vaddr=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestFlatAtomicCmpswapB32(unittest.TestCase): + def test_construction(self): + inst = FlatAtomicCmpswapB32(vaddr=vgpr(0), tmp=vgpr(1), + src=vgpr(2), comment="cas") + self.assertIn("flat_atomic_cmpswap_b32", str(inst)) + + def test_deepcopy(self): + inst = FlatAtomicCmpswapB32(vaddr=vgpr(0), tmp=vgpr(1), src=vgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, FlatAtomicCmpswapB32) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + inst = FlatAtomicCmpswapB32(vaddr=vgpr(0), tmp=vgpr(1), src=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(FlatAtomicCmpswapB32(vaddr=vgpr(0), tmp=vgpr(1), src=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestDSLoadInstructions(unittest.TestCase): + def test_construction_and_mnemonic(self): + for cls, mnemonic in _DS_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1), comment="dsld") + self.assertIn(mnemonic, str(inst)) + + def test_deepcopy(self): + for cls, _ in _DS_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _ in _DS_LOAD: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _ in _DS_LOAD: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src=vgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestDSLoad2Instructions(unittest.TestCase): + def test_construction_and_mnemonic(self): + for cls, mnemonic in _DS_LOAD2: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1), comment="dsld2") + self.assertIn(mnemonic, str(inst)) + + def test_deepcopy(self): + for cls, _ in _DS_LOAD2: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _ in _DS_LOAD2: + with self.subTest(cls=cls.__name__): + inst = cls(dst=vgpr(0), src=vgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _ in _DS_LOAD2: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dst=vgpr(0), src=vgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestDSStoreInstructions(unittest.TestCase): + def test_construction_and_mnemonic(self): + for cls, mnemonic in _DS_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(dstAddr=vgpr(0), src=vgpr(1), comment="dsst") + self.assertIn(mnemonic, str(inst)) + + def test_deepcopy(self): + for cls, _ in _DS_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(dstAddr=vgpr(0), src=vgpr(1)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _ in _DS_STORE: + with self.subTest(cls=cls.__name__): + inst = cls(dstAddr=vgpr(0), src=vgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _ in _DS_STORE: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dstAddr=vgpr(0), src=vgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestDSStore2Instructions(unittest.TestCase): + def test_construction_and_mnemonic(self): + for cls, mnemonic in _DS_STORE2: + with self.subTest(cls=cls.__name__): + inst = cls(dstAddr=vgpr(0), src0=vgpr(1), src1=vgpr(2), + comment="dsst2") + self.assertIn(mnemonic, str(inst)) + + def test_deepcopy(self): + for cls, _ in _DS_STORE2: + with self.subTest(cls=cls.__name__): + inst = cls(dstAddr=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, cls) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + for cls, _ in _DS_STORE2: + with self.subTest(cls=cls.__name__): + inst = cls(dstAddr=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + for cls, _ in _DS_STORE2: + with self.subTest(cls=cls.__name__): + m = Module() + m.add(cls(dstAddr=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestDSBPermuteB32(unittest.TestCase): + def test_construction(self): + inst = DSBPermuteB32(dstAddr=vgpr(0), src0=vgpr(1), src1=vgpr(2), + comment="perm") + self.assertIn("ds_bpermute_b32", str(inst)) + + def test_deepcopy(self): + inst = DSBPermuteB32(dstAddr=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, DSBPermuteB32) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + inst = DSBPermuteB32(dstAddr=vgpr(0), src0=vgpr(1), src1=vgpr(2)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(DSBPermuteB32(dstAddr=vgpr(0), src0=vgpr(1), src1=vgpr(2))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +class TestTensorLoadToLds(unittest.TestCase): + def test_construction(self): + inst = TensorLoadToLds(group0=vgpr(0), group1=vgpr(1), comment="tld") + self.assertIn("tensor_load_to_lds", str(inst)) + + def test_deepcopy(self): + inst = TensorLoadToLds(group0=vgpr(0), group1=vgpr(1)) + c = copy.deepcopy(inst) + self.assertIsInstance(c, TensorLoadToLds) + self.assertEqual(str(c), str(inst)) + + def test_has_to_stinky_logical(self): + inst = TensorLoadToLds(group0=vgpr(0), group1=vgpr(1)) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(TensorLoadToLds(group0=vgpr(0), group1=vgpr(1))) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +# =========================================================================== +# End-to-end coverage -- see ``tests/test_emission_consistency.py``. +# =========================================================================== +# +# Previously this file held a ``TestVMovB32EndToEnd`` class with weak +# substring assertions ("emit contains 'v_mov_b32'"). That has been +# superseded by ``tests/test_emission_consistency.py``, which compares +# the asm output of three production code paths byte-for-byte: +# +# (1) default rocisa -> str(module) +# (2) default rocisa -> toStinkyTofuModule(...).emitAssembly() +# (3) adapter -> module.to_stinky_asm(arch).emitAssembly() +# +# Cross-path byte equality is strictly stronger than "asm contains a +# given mnemonic" -- it would fail-fast on any drift in the adapter's +# right-path port, the rocisa->asm-IR bridge, or the logical-IR pipeline. +# +# The TextBlock-filtering behaviour ("TextBlock items don't leak into +# the logical-IR pipeline") is structurally covered by +# ``tests/test_code.py::TestCollectLogicalInsts::test_textblocks_and_logical_mixed``, +# which asserts the collector returns only logical leaves. + + +# =========================================================================== +# Branch instructions +# =========================================================================== + + +class TestBranchInstructionConstruction(unittest.TestCase): + """Branch instructions take (labelName, comment).""" + + def test_sbranch_default(self): + inst = SBranch("loop_start") + self.assertIsInstance(inst, BranchInstruction) + self.assertIsInstance(inst, Instruction) + self.assertEqual(inst.instStr, "s_branch") + self.assertEqual(inst.labelName, "loop_start") + + def test_sbranch_str(self): + inst = SBranch("loop_start", comment="jump back") + text = str(inst) + self.assertIn("s_branch", text) + self.assertIn("loop_start", text) + self.assertIn("jump back", text) + + def test_scbranch_scc0(self): + inst = SCBranchSCC0("done") + self.assertEqual(inst.instStr, "s_cbranch_scc0") + self.assertIn("done", str(inst)) + + def test_scbranch_scc1(self): + inst = SCBranchSCC1("skip") + self.assertEqual(inst.instStr, "s_cbranch_scc1") + + def test_scbranch_vccnz(self): + inst = SCBranchVCCNZ("active_label") + self.assertEqual(inst.instStr, "s_cbranch_vccnz") + + def test_scbranch_vccz(self): + inst = SCBranchVCCZ("exit") + self.assertEqual(inst.instStr, "s_cbranch_vccz") + + def test_scbranch_execz(self): + inst = SCBranchExecZ("all_done") + self.assertEqual(inst.instStr, "s_cbranch_execz") + + def test_scbranch_execnz(self): + inst = SCBranchExecNZ("still_active") + self.assertEqual(inst.instStr, "s_cbranch_execnz") + + def test_deepcopy(self): + inst = SBranch("target", comment="c") + dup = copy.deepcopy(inst) + self.assertIsInstance(dup, SBranch) + self.assertEqual(dup.labelName, "target") + self.assertEqual(str(dup), str(inst)) + self.assertIsNot(dup, inst) + + def test_getParams(self): + inst = SCBranchSCC0("lbl") + self.assertEqual(inst.getParams(), ["lbl"]) + self.assertEqual(inst.getDstParams(), []) + self.assertEqual(inst.getSrcParams(), ["lbl"]) + + def test_has_to_stinky_logical(self): + for cls in (SBranch, SCBranchSCC0, SCBranchSCC1, + SCBranchVCCNZ, SCBranchVCCZ, + SCBranchExecZ, SCBranchExecNZ): + inst = cls("lbl") + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None)), + f"{cls.__name__} missing to_stinky_logical") + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(SBranch("lbl")) + m.add(SCBranchSCC0("lbl")) + self.assertEqual(len(m._collect_logical_insts()), 2) + + +# =========================================================================== +# SEndpgm +# =========================================================================== + + +class TestSEndpgmConstruction(unittest.TestCase): + """SEndpgm is a zero-operand terminator instruction.""" + + def test_default(self): + inst = SEndpgm() + self.assertIsInstance(inst, Instruction) + self.assertEqual(inst.instStr, "s_endpgm") + + def test_str(self): + inst = SEndpgm(comment="end") + text = str(inst) + self.assertIn("s_endpgm", text) + self.assertIn("end", text) + + def test_deepcopy(self): + inst = SEndpgm(comment="fin") + dup = copy.deepcopy(inst) + self.assertIsInstance(dup, SEndpgm) + self.assertEqual(str(dup), str(inst)) + + def test_params(self): + inst = SEndpgm() + self.assertEqual(inst.getParams(), []) + self.assertEqual(inst.getDstParams(), []) + self.assertEqual(inst.getSrcParams(), []) + + def test_has_to_stinky_logical(self): + inst = SEndpgm() + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(SEndpgm()) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +# =========================================================================== +# Wait-count instructions +# =========================================================================== + + +class TestWaitCntInstructions(unittest.TestCase): + """_SWaitCnt, SWaitCnt, SWaitXCnt, SWaitTensorcnt.""" + + def test_swaitcnt_primitive_default(self): + inst = _SWaitCnt() + self.assertIsInstance(inst, Instruction) + self.assertEqual(inst.lgkmcnt, -1) + self.assertEqual(inst.vmcnt, -1) + + def test_swaitcnt_primitive_str_both_zero(self): + inst = _SWaitCnt(lgkmcnt=0, vmcnt=0) + self.assertIn("s_waitcnt 0", str(inst)) + + def test_swaitcnt_primitive_str_lgkm_only(self): + inst = _SWaitCnt(lgkmcnt=2) + self.assertIn("lgkmcnt(2)", str(inst)) + + def test_swaitcnt_primitive_str_both(self): + inst = _SWaitCnt(lgkmcnt=1, vmcnt=3) + text = str(inst) + self.assertIn("lgkmcnt(1)", text) + self.assertIn("vmcnt(3)", text) + + def test_swaitcnt_primitive_deepcopy(self): + inst = _SWaitCnt(lgkmcnt=4, vmcnt=2, comment="wait") + dup = copy.deepcopy(inst) + self.assertEqual(dup.lgkmcnt, 4) + self.assertEqual(dup.vmcnt, 2) + self.assertEqual(str(dup), str(inst)) + + def test_swaitcnt_composite(self): + inst = SWaitCnt(vlcnt=0, vscnt=0, dscnt=0, kmcnt=0, waitAll=True) + self.assertIsInstance(inst, Instruction) + self.assertTrue(inst.waitAll) + + def test_swaitcnt_composite_deepcopy(self): + inst = SWaitCnt(vlcnt=1, comment="sync") + dup = copy.deepcopy(inst) + self.assertEqual(dup.vlcnt, 1) + self.assertEqual(dup.vscnt, -1) + + def test_swaitxcnt(self): + inst = SWaitXCnt(cnt=5) + self.assertIn("s_wait_xcnt", str(inst)) + self.assertIn("5", str(inst)) + + def test_swaitxcnt_deepcopy(self): + inst = SWaitXCnt(cnt=3, comment="x") + dup = copy.deepcopy(inst) + self.assertEqual(dup.cnt, 3) + + def test_swaittensorcnt(self): + inst = SWaitTensorcnt(cnt=2) + self.assertIn("s_wait_tensorcnt", str(inst)) + self.assertIn("2", str(inst)) + + def test_swaittensorcnt_deepcopy(self): + inst = SWaitTensorcnt(cnt=1, comment="tensor") + dup = copy.deepcopy(inst) + self.assertEqual(dup.cnt, 1) + + def test_all_have_to_stinky_logical(self): + for inst in (_SWaitCnt(), SWaitCnt(), SWaitXCnt(), SWaitTensorcnt()): + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None)), + f"{type(inst).__name__} missing to_stinky_logical") + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(_SWaitCnt(lgkmcnt=0, vmcnt=0)) + m.add(SWaitTensorcnt(cnt=0)) + m.add(SWaitXCnt(cnt=0)) + # _SWaitCnt(lgkmcnt=0, vmcnt=0) decomposes into 2 typed waits + # (dscnt from lgkmcnt + loadcnt from vmcnt) + tensorcnt + xcnt = 4 + self.assertEqual(len(m._collect_logical_insts()), 4) + + +class TestSWaitAlu(unittest.TestCase): + """SWaitAlu — dependency counter wait instruction.""" + + def test_default_construction(self): + inst = SWaitAlu() + self.assertIsInstance(inst, Instruction) + self.assertEqual(inst.va_vdst, -1) + self.assertEqual(inst.va_sdst, -1) + self.assertEqual(inst.va_ssrc, -1) + self.assertEqual(inst.hold_cnt, -1) + self.assertEqual(inst.vm_vsrc, -1) + self.assertEqual(inst.va_vcc, -1) + self.assertEqual(inst.sa_sdst, -1) + + def test_keyword_construction(self): + inst = SWaitAlu(vm_vsrc=0, va_vdst=3, comment="wait valu") + self.assertEqual(inst.vm_vsrc, 0) + self.assertEqual(inst.va_vdst, 3) + self.assertEqual(inst.va_sdst, -1) + self.assertEqual(inst.comment, "wait valu") + + def test_deepcopy(self): + inst = SWaitAlu(va_vdst=2, hold_cnt=1, comment="dc") + dup = copy.deepcopy(inst) + self.assertIsNot(inst, dup) + self.assertEqual(dup.va_vdst, 2) + self.assertEqual(dup.hold_cnt, 1) + self.assertEqual(dup.comment, "dc") + self.assertEqual(dup.vm_vsrc, -1) + + def test_has_to_stinky_logical(self): + inst = SWaitAlu(vm_vsrc=0) + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_to_stinky_logical(self): + inst = SWaitAlu(va_vdst=1, vm_vsrc=0, comment="test") + logical = inst.to_stinky_logical() + self.assertIsNotNone(logical) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(SWaitAlu(vm_vsrc=0)) + m.add(SWaitAlu(va_vdst=3, va_sdst=1)) + self.assertEqual(len(m._collect_logical_insts()), 2) + + +class TestSSchedulingFence(unittest.TestCase): + """SSchedulingFence — scheduling barrier pseudo-instruction.""" + + def test_default_construction(self): + inst = SSchedulingFence() + self.assertIsInstance(inst, Instruction) + self.assertEqual(inst.comment, "") + + def test_with_comment(self): + inst = SSchedulingFence(comment="barrier") + self.assertEqual(inst.comment, "barrier") + + def test_deepcopy(self): + inst = SSchedulingFence(comment="fence") + dup = copy.deepcopy(inst) + self.assertIsNot(inst, dup) + self.assertEqual(dup.comment, "fence") + + def test_has_to_stinky_logical(self): + inst = SSchedulingFence() + self.assertTrue(callable(getattr(inst, "to_stinky_logical", None))) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_to_stinky_logical(self): + inst = SSchedulingFence(comment="test fence") + logical = inst.to_stinky_logical() + self.assertIsNotNone(logical) + + @unittest.skipUnless(_STINKY_OK, "stinkytofu binding not built") + def test_collected_by_module(self): + m = Module() + m.add(SSchedulingFence()) + self.assertEqual(len(m._collect_logical_insts()), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_label.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_label.py new file mode 100644 index 000000000000..67dce12dbe1f --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_label.py @@ -0,0 +1,482 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Standalone tests for ``rocisa_stinkytofu_adaptor.label``. + +Run from any working directory: + + python3 projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_label.py + +Or with pytest: + + pytest projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_label.py + +These tests pin behaviour that KernelWriter depends on for label +naming -- ``LabelManager`` produces unique-by-construction asm +labels and routes EVERY emitted label name through one of its +``getName / getNameInc / getUniqueName*`` accessors. Counter +semantics, error messages, and pickle / deepcopy round-trips must +all match ``rocisa::LabelManager`` byte-for-byte so swapping the +adaptor in cannot silently change emitted label names. +""" + +from __future__ import annotations + +import copy +import os +import pickle +import string +import sys +import unittest +from unittest import mock + +# --------------------------------------------------------------------------- +# Self-contained sys.path bootstrap (mirrors test_code.py). +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + + +from rocisa_stinkytofu_adaptor import label as _label # noqa: E402 +from rocisa_stinkytofu_adaptor.label import ( # noqa: E402 + LabelManager, + magicGenerator, +) + + +# =========================================================================== +# magicGenerator -- 16-char random ``[A-Z0-9]`` string. +# =========================================================================== + + +class TestMagicGeneratorOutput(unittest.TestCase): + """``magicGenerator`` returns a fresh 16-character string drawn from + the rocisa alphabet (uppercase letters + digits, no punctuation, + no lowercase). Mirror of label.hpp:34-43.""" + + def test_length_is_16(self): + # rocisa's ``LABEL_NAME_LENGTH = 17`` is misleading -- the + # loop iterates ``LABEL_NAME_LENGTH - 1 == 16`` times. We + # mirror the actual byte count. + for _ in range(50): + self.assertEqual(len(magicGenerator()), 16) + + def test_alphabet_is_uppercase_alnum(self): + # 36-char alphabet: A-Z + 0-9. No lowercase, no punctuation, + # no whitespace. + allowed = set(string.ascii_uppercase + string.digits) + for _ in range(50): + chars = set(magicGenerator()) + self.assertTrue( + chars.issubset(allowed), + msg=f"unexpected chars: {chars - allowed}", + ) + + def test_uniqueness_in_a_small_batch(self): + # 50 draws from a 36^16 space -- collision probability is + # astronomically low (~1e-22). Any collision indicates the + # RNG is broken. + batch = {magicGenerator() for _ in range(50)} + self.assertEqual(len(batch), 50) + + +class TestMagicGeneratorMonkeypatch(unittest.TestCase): + """``magicGenerator`` is a module-level free function and must + therefore be monkeypatchable via ``label._MAGIC_CHARS`` -- this is + the lever tests use to pin output to a deterministic value.""" + + def test_can_pin_alphabet(self): + # Patching the alphabet to a single character makes the output + # fully deterministic ("AAAAAAAAAAAAAAAA") -- proves the + # constant is the only source of randomness besides Python's + # global ``random`` state. + with mock.patch.object(_label, "_MAGIC_CHARS", "A"): + self.assertEqual(magicGenerator(), "A" * 16) + + +# =========================================================================== +# LabelManager.addName -- counter management. +# =========================================================================== + + +class TestLabelManagerAddName(unittest.TestCase): + """``addName`` inserts at 0 on first call, then bumps by 1 per + subsequent call. Mirror of label.hpp:54-65.""" + + def test_first_call_inserts_at_zero(self): + lm = LabelManager() + lm.addName("foo") + self.assertEqual(lm._labels, {"foo": 0}) + + def test_second_call_bumps_to_one(self): + lm = LabelManager() + lm.addName("foo") + lm.addName("foo") + self.assertEqual(lm._labels["foo"], 1) + + def test_repeated_calls_increment(self): + lm = LabelManager() + for _ in range(5): + lm.addName("foo") + # First call set 0; remaining 4 bumps brought it to 4. The + # counter is "occurrences MINUS one", NOT raw occurrences. + self.assertEqual(lm._labels["foo"], 4) + + def test_distinct_names_track_independently(self): + lm = LabelManager() + lm.addName("foo") + lm.addName("foo") + lm.addName("bar") + self.assertEqual(lm._labels, {"foo": 1, "bar": 0}) + + +# =========================================================================== +# LabelManager.getName -- non-bumping lookup with insert-on-miss. +# =========================================================================== + + +class TestLabelManagerGetName(unittest.TestCase): + """``getName`` returns ``name`` (count 0) or ``name_``, + inserts on miss but does NOT bump. Mirror of label.hpp:67-74.""" + + def test_first_call_inserts_at_zero_and_returns_bare_name(self): + lm = LabelManager() + self.assertEqual(lm.getName("foo"), "foo") + # Side effect: the missing-key insert MUST persist. + self.assertEqual(lm._labels, {"foo": 0}) + + def test_idempotent_for_existing_name(self): + # Calling ``getName`` twice must NOT bump the counter -- it's + # the non-bumping accessor. ``getNameInc`` is the bumping + # variant. + lm = LabelManager() + lm.getName("foo") + lm.getName("foo") + self.assertEqual(lm._labels["foo"], 0) + + def test_returns_suffixed_name_when_count_nonzero(self): + lm = LabelManager() + lm.addName("foo") + lm.addName("foo") # count is now 1 + self.assertEqual(lm.getName("foo"), "foo_1") + # Another addName brings count to 2. + lm.addName("foo") + self.assertEqual(lm.getName("foo"), "foo_2") + + +# =========================================================================== +# LabelManager.getNameInc -- bumping accessor. +# =========================================================================== + + +class TestLabelManagerGetNameInc(unittest.TestCase): + """``getNameInc`` bumps the counter then returns the qualified + name. Mirror of label.hpp:76-82.""" + + def test_first_call_returns_bare_name(self): + # ``addName`` on a fresh name sets count to 0 -> returns the + # bare name. + lm = LabelManager() + self.assertEqual(lm.getNameInc("foo"), "foo") + self.assertEqual(lm._labels["foo"], 0) + + def test_second_call_returns_underscore_one(self): + lm = LabelManager() + lm.getNameInc("foo") + self.assertEqual(lm.getNameInc("foo"), "foo_1") + self.assertEqual(lm._labels["foo"], 1) + + def test_third_call_returns_underscore_two(self): + lm = LabelManager() + lm.getNameInc("foo") + lm.getNameInc("foo") + self.assertEqual(lm.getNameInc("foo"), "foo_2") + + def test_inc_after_getName_first_returns_underscore_one(self): + # ``getName`` inserts at 0 (returns bare); a subsequent + # ``getNameInc`` bumps to 1 -- the *first* getNameInc on a + # name already-known-to-getName therefore returns ``foo_1``. + # This is the asymmetry between the two accessors. + lm = LabelManager() + lm.getName("foo") + self.assertEqual(lm.getNameInc("foo"), "foo_1") + + +# =========================================================================== +# LabelManager.getNameIndex -- specific-occurrence accessor. +# =========================================================================== + + +class TestLabelManagerGetNameIndex(unittest.TestCase): + """``getNameIndex`` returns the qualified name for a specific + occurrence index, raising ``RuntimeError`` (matching C++ + ``std::runtime_error``) on missing-name or out-of-range index. + Mirror of label.hpp:84-102.""" + + def test_index_zero_returns_bare_name(self): + lm = LabelManager() + lm.addName("foo") + self.assertEqual(lm.getNameIndex("foo", 0), "foo") + + def test_index_within_range_returns_suffixed_name(self): + lm = LabelManager() + for _ in range(3): + lm.addName("foo") # final count: 2 + self.assertEqual(lm.getNameIndex("foo", 1), "foo_1") + self.assertEqual(lm.getNameIndex("foo", 2), "foo_2") + + def test_missing_name_raises_runtime_error(self): + lm = LabelManager() + with self.assertRaises(RuntimeError) as cm: + lm.getNameIndex("never_added", 0) + # Byte-for-byte text parity with rocisa's exception message + # so any consumer that string-matches on it keeps working. + self.assertIn("You have to add a label first", str(cm.exception)) + + def test_index_exceeds_count_raises_runtime_error(self): + lm = LabelManager() + lm.addName("foo") # count is 0 + with self.assertRaises(RuntimeError) as cm: + lm.getNameIndex("foo", 1) + self.assertIn("The index 1 exceeded.", str(cm.exception)) + self.assertIn("(> 0)", str(cm.exception)) + + def test_getNameIndex_does_not_mutate_state(self): + # The method is a pure read -- it must not mutate the + # counter (unlike ``addName`` / ``getNameInc``). + lm = LabelManager() + lm.addName("foo") + lm.addName("foo") # count 1 + before = dict(lm._labels) + lm.getNameIndex("foo", 1) + self.assertEqual(lm._labels, before) + + +# =========================================================================== +# LabelManager.getUniqueName -- magicGenerator-seeded unique names. +# =========================================================================== + + +class TestLabelManagerGetUniqueName(unittest.TestCase): + """``getUniqueName`` loops on ``magicGenerator`` until the + candidate is absent from the registry, then routes through + ``getName`` (which inserts at count 0 and returns the bare + candidate). Mirror of label.hpp:104-114.""" + + def test_returns_a_16_char_name(self): + lm = LabelManager() + name = lm.getUniqueName() + self.assertEqual(len(name), 16) + + def test_inserts_into_registry(self): + # The returned candidate goes through ``getName`` which + # inserts at 0 -- so the registry must contain it post-call. + lm = LabelManager() + name = lm.getUniqueName() + self.assertIn(name, lm._labels) + self.assertEqual(lm._labels[name], 0) + + def test_loops_past_collisions(self): + # Pin ``magicGenerator`` to return a sequence where the + # first two outputs are already in the registry; the third + # is fresh. ``getUniqueName`` must skip the dupes and pick + # the third. + lm = LabelManager() + lm.addName("AAA") + lm.addName("BBB") + outputs = iter(["AAA", "BBB", "CCC"]) + with mock.patch.object(_label, "magicGenerator", lambda: next(outputs)): + name = lm.getUniqueName() + self.assertEqual(name, "CCC") + # ``CCC`` is now tracked at count 0; ``AAA`` / ``BBB`` were + # untouched (still at 0 from the addName calls above). + self.assertEqual(lm._labels["CCC"], 0) + self.assertEqual(lm._labels["AAA"], 0) + + +# =========================================================================== +# LabelManager.getUniqueNamePrefix -- prefix-scoped unique names. +# =========================================================================== + + +class TestLabelManagerGetUniqueNamePrefix(unittest.TestCase): + """``getUniqueNamePrefix(prefix)`` is the readable variant of + ``getUniqueName`` -- builds ``_`` candidates. + Mirror of label.hpp:116-126.""" + + def test_starts_with_prefix(self): + lm = LabelManager() + name = lm.getUniqueNamePrefix("loop") + # Length: 4 (prefix) + 1 (underscore) + 16 (magic) = 21. + self.assertEqual(len(name), 21) + self.assertTrue(name.startswith("loop_")) + + def test_inserts_into_registry(self): + lm = LabelManager() + name = lm.getUniqueNamePrefix("loop") + self.assertIn(name, lm._labels) + self.assertEqual(lm._labels[name], 0) + + def test_loops_past_collisions(self): + # Same shape as TestLabelManagerGetUniqueName.test_loops_ + # past_collisions but with a prefixed candidate space. + lm = LabelManager() + lm.addName("loop_AAA") + outputs = iter(["AAA", "BBB"]) + with mock.patch.object(_label, "magicGenerator", lambda: next(outputs)): + name = lm.getUniqueNamePrefix("loop") + self.assertEqual(name, "loop_BBB") + + +# =========================================================================== +# LabelManager.__deepcopy__ / pickle round-trip. +# =========================================================================== + + +class TestLabelManagerDeepCopy(unittest.TestCase): + """``copy.deepcopy(lm)`` produces an isolated clone seeded with a + COPY of the underlying map. Mirror of label.cpp:54-58.""" + + def test_deepcopy_preserves_counters(self): + lm = LabelManager() + lm.addName("foo") + lm.addName("foo") # count 1 + lm.addName("bar") # count 0 + clone = copy.deepcopy(lm) + self.assertEqual(clone._labels, {"foo": 1, "bar": 0}) + + def test_deepcopy_is_independent(self): + lm = LabelManager() + lm.addName("foo") + clone = copy.deepcopy(lm) + # Mutate clone; original must be untouched. + clone.addName("foo") + clone.addName("baz") + self.assertEqual(lm._labels, {"foo": 0}) + self.assertEqual(clone._labels, {"foo": 1, "baz": 0}) + + def test_deepcopy_empty_manager(self): + clone = copy.deepcopy(LabelManager()) + self.assertEqual(clone._labels, {}) + + +class TestLabelManagerPickle(unittest.TestCase): + """Pickle round-trip uses the 1-tuple ``(dict,)`` shape from + rocisa's ``__getstate__`` / ``__setstate__`` (label.cpp:59-64).""" + + def test_pickle_roundtrip_preserves_counters(self): + lm = LabelManager() + lm.addName("foo") + lm.addName("foo") + lm.addName("bar") + clone = pickle.loads(pickle.dumps(lm)) + self.assertEqual(clone._labels, lm._labels) + + def test_pickle_empty_manager(self): + clone = pickle.loads(pickle.dumps(LabelManager())) + self.assertEqual(clone._labels, {}) + + def test_pickle_state_is_isolated(self): + # ``__getstate__`` must copy the inner dict so post-pickle + # mutation of the live manager does NOT leak into the + # already-serialised bytes. + lm = LabelManager() + lm.addName("foo") + state = lm.__getstate__() + lm.addName("foo") # bump live to 1 + # State tuple must still reflect the pre-bump snapshot. + self.assertEqual(state, ({"foo": 0},)) + + def test_pickle_roundtrip_independent(self): + lm = LabelManager() + lm.addName("foo") + clone = pickle.loads(pickle.dumps(lm)) + clone.addName("foo") + # Bumping the clone must NOT affect the original (the inner + # dict was copied during __setstate__). + self.assertEqual(lm._labels, {"foo": 0}) + self.assertEqual(clone._labels, {"foo": 1}) + + +# =========================================================================== +# LabelManager misc -- repr, no public getData, public-API shape. +# =========================================================================== + + +class TestLabelManagerRepr(unittest.TestCase): + """``repr`` is adaptor-only convenience (rocisa has none) but it's + additive -- exposing a Python repr does not change observable + behaviour against rocisa.""" + + def test_repr_contains_class_name_and_state(self): + lm = LabelManager() + lm.addName("foo") + r = repr(lm) + self.assertIn("LabelManager", r) + self.assertIn("foo", r) + + +class TestLabelManagerPublicSurface(unittest.TestCase): + """The public Python API matches what rocisa's nanobind binding + exposes (label.cpp:38-64). Anything beyond is a parity break.""" + + def test_no_public_getData_method(self): + # rocisa intentionally does NOT bind ``getData`` to Python -- + # it's only used internally by ``__deepcopy__`` / + # ``__getstate__``. We must mirror that by not having a + # public ``getData`` either. + self.assertFalse(hasattr(LabelManager(), "getData")) + + def test_no_dict_taking_ctor(self): + # rocisa's nanobind binding only exposes ``nb::init<>()`` (no + # arg). The dict-taking ctor is internal. ``LabelManager`` + # must reject any positional arg. + with self.assertRaises(TypeError): + LabelManager({"foo": 0}) + + +# =========================================================================== +# Integration scenarios -- representative KernelWriter usage shapes. +# =========================================================================== + + +class TestLabelManagerScenarios(unittest.TestCase): + """End-to-end scenarios that mirror typical KernelWriter call + sequences -- exercises the accessors in combination.""" + + def test_loop_label_collision_disambiguation(self): + # Two nested loops both want "LoopTop"; the manager + # disambiguates the second occurrence by suffix. + lm = LabelManager() + outer = lm.getNameInc("LoopTop") + inner = lm.getNameInc("LoopTop") + self.assertEqual(outer, "LoopTop") + self.assertEqual(inner, "LoopTop_1") + + def test_get_name_then_get_name_index_round_trip(self): + # Caller emits 3 occurrences of "foo" then asks for each by + # index -- the i-th occurrence must round-trip via + # ``getNameIndex(name, i)``. + lm = LabelManager() + emitted = [lm.getNameInc("foo") for _ in range(3)] + self.assertEqual(emitted, ["foo", "foo_1", "foo_2"]) + for i, expected in enumerate(emitted): + self.assertEqual(lm.getNameIndex("foo", i), expected) + + def test_deepcopy_after_use_independent(self): + # Real-world parallel-map worker: clone a populated manager, + # both sides keep using their own copy. Mutations must NOT + # cross. + lm = LabelManager() + for _ in range(3): + lm.addName("base") # count 2 + worker = copy.deepcopy(lm) + worker.addName("base") # worker count 3 + worker.addName("worker_only") # worker count 0 + self.assertEqual(lm._labels, {"base": 2}) + self.assertEqual(worker._labels, {"base": 3, "worker_only": 0}) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_macro.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_macro.py new file mode 100644 index 000000000000..b5dec27f6452 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_macro.py @@ -0,0 +1,111 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Standalone tests for ``rocisa_stinkytofu_adaptor.macro``. + +Run from any working directory: + + python3 projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_macro.py + +Or with pytest: + + pytest projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_macro.py +""" + +from __future__ import annotations + +import os +import sys +import unittest + +# --------------------------------------------------------------------------- +# Self-contained sys.path bootstrap (mirrors test_functions.py). +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor import macro as _macro # noqa: E402 +from rocisa_stinkytofu_adaptor.code import Macro, Module # noqa: E402 + + +MACRO_PUBLIC_EXPORTS: tuple[str, ...] = ( + "MacroVMagicDiv", + "PseudoRandomGenerator", + "VMagicDiv", + "PseudoRandomGeneratorModule", +) + + +class TestMacroModuleExports(unittest.TestCase): + def test_all_symbols_exported(self): + for name in MACRO_PUBLIC_EXPORTS: + with self.subTest(name=name): + self.assertTrue(hasattr(_macro, name), + f"macro.{name} missing") + + def test_all_are_callable(self): + for name in MACRO_PUBLIC_EXPORTS: + with self.subTest(name=name): + self.assertTrue(callable(getattr(_macro, name))) + + +class TestMacroVMagicDiv(unittest.TestCase): + def test_algo1_returns_macro(self): + result = _macro.MacroVMagicDiv(1) + self.assertIsInstance(result, Macro) + text = result.toString() + self.assertIn(".macro V_MAGIC_DIV", text) + self.assertIn("v_mul_hi_u32", text) + self.assertIn("v_mul_lo_u32", text) + self.assertIn("v_lshrrev_b64", text) + self.assertIn(".endm", text) + + def test_algo2_returns_macro(self): + result = _macro.MacroVMagicDiv(2) + self.assertIsInstance(result, Macro) + text = result.toString() + self.assertIn("v_mul_hi_u32", text) + self.assertIn("v_mul_lo_u32", text) + self.assertIn("v_add_nc_u32", text) + self.assertIn("v_lshrrev_b32", text) + + +class TestVMagicDiv(unittest.TestCase): + def test_algo1_returns_module(self): + from rocisa_stinkytofu_adaptor.container import vgpr, sgpr + result = _macro.VMagicDiv(1, 10, vgpr(5), sgpr(2), sgpr(3), sgpr(4)) + self.assertIsInstance(result, Module) + text = result.toString() + self.assertIn("v_mul_hi_u32", text) + self.assertIn("v_lshrrev_b64", text) + + def test_algo2_returns_module(self): + from rocisa_stinkytofu_adaptor.container import vgpr, sgpr + result = _macro.VMagicDiv(2, 10, vgpr(5), sgpr(2), sgpr(3), sgpr(4)) + self.assertIsInstance(result, Module) + text = result.toString() + self.assertIn("v_add_nc_u32", text) + self.assertIn("v_lshrrev_b32", text) + + +class TestPseudoRandomGenerator(unittest.TestCase): + def test_returns_macro(self): + result = _macro.PseudoRandomGenerator() + self.assertIsInstance(result, Macro) + text = result.toString() + self.assertIn(".macro PRND_GENERATOR", text) + self.assertIn("v_xor_b32", text) + self.assertIn("v_mul_u32_u24", text) + + def test_module_form(self): + result = _macro.PseudoRandomGeneratorModule(0, 1, 2, 3) + self.assertIsInstance(result, Module) + text = result.toString() + self.assertIn("v_xor_b32", text) + self.assertIn("v_mul_u32_u24", text) + self.assertIn("PRND_GENERATOR end", text) + + +if __name__ == "__main__": + unittest.main() diff --git a/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_register.py b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_register.py new file mode 100644 index 000000000000..56e8d34cf738 --- /dev/null +++ b/projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_register.py @@ -0,0 +1,486 @@ +# Copyright Advanced Micro Devices, Inc., or its affiliates. +# SPDX-License-Identifier: MIT +"""Standalone tests for ``rocisa_stinkytofu_adaptor.register.RegisterPool``. + +Run from any working directory: + + python3 projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_register.py + +Or with pytest if available: + + pytest projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_register.py + +The tests assert behaviors that Tensile's KernelWriter implicitly depends on +(AMDGPU ABI, byte-for-byte parity with rocisa's allocator, etc.). Treat any +failure here as a regression that will silently corrupt generated asm. +""" + +from __future__ import annotations + +import copy +import io +import os +import sys +import unittest +from contextlib import redirect_stdout + +# --------------------------------------------------------------------------- +# Self-contained sys.path bootstrap so the test runs without any install / +# editable-mode setup. The ``rocisa_stinkytofu_adaptor`` Python package +# lives at: +# projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/rocisa_stinkytofu_adaptor/ +# This file lives at: +# projects/hipblaslt/tensilelite/rocisa_stinkytofu_adaptor/tests/test_register.py +# So the package's parent directory (where ``import +# rocisa_stinkytofu_adaptor`` resolves) is one level up. +# --------------------------------------------------------------------------- +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PKG_PARENT = os.path.normpath(os.path.join(_HERE, "..")) +if _PKG_PARENT not in sys.path: + sys.path.insert(0, _PKG_PARENT) + +from rocisa_stinkytofu_adaptor.enum import RegisterType # noqa: E402 +from rocisa_stinkytofu_adaptor.register import RegisterPool # noqa: E402 + + +# --------------------------------------------------------------------------- +# Status / Register inner types +# --------------------------------------------------------------------------- + + +class TestStatusEnum(unittest.TestCase): + """The Status enum values are wire-compatible with the rocisa C++ enum.""" + + def test_values_match_rocisa(self): + self.assertEqual(int(RegisterPool.Status.Unavailable), 0) + self.assertEqual(int(RegisterPool.Status.Available), 1) + self.assertEqual(int(RegisterPool.Status.InUse), 2) + + def test_attached_to_pool_class(self): + self.assertTrue(hasattr(RegisterPool, "Status")) + self.assertTrue(hasattr(RegisterPool, "Register")) + + +class TestRegisterRecord(unittest.TestCase): + def test_construct_and_repr(self): + r = RegisterPool.Register(RegisterPool.Status.Available, "tagX") + self.assertEqual(r.status, RegisterPool.Status.Available) + self.assertEqual(r.tag, "tagX") + self.assertIn("tagX", repr(r)) + + +# --------------------------------------------------------------------------- +# Construction +# --------------------------------------------------------------------------- + + +class TestConstruction(unittest.TestCase): + def test_empty_pool(self): + p = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=True) + self.assertEqual(p.size(), 0) + self.assertEqual(p.available(), 0) + + def test_initial_size_marks_unavailable(self): + p = RegisterPool(4, RegisterType.Vgpr, defaultPreventOverflow=False) + self.assertEqual(p.size(), 4) + self.assertEqual(p.available(), 0) + for r in p.getPool(): + self.assertEqual(r.status, RegisterPool.Status.Unavailable) + self.assertEqual(r.tag, "init") + + +# --------------------------------------------------------------------------- +# AMDGPU ABI: KernArgAddress sequence (the assertion that motivates Phase 2) +# --------------------------------------------------------------------------- + + +class TestKernArgAddressABI(unittest.TestCase): + """Tensile's _initKernel asserts SGPR0 == KernArgAddress. + + Reproduces the exact call sequence at KernelWriter.py:7456-7466. + """ + + def test_kernarg_address_at_index_0(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=True) + + # KernArgAddress is 2 sgprs (64-bit pointer), no alignment requirement + # at this call site; preventOverflow=False allows tail-grow. + idx = pool.checkOutAligned(2, 1, "KernArgAddress", preventOverflow=False) + self.assertEqual(idx, 0) + self.assertEqual(pool.size(), 2) + + def test_initial_sgpr_layout(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=True) + + idx_kern = pool.checkOutAligned(2, 1, "KernArgAddress", preventOverflow=False) + idx_wg0 = pool.checkOutAligned(1, 1, "WorkGroup0", preventOverflow=False) + idx_wg1 = pool.checkOutAligned(1, 1, "WorkGroup1", preventOverflow=False) + + self.assertEqual(idx_kern, 0) + self.assertEqual(idx_wg0, 2) + self.assertEqual(idx_wg1, 3) + self.assertEqual(pool.size(), 4) + + +# --------------------------------------------------------------------------- +# checkOut / checkOutAligned algorithm +# --------------------------------------------------------------------------- + + +class TestCheckOutAligned(unittest.TestCase): + def test_zero_size_raises(self): + pool = RegisterPool(4, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 4) + with self.assertRaises(ValueError): + pool.checkOutAligned(0, 1, "x") + + def test_first_fit_within_existing_pool(self): + pool = RegisterPool(8, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 8) + self.assertEqual(pool.available(), 8) + + idx = pool.checkOutAligned(2, 1, "a") + self.assertEqual(idx, 0) + self.assertEqual(pool.available(), 6) + + def test_alignment_grows_with_padding(self): + """alignment=4 forces a 4-aligned start, padding intermediate slots + with Available so subsequent allocs can reuse them.""" + pool = RegisterPool(0, RegisterType.Vgpr, defaultPreventOverflow=False) + # Take 3 slots first so the pool is at size=3 with [0,1,2] InUse. + pool.checkOutAligned(1, 1, "x", preventOverflow=False) + pool.checkOutAligned(1, 1, "y", preventOverflow=False) + pool.checkOutAligned(1, 1, "z", preventOverflow=False) + self.assertEqual(pool.size(), 3) + + # Now ask for 4 vgprs aligned to 4: must start at idx 4 (not 3). + idx = pool.checkOutAligned(4, 4, "valuC", preventOverflow=False) + self.assertEqual(idx, 4) + # pool[3] is the padding slot kept as Available for future reuse. + self.assertEqual(pool._pool[3].status, RegisterPool.Status.Available) + # pool[4..7] are the actual InUse slots. + for k in range(4, 8): + self.assertEqual(pool._pool[k].status, RegisterPool.Status.InUse) + + def test_reuse_after_checkin(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + a = pool.checkOutAligned(2, 1, "A", preventOverflow=False) + b = pool.checkOutAligned(2, 1, "B", preventOverflow=False) + self.assertEqual(a, 0) + self.assertEqual(b, 2) + + pool.checkIn(a) + c = pool.checkOutAligned(2, 1, "C", preventOverflow=False) + # First-fit must reclaim the freed [0,1]. + self.assertEqual(c, 0) + self.assertEqual(pool._pool[0].tag, "C") + + def test_skip_inuse_blocks(self): + """A run shorter than size should be skipped over by the i+=j hack.""" + pool = RegisterPool(8, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 8) + # Make a 2-slot hole at [3,4] by allocating non-contiguous tmps. + pool._pool[2].status = RegisterPool.Status.InUse + pool._pool[2].tag = "blocker" + pool._pool[5].status = RegisterPool.Status.InUse + pool._pool[5].tag = "blocker" + # Available now: [0,1] [3,4] [6,7]; ask for 3 contiguous. + idx = pool.checkOutAligned(3, 1, "needs3") + # Cannot fit at 0,1,2 (2 is blocked); cannot fit at 3,4,5 (5 blocked); + # must land at 6,7 + tail-grow 1. + self.assertEqual(idx, 6) + self.assertEqual(pool.size(), 9) + + def test_prevent_overflow_raises(self): + pool = RegisterPool(2, RegisterType.Sgpr, defaultPreventOverflow=True) + pool.add(0, 2) + # Available = 2; ask for 4 with preventOverflow inherited from default. + with self.assertRaises(RuntimeError): + pool.checkOutAligned(4, 1, "x") + + def test_explicit_prevent_overflow_overrides_default(self): + """preventOverflow=False (the int 0, passed by Tensile's defineSgpr) + must permit tail-grow even when defaultPreventOverflow=True. + + rocisa's tail-walk loop is ``for (i = size-1; i > 0; --i)`` — index 0 + is intentionally NOT visited, so a 2-slot all-Available pool grown by + 4 lands at start=1 and ends at size=5 (NOT 0 / 4). We mirror this + quirk 1:1 to keep byte-for-byte parity with the nanobind backend. + """ + pool = RegisterPool(2, RegisterType.Sgpr, defaultPreventOverflow=True) + pool.add(0, 2) + idx = pool.checkOutAligned(4, 1, "x", preventOverflow=False) + self.assertEqual(idx, 1) + self.assertEqual(pool.size(), 5) + # pool[0] remains Available (untouched by the tail-walk). + self.assertEqual(pool._pool[0].status, RegisterPool.Status.Available) + + +# --------------------------------------------------------------------------- +# checkIn lifecycle +# --------------------------------------------------------------------------- + + +class TestCheckIn(unittest.TestCase): + def test_checkin_releases_slots(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + idx = pool.checkOutAligned(3, 1, "tmp", preventOverflow=False) + for k in range(idx, idx + 3): + self.assertEqual(pool._pool[k].status, RegisterPool.Status.InUse) + + pool.checkIn(idx) + for k in range(idx, idx + 3): + self.assertEqual(pool._pool[k].status, RegisterPool.Status.Available) + + def test_checkin_unknown_warns_does_not_raise(self): + pool = RegisterPool(4, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 4) + buf = io.StringIO() + with redirect_stdout(buf): + pool.checkIn(99) # never checked out + self.assertIn("never checked out", buf.getvalue()) + + +# --------------------------------------------------------------------------- +# checkOutMulti +# --------------------------------------------------------------------------- + + +class TestCheckOutMulti(unittest.TestCase): + def test_partition_lump_sum(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + idx_vec = pool.checkOutMulti([2, 1, 3], 1, ["A", "B", "C"]) + self.assertEqual(idx_vec, [0, 2, 3]) + self.assertEqual(pool._pool[0].tag, "A") + self.assertEqual(pool._pool[1].tag, "A") + self.assertEqual(pool._pool[2].tag, "B") + self.assertEqual(pool._pool[3].tag, "C") + self.assertEqual(pool._pool[5].tag, "C") + + # Each partition should be independently checkInable. + pool.checkIn(2) + self.assertEqual(pool._pool[2].status, RegisterPool.Status.Available) + self.assertEqual(pool._pool[3].status, RegisterPool.Status.InUse) + + def test_length_mismatch_raises(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + with self.assertRaises(ValueError): + pool.checkOutMulti([1, 2], 1, ["only one tag"]) + + +# --------------------------------------------------------------------------- +# add / remove / addRange +# --------------------------------------------------------------------------- + + +class TestAddRemove(unittest.TestCase): + def test_add_marks_available(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 4, "manual") + self.assertEqual(pool.size(), 4) + self.assertEqual(pool.available(), 4) + for r in pool.getPool(): + self.assertEqual(r.status, RegisterPool.Status.Available) + self.assertEqual(r.tag, "manual") + + def test_addRange_returns_string(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + self.assertEqual(pool.addRange(2, 5, "rr"), "2-5") + self.assertEqual(pool.addRange(7, 7, "single"), "7") + + def test_remove_flips_to_unavailable(self): + pool = RegisterPool(4, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 4) + pool.remove(1, 2, "rm") + states = [r.status for r in pool.getPool()] + self.assertEqual(states[0], RegisterPool.Status.Available) + self.assertEqual(states[1], RegisterPool.Status.Unavailable) + self.assertEqual(states[2], RegisterPool.Status.Unavailable) + self.assertEqual(states[3], RegisterPool.Status.Available) + + +# --------------------------------------------------------------------------- +# Free-state toggle: addFromCheckOut / removeFromCheckOut +# --------------------------------------------------------------------------- + + +class TestFreeStateToggle(unittest.TestCase): + """Used by KernelWriter.freeSgprVarPool to temporarily lend out a slot + while keeping the name binding intact.""" + + def test_round_trip(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + idx = pool.checkOutAligned(2, 1, "A", preventOverflow=False) + self.assertEqual(pool._pool[idx].status, RegisterPool.Status.InUse) + + pool.addFromCheckOut(idx) + # Slots become Available; bookkeeping moved to Temp side. + self.assertEqual(pool._pool[idx].status, RegisterPool.Status.Available) + self.assertNotIn(idx, pool._checkOutSize) + self.assertIn(idx, pool._checkOutSizeTemp) + + pool.removeFromCheckOut(idx) + # Back to InUse, with the original tag restored. + self.assertEqual(pool._pool[idx].status, RegisterPool.Status.InUse) + self.assertEqual(pool._pool[idx].tag, "A") + self.assertIn(idx, pool._checkOutSize) + self.assertNotIn(idx, pool._checkOutSizeTemp) + + def test_addFromCheckOut_unknown_raises(self): + pool = RegisterPool(4, RegisterType.Sgpr, defaultPreventOverflow=False) + with self.assertRaises(RuntimeError): + pool.addFromCheckOut(0) + + +# --------------------------------------------------------------------------- +# checkFinalState +# --------------------------------------------------------------------------- + + +class TestCheckFinalState(unittest.TestCase): + def test_clean_state_passes(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + idx = pool.checkOutAligned(2, 1, "tmp", preventOverflow=False) + pool.checkIn(idx) + pool.checkFinalState() # must not raise + + def test_dirty_state_raises(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.checkOutAligned(2, 1, "leaked", preventOverflow=False) + with self.assertRaises(RuntimeError): + pool.checkFinalState() + + +# --------------------------------------------------------------------------- +# availableBlock* introspection +# --------------------------------------------------------------------------- + + +class TestAvailability(unittest.TestCase): + def test_available_count(self): + pool = RegisterPool(8, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 8) + self.assertEqual(pool.available(), 8) + pool.checkOutAligned(3, 1, "x") + self.assertEqual(pool.available(), 5) + + def test_available_block_at_end(self): + pool = RegisterPool(8, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 8) + pool.checkOutAligned(3, 1, "x") # idx 0..2 InUse + # Tail is 8-3 = 5 contiguous Available slots. + self.assertEqual(pool.availableBlockAtEnd(), 5) + + def test_available_block_aligned(self): + pool = RegisterPool(8, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.add(0, 8) + # Available [0..7]; ask blocks of size 4 aligned to 4. + # 4-aligned starts: 0, 4. Two blocks of 4 fit. Total = 8. + self.assertEqual(pool.availableBlock(4, 4), 8) + + +# --------------------------------------------------------------------------- +# Pool growth helpers +# --------------------------------------------------------------------------- + + +class TestPoolGrowth(unittest.TestCase): + def test_appendPool_adds_available(self): + pool = RegisterPool(2, RegisterType.Sgpr, defaultPreventOverflow=False) + self.assertEqual(pool.size(), 2) + pool.appendPool(6) + self.assertEqual(pool.size(), 6) + # Original 2 stay Unavailable; appended 4 are Available. + for r in pool.getPool()[:2]: + self.assertEqual(r.status, RegisterPool.Status.Unavailable) + for r in pool.getPool()[2:]: + self.assertEqual(r.status, RegisterPool.Status.Available) + self.assertEqual(r.tag, "append pool") + + def test_appendPool_smaller_is_noop(self): + pool = RegisterPool(4, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.appendPool(2) + self.assertEqual(pool.size(), 4) + + +# --------------------------------------------------------------------------- +# Occupancy limits +# --------------------------------------------------------------------------- + + +class TestOccupancyLimit(unittest.TestCase): + def test_set_and_reset(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.setOccupancyLimit(maxSize=10, size=8) + self.assertEqual(pool._occupancyLimitSize, 8) + self.assertEqual(pool._occupancyLimitMaxSize, 10) + pool.resetOccupancyLimit() + self.assertEqual(pool._occupancyLimitSize, 0) + self.assertEqual(pool._occupancyLimitMaxSize, 0) + + +# --------------------------------------------------------------------------- +# deepcopy +# --------------------------------------------------------------------------- + + +class TestDeepCopy(unittest.TestCase): + def test_deepcopy_independent_state(self): + pool = RegisterPool(0, RegisterType.Sgpr, defaultPreventOverflow=False) + pool.checkOutAligned(2, 1, "A", preventOverflow=False) + pool.checkOutAligned(1, 1, "B", preventOverflow=False) + + clone = copy.deepcopy(pool) + self.assertEqual(clone.size(), pool.size()) + self.assertEqual(clone.available(), pool.available()) + self.assertEqual( + [r.status for r in clone.getPool()], + [r.status for r in pool.getPool()], + ) + + # Mutate clone; original must not change. + clone.checkOutAligned(2, 1, "C", preventOverflow=False) + self.assertNotEqual(clone.size(), pool.size()) + + +# --------------------------------------------------------------------------- +# Tensile-realistic mini scenario: replay the head of _initKernel +# --------------------------------------------------------------------------- + + +class TestTensileInitKernelHead(unittest.TestCase): + """Simulates KernelWriter.py:7383 + 7456-7466 to ensure ABI parity.""" + + def test_replay(self): + sgprPool = RegisterPool( + 0, RegisterType.Sgpr, defaultPreventOverflow=True + ) + sgprs = {} + + def defineSgpr(name, n, align=1): + idx = sgprPool.checkOutAligned( + n, align, tag=name, preventOverflow=False + ) + sgprs[name] = idx + return idx + + defineSgpr("KernArgAddress", 2) # rpga = 2 (64-bit pointer) + defineSgpr("WorkGroup0", 1) + defineSgpr("WorkGroup1", 1) + defineSgpr("ArgType", 1) + defineSgpr("StaggerU", 1) + defineSgpr("WGM", 1) + + # AMDGPU ABI: kernarg pointer must live in SGPR0. + self.assertEqual(sgprs["KernArgAddress"], 0) + # The rest are packed densely with no padding. + self.assertEqual(sgprs["WorkGroup0"], 2) + self.assertEqual(sgprs["WorkGroup1"], 3) + self.assertEqual(sgprs["ArgType"], 4) + self.assertEqual(sgprs["StaggerU"], 5) + self.assertEqual(sgprs["WGM"], 6) + self.assertEqual(sgprPool.size(), 7) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/shared/stinkytofu/.clang-tidy b/shared/stinkytofu/.clang-tidy index 3c65f61762d1..7270fc265d77 100644 --- a/shared/stinkytofu/.clang-tidy +++ b/shared/stinkytofu/.clang-tidy @@ -30,6 +30,11 @@ # Excluded performance checks: # -performance-enum-size (too noisy, low value) # +# Excluded clang-analyzer checks: +# -clang-analyzer-core.NullDereference (false positive in nanobind try_cast templates) +# -clang-analyzer-optin.performance.Padding +# -clang-analyzer-optin.core.EnumCastOutOfRange +# # Not included (style enforcement, not bug-finding): # readability-*, modernize-*, google-*, fuchsia-* Checks: > @@ -45,6 +50,7 @@ Checks: > -bugprone-string-literal-with-embedded-nul, -bugprone-macro-parentheses, clang-analyzer-*, + -clang-analyzer-core.NullDereference, -clang-analyzer-optin.performance.Padding, -clang-analyzer-optin.core.EnumCastOutOfRange, misc-*, diff --git a/shared/stinkytofu/hardware/src/gfx/Gfx1250/Gfx1250Instructions.def b/shared/stinkytofu/hardware/src/gfx/Gfx1250/Gfx1250Instructions.def index 6ec057d60c56..fcc9db2396f8 100644 --- a/shared/stinkytofu/hardware/src/gfx/Gfx1250/Gfx1250Instructions.def +++ b/shared/stinkytofu/hardware/src/gfx/Gfx1250/Gfx1250Instructions.def @@ -142,7 +142,7 @@ DEF_BATCH(.format = FLAT_ATOMIC, FlatAtomicPkAddF16Inst, "flat_atomic_pk_add_f16", FlatAtomicPkAddBf16Inst, "flat_atomic_pk_add_bf16", FlatAtomicIncU32Inst, "flat_atomic_inc_u32", - FlatAtomicDecU32Inst, "flat_atomic_dec_u32", + FlatAtomicDecU32Inst, "flat_atomic_dec_u32", .logical = "FlatAtomicDecU32", FlatAtomicMinF64Inst, "flat_atomic_min_num_f64", .operand_fields = { {D0, .size=64}, {S1, .size=64} }, FlatAtomicMaxF64Inst, "flat_atomic_max_num_f64", @@ -743,6 +743,7 @@ DEF_T(VSwapB32Inst, "v_swap_b32", .format = VOP1, .logical = "VSwapB32", // v_movrelsd_2_b32: like v_mov_b32 but the dst VGPR index is offset by M0 at // runtime (indirect VGPR write). DEF_T(VMovRelsD2B32Inst, "v_movrelsd_2_b32", .format = VOP1, + .logical = "VMovRelsD2B32", .operand_fields = { {D0, vdst, vgpr, 32}, {S0, src0, vgpr, 32}, @@ -1216,6 +1217,7 @@ DEF_T(VCvtScalePk8F16Fp8Inst, "v_cvt_scale_pk8_f16_fp8", // Pack-of-8 conversion (8x F32 -> 8x FP8/BF8) with F32 scale. DEF_T(VCvtScalef32Pk8Fp8F32Inst, "v_cvt_scalef32_pk8_fp8_f32", .format = VOP3, + .logical = "VCvtScalePk8F32toFP8", .operand_fields = { {D0, vdst, vgpr, 64}, {S0, src0, src_vgpr, 256}, @@ -1224,6 +1226,7 @@ DEF_T(VCvtScalef32Pk8Fp8F32Inst, "v_cvt_scalef32_pk8_fp8_f32", ) DEF_T(VCvtScalef32Pk8Bf8F32Inst, "v_cvt_scalef32_pk8_bf8_f32", .format = VOP3, + .logical = "VCvtScalePk8F32toBF8", .operand_fields = { {D0, vdst, vgpr, 64}, {S0, src0, src_vgpr, 256}, @@ -1233,6 +1236,7 @@ DEF_T(VCvtScalef32Pk8Bf8F32Inst, "v_cvt_scalef32_pk8_bf8_f32", // Stochastic-rounding pack-of-8 conversion (8x F32 -> 8x FP8) with F32 scale. DEF_T(VCvtScalef32SrPk8Fp8F32Inst, "v_cvt_scalef32_sr_pk8_fp8_f32", .format = VOP3, + .logical = "VCvtScaleSRPkF32toFP8", .operand_fields = { {D0, vdst, vgpr, 64}, {S0, src0, src_vgpr, 256}, diff --git a/shared/stinkytofu/include/stinkytofu/bindings/python/LogicalModule.hpp b/shared/stinkytofu/include/stinkytofu/bindings/python/LogicalModule.hpp index 48a76fec4840..b3da1310b676 100644 --- a/shared/stinkytofu/include/stinkytofu/bindings/python/LogicalModule.hpp +++ b/shared/stinkytofu/include/stinkytofu/bindings/python/LogicalModule.hpp @@ -33,6 +33,47 @@ namespace stinkytofu { // Forward declarations class LogicalInstruction; +/// Entry for a .set directive to be emitted inline with instructions. +/// @c position is the instruction index before which the directive is inserted. +/// @c order is the global insertion sequence used to interleave with other entry types. +struct SetDirectiveEntry { + size_t position; + size_t order; + std::string symbol; + std::string value; +}; + +/// Entry for a label to be emitted inline with instructions. +/// @c position is the instruction index before which the label is inserted. +/// @c order is the global insertion sequence used to interleave with other entry types. +struct LabelEntry { + size_t position; + size_t order; + std::string labelName; + uint16_t alignment; + std::string comment; +}; + +/// Entry for a textblock (comment/raw text) to be emitted inline with instructions. +/// @c position is the instruction index before which the textblock is inserted. +/// @c order is the global insertion sequence used to interleave with other entry types. +struct TextBlockEntry { + size_t position; + size_t order; + std::string text; +}; + +/// Entry for an instruction-group scope marker. +/// @c position is the instruction count at the time the marker was recorded. +/// @c order is the global insertion sequence used to interleave with other entry types. +/// @c isBegin == true marks the start of a named group scope; false marks the end. +struct GroupMarkerEntry { + size_t position; + size_t order; + std::string name; + bool isBegin; +}; + // ======================================================================== // PYTHON-SPECIFIC MODULE - Can be removed when Python bindings are deprecated // ======================================================================== @@ -118,6 +159,72 @@ class STINKYTOFU_EXPORT PyLogicalModule { */ std::shared_ptr add(std::shared_ptr inst); + /** + * @brief Record a .set directive to be emitted inline at the current position. + * + * The directive will appear in the output immediately before the next + * instruction added after this call, preserving source ordering. + * + * @param symbol Symbol name (e.g. "vgprBase") + * @param value Value expression (e.g. "516" or "vgprBase+0") + */ + void addSetDirective(const std::string& symbol, const std::string& value); + + /** + * @brief Get all recorded .set directive entries (position-tagged). + */ + const std::vector& getSetDirectives() const; + + /** + * @brief Record a label to be emitted inline at the current position. + * + * @param labelName Full label name (e.g. "label_0042") + * @param alignment Alignment (1 = no .align prefix) + * @param comment Optional comment + */ + void addLabel(const std::string& labelName, uint16_t alignment = 1, + const std::string& comment = ""); + + /** + * @brief Get all recorded label entries (position-tagged). + */ + const std::vector& getLabels() const; + + /** + * @brief Record a textblock (comment / raw asm text) at the current position. + * + * @param text Raw text content (e.g. a block comment string) + */ + void addTextBlock(const std::string& text); + + /** + * @brief Get all recorded textblock entries (position-tagged). + */ + const std::vector& getTextBlocks() const; + + /** + * @brief Mark the beginning of a named instruction-group scope. + * + * All instructions added between a matching beginGroup/endGroup pair + * are considered part of the named group. The C++ lowering pipeline + * uses these markers to populate StinkyAsmModule instruction-group + * ranges (mirroring the native toStinkyTofuModule behaviour). + * + * @param name Group name (e.g. "noLoadLoopBody") + */ + void beginGroup(const std::string& name); + + /** + * @brief Mark the end of a named instruction-group scope. + * @param name Group name (must match a preceding beginGroup call) + */ + void endGroup(const std::string& name); + + /** + * @brief Get all recorded group marker entries (position-tagged). + */ + const std::vector& getGroupMarkers() const; + /** * @brief Get all IR instructions in this module (const version) * @return Vector of IR instructions (shared ownership) diff --git a/shared/stinkytofu/include/stinkytofu/ir/asm/StinkySignature.hpp b/shared/stinkytofu/include/stinkytofu/ir/asm/StinkySignature.hpp index 9e6be20fb55c..d6a4066e933e 100644 --- a/shared/stinkytofu/include/stinkytofu/ir/asm/StinkySignature.hpp +++ b/shared/stinkytofu/include/stinkytofu/ir/asm/StinkySignature.hpp @@ -232,8 +232,10 @@ struct SrdUpperValue125X : public BitfieldUnion { std::string desc() const override; }; -// Factory function to create appropriate SRD value based on ISA version -std::shared_ptr createSrdUpperValue(const std::array& isa); +// Factory function to create appropriate SRD value based on ISA version. +// Exported for the _stinkytofu.so Python module (libstinkytofu hides all +// symbols by default; see shared/stinkytofu/CMakeLists.txt). +STINKYTOFU_EXPORT std::shared_ptr createSrdUpperValue(const std::array& isa); /*************************************** * Signature Argument diff --git a/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalInstructionData.hpp b/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalInstructionData.hpp index 74390c213bf4..ad5629052679 100644 --- a/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalInstructionData.hpp +++ b/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalInstructionData.hpp @@ -124,6 +124,32 @@ struct SMFMAData { // Control Flow // ======================================================================== +/** + * @brief Data for SWaitAlu instructions (logical IR) + * + * Carries the 7 optional dependency counter fields. + * A field value of -1 means "not specified" (omit from emitted instruction). + */ +struct SWaitAluLogicalData { + int va_vdst; + int va_sdst; + int va_ssrc; + int hold_cnt; + int vm_vsrc; + int va_vcc; + int sa_sdst; + + SWaitAluLogicalData(int va_vdst_ = -1, int va_sdst_ = -1, int va_ssrc_ = -1, int hold_cnt_ = -1, + int vm_vsrc_ = -1, int va_vcc_ = -1, int sa_sdst_ = -1) + : va_vdst(va_vdst_), + va_sdst(va_sdst_), + va_ssrc(va_ssrc_), + hold_cnt(hold_cnt_), + vm_vsrc(vm_vsrc_), + va_vcc(va_vcc_), + sa_sdst(sa_sdst_) {} +}; + /** * @brief Data for Label instructions (logical IR) * Note: Different from asm::LabelData which is a modifier diff --git a/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalInstructions.hpp b/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalInstructions.hpp index 928e601cddf2..395430b75338 100644 --- a/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalInstructions.hpp +++ b/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalInstructions.hpp @@ -84,9 +84,11 @@ class LogicalInstruction : public IRBase { std::string comment; ///< Optional comment // Instruction modifiers (optional, only used by specific instructions) - std::optional dpp; ///< Data parallel processing modifier - std::optional sdwa; ///< Sub-dword addressing modifier - std::optional ds; ///< LDS/GDS modifier + std::optional dpp; ///< Data parallel processing modifier + std::optional sdwa; ///< Sub-dword addressing modifier + std::optional ds; ///< LDS/GDS modifier + std::optional mubuf; ///< MUBUF (buffer load/store) modifier + std::optional vop3; ///< VOP3P (op_sel) modifier /// LLVM-style casting support static bool classof(const IRBase* ir) { @@ -285,6 +287,20 @@ class LogicalInstruction : public IRBase { return (opcode_ == logical::SMFMA) ? static_cast(specialData_) : nullptr; } + /** + * @brief Get SWaitAlu data (returns nullptr if not SWaitAlu) + */ + SWaitAluLogicalData* asSWaitAlu() { + return (opcode_ == logical::SWaitAlu) ? static_cast(specialData_) + : nullptr; + } + + const SWaitAluLogicalData* asSWaitAlu() const { + return (opcode_ == logical::SWaitAlu) + ? static_cast(specialData_) + : nullptr; + } + /** * @brief Get Label data (returns nullptr if not Label) */ @@ -334,6 +350,12 @@ class LogicalInstruction : public IRBase { case logical::IntrinsicCall: delete static_cast(specialData_); break; + case logical::SWaitAlu: + delete static_cast(specialData_); + break; + case logical::SchedulingFence: + // No special data for SchedulingFence + break; default: // No special data for regular instructions break; diff --git a/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalOpcode.hpp b/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalOpcode.hpp index 65c8a99fc918..616c2d756c70 100644 --- a/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalOpcode.hpp +++ b/shared/stinkytofu/include/stinkytofu/ir/logical/LogicalOpcode.hpp @@ -64,6 +64,8 @@ enum Opcode : uint16_t { SMFMA, Label, IntrinsicCall, + SWaitAlu, + SchedulingFence, NUM_OPCODES // Sentinel value }; diff --git a/shared/stinkytofu/include/stinkytofu/transforms/logical/CompositeInstructionLoweringPass.hpp b/shared/stinkytofu/include/stinkytofu/transforms/logical/CompositeInstructionLoweringPass.hpp index 8d9cf6f6a1f8..bfa27d0fb5f0 100644 --- a/shared/stinkytofu/include/stinkytofu/transforms/logical/CompositeInstructionLoweringPass.hpp +++ b/shared/stinkytofu/include/stinkytofu/transforms/logical/CompositeInstructionLoweringPass.hpp @@ -49,6 +49,22 @@ class Pass; * - If supported: Single v_lshl_or_b32 instruction * - Fallback: v_lshlrev_b32 + v_or_b32 * + * - SAddU64: + * - If supported: Single s_add_u64 instruction + * - Fallback: s_add_u32 + s_addc_u32 (carry via SCC) + * + * - VAddNCU64: + * - If supported: Single v_add_nc_u64 instruction + * - Fallback: v_add_co_u32 + v_add_co_ci_u32 (carry via VCC) + * + * - VAddLShiftLeftU32: + * - If supported: Single v_add_lshl_u32 instruction + * - Fallback: v_add_u32 + v_lshlrev_b32 + * + * - VLShiftLeftAddU32: + * - If supported: Single v_lshl_add_u32 instruction + * - Fallback: v_lshlrev_b32 + v_add_u32 + * * This pass runs BEFORE ToStinkyAsmPass and operates on IRList, * expanding composite instructions in-place. * diff --git a/shared/stinkytofu/include/stinkytofu/transforms/logical/LowerLogicalModulePipeline.hpp b/shared/stinkytofu/include/stinkytofu/transforms/logical/LowerLogicalModulePipeline.hpp new file mode 100644 index 000000000000..ff6ea4acc498 --- /dev/null +++ b/shared/stinkytofu/include/stinkytofu/transforms/logical/LowerLogicalModulePipeline.hpp @@ -0,0 +1,95 @@ +/* ************************************************************************ + * Copyright (C) 2025-2026 Advanced Micro Devices, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * ************************************************************************ */ +#pragma once + +#include +#include + +#include "stinkytofu/Export.hpp" +#include "stinkytofu/bindings/python/Module.hpp" + +namespace stinkytofu { +class Function; +class PyLogicalModule; +struct GemmTileConfig; + +/** + * @brief Run the standard logical-IR lowering pipeline on a Function in-place. + * + * Equivalent to the inline boilerplate that already appears in many unit tests + * (e.g. LogicalToAsmPipelineTest, RegisterWidthValidationTest): + * + * @code + * PassManager pm; + * pm.setGemmTileConfig(config); + * pm.addPass(createCompositeInstructionLoweringPass()); + * pm.addPass(createToStinkyAsmPass()); + * pm.run(func); + * @endcode + * + * After this returns, every reachable LogicalInstruction in @p func has been + * replaced with a StinkyInstruction (asm IR), and the Function is ready to be + * emitted via StinkyAsmModule::emitAssembly() or fed into the asm-side + * Backend optimization pipeline. + * + * @param func Function to lower (mutated in place). + * @param config GemmTileConfig used by the passes. @c config.arch must be set; + * tile / wave fields can be left zero for trivial bring-up cases. + */ +STINKYTOFU_EXPORT void runLogicalLoweringPipeline(Function& func, const GemmTileConfig& config); + +/** + * @brief One-shot helper: build a StinkyAsmModule from a Python-side + * PyLogicalModule by running the standard logical-IR lowering pipeline. + * + * This is the "left-path" sibling of + * @c stinkytofu::toStinkyTofuModule() (which converts the right path, + * rocisa::Module → StinkyAsmModule). It is the recommended entry point for + * KernelWriter-style code that builds logical IR in Python and just wants a + * StinkyAsmModule it can call @c emitAssembly() on. + * + * Steps performed internally: + * 1. Construct a fresh StinkyAsmModule (which already owns an "entry" block). + * 2. Append the externally-owned LogicalInstruction* nodes from + * @p module into that entry block. + * 3. Run @c runLogicalLoweringPipeline() on the underlying Function. + * 4. Detach any LogicalInstruction nodes whose lifetime is owned by Python + * (so the C++ IRList does not delete them when the StinkyAsmModule dies). + * + * The caller must keep @p module alive for as long as the returned + * StinkyAsmModule is in use, mirroring the lifetime contract of + * @c PyLogicalFunction. + * + * @param module Source PyLogicalModule (Python-side high-level IR). + * @param arch Target GPU architecture [major, minor, stepping]. + * @param moduleOptions Module-level options propagated to the asm module and + * used to populate the GemmTileConfig for the lowering + * passes. Defaults to a zero-initialized options struct + * suitable for trivial vertical-slice tests. + * @return shared_ptr to the produced StinkyAsmModule. + */ +STINKYTOFU_EXPORT std::shared_ptr lowerLogicalModuleToAsm( + PyLogicalModule& module, std::array arch, + const StinkyAsmModule::ModuleOptions& moduleOptions = {}); + +} // namespace stinkytofu diff --git a/shared/stinkytofu/python_module/src/HardwareCaps.cpp b/shared/stinkytofu/python_module/src/HardwareCaps.cpp index d7ce8af99165..62fdfc0c5aa9 100644 --- a/shared/stinkytofu/python_module/src/HardwareCaps.cpp +++ b/shared/stinkytofu/python_module/src/HardwareCaps.cpp @@ -183,12 +183,29 @@ std::map initAsmCaps(const IsaVersion& v, const MnemonicMap& m "buffer_load_dwordx4 v[10:13], v[0], s[0:3], null offen offset:0, scope:SCOPE_DEV"}); rv["HasNTModifier"] = tryAsm(isaName, ws, "buffer_load_dwordx4 v[10:13], v[0], s[0:3], 0, offen offset:0, nt"); + // gfx1250 temporal-hint (th:) and non-volatile (nv) modifiers replace the + // legacy nt bit on buffer/global/flat memory ops. + rv["HasTHModifier"] = tryAsmAny( + isaName, ws, + {"buffer_load_dwordx4 v[10:13], v[0], s[0:3], 0 offen offset:0 th:TH_LOAD_NT", + "buffer_load_dwordx4 v[10:13], v[0], s[0:3], null offen offset:0 th:TH_LOAD_NT"}); + rv["HasNVModifier"] = + tryAsmAny(isaName, ws, + {"buffer_load_dwordx4 v[10:13], v[0], s[0:3], 0 offen offset:0 nv", + "buffer_load_dwordx4 v[10:13], v[0], s[0:3], null offen offset:0 nv"}); + rv["HasGlobalPrefetch"] = + tryAsm(isaName, ws, "global_prefetch_b8 v[0:1], off scope:SCOPE_SE th:TH_LOAD_NT"); rv["HasMUBUFConst"] = tryAsmAny(isaName, ws, {"buffer_load_dword v40, v36, s[24:27], 1 offen offset:0", "buffer_load_b32 v40, v36, s[24:27], 1 offen offset:0"}); rv["HasSCMPK"] = hasMnemonic(m, "s_cmpk_gt_u32"); rv["HasNewBarrier"] = hasMnemonic(m, "s_barrier_wait"); + rv["HasClusterBarrier"] = tryAsm(isaName, ws, "s_barrier_wait -3"); + rv["HasWMMA_AccImmZero"] = + tryAsm(isaName, ws, "v_wmma_f32_16x16x32_bf16 v[0:7], v[8:15], v[8:15], 0"); + rv["s_add_u64"] = tryAsm(isaName, ws, "s_add_u64 s[0:1], s[0:1], s[2:3]"); + rv["v_add_nc_u64"] = tryAsm(isaName, ws, "v_add_nc_u64 v[0:1], v[2:3], v[4:5]"); rv["HasTDM"] = hasMnemonic(m, "tensor_load_to_lds"); rv["s_delay_alu"] = hasMnemonic(m, "s_delay_alu"); @@ -251,6 +268,7 @@ std::map initArchCaps(const IsaVersion& v) { rv["HasSchedMode"] = checkMajorIn(v[0], {12}); rv["HasAccCD"] = checkInList(v, {{9, 0, 10}, {9, 4, 2}, {9, 5, 0}}); rv["ArchAccUnifiedRegs"] = checkInList(v, {{9, 0, 10}, {9, 4, 2}, {9, 5, 0}}); + rv["MaxWavesPerSimd"] = rv["ArchAccUnifiedRegs"] ? 8 : 10; rv["CrosslaneWait"] = checkInList(v, {{9, 4, 2}, {9, 5, 0}}); rv["TransOpWait"] = checkInList(v, {{9, 4, 2}, {9, 5, 0}, {12, 5, 0}}); rv["SDWAWait"] = checkInList(v, {{9, 4, 2}, {9, 5, 0}, {12, 5, 0}}); @@ -310,6 +328,8 @@ std::map initRegCaps(const IsaVersion& v, rv["PhysicalMaxVgprCU"] = 0; } + rv["GlobalPrefetchSize"] = 256; + return rv; } @@ -337,6 +357,12 @@ CacheEntry g_hwcapsCache[kMaxArchs]; } // namespace HardwareCapsResult HardwareCaps::query(uint32_t major, uint32_t minor, uint32_t stepping) { + // Resolve by ISA tuple first. ``getGfxArchID`` asserts on unknown triples; + // callers such as ``caps.getCaps`` must get an empty result instead of + // aborting the Python interpreter. + const auto* info = ArchHelper::getInstance().getArchInfo(major, minor, stepping); + if (!info) return {}; + auto archID = getGfxArchID(major, minor, stepping); auto idx = static_cast(archID); if (idx >= kMaxArchs) return {}; diff --git a/shared/stinkytofu/python_module/src/python_bindings.cpp b/shared/stinkytofu/python_module/src/python_bindings.cpp index f311669a854e..8edc72e76956 100644 --- a/shared/stinkytofu/python_module/src/python_bindings.cpp +++ b/shared/stinkytofu/python_module/src/python_bindings.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -40,11 +41,13 @@ #include "stinkytofu/hardware/GfxIsa.hpp" #include "stinkytofu/hardware/ToolchainCaps.hpp" #include "stinkytofu/ir/asm/StinkyAsmIR.hpp" +#include "stinkytofu/ir/asm/StinkySignature.hpp" #include "stinkytofu/ir/logical/IntrinsicCall.hpp" #include "stinkytofu/ir/logical/IntrinsicLibrary.hpp" #include "stinkytofu/ir/logical/IntrinsicRegistry.hpp" #include "stinkytofu/ir/logical/LogicalInstructions.hpp" #include "stinkytofu/pipeline/BackendRegistry.hpp" +#include "stinkytofu/transforms/logical/LowerLogicalModulePipeline.hpp" namespace nb = nanobind; using namespace stinkytofu; @@ -104,6 +107,48 @@ NB_MODULE(_stinkytofu, m) { .value("InnerRegionEnd", PipelineExtensionPoint::InnerRegionEnd) .value("AfterRegionPasses", PipelineExtensionPoint::AfterRegionPasses); + // ------------------------------------------------------------------------ + // Bind CloneSpec so Python can construct entries for ModuleOptions::CloneList. + // ------------------------------------------------------------------------ + nb::class_(m, "CloneSpec") + .def(nb::init(), nb::arg("name"), nb::arg("startLabel")) + .def_rw("name", &CloneSpec::name) + .def_rw("startLabel", &CloneSpec::startLabel); + + // ------------------------------------------------------------------------ + // Logical-IR -> Asm-IR one-shot lowering helper + // ------------------------------------------------------------------------ + // Left-path entry point for KernelWriter-style Python code: build a + // LogicalModule, then call lower_logical_module(...) to get back a + // StinkyAsmModule ready for .emitAssembly() / .runOptimizationPipeline(). + // Mirrors the right path (toStinkyTofuModule for rocisa::Module). The + // returned StinkyAsmModule borrows the LogicalInstructions from @p module + // for IR-list membership but does NOT own them: keep @p module alive for + // as long as the returned asm module is in use. + m.def( + "lower_logical_module", + [](PyLogicalModule& module, std::array arch, const nb::object& options_obj) { + StinkyAsmModule::ModuleOptions moduleOptions{}; + moduleOptions.SwPrefetchScratchSgpr = -1; + if (nb::isinstance(options_obj)) { + nb::dict options = nb::cast(options_obj); +#define SET_MODULE_OPTION_LLM(name, type) \ + if (options.contains(#name)) nb::try_cast(options[#name], moduleOptions.name); + MODULE_OPTIONS_LIST(SET_MODULE_OPTION_LLM) +#undef SET_MODULE_OPTION_LLM + } + return lowerLogicalModuleToAsm(module, arch, moduleOptions); + }, + nb::arg("module"), nb::arg("arch"), nb::arg("options") = nb::none(), + "Run the standard logical-IR lowering pipeline on @p module and " + "return the resulting StinkyAsmModule. @p arch is " + "[major, minor, stepping] (e.g. [12, 5, 0] for gfx1250) and is used " + "to look up the per-arch logical-to-asm mnemonic map. Keep @p module " + "alive while using the returned StinkyAsmModule (it borrows " + "LogicalInstruction nodes from it for IR-list membership). " + "Optional @p options dict sets ModuleOptions fields (same keys as " + "toStinkyTofuModule: CloneList, OptLevel, wavefrontSize, etc.)."); + // ======================================================================== // Register Types // ======================================================================== @@ -111,6 +156,7 @@ NB_MODULE(_stinkytofu, m) { .value("V", RegType::V, "Vector Register (VGPR)") .value("S", RegType::S, "Scalar Register (SGPR)") .value("A", RegType::A, "Accumulator Register (AGPR)") + .value("M", RegType::M, "Memory descriptor register (MGPR)") .value("ACC", RegType::ACC, "Accumulator Register (alternative)") .value("AGPR", RegType::AGPR, "Accumulator GPR") .value("VCC", RegType::VCC, "Vector Condition Code") @@ -130,11 +176,37 @@ NB_MODULE(_stinkytofu, m) { // We need to expose StinkyRegister so nanobind can convert it properly. // However, users typically don't construct Register directly - they use helper functions. nb::class_(m, "Register") + // --- Constructors --------------------------------------------------- .def(nb::init<>(), "Create a null register") - .def(nb::init(), nb::arg("type"), nb::arg("index"), - nb::arg("count") = 1, "Create a register (e.g., Register('v', 0, 1) for v0)") + // Reg ctor with validation: silently accepting unknown type strings (the + // raw nb::init binding) lets typos like Register("vgprFoo", 0, 1) build + // a UNKNOWN-typed register that fails far away from the call site. + .def( + "__init__", + [](StinkyRegister* self, const std::string& type, uint32_t index, uint16_t count) { + if (!isValidRegTypeString(type)) { + throw nb::value_error(("Register: unknown register type '" + type + + "'; expected one of v/s/a/m/... (see RegisterType.def)") + .c_str()); + } + new (self) StinkyRegister(type, index, count); + }, + nb::arg("type"), nb::arg("index"), nb::arg("count") = 1, + "Create a register (e.g., Register('v', 0, 1) for v0). Raises on unknown type.") .def(nb::init(), nb::arg("value"), "Create a float literal") .def(nb::init(), nb::arg("value"), "Create an int literal") + // Single-string ctor → LiteralString. Used for keywords like MUBUF "off". + // Distinct from the (type,index,count) overload by arg count. + .def( + "__init__", + [](StinkyRegister* self, const std::string& literal_string) { + new (self) StinkyRegister(literal_string); + }, + nb::arg("literal_string"), + "Create a literal string register (e.g., Register('off') for the MUBUF " + "'off' keyword). Stored as LiteralString.") + + // --- Type / identity properties ------------------------------------ .def_prop_ro( "reg_type", [](const StinkyRegister& r) -> RegType { @@ -143,7 +215,7 @@ NB_MODULE(_stinkytofu, m) { } return RegType::UNKNOWN; }, - "Get the register type (V, S, A, etc.)") + "Get the register type (V, S, A, etc.). UNKNOWN for non-Register values.") .def_prop_ro( "index", [](const StinkyRegister& r) -> int { @@ -152,7 +224,7 @@ NB_MODULE(_stinkytofu, m) { } return -1; }, - "Get the register index") + "Get the register index (-1 for non-Register values).") .def_prop_ro( "count", [](const StinkyRegister& r) -> int { @@ -161,14 +233,162 @@ NB_MODULE(_stinkytofu, m) { } return 0; }, - "Get the register count (number of consecutive registers)") + "Get the register count (number of consecutive registers).") + .def_prop_ro( + "is_register", + [](const StinkyRegister& r) { return r.dataType == StinkyRegister::Type::Register; }, + "True iff this carries a register reference (not a literal).") .def_prop_ro( "is_literal", [](const StinkyRegister& r) -> bool { return r.dataType == StinkyRegister::Type::LiteralInt || - r.dataType == StinkyRegister::Type::LiteralDouble; + r.dataType == StinkyRegister::Type::LiteralDouble || + r.dataType == StinkyRegister::Type::LiteralString; + }, + "True iff this is any literal (int/double/string).") + .def_prop_ro( + "is_literal_string", + [](const StinkyRegister& r) { + return r.dataType == StinkyRegister::Type::LiteralString; + }, + "True iff this is a LiteralString (e.g., MUBUF 'off' keyword).") + .def_prop_ro( + "literal_string", + [](const StinkyRegister& r) -> std::optional { + if (r.dataType == StinkyRegister::Type::LiteralString) { + return r.literalValue; + } + return std::nullopt; + }, + "Get the literal string content, or None if not a LiteralString.") + + // --- RegName accessors (rocisa-style symbolic name carrier) -------- + // stinkytofu's literalValue field doubles as the symbolic name slot for + // Register-typed values. We marshal rocisa's (name, offsets[]) struct + // to a "name+1+2+..." string at the binding boundary so stinkytofu's + // existing IR transforms (e.g. LegalizationUtils::adjustSymbolicRegName) + // can act on it without learning a new struct type. + .def( + "set_reg_name", + [](StinkyRegister& r, const std::string& name, const std::vector& offsets) { + std::string full = name; + for (int o : offsets) { + full += "+" + std::to_string(o); + } + r.setSymbolicName(full); + }, + nb::arg("name"), nb::arg("offsets") = std::vector{}, + "Attach a rocisa-style RegName (name + offsets) to this register. " + "Stored on `literalValue`; encoded as 'name+offset1+offset2+...'.") + .def( + "get_reg_name", + [](const StinkyRegister& r) -> std::pair> { + std::string s = r.getSymbolicName(); + std::vector offsets; + if (s.empty()) { + return {"", offsets}; + } + auto pos = s.find('+'); + std::string base = (pos == std::string::npos) ? s : s.substr(0, pos); + while (pos != std::string::npos) { + auto next = s.find('+', pos + 1); + std::string token = s.substr( + pos + 1, next == std::string::npos ? std::string::npos : next - pos - 1); + offsets.push_back(std::stoi(token)); + pos = next; + } + return {base, offsets}; + }, + "Read the attached RegName as (name, offsets[]). Returns ('', []) when no name set.") + .def( + "has_reg_name", [](const StinkyRegister& r) { return r.hasSymbolicName(); }, + "Whether this register currently carries a symbolic RegName.") + .def( + "clear_reg_name", [](StinkyRegister& r) { r.setSymbolicName(""); }, + "Detach any RegName, leaving the numeric (type,index,count) identity.") + + // --- Modifier flags (isMinus / isAbs) ------------------------------ + // C++ already stores isMinus/isAbs as 1-bit fields on reg; we just + // expose Python toggles. Both setters are no-ops on literals. + .def_prop_ro( + "is_minus", + [](const StinkyRegister& r) -> bool { + return r.dataType == StinkyRegister::Type::Register && r.reg.isMinus; + }, + "Whether the `-` (negate) modifier is set (`-v0`).") + .def( + "set_minus", + [](StinkyRegister& r, bool value) { + if (r.dataType == StinkyRegister::Type::Register) { + r.reg.isMinus = value ? 1u : 0u; + } + }, + nb::arg("value"), "Toggle the `-` modifier. No-op on literals.") + .def_prop_ro( + "is_abs", + [](const StinkyRegister& r) -> bool { + return r.dataType == StinkyRegister::Type::Register && r.reg.isAbs; + }, + "Whether the `abs(...)` modifier is set (`abs(v0)`).") + .def( + "set_abs", + [](StinkyRegister& r, bool value) { + if (r.dataType == StinkyRegister::Type::Register) { + r.reg.isAbs = value ? 1u : 0u; + } + }, + nb::arg("value"), "Toggle the `abs(...)` modifier. No-op on literals.") + + // --- MSB offset (VGPR bank selector for idx >= 256) ----------------- + .def_prop_ro( + "offset", + [](const StinkyRegister& r) -> int { + return r.dataType == StinkyRegister::Type::Register ? r.reg.offset : 0; + }, + "The register offset (e.g. -512 for VGPR MSB bank 2).") + .def( + "set_offset", + [](StinkyRegister& r, int value) { + if (r.dataType == StinkyRegister::Type::Register) { + r.reg.offset = static_cast(value); + } + }, + nb::arg("value"), + "Set the register offset for MSB encoding (e.g. -512 = bank 2). No-op on literals.") + + // --- Hash / equality (KernelWriter uses Registers as dict keys) ---- + .def("__hash__", + [](const StinkyRegister& r) -> Py_hash_t { return static_cast(r.hash()); }) + .def( + "__eq__", + [](const StinkyRegister& r, const nb::object& other) -> bool { + if (!nb::isinstance(other)) { + return false; + } + return r == nb::cast(other); }, - "Check if this is a literal value") + nb::arg("other").none(true)) + .def( + "__ne__", + [](const StinkyRegister& r, const nb::object& other) -> bool { + if (!nb::isinstance(other)) { + return true; + } + return r != nb::cast(other); + }, + nb::arg("other").none(true)) + + // --- Copy semantics (KernelWriter does copy.deepcopy on registers) - + // StinkyRegister is value-like (trivially copyable for the union; the + // std::string literalValue copies fine). Same impl for both shallow + // and deep copy because there are no nested references. + .def("__copy__", [](const StinkyRegister& r) { return StinkyRegister(r); }) + .def( + "__deepcopy__", + [](const StinkyRegister& r, nb::handle /*memo*/) { return StinkyRegister(r); }, + nb::arg("memo")) + + // --- Debug repr ----------------------------------------------------- .def("__repr__", [](const StinkyRegister& r) -> std::string { if (r.dataType == StinkyRegister::Type::Register) { std::string typeStr; @@ -186,16 +406,27 @@ NB_MODULE(_stinkytofu, m) { typeStr = "?"; break; } + std::string body; if (r.reg.num == 1) { - return ""; + body = typeStr + std::to_string(r.reg.idx); } else { - return ""; + body = typeStr + "[" + std::to_string(r.reg.idx) + ":" + + std::to_string(r.reg.idx + r.reg.num - 1) + "]"; + } + std::string mods; + if (r.reg.isMinus) mods += " -"; + if (r.reg.isAbs) mods += " abs"; + std::string name; + if (!r.literalValue.empty()) { + name = " name=" + r.literalValue; } + return ""; } else if (r.dataType == StinkyRegister::Type::LiteralInt) { return ""; } else if (r.dataType == StinkyRegister::Type::LiteralDouble) { return ""; + } else if (r.dataType == StinkyRegister::Type::LiteralString) { + return ""; } return std::string(""); }); @@ -220,6 +451,10 @@ NB_MODULE(_stinkytofu, m) { nb::arg("index"), nb::arg("count") = 1, "Create an accumulator VGPR register (alias for agpr)"); + m.def( + "mgpr", [](int index, int count) { return StinkyRegister("m", index, count); }, + nb::arg("index"), nb::arg("count") = 1, "Create an MGPR (Memory descriptor) register"); + m.def( "literal", [](float value) { return StinkyRegister(value); }, nb::arg("value"), "Create a float literal"); @@ -277,6 +512,13 @@ NB_MODULE(_stinkytofu, m) { .value("MSB8", VgprMsbMode::Msb8) .value("MSB16", VgprMsbMode::Msb16); + nb::enum_(m, "MUBUFScope") + .value("SCOPE_NONE", MUBUFScope::SCOPE_NONE) + .value("SCOPE_CU", MUBUFScope::SCOPE_CU) + .value("SCOPE_SE", MUBUFScope::SCOPE_SE) + .value("SCOPE_DEV", MUBUFScope::SCOPE_DEV) + .value("SCOPE_SYS", MUBUFScope::SCOPE_SYS); + // ======================================================================== // PyLogicalModule - Python-Specific High-Level IR Container // ======================================================================== @@ -286,6 +528,18 @@ NB_MODULE(_stinkytofu, m) { "Create a new IR module with the given kernel name") .def("add", &PyLogicalModule::add, nb::arg("instruction"), "Add a high-level IR instruction to the module (shared ownership)") + .def("add_set_directive", &PyLogicalModule::addSetDirective, nb::arg("symbol"), + nb::arg("value"), + "Record a .set directive at the current position in the instruction stream") + .def("add_label", &PyLogicalModule::addLabel, nb::arg("label_name"), + nb::arg("alignment") = 1, nb::arg("comment") = "", + "Record a label at the current position in the instruction stream") + .def("add_textblock", &PyLogicalModule::addTextBlock, nb::arg("text"), + "Record a textblock (comment/raw text) at the current position") + .def("begin_group", &PyLogicalModule::beginGroup, nb::arg("name"), + "Mark the beginning of a named instruction-group scope") + .def("end_group", &PyLogicalModule::endGroup, nb::arg("name"), + "Mark the end of a named instruction-group scope") .def("getName", &PyLogicalModule::getName, "Get the kernel name") .def( "dump", @@ -316,7 +570,41 @@ NB_MODULE(_stinkytofu, m) { inst.dump(oss); return oss.str(); }, - "Dump the instruction to a string"); + "Dump the instruction to a string") + .def( + "set_ds", + [](LogicalInstruction& inst, int na, int offset, int offset0, int offset1, bool gds) { + inst.ds = DSModifiers(na, offset, offset0, offset1, gds); + }, + nb::arg("na") = 1, nb::arg("offset") = 0, nb::arg("offset0") = 0, + nb::arg("offset1") = 0, nb::arg("gds") = false, "Set DS (LDS/GDS) modifiers") + .def( + "set_mubuf", + [](LogicalInstruction& inst, bool offen, int offset, bool glc, bool slc, bool nt, + int scope, int th, bool isStore) { + MUBUFScope mScope = static_cast(scope); + TemporalHint mTh = static_cast(th); + inst.mubuf = MUBUFModifiers( + offen, offset, glc, slc, nt, /*lds=*/false, isStore, /*hasMUBUFConst=*/false, + /*hasGLCModifier=*/false, /*hasSC0Modifier=*/false, mScope, mTh); + }, + nb::arg("offen") = false, nb::arg("offset") = 0, nb::arg("glc") = false, + nb::arg("slc") = false, nb::arg("nt") = false, nb::arg("scope") = 0, nb::arg("th") = -1, + nb::arg("is_store") = false, + "Set MUBUF modifiers (offen, offset, glc, slc, nt, scope, th, is_store)") + .def( + "add_src", + [](LogicalInstruction& inst, const StinkyRegister& reg) { inst.srcs.push_back(reg); }, + nb::arg("reg"), "Add an additional source register operand") + .def( + "set_vop3", + [](LogicalInstruction& inst, const std::vector& op_sel, + const std::vector& op_sel_hi, const std::vector& byte_sel) { + inst.vop3 = VOP3PModifiers(op_sel, op_sel_hi, byte_sel); + }, + nb::arg("op_sel") = std::vector{}, nb::arg("op_sel_hi") = std::vector{}, + nb::arg("byte_sel") = std::vector{}, + "Set VOP3P (op_sel/op_sel_hi/byte_sel) modifiers"); // ======================================================================== // Auto-generated Python bindings for all IR instructions (~273 classes) @@ -374,6 +662,32 @@ NB_MODULE(_stinkytofu, m) { nb::arg("metadata"), nb::arg("neg") = false, nb::arg("comment") = "", "Create an SMFMA instruction"); + // SWaitAlu - Dependency counter wait instruction + m.def( + "SWaitAlu", + [](int va_vdst, int va_sdst, int va_ssrc, int hold_cnt, int vm_vsrc, int va_vcc, + int sa_sdst, const std::string& comment) { + auto* inst = IRBase::createIR(logical::SWaitAlu); + auto* data = new SWaitAluLogicalData(va_vdst, va_sdst, va_ssrc, hold_cnt, vm_vsrc, + va_vcc, sa_sdst); + inst->setSpecialData(data); + inst->comment = comment; + return makeLogicalInstructionShared(inst); + }, + nb::arg("va_vdst") = -1, nb::arg("va_sdst") = -1, nb::arg("va_ssrc") = -1, + nb::arg("hold_cnt") = -1, nb::arg("vm_vsrc") = -1, nb::arg("va_vcc") = -1, + nb::arg("sa_sdst") = -1, nb::arg("comment") = "", "Create an SWaitAlu instruction"); + + // SchedulingFence - Scheduling barrier pseudo-instruction + m.def( + "SchedulingFence", + [](const std::string& comment) { + auto* inst = IRBase::createIR(logical::SchedulingFence); + inst->comment = comment; + return makeLogicalInstructionShared(inst); + }, + nb::arg("comment") = "", "Create a SchedulingFence pseudo-instruction"); + // TensorLoadToLds - Higher-level tensor load operation m.def( "TensorLoadToLds", @@ -492,6 +806,37 @@ NB_MODULE(_stinkytofu, m) { " - string literals (for special values)\n\n" "The intrinsic will be expanded during optimization by IntrinsicExpansionPass."); + // ======================================================================== + // SRD Upper Value (rocisa.code.SrdUpperValue replacement) + // ======================================================================== + // Mirrors rocisa::SrdUpperValue (rocisa/src/code.cpp:56-82). Tensile + // calls SrdUpperValue(IsaVersion) -> BitfieldUnion and immediately + // reads .desc() / .getValue() to embed the SRD upper 32 bits as a + // packed literal in the kernel signature (KernelWriterAssembly.py:1497). + // + // ISA dispatch lives in createSrdUpperValue(); Python receives a thin + // BitfieldUnion handle with the rocisa-shaped surface API. + nb::class_(m, "BitfieldUnion") + .def("__str__", &BitfieldUnion::toString) + .def("getValue", &BitfieldUnion::getValue) + .def("toString", &BitfieldUnion::toString) + .def("desc", &BitfieldUnion::desc); + + m.def( + "SrdUpperValue", + [](const nb::object& isa) { + std::array version{}; + if (nb::isinstance(isa) || nb::isinstance(isa)) { + version = {nb::cast(isa[0]), nb::cast(isa[1]), nb::cast(isa[2])}; + } else { + version = nb::cast>(isa); + } + return createSrdUpperValue(version); + }, + nb::arg("isa"), + "Create the SRD upper-32-bit literal for the given ISA " + "(major, minor, stepping) tuple or 3-element array."); + // ======================================================================== // Architecture support query // ======================================================================== diff --git a/shared/stinkytofu/python_module/stinkytofu/__init__.py b/shared/stinkytofu/python_module/stinkytofu/__init__.py index 9c6b780de959..e2d6c62aec7d 100644 --- a/shared/stinkytofu/python_module/stinkytofu/__init__.py +++ b/shared/stinkytofu/python_module/stinkytofu/__init__.py @@ -117,4 +117,4 @@ def Intrinsic(name, **kwargs): f" Modified: {', '.join(_preview)}\n" " Rebuild: cmake --build --target stinkytofu_python" ) - del _bi, _so, _so_mtime, _stale, _build_dirs, _source_root, Path + del _bi, _so, _so_mtime, _stale, _build_dirs, _source_root, _scan_dirs, Path diff --git a/shared/stinkytofu/python_module/tests/test_comgr.py b/shared/stinkytofu/python_module/tests/test_comgr.py index 9b990983dc01..aad27d670c7f 100644 --- a/shared/stinkytofu/python_module/tests/test_comgr.py +++ b/shared/stinkytofu/python_module/tests/test_comgr.py @@ -217,6 +217,33 @@ def test_has_xcnt(self): if asm["HasXcnt"]: assert asm["MaxXcnt"] == 63 + def test_has_th_modifier(self): + asm = self._caps() + expected = int( + self._asm_any( + "buffer_load_dwordx4 v[10:13], v[0], s[0:3], 0 offen offset:0 th:TH_LOAD_NT", + "buffer_load_dwordx4 v[10:13], v[0], s[0:3], null offen offset:0 th:TH_LOAD_NT", + ) + ) + assert asm["HasTHModifier"] == expected + + def test_has_nv_modifier(self): + asm = self._caps() + expected = int( + self._asm_any( + "buffer_load_dwordx4 v[10:13], v[0], s[0:3], 0 offen offset:0 nv", + "buffer_load_dwordx4 v[10:13], v[0], s[0:3], null offen offset:0 nv", + ) + ) + assert asm["HasNVModifier"] == expected + + def test_has_global_prefetch(self): + asm = self._caps() + expected = int( + self._asm("global_prefetch_b8 v[0:1], off scope:SCOPE_SE th:TH_LOAD_NT") + ) + assert asm["HasGlobalPrefetch"] == expected + class TestArchCaps: """Verify archCaps values for gfx1250 are correct based on ISA version checks.""" diff --git a/shared/stinkytofu/python_module/tests/test_register_bindings.py b/shared/stinkytofu/python_module/tests/test_register_bindings.py new file mode 100644 index 000000000000..c60b04c7b9c2 --- /dev/null +++ b/shared/stinkytofu/python_module/tests/test_register_bindings.py @@ -0,0 +1,501 @@ +"""Tests for the StinkyRegister Python bindings added for the +rocisa→stinkytofu adapter. + +Scope: every surface the rocisa-shaped adapter wrapper needs in order to +back the RegisterContainer + RegName shapes that Tensile KernelWriter +expects. KernelWriter call sites this file pins down: + + * RegisterContainer.setMinus(True) (Components/GSU.py:466) + * RegisterContainer.getMinus() (KernelWriterAssembly.py:8669, + KernelWriterModules.py:313, + Components/GlobalWriteBatch.py:2238...) + * rc.regName truthy / falsy guard (Activation.py:1309,1314) + * rc.regName.addOffset(N) (KernelWriterAssembly.py:11483...) + * rc.regName.setOffset(i, v) (Activation.py:967) + * rc.regName.name access (Activation.py:1467) + * str(rc.regName) (Components/LocalRead.py:486) + * rc.setInlineAsm(True) (Activation.py:1310; wrapper-only, + not delegated to stinkytofu — included + here as the contract the wrapper expects) + * MUBUF "off" keyword (Components/GlobalWriteBatch) + * rc as dict key, rc == other (instruction de-dup, IR transforms) + * copy.deepcopy(rc) (KernelWriter pre-emit cloning) + +Run via: + PYTHONPATH= \ + pytest shared/stinkytofu/python_module/tests/test_register_bindings.py +""" + +import copy +import os +import sys + +import pytest + +# Prefer PYTHONPATH; fall back to the in-tree standalone build only when the +# direct import fails. Inserting an in-tree dir at sys.path[0] unconditionally +# (as test_ir_basic.py historically did) tends to shadow a fresh build that +# the developer staged via PYTHONPATH. +try: + import stinkytofu # noqa: E402 +except ImportError: + sys.path.append(os.path.join(os.path.dirname(__file__), "../../build/lib")) + import stinkytofu # noqa: E402 + +from stinkytofu import Register, RegType, vgpr, sgpr, agpr, accvgpr, mgpr # noqa: E402 + + +# --------------------------------------------------------------------------- +# Constructors +# --------------------------------------------------------------------------- + + +class TestConstructors: + def test_zero_arg_ctor_is_invalid_register(self): + r = Register() + assert not r.is_register + assert not r.is_literal + assert r.reg_type == RegType.UNKNOWN + assert r.index == -1 + assert r.count == 0 + + def test_three_arg_ctor_with_valid_type(self): + r = Register("v", 5, 4) + assert r.is_register + assert r.reg_type == RegType.V + assert r.index == 5 + assert r.count == 4 + + def test_three_arg_ctor_with_invalid_type_raises(self): + """Regression: previously Register('vgprFoo', 0, 1) silently built a + UNKNOWN-typed register and only failed far downstream.""" + with pytest.raises(ValueError, match="unknown register type"): + Register("vgprFoo", 0, 1) + with pytest.raises(ValueError): + Register("notARegType", 0, 1) + + def test_three_arg_ctor_count_default_is_one(self): + r = Register("s", 10) + assert r.count == 1 + + def test_int_literal_ctor(self): + r = Register(42) + assert not r.is_register + assert r.is_literal + assert not r.is_literal_string + + def test_float_literal_ctor(self): + r = Register(3.14) + assert not r.is_register + assert r.is_literal + assert not r.is_literal_string + + def test_single_string_ctor_builds_literal_string(self): + """Used for MUBUF 'off' keyword and similar literal-string operands.""" + r = Register("off") + assert not r.is_register + assert r.is_literal + assert r.is_literal_string + assert r.literal_string == "off" + + def test_factory_functions_still_work(self): + v = vgpr(0, 2) + s = sgpr(7) + a = agpr(3, 4) + assert (v.reg_type, v.index, v.count) == (RegType.V, 0, 2) + assert (s.reg_type, s.index, s.count) == (RegType.S, 7, 1) + assert (a.reg_type, a.index, a.count) == (RegType.A, 3, 4) + + def test_accvgpr_alias_of_agpr(self): + # accvgpr is an alias kept for rocisa-style call sites. + a = accvgpr(3, 4) + b = agpr(3, 4) + assert (a.reg_type, a.index, a.count) == (RegType.A, 3, 4) + assert a == b + + def test_mgpr_helper(self): + # mgpr helper exists for the rocisa "m" register type + # (memory descriptor); required by the adapter's to_stinky() + # path for tensilelite kernels that emit MUBUF/FLAT addr pairs. + m = mgpr(2, 4) + assert m.is_register + assert (m.reg_type, m.index, m.count) == (RegType.M, 2, 4) + + def test_mgpr_default_count(self): + m = mgpr(5) + assert (m.reg_type, m.index, m.count) == (RegType.M, 5, 1) + + +# --------------------------------------------------------------------------- +# Type queries +# --------------------------------------------------------------------------- + + +class TestTypeQueries: + def test_register_is_not_literal(self): + v = vgpr(0) + assert v.is_register and not v.is_literal + + def test_int_literal_classification(self): + r = Register(42) + assert r.is_literal and not r.is_register and not r.is_literal_string + + def test_float_literal_classification(self): + r = Register(3.14) + assert r.is_literal and not r.is_literal_string + + def test_literal_string_classification(self): + r = Register("off") + assert r.is_literal + assert r.is_literal_string + assert not r.is_register + + def test_literal_string_accessor_on_non_literal_string_is_none(self): + assert vgpr(0).literal_string is None + assert Register(42).literal_string is None + assert Register(3.14).literal_string is None + + def test_literal_string_accessor_on_literal_string(self): + assert Register("off").literal_string == "off" + assert Register("inline_asm_blob").literal_string == "inline_asm_blob" + assert Register("").literal_string == "" + + +# --------------------------------------------------------------------------- +# RegName (symbolic name + offsets) — round-trip +# --------------------------------------------------------------------------- + + +class TestRegName: + def test_register_initially_has_no_reg_name(self): + r = vgpr(0) + assert not r.has_reg_name() + assert r.get_reg_name() == ("", []) + + def test_set_and_read_back_simple_name(self): + r = vgpr(0, 4) + r.set_reg_name("vgprLocalWriteAddrA") + assert r.has_reg_name() + assert r.get_reg_name() == ("vgprLocalWriteAddrA", []) + + def test_set_and_read_back_name_with_offsets(self): + r = vgpr(0, 4) + r.set_reg_name("ValuC", [1, 2, 3]) + assert r.has_reg_name() + name, offsets = r.get_reg_name() + assert name == "ValuC" + assert offsets == [1, 2, 3] + + def test_set_with_default_offsets_is_empty_list(self): + r = vgpr(0) + r.set_reg_name("foo") + assert r.get_reg_name() == ("foo", []) + + def test_clear_reg_name(self): + r = vgpr(0) + r.set_reg_name("foo", [1, 2]) + assert r.has_reg_name() + r.clear_reg_name() + assert not r.has_reg_name() + assert r.get_reg_name() == ("", []) + + def test_set_reg_name_overwrites_previous(self): + r = vgpr(0) + r.set_reg_name("foo", [1]) + r.set_reg_name("bar", [2, 3]) + assert r.get_reg_name() == ("bar", [2, 3]) + + def test_reg_name_with_zero_offset(self): + r = vgpr(0) + r.set_reg_name("foo", [0]) + assert r.get_reg_name() == ("foo", [0]) + + def test_reg_name_with_large_offsets(self): + r = vgpr(0) + r.set_reg_name("foo", [256, 1024, 65535]) + assert r.get_reg_name() == ("foo", [256, 1024, 65535]) + + def test_setting_empty_name_is_treated_as_clear(self): + r = vgpr(0) + r.set_reg_name("foo") + r.set_reg_name("") + # setSymbolicName("") leaves literalValue empty → hasSymbolicName()→False + assert not r.has_reg_name() + + +class TestRegNameEncoding: + """The (name, offsets[]) ↔ "name+1+2+..." encoding has to be byte-for-byte + stable so stinkytofu IR transforms (LegalizationUtils::adjustSymbolicRegName) + and adapter consumers interoperate without ambiguity.""" + + def test_str_format_matches_rocisa_layout(self): + # Encoding is "name+offset0+offset1+..."; split between name and + # offsets is on the FIRST '+'. + r = vgpr(0) + r.set_reg_name("ValuC", [1, 2, 3]) + # No raw-string getter, so verify by round-tripping. + assert r.get_reg_name() == ("ValuC", [1, 2, 3]) + + def test_offsets_join_back_correctly_when_first_offset_is_dropped(self): + """KernelWriterAssembly.py:11483 emulates `regName.addOffset(1)` by + appending; verify we can do the same via get→append→set.""" + r = vgpr(0) + r.set_reg_name("foo", [2]) + name, offsets = r.get_reg_name() + offsets.append(1) + r.set_reg_name(name, offsets) + assert r.get_reg_name() == ("foo", [2, 1]) + + +# --------------------------------------------------------------------------- +# Modifier flags +# --------------------------------------------------------------------------- + + +class TestModifiers: + def test_is_minus_default_false(self): + assert vgpr(0).is_minus is False + + def test_is_abs_default_false(self): + assert vgpr(0).is_abs is False + + def test_set_minus_toggles(self): + r = vgpr(0) + r.set_minus(True) + assert r.is_minus is True + r.set_minus(False) + assert r.is_minus is False + + def test_set_abs_toggles(self): + r = sgpr(0) + r.set_abs(True) + assert r.is_abs is True + r.set_abs(False) + assert r.is_abs is False + + def test_minus_and_abs_independent(self): + r = vgpr(0) + r.set_minus(True) + r.set_abs(True) + assert r.is_minus and r.is_abs + r.set_minus(False) + assert not r.is_minus and r.is_abs + + def test_set_minus_on_literal_is_noop(self): + for r in (Register(42), Register(3.14), Register("off")): + r.set_minus(True) + assert r.is_minus is False + + def test_set_abs_on_literal_is_noop(self): + for r in (Register(42), Register(3.14), Register("off")): + r.set_abs(True) + assert r.is_abs is False + + def test_modifiers_survive_set_reg_name(self): + """setting RegName shouldn't trash the modifier bits.""" + r = vgpr(0) + r.set_minus(True) + r.set_abs(True) + r.set_reg_name("foo", [1]) + assert r.is_minus and r.is_abs + assert r.get_reg_name() == ("foo", [1]) + + +# --------------------------------------------------------------------------- +# Hash & equality +# --------------------------------------------------------------------------- + + +class TestHashAndEquality: + def test_equal_registers_hash_equal(self): + a = vgpr(5, 2) + b = vgpr(5, 2) + assert a == b + assert hash(a) == hash(b) + + def test_different_registers_compare_unequal(self): + assert vgpr(0) != vgpr(1) + assert vgpr(0, 1) != vgpr(0, 2) + assert vgpr(0) != sgpr(0) + + def test_can_be_used_as_dict_key(self): + # KernelWriter / stinkytofu IR transforms put registers in dicts to + # track def/use chains. The C++ side already supports it; binding + # has to expose __hash__ + __eq__ for it to work from Python. + d = {} + a = vgpr(0) + d[a] = "alpha" + # Lookup with a freshly-built but equal register + b = vgpr(0) + assert d[b] == "alpha" + + def test_int_literal_equality(self): + assert Register(42) == Register(42) + assert Register(42) != Register(43) + + def test_literal_string_equality(self): + assert Register("off") == Register("off") + assert Register("off") != Register("none") + + def test_register_vs_non_register_comparison_returns_false_not_raises(self): + r = vgpr(0) + # Comparison against unrelated Python types must not raise; KernelWriter + # does heterogeneous comparisons (e.g., in DCE / dedup passes). + assert (r == "vgpr0") is False + assert (r == 42) is False + assert (r == None) is False # noqa: E711 (intentional eq test) + assert r != "vgpr0" + assert r != 42 + + def test_unequal_after_set_minus(self): + a = vgpr(0) + b = vgpr(0) + b.set_minus(True) + # Equality only compares (type, idx, num); modifier bits don't + # participate — `-v0` and `v0` refer to the same SSA register, + # the `-` is an instruction operand modifier. Pin the contract. + assert a == b + assert hash(a) == hash(b) + + +# --------------------------------------------------------------------------- +# Copy / deepcopy +# --------------------------------------------------------------------------- + + +class TestCopy: + def test_copy_produces_equal_but_independent_register(self): + a = vgpr(0) + a.set_minus(True) + a.set_reg_name("foo", [1, 2]) + b = copy.copy(a) + assert b == a + assert b is not a + assert b.is_minus + assert b.get_reg_name() == ("foo", [1, 2]) + + def test_deepcopy_produces_independent_register(self): + a = vgpr(0, 4) + a.set_reg_name("ValuC", [3]) + b = copy.deepcopy(a) + # Mutate the copy; original must not move + b.set_reg_name("ValuC", [3, 5]) + b.set_minus(True) + assert a.get_reg_name() == ("ValuC", [3]) + assert a.is_minus is False + assert b.get_reg_name() == ("ValuC", [3, 5]) + assert b.is_minus is True + + def test_deepcopy_of_literal_string(self): + a = Register("off") + b = copy.deepcopy(a) + assert b == a + assert b.literal_string == "off" + + +# --------------------------------------------------------------------------- +# Scenarios mirroring real KernelWriter usage patterns +# --------------------------------------------------------------------------- + + +class TestKernelWriterScenarios: + """Each test maps to a concrete call site in Tensile (cited in test name). + These are the contracts the rocisa-shaped adapter wrapper relies on; if + any of these break, the wrapper breaks in turn.""" + + def test_get_minus_emulation_via_copy_plus_set_minus(self): + # getMinus() returns a NEW container with isMinus=True; wrapper + # backs this with copy + set_minus on the underlying Register. + # See KernelWriterAssembly.py:8669 (`ar.getMinus()`). + original = vgpr(5) + neg = copy.copy(original) + neg.set_minus(True) + assert original.is_minus is False + assert neg.is_minus is True + + def test_set_minus_on_existing_container(self): + # Components/GSU.py:466 — `m.setMinus(True)` + r = sgpr(0, 2) + r.set_minus(True) + assert r.is_minus is True + + def test_reg_name_truthy_check(self): + # Activation.py:1309 — `if not item.dst.regName:` + plain = vgpr(0) + named = vgpr(0) + named.set_reg_name("ValuC") + assert plain.has_reg_name() is False + assert named.has_reg_name() is True + + def test_reg_name_add_offset_via_round_trip(self): + # KernelWriterAssembly.py:11483 — `new_src.regName.addOffset(1)` + # The adapter RegName wrapper will own .addOffset() and sync to the + # underlying Register via set_reg_name. Verify the round-trip works + # without information loss across multiple mutations. + r = vgpr(0) + r.set_reg_name("ValuC", [0]) + for k in range(5): + name, offsets = r.get_reg_name() + offsets.append(k) + r.set_reg_name(name, offsets) + assert r.get_reg_name() == ("ValuC", [0, 0, 1, 2, 3, 4]) + + def test_reg_name_set_offset_via_round_trip(self): + # Activation.py:967 — `vgpr.regName.setOffset(0, vgprIn)` + r = vgpr(0) + r.set_reg_name("ValuC", [10, 20, 30]) + name, offsets = r.get_reg_name() + offsets[0] = 99 + r.set_reg_name(name, offsets) + assert r.get_reg_name() == ("ValuC", [99, 20, 30]) + + def test_reg_name_name_access(self): + # Activation.py:1467 — `param.regName.name` + r = vgpr(0) + r.set_reg_name("ValuC", [1, 2]) + name, _ = r.get_reg_name() + assert name == "ValuC" + + def test_str_of_reg_name_resembles_rocisa(self): + # Components/LocalRead.py:486 — `str(v0t.regName)` is expected to + # produce "ValuC+1+2"-style strings. The adapter wrapper owns + # __str__; binding only needs to preserve the components so the + # wrapper can format them. + r = vgpr(0) + r.set_reg_name("ValuC", [1, 2]) + name, offsets = r.get_reg_name() + rocisa_style = name + "".join(f"+{o}" for o in offsets) + assert rocisa_style == "ValuC+1+2" + + def test_mubuf_off_keyword(self): + # The no-address MUBUF case (isOff=True flag in RegisterContainer) + # surfaces in stinkytofu as a LiteralString operand. + off = Register("off") + assert off.is_literal_string + assert off.literal_string == "off" + # Wrapper dispatches on is_literal_string to set its own isOff + # flag rather than reading reg_type (which is UNKNOWN here). + assert off.reg_type == RegType.UNKNOWN + + def test_dict_lookup_after_deepcopy(self): + # IR transforms in stinkytofu (e.g., def/use chain) build dicts keyed + # on Register. KernelWriter often passes deep-copied registers; lookup + # must still hit by value, not by identity. + seen = {vgpr(0, 4): "lwA", sgpr(10): "Alpha"} + probe_v = copy.deepcopy(vgpr(0, 4)) + probe_s = copy.deepcopy(sgpr(10)) + assert seen[probe_v] == "lwA" + assert seen[probe_s] == "Alpha" + + def test_clear_reg_name_for_holder_resolution(self): + # RegisterContainer.replaceRegName(srcName, dst:int) resolves a + # holder-style name into a numeric idx and clears regName; the + # adapter wrapper calls clear_reg_name as part of that step. + r = vgpr(0) + r.set_reg_name("HOLDER_NAME", []) + # Wrapper would compute resolvedIdx from the name lookup, then: + r.clear_reg_name() + # Numeric identity preserved, symbolic info gone. + assert not r.has_reg_name() + assert r.reg_type == RegType.V + assert r.index == 0 diff --git a/shared/stinkytofu/src/bindings/python/LogicalModule.cpp b/shared/stinkytofu/src/bindings/python/LogicalModule.cpp index a64be389ef85..e66334e6a31d 100644 --- a/shared/stinkytofu/src/bindings/python/LogicalModule.cpp +++ b/shared/stinkytofu/src/bindings/python/LogicalModule.cpp @@ -30,6 +30,11 @@ namespace stinkytofu { struct PyLogicalModule::Impl { std::string name; std::vector> instructions; + std::vector setDirectives; + std::vector labels; + std::vector textBlocks; + std::vector groupMarkers; + size_t globalOrder = 0; Impl(const std::string& name) : name(name) {} @@ -78,6 +83,48 @@ size_t PyLogicalModule::size() const { return pImpl->instructions.size(); } +void PyLogicalModule::addSetDirective(const std::string& symbol, const std::string& value) { + pImpl->setDirectives.push_back( + SetDirectiveEntry{pImpl->instructions.size(), pImpl->globalOrder++, symbol, value}); +} + +const std::vector& PyLogicalModule::getSetDirectives() const { + return pImpl->setDirectives; +} + +void PyLogicalModule::addLabel(const std::string& labelName, uint16_t alignment, + const std::string& comment) { + pImpl->labels.push_back(LabelEntry{pImpl->instructions.size(), pImpl->globalOrder++, labelName, + alignment, comment}); +} + +const std::vector& PyLogicalModule::getLabels() const { + return pImpl->labels; +} + +void PyLogicalModule::addTextBlock(const std::string& text) { + pImpl->textBlocks.push_back( + TextBlockEntry{pImpl->instructions.size(), pImpl->globalOrder++, text}); +} + +const std::vector& PyLogicalModule::getTextBlocks() const { + return pImpl->textBlocks; +} + +void PyLogicalModule::beginGroup(const std::string& name) { + pImpl->groupMarkers.push_back( + GroupMarkerEntry{pImpl->instructions.size(), pImpl->globalOrder++, name, true}); +} + +void PyLogicalModule::endGroup(const std::string& name) { + pImpl->groupMarkers.push_back( + GroupMarkerEntry{pImpl->instructions.size(), pImpl->globalOrder++, name, false}); +} + +const std::vector& PyLogicalModule::getGroupMarkers() const { + return pImpl->groupMarkers; +} + void PyLogicalModule::dump(std::ostream& out) const { out << "LogicalModule: " << pImpl->name << "\n"; out << "Instructions: " << pImpl->instructions.size() << "\n"; diff --git a/shared/stinkytofu/src/ir/logical/LogicalInstructionDefs.inc b/shared/stinkytofu/src/ir/logical/LogicalInstructionDefs.inc index 5aef20d32965..354e0682f29c 100644 --- a/shared/stinkytofu/src/ir/logical/LogicalInstructionDefs.inc +++ b/shared/stinkytofu/src/ir/logical/LogicalInstructionDefs.inc @@ -667,7 +667,7 @@ {"VCndMaskB32", "VCndMaskB32", - 2, + 3, true, "Vector Other", true, @@ -743,6 +743,16 @@ false, false, false, + "IF_VALU"}, + + {"VMovRelsD2B32", + "VMovRelsD2B32", + 1, + true, + "Vector Move Indirect", + false, + false, + false, "IF_VALU"}, {"VSwapB32", @@ -1553,6 +1563,36 @@ true, true, false, + "IF_VALU"}, + + {"VCvtScalePk8F32toFP8", + "VCvtScalePk8F32toFP8", + 2, + true, + "Vector Conversion", + false, + false, + false, + "IF_VALU"}, + + {"VCvtScalePk8F32toBF8", + "VCvtScalePk8F32toBF8", + 2, + true, + "Vector Conversion", + false, + false, + false, + "IF_VALU"}, + + {"VCvtScaleSRPkF32toFP8", + "VCvtScaleSRPkF32toFP8", + 3, + true, + "Vector Conversion", + false, + false, + false, "IF_VALU"}, {"VCvtPkF32toBF16", @@ -1741,6 +1781,236 @@ false, "IF_DSStore"}, + {"DSLoadB192", + "DSLoadB192", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSLoadB96TrB6", + "DSLoadB96TrB6", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSLoadB64TrB4", + "DSLoadB64TrB4", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSLoadB64TrB8", + "DSLoadB64TrB8", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSLoadB64TrB16", + "DSLoadB64TrB16", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSLoadB128TrB16", + "DSLoadB128TrB16", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSStoreB192", + "DSStoreB192", + 2, + false, + "Memory LDS", + false, + false, + false, + "IF_DSStore"}, + + {"DSStoreB8HID16", + "DSStoreB8HID16", + 2, + false, + "Memory LDS", + false, + false, + false, + "IF_DSStore"}, + + {"DSStoreD16HIB16", + "DSStoreD16HIB16", + 2, + false, + "Memory LDS", + false, + false, + false, + "IF_DSStore"}, + + {"VRcpF64", + "VRcpF64", + 1, + true, + "Vector Transcendental", + true, + true, + false, + "IF_Transcendental|IF_VALU"}, + + {"VCvtF64toU32", + "VCvtF64toU32", + 1, + true, + "Vector Conversion", + true, + true, + false, + "IF_VALU"}, + + {"VCvtU32toF64", + "VCvtU32toF64", + 1, + true, + "Vector Conversion", + true, + true, + false, + "IF_VALU"}, + + {"VCvtPkF32toF16", + "VCvtPkF32toF16", + 2, + true, + "Vector Conversion", + true, + true, + false, + "IF_VALU"}, + + {"PVCvtBF16toFP32", + "PVCvtBF16toFP32", + 1, + true, + "Vector Conversion", + true, + true, + false, + "IF_VALU"}, + + {"SBfeU32", + "SBfeU32", + 2, + true, + "Scalar Arithmetic", + false, + false, + false, + "IF_SALU"}, + + {"VNop", + "VNop", + 0, + false, + "Vector Control", + false, + false, + false, + ""}, + + {"GlobalInv", + "GlobalInv", + 0, + false, + "Memory Cache", + false, + false, + false, + "IF_HasSideEffect"}, + + {"GlobalWb", + "GlobalWb", + 0, + false, + "Memory Cache", + false, + false, + false, + "IF_HasSideEffect"}, + + {"SSleep", + "SSleep", + 1, + false, + "Scalar Control", + false, + false, + false, + ""}, + + {"SSetPrior", + "SSetPrior", + 1, + false, + "Scalar Control", + false, + false, + false, + ""}, + + {"SSetPCB64", + "SSetPCB64", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_Branch"}, + + {"SSwapPCB64", + "SSwapPCB64", + 1, + true, + "Scalar Control", + false, + false, + false, + "IF_Branch"}, + + {"GlobalPrefetchB8", + "GlobalPrefetchB8", + 2, + false, + "Memory Memory", + false, + false, + false, + "IF_GLOBALLoad"}, + {"BufferLoadU8", "BufferLoadU8", 1, @@ -2219,6 +2489,16 @@ false, false, false, + "IF_FLATAtomic"}, + + {"FlatAtomicDecU32", + "FlatAtomicDecU32", + 2, + true, + "Memory Atomic", + false, + false, + false, "IF_FLATAtomic"}, {"SAbsI32", @@ -2241,6 +2521,26 @@ false, "IF_Barrier"}, + {"SNop", + "SNop", + 1, + false, + "Scalar Control", + false, + false, + false, + ""}, + + {"SDelayAlu", + "SDelayAlu", + 1, + false, + "Scalar Control", + false, + false, + false, + ""}, + {"SMaxI32", "SMaxI32", 2, @@ -2765,13 +3065,565 @@ false, "IF_VALU"}, - {"TensorLoadToLds", - "TensorLoadToLds: Loads tensor data to LDS", - 4, + {"SAddU64", + "Scalar 64-bit unsigned add (composite)", + 2, + true, + "Scalar Arithmetic", false, - "Tensor Memory", false, false, + "IF_Commutative|IF_SALU"}, + + {"VAddNCU64", + "Vector 64-bit unsigned add no-carry (composite)", + 2, + true, + "Vector Arithmetic", + true, + true, + false, + "IF_Commutative|IF_VALU"}, + + {"VAddLShiftLeftU32", + "Vector add then left-shift (composite)", + 3, + true, + "Vector Arithmetic", + true, + true, false, - "IF_TENSORLoadToLds", - 2}, + "IF_VALU"}, + + {"VLShiftLeftAddU32", + "Vector left-shift then add (composite)", + 3, + true, + "Vector Arithmetic", + true, + true, + false, + "IF_VALU"}, + + {"TensorLoadToLds", + "TensorLoadToLds: Loads tensor data to LDS", + 4, + false, + "Tensor Memory", + false, + false, + false, + "IF_TENSORLoadToLds", + 2}, + + {"SLoadB32", + "SLoadB32", + 2, + true, + "Memory Scalar", + false, + false, + false, + "IF_SMemLoad"}, + + {"SLoadB64", + "SLoadB64", + 2, + true, + "Memory Scalar", + false, + false, + false, + "IF_SMemLoad"}, + + {"SLoadB128", + "SLoadB128", + 2, + true, + "Memory Scalar", + false, + false, + false, + "IF_SMemLoad"}, + + {"SLoadB256", + "SLoadB256", + 2, + true, + "Memory Scalar", + false, + false, + false, + "IF_SMemLoad"}, + + {"SLoadB512", + "SLoadB512", + 2, + true, + "Memory Scalar", + false, + false, + false, + "IF_SMemLoad"}, + + {"SStoreB32", + "SStoreB32", + 3, + false, + "Memory Scalar", + false, + false, + false, + "IF_SMemStore"}, + + {"SStoreB64", + "SStoreB64", + 3, + false, + "Memory Scalar", + false, + false, + false, + "IF_SMemStore"}, + + {"SStoreB128", + "SStoreB128", + 3, + false, + "Memory Scalar", + false, + false, + false, + "IF_SMemStore"}, + + {"SStoreB256", + "SStoreB256", + 3, + false, + "Memory Scalar", + false, + false, + false, + "IF_SMemStore"}, + + {"SStoreB512", + "SStoreB512", + 3, + false, + "Memory Scalar", + false, + false, + false, + "IF_SMemStore"}, + + {"SAtomicInc", + "SAtomicInc", + 2, + true, + "Memory Scalar", + false, + false, + false, + "IF_SMemAtomic"}, + + {"SAtomicDec", + "SAtomicDec", + 1, + true, + "Memory Scalar", + false, + false, + false, + "IF_SMemAtomic"}, + + {"SCSelectB64", + "SCSelectB64", + 2, + true, + "Scalar Arithmetic", + false, + false, + false, + "IF_SALU"}, + + {"SCmpKEQU32", + "SCmpKEQU32", + 2, + false, + "Scalar Compare", + false, + false, + false, + "IF_SALU"}, + + {"SCmpKGeU32", + "SCmpKGeU32", + 2, + false, + "Scalar Compare", + false, + false, + false, + "IF_SALU"}, + + {"SCmpKGtU32", + "SCmpKGtU32", + 2, + false, + "Scalar Compare", + false, + false, + false, + "IF_SALU"}, + + {"SCmpKLGU32", + "SCmpKLGU32", + 2, + false, + "Scalar Compare", + false, + false, + false, + "IF_SALU"}, + + {"SFlbitI32B32", + "SFlbitI32B32", + 1, + true, + "Scalar Arithmetic", + false, + false, + false, + "IF_SALU"}, + + {"VReadlaneB32", + "VReadlaneB32", + 2, + true, + "Vector Lane", + false, + false, + false, + "IF_VALU"}, + + {"VWritelaneB32", + "VWritelaneB32", + 2, + true, + "Vector Lane", + false, + false, + false, + "IF_VALU"}, + + {"VPermlane16SwapB32", + "VPermlane16SwapB32", + 1, + true, + "Vector Lane", + false, + false, + false, + "IF_VALU"}, + + {"VPermlane32SwapB32", + "VPermlane32SwapB32", + 1, + true, + "Vector Lane", + false, + false, + false, + "IF_VALU"}, + + {"VCvtFP8toF16", + "VCvtFP8toF16", + 1, + true, + "Vector Conversion", + true, + true, + false, + "IF_VALU"}, + + {"VCvtPkF32toFP16", + "VCvtPkF32toFP16", + 2, + true, + "Vector Conversion", + true, + true, + false, + "IF_VALU"}, + + {"BufferLoadB16", + "BufferLoadB16", + 1, + true, + "Memory Memory", + false, + false, + false, + "IF_MUBUFLoad"}, + + {"BufferLoadB192", + "BufferLoadB192", + 1, + true, + "Memory Memory", + false, + false, + false, + "IF_MUBUFLoad"}, + + {"FlatLoadB192", + "FlatLoadB192", + 1, + true, + "Memory Memory", + false, + false, + false, + "IF_FLATLoad"}, + + {"BufferStoreD16U8", + "BufferStoreD16U8", + 2, + true, + "Memory Memory", + false, + false, + false, + "IF_MUBUFStore"}, + + {"BufferStoreD16B16", + "BufferStoreD16B16", + 2, + true, + "Memory Memory", + false, + false, + false, + "IF_MUBUFStore"}, + + {"FlatStoreD16B16", + "FlatStoreD16B16", + 2, + true, + "Memory Memory", + false, + false, + false, + "IF_FLATStore"}, + + {"DSLoadB16", + "DSLoadB16", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSLoadD16HIU8", + "DSLoadD16HIU8", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSLoadD16HIU16", + "DSLoadD16HIU16", + 1, + true, + "Memory LDS", + false, + false, + false, + "IF_DSRead"}, + + {"DSStoreU16", + "DSStoreU16", + 2, + false, + "Memory LDS", + false, + false, + false, + "IF_DSStore"}, + + {"DSStoreB256", + "DSStoreB256", + 2, + false, + "Memory LDS", + false, + false, + false, + "IF_DSStore"}, + + {"GlobalLoadTR8B64", + "GlobalLoadTR8B64", + 2, + true, + "Memory Memory", + false, + false, + false, + "IF_GLOBALLoad"}, + + {"GlobalLoadTR16B128", + "GlobalLoadTR16B128", + 2, + true, + "Memory Memory", + false, + false, + false, + "IF_GLOBALLoad"}, + + {"SDcacheWb", + "SDcacheWb", + 0, + false, + "Memory Cache", + false, + false, + false, + "IF_HasSideEffect"}, + + {"SSetVgprMsb", + "SSetVgprMsb", + 1, + false, + "Scalar Control", + false, + false, + false, + ""}, + + // ======================================================================== + // Branch / Control Flow + // ======================================================================== + + {"SBranch", + "SBranch", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_Branch"}, + + {"SCBranchSCC0", + "SCBranchSCC0", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_Branch|IF_ConditionalBranch|IF_ImplicitReadSCC"}, + + {"SCBranchSCC1", + "SCBranchSCC1", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_Branch|IF_ConditionalBranch|IF_ImplicitReadSCC"}, + + {"SCBranchVCCNZ", + "SCBranchVCCNZ", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_Branch|IF_ConditionalBranch|IF_ImplicitReadVCC"}, + + {"SCBranchVCCZ", + "SCBranchVCCZ", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_Branch|IF_ConditionalBranch|IF_ImplicitReadVCC"}, + + {"SCBranchExecZ", + "SCBranchExecZ", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_Branch|IF_ConditionalBranch|IF_ImplicitReadEXEC"}, + + {"SCBranchExecNZ", + "SCBranchExecNZ", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_Branch|IF_ConditionalBranch|IF_ImplicitReadEXEC"}, + + // ======================================================================== + // Wait / Synchronization + // ======================================================================== + + {"SWaitCnt", + "SWaitCnt", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_WaitCnt"}, + + {"SWaitTensorcnt", + "SWaitTensorcnt", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_WaitTensorCnt"}, + + {"SWaitXCnt", + "SWaitXCnt", + 1, + false, + "Scalar Control", + false, + false, + false, + "IF_WaitCnt"}, + + // ======================================================================== + // Program Termination + // ======================================================================== + + {"SEndpgm", + "SEndpgm", + 0, + false, + "Scalar Control", + false, + false, + false, + "IF_HasSideEffect"}, diff --git a/shared/stinkytofu/src/transforms/logical/CMakeLists.txt b/shared/stinkytofu/src/transforms/logical/CMakeLists.txt index 00c04c6b12af..2edfc198e630 100644 --- a/shared/stinkytofu/src/transforms/logical/CMakeLists.txt +++ b/shared/stinkytofu/src/transforms/logical/CMakeLists.txt @@ -6,4 +6,5 @@ target_sources(stinkytofu_objs PRIVATE ToStinkyAsmPass.cpp CompositeInstructionLoweringPass.cpp LogicalPeepholePass.cpp + LowerLogicalModulePipeline.cpp ) diff --git a/shared/stinkytofu/src/transforms/logical/CompositeInstructionLoweringPass.cpp b/shared/stinkytofu/src/transforms/logical/CompositeInstructionLoweringPass.cpp index 415a5bcfd01a..d7ae09f2fa18 100644 --- a/shared/stinkytofu/src/transforms/logical/CompositeInstructionLoweringPass.cpp +++ b/shared/stinkytofu/src/transforms/logical/CompositeInstructionLoweringPass.cpp @@ -168,6 +168,86 @@ class CompositeInstructionLoweringPassImpl : public Pass { // For now, keep as-is (TODO: expand to lshlrev + or) result.push_back(irInst); } + } + // ================================================================ + // SAddU64: 64-bit scalar add (composite) + // ================================================================ + else if (irInst->getOpcode() == logical::SAddU64) { + if (isInstructionSupported("SAddU64", arch)) { + result.push_back(irInst); + } else { + // Fallback: SAddU32(lo) + SAddCU32(hi) via SCC carry + const auto& dst = irInst->dests[0]; + const auto& src0 = irInst->srcs[0]; + const auto& src1 = irInst->srcs[1]; + + auto* addLo = SAddU32(dst, src0, src1, irInst->comment + " (lo)"); + auto* addHi = SAddCU32(dst, src0, src1, irInst->comment + " (hi+carry)"); + result.push_back(addLo); + result.push_back(addHi); + } + } + // ================================================================ + // VAddNCU64: 64-bit vector add no-carry (composite) + // ================================================================ + else if (irInst->getOpcode() == logical::VAddNCU64) { + if (isInstructionSupported("VAddNCU64", arch)) { + result.push_back(irInst); + } else { + // Fallback: VAddCOU32(lo, carry->VCC) + VAddCCOU32(hi, VCC) + const auto& dst = irInst->dests[0]; + const auto& src0 = irInst->srcs[0]; + const auto& src1 = irInst->srcs[1]; + + auto* addLo = VAddCOU32(dst, src0, src1, std::nullopt, std::nullopt, + irInst->comment + " (lo)"); + auto* addHi = VAddCCOU32(dst, src0, src1, std::nullopt, std::nullopt, + irInst->comment + " (hi+carry)"); + result.push_back(addLo); + result.push_back(addHi); + } + } + // ================================================================ + // VAddLShiftLeftU32: dst = (src0 + src1) << src2 + // ================================================================ + else if (irInst->getOpcode() == logical::VAddLShiftLeftU32) { + if (isInstructionSupported("VAddLShiftLeftU32", arch)) { + result.push_back(irInst); + } else { + // Fallback: VAddU32(dst, src0, src1) + VLShiftLeftB32(dst, src2, dst) + const auto& dst = irInst->dests[0]; + const auto& src0 = irInst->srcs[0]; + const auto& src1 = irInst->srcs[1]; + const auto& src2 = irInst->srcs[2]; + + auto* add = VAddU32(dst, src0, src1, std::nullopt, std::nullopt, + irInst->comment + " (add)"); + auto* shift = VLShiftLeftB32(dst, src2, dst, std::nullopt, std::nullopt, + irInst->comment + " (lshl)"); + result.push_back(add); + result.push_back(shift); + } + } + // ================================================================ + // VLShiftLeftAddU32: dst = (src0 << src2) + src1 + // ================================================================ + else if (irInst->getOpcode() == logical::VLShiftLeftAddU32) { + if (isInstructionSupported("VLShiftLeftAddU32", arch)) { + result.push_back(irInst); + } else { + // Fallback: VLShiftLeftB32(dst, src2, src0) + VAddU32(dst, dst, src1) + const auto& dst = irInst->dests[0]; + const auto& src0 = irInst->srcs[0]; + const auto& src1 = irInst->srcs[1]; + const auto& src2 = irInst->srcs[2]; + + auto* shift = VLShiftLeftB32(dst, src2, src0, std::nullopt, std::nullopt, + irInst->comment + " (lshl)"); + auto* add = + VAddU32(dst, dst, src1, std::nullopt, std::nullopt, irInst->comment + " (add)"); + result.push_back(shift); + result.push_back(add); + } } else { // Unknown composite - keep as-is result.push_back(irInst); diff --git a/shared/stinkytofu/src/transforms/logical/LowerLogicalModulePipeline.cpp b/shared/stinkytofu/src/transforms/logical/LowerLogicalModulePipeline.cpp new file mode 100644 index 000000000000..b784c47b22b3 --- /dev/null +++ b/shared/stinkytofu/src/transforms/logical/LowerLogicalModulePipeline.cpp @@ -0,0 +1,246 @@ +/* ************************************************************************ + * Copyright (C) 2025-2026 Advanced Micro Devices, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * ************************************************************************ */ + +#include "stinkytofu/transforms/logical/LowerLogicalModulePipeline.hpp" + +#include +#include +#include +#include +#include +#include + +#include "stinkytofu/bindings/python/LogicalModule.hpp" +#include "stinkytofu/core/BasicBlock.hpp" +#include "stinkytofu/core/Function.hpp" +#include "stinkytofu/core/PassManager.hpp" +#include "stinkytofu/core/Types.hpp" +#include "stinkytofu/hardware/ArchHelper.hpp" +#include "stinkytofu/ir/asm/StinkyAsmDirectives.hpp" +#include "stinkytofu/ir/asm/StinkyAsmIR.hpp" +#include "stinkytofu/ir/logical/LogicalInstructions.hpp" +#include "stinkytofu/pipeline/BackendRegistry.hpp" +#include "stinkytofu/transforms/logical/CompositeInstructionLoweringPass.hpp" +#include "stinkytofu/transforms/logical/ToStinkyAsmPass.hpp" + +namespace stinkytofu { + +void runLogicalLoweringPipeline(Function& func, const GemmTileConfig& config) { + PassManager pm; + pm.setGemmTileConfig(config); + pm.addPass(createCompositeInstructionLoweringPass()); + pm.addPass(createToStinkyAsmPass()); + pm.run(func); +} + +namespace { + +GemmTileConfig configFromOptions(std::array arch, + const StinkyAsmModule::ModuleOptions& opts) { + GemmTileConfig cfg; + cfg.arch = arch; + cfg.TileA0 = static_cast(opts.TileA0); + cfg.TileB0 = static_cast(opts.TileB0); + cfg.TileM0 = static_cast(opts.TileM0); + cfg.NumGRA = opts.NumGRA; + cfg.NumGRB = opts.NumGRB; + cfg.NumGRM = opts.NumGRM; + cfg.NumWaves = static_cast(opts.WaveGroup0 * opts.WaveGroup1); + return cfg; +} + +} // anonymous namespace + +std::shared_ptr lowerLogicalModuleToAsm( + PyLogicalModule& module, std::array arch, + const StinkyAsmModule::ModuleOptions& moduleOptions) { + auto asmModule = std::make_shared(module.getName(), arch, moduleOptions); + + // Register instruction groups from the target backend's pipeline. + if (auto* pipeline = BackendRegistry::getArchPipeline(arch)) { + for (const auto& groupName : pipeline->groupNames) { + asmModule->addGroup(groupName); + } + } + + Function& func = asmModule->getFunction(); + BasicBlock* entryBB = func.getEntryBlock(); + assert(entryBB && "StinkyAsmModule must have an entry basic block"); + + GfxArchID archId = getGfxArchID(arch[0], arch[1], arch[2]); + + { + PyLogicalFunction pyFunc(&func); + + const auto& instructions = module.getInstructions(); + const auto& directives = module.getSetDirectives(); + const auto& labels = module.getLabels(); + const auto& textBlocks = module.getTextBlocks(); + const auto& groupMarkers = module.getGroupMarkers(); + size_t dirIdx = 0; + size_t lblIdx = 0; + size_t tbIdx = 0; + size_t gmIdx = 0; + + // Active group names stack — tracks which groups the current + // instruction position is inside. Only groups that are also + // registered (via addGroup above) will actually be updated. + std::vector activeGroups; + + AsmIRBuilder irBuilder(*entryBB, archId); + + // Process group markers whose position/order are eligible at the + // current emission point. Group markers toggle the active-group + // stack but do not emit IR. + auto processGroupMarkers = [&](size_t pos, size_t maxOrder) { + while (gmIdx < groupMarkers.size() && groupMarkers[gmIdx].position <= pos && + groupMarkers[gmIdx].order < maxOrder) { + if (groupMarkers[gmIdx].isBegin) { + activeGroups.push_back(groupMarkers[gmIdx].name); + } else { + auto it = std::find(activeGroups.rbegin(), activeGroups.rend(), + groupMarkers[gmIdx].name); + if (it != activeGroups.rend()) { + activeGroups.erase(std::next(it).base()); + } + } + ++gmIdx; + } + }; + + // Build the pointer vector for updateInstructionGroups from active groups. + auto buildGroupPtrs = [&]() -> std::vector { + std::vector ptrs; + ptrs.reserve(activeGroups.size()); + for (auto& g : activeGroups) { + ptrs.push_back(&g); + } + return ptrs; + }; + + auto emitNextItem = [&](int type) { + const auto instsCountBefore = entryBB->size(); + switch (type) { + case 0: { + AsmDirective* dir = IRBase::createIR(); + dir->kind = AsmDirectiveKind::SET; + dir->name = ".set"; + dir->symbol = directives[dirIdx].symbol; + dir->value = directives[dirIdx].value; + entryBB->appendIR(dir); + ++dirIdx; + break; + } + case 1: { + StinkyInstruction* labelInst = + irBuilder.createLabel(labels[lblIdx].labelName, labels[lblIdx].alignment); + if (!labels[lblIdx].comment.empty()) { + labelInst->addModifier(CommentData{labels[lblIdx].comment}); + } + ++lblIdx; + break; + } + case 2: { + AsmDirective* dir = IRBase::createIR(); + dir->kind = AsmDirectiveKind::TEXTBLOCK; + dir->value = textBlocks[tbIdx].text; + entryBB->appendIR(dir); + ++tbIdx; + break; + } + default: + break; + } + asmModule->updateInstructionGroups(buildGroupPtrs(), instsCountBefore); + }; + + auto emitItemsAtPosition = [&](size_t pos) { + while (true) { + size_t bestOrder = SIZE_MAX; + int bestType = -1; + + if (dirIdx < directives.size() && directives[dirIdx].position <= pos && + directives[dirIdx].order < bestOrder) { + bestOrder = directives[dirIdx].order; + bestType = 0; + } + if (lblIdx < labels.size() && labels[lblIdx].position <= pos && + labels[lblIdx].order < bestOrder) { + bestOrder = labels[lblIdx].order; + bestType = 1; + } + if (tbIdx < textBlocks.size() && textBlocks[tbIdx].position <= pos && + textBlocks[tbIdx].order < bestOrder) { + bestOrder = textBlocks[tbIdx].order; + bestType = 2; + } + + if (bestType == -1) break; + // Process any group markers that precede this item. + processGroupMarkers(pos, bestOrder); + emitNextItem(bestType); + } + }; + + for (size_t i = 0; i < instructions.size(); ++i) { + emitItemsAtPosition(i); + // Process group markers at this instruction position. + processGroupMarkers(i, SIZE_MAX); + + const auto instsCountBefore = entryBB->size(); + entryBB->appendIR(static_cast(instructions[i].get())); + asmModule->updateInstructionGroups(buildGroupPtrs(), instsCountBefore); + } + // Trailing items (after all instructions) + emitItemsAtPosition(SIZE_MAX); + // Process any remaining group markers. + processGroupMarkers(SIZE_MAX, SIZE_MAX); + + // Debug: report instruction group ranges after population. + if (std::getenv("DEBUG_STINKY_GROUPS")) { + auto* pipeline = BackendRegistry::getArchPipeline(arch); + if (pipeline) { + std::cerr << "[DEBUG_STINKY_GROUPS] Instruction groups after population:\n"; + for (const auto& groupName : pipeline->groupNames) { + auto range = asmModule->findGroupRange(groupName); + if (range) { + size_t count = 0; + for (auto it = range->first; it != range->second; ++it) ++count; + ++count; // include last + std::cerr << " " << groupName << ": populated (" << count + << " instructions)\n"; + } else { + std::cerr << " " << groupName << ": EMPTY (no range)\n"; + } + } + std::cerr << " Total BB size: " << entryBB->size() << "\n"; + } + } + + runLogicalLoweringPipeline(func, configFromOptions(arch, moduleOptions)); + } + + return asmModule; +} + +} // namespace stinkytofu diff --git a/shared/stinkytofu/src/transforms/logical/ToStinkyAsmPass.cpp b/shared/stinkytofu/src/transforms/logical/ToStinkyAsmPass.cpp index a3e5d9fa4858..d0bb69145c50 100644 --- a/shared/stinkytofu/src/transforms/logical/ToStinkyAsmPass.cpp +++ b/shared/stinkytofu/src/transforms/logical/ToStinkyAsmPass.cpp @@ -26,6 +26,7 @@ #include "stinkytofu/core/PassManager.hpp" #include "stinkytofu/hardware/ArchHelper.hpp" #include "stinkytofu/ir/asm/StinkyAsmIR.hpp" +#include "stinkytofu/ir/asm/StinkyModifiers.hpp" #include "stinkytofu/ir/logical/LogicalInstructions.hpp" #include "stinkytofu/support/Casting.hpp" #include "stinkytofu/support/ErrorHandling.hpp" @@ -136,6 +137,38 @@ StinkyInstruction* createAsmFromIR(LogicalInstruction* irInst, GfxArchID arch) { mnemonic = generateMXMFMAMnemonic(data->m, data->n, data->k, data->block, data->instType); } // ==================================================================== + // Special Instructions: SWaitAlu, SchedulingFence + // ==================================================================== + else if (irInst->getOpcode() == logical::SWaitAlu) { + const SWaitAluLogicalData* data = irInst->asSWaitAlu(); + if (!data) { + STINKY_UNREACHABLE("SWaitAlu instruction has no SWaitAluLogicalData"); + return nullptr; + } + isaOpcode = getMnemonicToIsaOpcode("s_wait_alu", arch); + const HwInstDesc* desc = getMCIDByIsaOp(isaOpcode, arch); + if (!desc) { + STINKY_UNREACHABLE("SWaitAlu: s_wait_alu not supported on this architecture"); + return nullptr; + } + StinkyInstruction* asmInst = IRBase::createIR(desc); + SWaitAluData waitAluData(data->va_vdst, data->va_sdst, data->va_ssrc, data->hold_cnt, + data->vm_vsrc, data->va_vcc, data->sa_sdst); + asmInst->addModifier(waitAluData); + if (!irInst->comment.empty()) { + asmInst->addModifier(CommentData(irInst->comment)); + } + return asmInst; + } else if (irInst->getOpcode() == logical::SchedulingFence) { + static const HwInstDesc fenceMCID{ + GFX::FENCE, GFX::FENCE, 0, 0, 0, "FENCE", makeFlagSet({InstFlag::IF_HasSideEffect})}; + StinkyInstruction* asmInst = IRBase::createIR(&fenceMCID); + if (!irInst->comment.empty()) { + asmInst->addModifier(CommentData(irInst->comment)); + } + return asmInst; + } + // ==================================================================== // Regular instructions: per-arch map only (LogicalToAsmMappings_generated.inc) // Every lowering for each arch must be in the map; no fallback. // ==================================================================== @@ -165,12 +198,36 @@ StinkyInstruction* createAsmFromIR(LogicalInstruction* irInst, GfxArchID arch) { // Create the assembly instruction StinkyInstruction* asmInst = IRBase::createIR(desc); - // Copy operands from IR to assembly - if (!irInst->dests.empty()) { - asmInst->setDestRegs(irInst->dests); + // Copy operands from IR to assembly. + // Handle the store-instruction mismatch: LogicalInstruction may place vdata + // in dests (it's the "output" of the Python instruction), but the HW format + // (e.g. MUBUF_STORE) defines ALL operand fields as sources (S0..S3). When + // the HW descriptor has zero dest fields but irInst has dests, prepend them + // to srcRegs so that collectVgprMsbSlots lines up registers with HW fields. + bool hwHasDestField = false; + if (desc) { + for (const auto& f : desc->operandFields) { + if (f.isDest || f.isReadWrite) { + hwHasDestField = true; + break; + } + } } - if (!irInst->srcs.empty()) { - asmInst->setSrcRegs(irInst->srcs); + + if (!irInst->dests.empty() && !hwHasDestField) { + // HW has no dest field — logical dests are really src operands. + std::vector merged; + merged.reserve(irInst->dests.size() + irInst->srcs.size()); + merged.insert(merged.end(), irInst->dests.begin(), irInst->dests.end()); + merged.insert(merged.end(), irInst->srcs.begin(), irInst->srcs.end()); + asmInst->setSrcRegs(merged); + } else { + if (!irInst->dests.empty()) { + asmInst->setDestRegs(irInst->dests); + } + if (!irInst->srcs.empty()) { + asmInst->setSrcRegs(irInst->srcs); + } } // Copy comment @@ -178,7 +235,49 @@ StinkyInstruction* createAsmFromIR(LogicalInstruction* irInst, GfxArchID arch) { asmInst->addModifier(CommentData(irInst->comment)); } - // TODO: Copy DPP, SDWA, DS modifiers when needed + // Copy instruction modifiers from logical IR to assembly IR + if (irInst->ds.has_value()) { + asmInst->addModifier(irInst->ds.value()); + } + if (irInst->mubuf.has_value()) { + asmInst->addModifier(irInst->mubuf.value()); + } + if (irInst->dpp.has_value()) { + asmInst->addModifier(irInst->dpp.value()); + } + if (irInst->sdwa.has_value()) { + asmInst->addModifier(irInst->sdwa.value()); + } + if (irInst->vop3.has_value()) { + asmInst->addModifier(irInst->vop3.value()); + } + + // MFMA/SMFMA/MXMFMA: attach MFMAModifiers so downstream passes + // (RegionClonePass, SetMatrixReusePass) can identify these instructions. + if (irInst->getOpcode() == logical::MFMA || irInst->getOpcode() == logical::SMFMA || + irInst->getOpcode() == logical::MXMFMA) { + MFMAModifiers mod; + if (irInst->getOpcode() == logical::MFMA) { + const MFMAData* data = irInst->asMFMA(); + if (data && data->neg) { + mod.negBits.negLo = {1, 1, 0}; + mod.negBits.numSrcs = 2; + } + } else if (irInst->getOpcode() == logical::SMFMA) { + const SMFMAData* data = irInst->asSMFMA(); + if (data && data->neg) { + mod.negBits.negLo = {1, 1, 0}; + mod.negBits.numSrcs = 2; + } + } else if (irInst->getOpcode() == logical::MXMFMA) { + const MXMFMAData* data = irInst->asMXMFMA(); + if (data) { + mod.reuseA = data->reuseA; + mod.reuseB = data->reuseB; + } + } + asmInst->addModifier(mod); + } return asmInst; } diff --git a/shared/stinkytofu/tests/unit/logical/LogicalToAsmMultiArchTest.cpp b/shared/stinkytofu/tests/unit/logical/LogicalToAsmMultiArchTest.cpp index 25b0ce3b6f58..ca3c768ae4e3 100644 --- a/shared/stinkytofu/tests/unit/logical/LogicalToAsmMultiArchTest.cpp +++ b/shared/stinkytofu/tests/unit/logical/LogicalToAsmMultiArchTest.cpp @@ -157,7 +157,7 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { case logical::VPrngB32: return VPrngB32(vgpr(0), vgpr(1)); case logical::VCndMaskB32: - return VCndMaskB32(vgpr(0), vgpr(1), vgpr(2)); + return VCndMaskB32(vgpr(0), vgpr(1), vgpr(2), vgpr(3)); case logical::VLShiftLeftB16: return VLShiftLeftB16(vgpr(0), vgpr(1), vgpr(2)); case logical::VLShiftLeftB32: @@ -184,6 +184,8 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return VBfeU32(vgpr(0), vgpr(1), vgpr(2), vgpr(3)); case logical::VBfiB32: return VBfiB32(vgpr(0), vgpr(1), vgpr(2), vgpr(3)); + case logical::VMovRelsD2B32: + return VMovRelsD2B32(vgpr(0), vgpr(1)); case logical::VAccvgprReadB32: return VAccvgprReadB32(vgpr(0), vgpr(1)); case logical::VAccvgprWrite: @@ -336,6 +338,26 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return VCvtScaleSRF16toBF8(vgpr(0), vgpr(1), vgpr(2)); case logical::VCvtPkF32toBF16: return VCvtPkF32toBF16(vgpr(0), vgpr(1), vgpr(2)); + case logical::VCvtScalePk8F32toFP8: + return VCvtScalePk8F32toFP8(vgpr(0), vgpr(1), vgpr(2)); + case logical::VCvtScalePk8F32toBF8: + return VCvtScalePk8F32toBF8(vgpr(0), vgpr(1), vgpr(2)); + case logical::VCvtScaleSRPkF32toFP8: + return VCvtScaleSRPkF32toFP8(vgpr(0), vgpr(1), vgpr(2), vgpr(3)); + case logical::VCvtFP8toF16: + return VCvtFP8toF16(vgpr(0), vgpr(1)); + case logical::VCvtPkF32toFP16: + return VCvtPkF32toFP16(vgpr(0), vgpr(1), vgpr(2)); + case logical::VCvtPkF32toF16: + return VCvtPkF32toF16(vgpr(0), vgpr(1), vgpr(2)); + case logical::VRcpF64: + return VRcpF64(vgpr(0), vgpr(1)); + case logical::VCvtF64toU32: + return VCvtF64toU32(vgpr(0), vgpr(1)); + case logical::VCvtU32toF64: + return VCvtU32toF64(vgpr(0), vgpr(1)); + case logical::PVCvtBF16toFP32: + return PVCvtBF16toFP32(vgpr(0), vgpr(1)); case logical::DSBPermuteB32: return DSBPermuteB32(vgpr(0), vgpr(1)); case logical::DSLoadU8: @@ -374,6 +396,34 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return DSStore2B32(vgpr(0), vgpr(1), vgpr(2)); case logical::DSStore2B64: return DSStore2B64(vgpr(0), vgpr(1), vgpr(2)); + case logical::DSLoadB192: + return DSLoadB192(vgpr(0), vgpr(1)); + case logical::DSLoadB96TrB6: + return DSLoadB96TrB6(vgpr(0), vgpr(1)); + case logical::DSLoadB64TrB4: + return DSLoadB64TrB4(vgpr(0), vgpr(1)); + case logical::DSLoadB64TrB8: + return DSLoadB64TrB8(vgpr(0), vgpr(1)); + case logical::DSLoadB64TrB16: + return DSLoadB64TrB16(vgpr(0), vgpr(1)); + case logical::DSLoadB128TrB16: + return DSLoadB128TrB16(vgpr(0), vgpr(1)); + case logical::DSStoreB192: + return DSStoreB192(vgpr(0), vgpr(1)); + case logical::DSStoreB8HID16: + return DSStoreB8HID16(vgpr(0), vgpr(1)); + case logical::DSStoreD16HIB16: + return DSStoreD16HIB16(vgpr(0), vgpr(1)); + case logical::DSLoadB16: + return DSLoadB16(vgpr(0), vgpr(1)); + case logical::DSLoadD16HIU8: + return DSLoadD16HIU8(vgpr(0), vgpr(1)); + case logical::DSLoadD16HIU16: + return DSLoadD16HIU16(vgpr(0), vgpr(1)); + case logical::DSStoreU16: + return DSStoreU16(vgpr(0), vgpr(1)); + case logical::DSStoreB256: + return DSStoreB256(vgpr(0), vgpr(1)); case logical::BufferLoadU8: return BufferLoadU8(vgpr(0), vgpr(1)); case logical::BufferLoadI8: @@ -402,6 +452,10 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return BufferLoadD16B16(vgpr(0), vgpr(1)); case logical::BufferLoadD16HIB16: return BufferLoadD16HIB16(vgpr(0), vgpr(1)); + case logical::BufferLoadB16: + return BufferLoadB16(vgpr(0), vgpr(1)); + case logical::BufferLoadB192: + return BufferLoadB192(vgpr(0), vgpr(1)); case logical::BufferStoreB8: return BufferStoreB8(vgpr(0), vgpr(1), vgpr(2)); case logical::BufferStoreD16HIU8: @@ -418,6 +472,10 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return BufferStoreB96(vgpr(0), vgpr(1), vgpr(2)); case logical::BufferStoreB128: return BufferStoreB128(vgpr(0), vgpr(1), vgpr(2)); + case logical::BufferStoreD16U8: + return BufferStoreD16U8(vgpr(0), vgpr(1), vgpr(2)); + case logical::BufferStoreD16B16: + return BufferStoreD16B16(vgpr(0), vgpr(1), vgpr(2)); case logical::BufferAtomicAddF32: return BufferAtomicAddF32(vgpr(0), vgpr(1)); case logical::BufferAtomicCmpswapB32: @@ -452,6 +510,8 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return FlatLoadB96(vgpr(0), vgpr(1)); case logical::FlatLoadB128: return FlatLoadB128(vgpr(0), vgpr(1)); + case logical::FlatLoadB192: + return FlatLoadB192(vgpr(0), vgpr(1)); case logical::FlatStoreB8: return FlatStoreB8(vgpr(0), vgpr(1), vgpr(2)); case logical::FlatStoreD16HIB8: @@ -468,12 +528,20 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return FlatStoreB96(vgpr(0), vgpr(1), vgpr(2)); case logical::FlatStoreB128: return FlatStoreB128(vgpr(0), vgpr(1), vgpr(2)); + case logical::FlatStoreD16B16: + return FlatStoreD16B16(vgpr(0), vgpr(1), vgpr(2)); case logical::FlatAtomicCmpswapB32: return FlatAtomicCmpswapB32(vgpr(0), vgpr(1), vgpr(2)); + case logical::FlatAtomicDecU32: + return FlatAtomicDecU32(vgpr(0), vgpr(1), vgpr(2)); case logical::SAbsI32: return SAbsI32(sgpr(0), sgpr(1)); case logical::SBarrier: return SBarrier(); + case logical::SNop: + return SNop(literal(3)); + case logical::SDelayAlu: + return SDelayAlu(literal(0)); case logical::SMaxI32: return SMaxI32(sgpr(0), sgpr(1), sgpr(2)); case logical::SMaxU32: @@ -538,6 +606,16 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return SMovB32(sgpr(0), sgpr(1)); case logical::SMovB64: return SMovB64(sgpr(0), sgpr(1)); + case logical::SLoadB32: + return SLoadB32(sgpr(0), sgpr(2, 2), literal(0)); + case logical::SLoadB64: + return SLoadB64(sgpr(0, 2), sgpr(4, 2), literal(0)); + case logical::SLoadB128: + return SLoadB128(sgpr(0, 4), sgpr(8, 2), literal(0)); + case logical::SLoadB256: + return SLoadB256(sgpr(0, 8), sgpr(16, 2), literal(0)); + case logical::SLoadB512: + return SLoadB512(sgpr(0, 16), sgpr(32, 2), literal(0)); case logical::SCMovB32: return SCMovB32(sgpr(0), sgpr(1)); case logical::SCMovB64: @@ -578,6 +656,22 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return VMovB64(vgpr(0), vgpr(1)); case logical::VLShiftLeftOrB32: return VLShiftLeftOrB32(vgpr(0), vgpr(1), vgpr(2), vgpr(3)); + case logical::VAddLShiftLeftU32: + return VAddLShiftLeftU32(vgpr(0), vgpr(1), vgpr(2), vgpr(3)); + case logical::VLShiftLeftAddU32: + return VLShiftLeftAddU32(vgpr(0), vgpr(1), vgpr(2), vgpr(3)); + case logical::VAddNCU64: + return VAddNCU64(vgpr(0), vgpr(1), vgpr(2)); + case logical::VReadlaneB32: + return VReadlaneB32(vgpr(0), vgpr(1), vgpr(2)); + case logical::VWritelaneB32: + return VWritelaneB32(vgpr(0), vgpr(1), vgpr(2)); + case logical::VPermlane16SwapB32: + return VPermlane16SwapB32(vgpr(0), vgpr(1)); + case logical::VPermlane32SwapB32: + return VPermlane32SwapB32(vgpr(0), vgpr(1)); + case logical::VNop: + return VNop(); case logical::MFMA: return MFMA("f32", "f32", 16, 16, 4, 1, false, vgpr(0), vgpr(1), vgpr(2)); case logical::MXMFMA: @@ -591,6 +685,84 @@ static LogicalInstruction* createTestInstruction(logical::Opcode opcode) { return Label("test_label"); case logical::IntrinsicCall: return nullptr; // Special: handled separately + case logical::SBranch: + return SBranch(literal(0)); + case logical::SCBranchSCC0: + return SCBranchSCC0(literal(0)); + case logical::SCBranchSCC1: + return SCBranchSCC1(literal(0)); + case logical::SCBranchVCCNZ: + return SCBranchVCCNZ(literal(0)); + case logical::SCBranchVCCZ: + return SCBranchVCCZ(literal(0)); + case logical::SCBranchExecZ: + return SCBranchExecZ(literal(0)); + case logical::SCBranchExecNZ: + return SCBranchExecNZ(literal(0)); + case logical::SWaitCnt: + return SWaitCnt(literal(0)); + case logical::SWaitTensorcnt: + return SWaitTensorcnt(literal(0)); + case logical::SWaitXCnt: + return SWaitXCnt(literal(0)); + case logical::SEndpgm: + return SEndpgm(); + case logical::SAddU64: + return SAddU64(sgpr(0), sgpr(1), sgpr(2)); + case logical::SBfeU32: + return SBfeU32(sgpr(0), sgpr(1), sgpr(2)); + case logical::SStoreB32: + return SStoreB32(sgpr(0), sgpr(2, 2), literal(0)); + case logical::SStoreB64: + return SStoreB64(sgpr(0, 2), sgpr(4, 2), literal(0)); + case logical::SStoreB128: + return SStoreB128(sgpr(0, 4), sgpr(8, 2), literal(0)); + case logical::SStoreB256: + return SStoreB256(sgpr(0, 8), sgpr(16, 2), literal(0)); + case logical::SStoreB512: + return SStoreB512(sgpr(0, 16), sgpr(32, 2), literal(0)); + case logical::SAtomicInc: + return SAtomicInc(sgpr(0), sgpr(1), sgpr(2)); + case logical::SAtomicDec: + return SAtomicDec(sgpr(0), sgpr(1)); + case logical::SCSelectB64: + return SCSelectB64(sgpr(0), sgpr(1), sgpr(2)); + case logical::SCmpKEQU32: + return SCmpKEQU32(sgpr(0), sgpr(1)); + case logical::SCmpKGeU32: + return SCmpKGeU32(sgpr(0), sgpr(1)); + case logical::SCmpKGtU32: + return SCmpKGtU32(sgpr(0), sgpr(1)); + case logical::SCmpKLGU32: + return SCmpKLGU32(sgpr(0), sgpr(1)); + case logical::SFlbitI32B32: + return SFlbitI32B32(sgpr(0), sgpr(1)); + case logical::SSetPCB64: + return SSetPCB64(sgpr(0)); + case logical::SSwapPCB64: + return SSwapPCB64(sgpr(0), sgpr(1)); + case logical::SSleep: + return SSleep(literal(1)); + case logical::SSetPrior: + return SSetPrior(literal(0)); + case logical::SDcacheWb: + return SDcacheWb(); + case logical::SSetVgprMsb: + return SSetVgprMsb(literal(0)); + case logical::GlobalInv: + return GlobalInv(); + case logical::GlobalWb: + return GlobalWb(); + case logical::GlobalPrefetchB8: + return GlobalPrefetchB8(vgpr(0), vgpr(1)); + case logical::GlobalLoadTR8B64: + return GlobalLoadTR8B64(vgpr(0), vgpr(1), vgpr(2)); + case logical::GlobalLoadTR16B128: + return GlobalLoadTR16B128(vgpr(0), vgpr(1), vgpr(2)); + case logical::SWaitAlu: + return nullptr; + case logical::SchedulingFence: + return nullptr; default: return nullptr; } @@ -611,9 +783,179 @@ static const std::vector EXPECTED_LOWERING_GFX1250 = { {logical::BufferAtomicAddF32, "buffer_atomic_add_f32"}, {logical::DSLoadB32, "ds_load_b32"}, {logical::DSLoadB64, "ds_load_b64"}, + {logical::SLoadB32, "s_load_b32"}, + {logical::SLoadB64, "s_load_b64"}, + {logical::SLoadB128, "s_load_b128"}, + {logical::SLoadB256, "s_load_b256"}, + {logical::SLoadB512, "s_load_b512"}, + {logical::SNop, "s_nop"}, {logical::DSStoreB32, "ds_store_b32"}, {logical::DSStoreB64, "ds_store_b64"}, - // Add more: {logical::YourOpcode, "expected_mnemonic"}, + // Scalar Arithmetic + {logical::SAddI32, "s_add_i32"}, + {logical::SAddU32, "s_add_u32"}, + {logical::SAddCU32, "s_addc_u32"}, + {logical::SMulI32, "s_mul_i32"}, + {logical::SMulHII32, "s_mul_hi_i32"}, + {logical::SMulHIU32, "s_mul_hi_u32"}, + {logical::SMulLOU32, "s_mul_lo_u32"}, + {logical::SSubI32, "s_sub_i32"}, + {logical::SSubU32, "s_sub_u32"}, + {logical::SSubBU32, "s_subb_u32"}, + // Scalar Shift + {logical::SLShiftLeftB32, "s_lshl_b32"}, + {logical::SLShiftRightB32, "s_lshr_b32"}, + {logical::SLShiftLeftB64, "s_lshl_b64"}, + {logical::SLShiftRightB64, "s_lshr_b64"}, + {logical::SAShiftRightI32, "s_ashr_i32"}, + {logical::SLShiftLeft1AddU32, "s_lshl1_add_u32"}, + {logical::SLShiftLeft2AddU32, "s_lshl2_add_u32"}, + {logical::SLShiftLeft3AddU32, "s_lshl3_add_u32"}, + {logical::SLShiftLeft4AddU32, "s_lshl4_add_u32"}, + // Scalar Bitwise + {logical::SAndB32, "s_and_b32"}, + {logical::SAndB64, "s_and_b64"}, + {logical::SAndN2B32, "s_andn2_b32"}, + {logical::SOrB32, "s_or_b32"}, + {logical::SOrB64, "s_or_b64"}, + {logical::SXorB32, "s_xor_b32"}, + {logical::SAndSaveExecB32, "s_and_saveexec_b32"}, + {logical::SAndSaveExecB64, "s_and_saveexec_b64"}, + {logical::SOrSaveExecB32, "s_or_saveexec_b32"}, + {logical::SOrSaveExecB64, "s_or_saveexec_b64"}, + // Scalar Control + {logical::SBarrier, "s_barrier"}, + {logical::SGetRegB32, "s_getreg_b32"}, + {logical::SSetRegB32, "s_setreg_b32"}, + {logical::SSetRegIMM32B32, "s_setreg_IMM32_b32"}, + // Vector Arithmetic + {logical::VAddU32, "v_add_nc_u32"}, + {logical::VAddF32, "v_add_f32"}, + {logical::VSubF32, "v_sub_f32"}, + {logical::VSubI32, "v_sub_i32"}, + {logical::VSubU32, "v_sub_nc_u32"}, + {logical::VMulF32, "v_mul_f32"}, + {logical::VMulLOU32, "v_mul_lo_u32"}, + {logical::VMulHIU32, "v_mul_hi_u32"}, + {logical::VMulHII32, "v_mul_hi_i32"}, + {logical::VMulI32I24, "v_mul_i32_i24"}, + {logical::VMulU32U24, "v_mul_u32_u24"}, + {logical::VFmaF32, "v_fma_f32"}, + {logical::VFmaMixF32, "v_fma_mix_f32"}, + // Vector Bitwise + {logical::VAndB32, "v_and_b32"}, + {logical::VOrB32, "v_or_b32"}, + {logical::VXorB32, "v_xor_b32"}, + {logical::VAndOrB32, "v_and_or_b32"}, + {logical::VCndMaskB32, "v_cndmask_b32"}, + // Vector Shift + {logical::VLShiftLeftB32, "v_lshlrev_b32"}, + {logical::VLShiftRightB32, "v_lshrrev_b32"}, + {logical::VLShiftLeftB64, "v_lshlrev_b64"}, + {logical::VLShiftRightB64, "v_lshrrev_b64"}, + // Vector Other + {logical::VReadfirstlaneB32, "v_readfirstlane_b32"}, + // Scalar Compare + {logical::SCmpEQI32, "s_cmp_eq_i32"}, + {logical::SCmpEQU32, "s_cmp_eq_u32"}, + {logical::SCmpEQU64, "s_cmp_eq_u64"}, + {logical::SCmpGeI32, "s_cmp_ge_i32"}, + {logical::SCmpGeU32, "s_cmp_ge_u32"}, + {logical::SCmpGtI32, "s_cmp_gt_i32"}, + {logical::SCmpGtU32, "s_cmp_gt_u32"}, + {logical::SCmpLeI32, "s_cmp_le_i32"}, + {logical::SCmpLeU32, "s_cmp_le_u32"}, + {logical::SCmpLgU32, "s_cmp_lg_u32"}, + {logical::SCmpLgI32, "s_cmp_lg_i32"}, + {logical::SCmpLgU64, "s_cmp_lg_u64"}, + {logical::SCmpLtI32, "s_cmp_lt_i32"}, + {logical::SCmpLtU32, "s_cmp_lt_u32"}, + {logical::SBitcmp1B32, "s_bitcmp1_b32"}, + // Vector Compare + {logical::VCmpEQF32, "v_cmp_eq_f32"}, + {logical::VCmpEQF64, "v_cmp_eq_f64"}, + {logical::VCmpEQU32, "v_cmp_eq_u32"}, + {logical::VCmpEQI32, "v_cmp_eq_i32"}, + {logical::VCmpGEF16, "v_cmp_ge_f16"}, + {logical::VCmpGTF16, "v_cmp_gt_f16"}, + {logical::VCmpGEF32, "v_cmp_ge_f32"}, + {logical::VCmpGTF32, "v_cmp_gt_f32"}, + {logical::VCmpGEF64, "v_cmp_ge_f64"}, + {logical::VCmpGTF64, "v_cmp_gt_f64"}, + {logical::VCmpGEI32, "v_cmp_ge_i32"}, + {logical::VCmpGTI32, "v_cmp_gt_i32"}, + {logical::VCmpGEU32, "v_cmp_ge_u32"}, + {logical::VCmpGtU32, "v_cmp_gt_u32"}, + {logical::VCmpLeU32, "v_cmp_le_u32"}, + {logical::VCmpLeI32, "v_cmp_le_i32"}, + {logical::VCmpLtI32, "v_cmp_lt_i32"}, + {logical::VCmpLtU32, "v_cmp_lt_u32"}, + {logical::VCmpUF32, "v_cmp_u_f32"}, + {logical::VCmpNeI32, "v_cmp_ne_i32"}, + {logical::VCmpNeU32, "v_cmp_ne_u32"}, + {logical::VCmpNeU64, "v_cmp_ne_u64"}, + {logical::VCmpClassF32, "v_cmp_class_f32"}, + // Vector CompareX + {logical::VCmpXClassF32, "v_cmpx_class_f32"}, + {logical::VCmpXEqU32, "v_cmpx_eq_u32"}, + {logical::VCmpXGeU32, "v_cmpx_ge_u32"}, + {logical::VCmpXGtU32, "v_cmpx_gt_u32"}, + {logical::VCmpXLeU32, "v_cmpx_le_u32"}, + {logical::VCmpXLeI32, "v_cmpx_le_i32"}, + {logical::VCmpXLtF32, "v_cmpx_lt_f32"}, + {logical::VCmpXLtI32, "v_cmpx_lt_i32"}, + {logical::VCmpXLtU32, "v_cmpx_lt_u32"}, + {logical::VCmpXLtU64, "v_cmpx_lt_u64"}, + {logical::VCmpXNeU16, "v_cmpx_ne_u16"}, + {logical::VCmpXNeU32, "v_cmpx_ne_u32"}, + // Scalar Min/Max/Abs + {logical::SAbsI32, "s_abs_i32"}, + {logical::SMaxI32, "s_max_i32"}, + {logical::SMaxU32, "s_max_u32"}, + {logical::SMinI32, "s_min_i32"}, + {logical::SMinU32, "s_min_u32"}, + // Vector Unary (transcendental / bitwise) + {logical::VExpF16, "v_exp_f16"}, + {logical::VExpF32, "v_exp_f32"}, + {logical::VRcpF16, "v_rcp_f16"}, + {logical::VRcpF32, "v_rcp_f32"}, + {logical::VRcpIFlagF32, "v_rcp_iflag_f32"}, + {logical::VRsqF16, "v_rsq_f16"}, + {logical::VRsqF32, "v_rsq_f32"}, + {logical::VNotB32, "v_not_b32"}, + {logical::VRndneF32, "v_rndne_f32"}, + // Vector Min/Max + {logical::VMaxF16, "v_max_f16"}, + {logical::VMaxF32, "v_max_f32"}, + {logical::VMaxF64, "v_max_f64"}, + {logical::VMaxI32, "v_max_i32"}, + {logical::VMaxPKF16, "v_pk_max_f16"}, + {logical::VMinF16, "v_min_f16"}, + {logical::VMinF32, "v_min_f32"}, + {logical::VMinF64, "v_min_f64"}, + {logical::VMinI32, "v_min_i32"}, + // Vector Ternary (med3, lshl_or) + {logical::VMed3I32, "v_med3_i32"}, + {logical::VMed3F32, "v_med3_f32"}, + {logical::VLShiftLeftOrB32, "v_lshl_or_b32"}, + // Vector Shift (ashr) + {logical::VAShiftRightI32, "v_ashrrev_i32"}, + // Vector Pack + {logical::VPackF16toB32, "v_pack_b32_f16"}, + // Branch / Control Flow + {logical::SBranch, "s_branch"}, + {logical::SCBranchSCC0, "s_cbranch_scc0"}, + {logical::SCBranchSCC1, "s_cbranch_scc1"}, + {logical::SCBranchVCCNZ, "s_cbranch_vccnz"}, + {logical::SCBranchVCCZ, "s_cbranch_vccz"}, + {logical::SCBranchExecZ, "s_cbranch_execz"}, + {logical::SCBranchExecNZ, "s_cbranch_execnz"}, + // Wait / Sync + {logical::SWaitCnt, "s_waitcnt"}, + {logical::SWaitTensorcnt, "s_wait_tensorcnt"}, + {logical::SWaitXCnt, "s_wait_xcnt"}, + // End program + {logical::SEndpgm, "s_endpgm"}, }; /** Returns expected asm mnemonic for (opcode, arch) if we have one; else nullopt. */ @@ -730,6 +1072,43 @@ TEST(LogicalToAsmComprehensive, AllInstructionsAllArchitectures) { logical::VCvtScalePkF16toBF8, logical::VCvtScaleSRF16toFP8, logical::VCvtScaleSRF16toBF8, + logical::SWaitAlu, + logical::SchedulingFence, + logical::DSLoadB64TrB16, + logical::GlobalLoadTR8B64, + logical::GlobalLoadTR16B128, + logical::SSetVgprMsb, + logical::SDcacheWb, + logical::VCvtFP8toF16, + logical::VCvtPkF32toFP16, + logical::SStoreB32, + logical::SStoreB64, + logical::SStoreB128, + logical::SStoreB256, + logical::SStoreB512, + logical::SAtomicInc, + logical::SAtomicDec, + logical::SCSelectB64, + logical::SCmpKEQU32, + logical::SCmpKGeU32, + logical::SCmpKGtU32, + logical::SCmpKLGU32, + logical::SFlbitI32B32, + logical::VReadlaneB32, + logical::VWritelaneB32, + logical::VPermlane16SwapB32, + logical::VPermlane32SwapB32, + logical::BufferLoadB16, + logical::BufferLoadB192, + logical::FlatLoadB192, + logical::BufferStoreD16U8, + logical::BufferStoreD16B16, + logical::FlatStoreD16B16, + logical::DSLoadB16, + logical::DSLoadD16HIU8, + logical::DSLoadD16HIU16, + logical::DSStoreU16, + logical::DSStoreB256, }; // Architecture-specific instructions: only test on the listed architecture(s). @@ -737,8 +1116,10 @@ TEST(LogicalToAsmComprehensive, AllInstructionsAllArchitectures) { using ArchTuple = std::tuple; std::map> ARCH_SPECIFIC = { // gfx1250 only - {logical::TensorLoadToLds, {{12, 5, 0}}}, - {logical::MXMFMA, {{12, 5, 0}}}, + {logical::TensorLoadToLds, {{12, 5, 0}}}, {logical::MXMFMA, {{12, 5, 0}}}, + {logical::DSLoadB96TrB6, {{12, 5, 0}}}, {logical::DSLoadB64TrB4, {{12, 5, 0}}}, + {logical::DSLoadB64TrB8, {{12, 5, 0}}}, {logical::DSLoadB128TrB16, {{12, 5, 0}}}, + {logical::DSLoadB192, {{12, 5, 0}}}, {logical::DSStoreB192, {{12, 5, 0}}}, }; std::cout << "Testing " << testedOpcodes.size() << " instructions on " << archs.size() diff --git a/shared/stinkytofu/tools/tablegen/CMakeLists.txt b/shared/stinkytofu/tools/tablegen/CMakeLists.txt index 3e176195ca37..9b7f817e1a09 100644 --- a/shared/stinkytofu/tools/tablegen/CMakeLists.txt +++ b/shared/stinkytofu/tools/tablegen/CMakeLists.txt @@ -14,6 +14,18 @@ set(SOURCES ) add_executable(tablegen "${SOURCES}") + +# GenLogicalIR.cpp #includes LogicalInstructionDefs.inc (not listed in other DEPENDS). +# Declare it explicitly so Ninja reruns tablegen / rebuilds this TU when the .inc +# changes even if implicit depfiles or caching miss the edge (avoids stale +# _stinkytofu.so vs source-tree .inc mtime in stinkytofu/__init__.py). +get_filename_component(LOGICAL_INSTRUCTION_DEFS + "${CMAKE_CURRENT_SOURCE_DIR}/../../src/ir/logical/LogicalInstructionDefs.inc" + ABSOLUTE) +set_source_files_properties( + "${CMAKE_CURRENT_SOURCE_DIR}/GenLogicalIR.cpp" + PROPERTIES OBJECT_DEPENDS "${LOGICAL_INSTRUCTION_DEFS}") + target_link_libraries(tablegen PRIVATE stinkytofu-warnings) # tablegen compiles IRLexer/PatternParser directly (not via libstinkytofu), # so it needs stinkytofu_EXPORTS to get dllexport instead of dllimport. @@ -125,7 +137,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E make_directory "${TABLEGEN_OUTPUT_DIR}/stinkytofu/ir/logical" COMMAND ${CMAKE_COMMAND} -E make_directory "${TABLEGEN_OUTPUT_DIR}/stinkytofu/ir/rocisa" COMMAND $ "${TABLEGEN_OUTPUT_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/../../hardware" - DEPENDS tablegen instruction_generated ${HARDWARE_SOURCE_FILES} ${PATTERN_FILE} ${HLIR_PATTERN_FILE} + DEPENDS tablegen instruction_generated ${HARDWARE_SOURCE_FILES} ${PATTERN_FILE} ${HLIR_PATTERN_FILE} "${LOGICAL_INSTRUCTION_DEFS}" COMMENT "Generating ISA definitions, IR classes, and pattern matchers with tablegen..." VERBATIM ) diff --git a/shared/stinkytofu/tools/tablegen/GenLogicalIR.cpp b/shared/stinkytofu/tools/tablegen/GenLogicalIR.cpp index 6917a2e5fc7f..b5ec2a9c5fdf 100644 --- a/shared/stinkytofu/tools/tablegen/GenLogicalIR.cpp +++ b/shared/stinkytofu/tools/tablegen/GenLogicalIR.cpp @@ -329,6 +329,10 @@ static bool genOpcodeMappings(const std::string& outdir) { out << " return \"Label\";\n"; out << " case IntrinsicCall:\n"; out << " return \"IntrinsicCall\";\n"; + out << " case SWaitAlu:\n"; + out << " return \"SWaitAlu\";\n"; + out << " case SchedulingFence:\n"; + out << " return \"SchedulingFence\";\n"; out << " default:\n"; out << " return \"INVALID\";\n"; @@ -359,6 +363,10 @@ static bool genOpcodeMappings(const std::string& outdir) { out << " return \"label\";\n"; out << " case IntrinsicCall:\n"; out << " return \"intrinsic_call\";\n"; + out << " case SWaitAlu:\n"; + out << " return \"s_wait_alu\";\n"; + out << " case SchedulingFence:\n"; + out << " return \"scheduling_fence\";\n"; out << " default:\n"; out << " return \"invalid\";\n"; @@ -585,6 +593,10 @@ static bool genIRClasses(const std::string& outdir) { out << " case logical::VMulPKF32:\n"; out << " case logical::VMovB64:\n"; out << " case logical::VLShiftLeftOrB32:\n"; + out << " case logical::SAddU64:\n"; + out << " case logical::VAddNCU64:\n"; + out << " case logical::VAddLShiftLeftU32:\n"; + out << " case logical::VLShiftLeftAddU32:\n"; out << " return true;\n"; out << " default:\n"; out << " return false;\n";