diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1f379810..47f5b5a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,3 +7,8 @@ /CONTRIBUTING.md @PTO-ISA/pycircuit-maintainers /SECURITY.md @PTO-ISA/pycircuit-maintainers /CODE_OF_CONDUCT.md @PTO-ISA/pycircuit-maintainers + +# Agentic Circuit remains a separate semantic and package boundary in the +# shared repository, but follows the same maintainer review authority. +/components/agentic-circuit/ @PTO-ISA/pycircuit-maintainers +/docs/acir/ @PTO-ISA/pycircuit-maintainers diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9449c64..4889a1d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,24 +6,27 @@ repos: - id: check-yaml args: [--unsafe] - id: end-of-file-fixer + exclude: ^components/agentic-circuit/ - id: trailing-whitespace + exclude: ^components/agentic-circuit/ - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.8 hooks: - id: ruff - exclude: ^(designs/XiangShan-pyc/|designs/outerCube/|designs/BypassUnit/|designs/RegisterFile/) + exclude: ^(components/agentic-circuit/|designs/XiangShan-pyc/|designs/outerCube/|designs/BypassUnit/|designs/RegisterFile/) - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black + exclude: ^components/agentic-circuit/ - repo: https://github.com/DavidAnson/markdownlint-cli2 rev: v0.18.1 hooks: - id: markdownlint-cli2 - exclude: ^(docs/gates/logs/|designs/outerCube/.*\.md$|designs/XiangShan-pyc/docs/design_hierarchy_plan\.md$|docs/cycle_balance_improvement.*\.md$) + exclude: ^(components/agentic-circuit/|docs/gates/logs/|designs/outerCube/.*\.md$|designs/XiangShan-pyc/docs/design_hierarchy_plan\.md$|docs/cycle_balance_improvement.*\.md$) - repo: local hooks: diff --git a/CMakeLists.txt b/CMakeLists.txt index 35439be0..d05b1662 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,8 @@ include(GNUInstallDirs) option(PYC_BUILD_MLIR_TOOLS "Build MLIR-based pyc tools" ON) option(PYC_BUILD_RUNTIME_LIB "Build pyc6 runtime library" ON) +option(PYC_BUILD_AGENTIC_CIRCUIT "Build the integrated Agentic Circuit component" OFF) +option(PYC_BUILD_AGENTIC_CIRCUIT_TESTS "Build Agentic Circuit tests" OFF) option(PYC_RUNTIME_ENABLE_ZLIB_TRACE "Enable gzip trace output in runtime (optional)" OFF) option(PYC_RUNTIME_BUILD_SHARED "Build shared pyc6 runtime library" OFF) option(PYC_INSTALL_TEMPLATES "Install runtime template libraries" ON) @@ -34,6 +36,13 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") endif() endif() +if(PYC_BUILD_AGENTIC_CIRCUIT) + # Discover LLVM/MLIR in the common parent scope so the PYC and ACIR sibling + # subprojects share one imported target graph. + find_package(LLVM 22.1.8 EXACT CONFIG REQUIRED) + find_package(MLIR 22.1.8 EXACT CONFIG REQUIRED) +endif() + if(PYC_BUILD_MLIR_TOOLS) add_subdirectory(compiler/mlir) endif() @@ -42,6 +51,17 @@ if(PYC_BUILD_RUNTIME_LIB) add_subdirectory(runtime/cpp) endif() +if(PYC_BUILD_AGENTIC_CIRCUIT) + if(NOT PYC_BUILD_MLIR_TOOLS OR NOT PYC_BUILD_RUNTIME_LIB) + message(FATAL_ERROR + "PYC_BUILD_AGENTIC_CIRCUIT requires PYC_BUILD_MLIR_TOOLS=ON and " + "PYC_BUILD_RUNTIME_LIB=ON so ACIR-to-PYC uses the repo-local pyc6 toolchain") + endif() + set(ACIR_BUILD_TESTING "${PYC_BUILD_AGENTIC_CIRCUIT_TESTS}" CACHE BOOL + "Build Agentic Circuit tests" FORCE) + add_subdirectory(components/agentic-circuit) +endif() + if(PYC_INSTALL_TEMPLATES) install( DIRECTORY runtime/cpp diff --git a/LICENSE b/LICENSE index ea8f6036..cc7bc6d7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2026, Kevin Zhou +Copyright (c) 2026, PTO-ISA Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index 39935770..ef29d0ab 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # pyCircuit 6

- License + License Python MLIR CI @@ -9,12 +9,15 @@ Latest release

-pyCircuit is a Python hardware construction language. It lowers cycle-aware -designs to a verified MLIR dialect, then emits synthesizable Verilog and a C++ -cycle model from the same IR. +pyCircuit is a Python hardware construction and architecture-modeling +repository. The `pycircuit` frontend lowers cycle-aware designs to verified PYC +MLIR and emits synthesizable Verilog and a C++ cycle model. The retained +`agentic_circuit` frontend lowers architecture/process/queue descriptions to +ACIR, then targets either ACSim/gfsim or the pyCircuit 6 hardware flow. [`PTO-ISA/pyCircuit`](https://github.com/PTO-ISA/pyCircuit) is the canonical -repository, release authority, and only source of truth. +repository, release authority, and only active source of truth for both +pyCircuit and Agentic Circuit. [`LinxISA/pyCircuit`](https://github.com/LinxISA/pyCircuit) is its downstream fork for Linx integration work. @@ -43,6 +46,14 @@ pre-commit install bash flows/scripts/pyc build ``` +The Agentic Circuit frontend remains a separate distribution and Python +namespace in the same source repository: + +```bash +python3 -m pip install -e components/agentic-circuit +agentic-circuit --help +``` + Release wheels, once published, use the distribution name `pycircuit-hisi`; the Python import remains `pycircuit`. The repository does not claim a PyPI release until the corresponding PTO-ISA release workflow has completed. @@ -51,6 +62,24 @@ The staged compiler is installed under `.pycircuit_out/toolchain/install/`. Set `PYC_TOOLCHAIN_ROOT` to that directory when running end-to-end builds from a source checkout. +## Agentic Circuit and ACIR + +ACIR remains an independent, upper-level MLIR dialect. It is not folded into +the PYC dialect and does not replace the Cycle-Aware Signal model: + +```text +agentic_circuit frontend -> ACPy 0.3 -> ACIR + |-> ACSim -> gfsim + `-> PYC -> pycc -> pyc6 C++ / Verilog + +pycircuit frontend -> Cycle-Aware Signal -> PYC -> pycc -> pyc6 C++ / Verilog +``` + +The public `agentic_circuit` import and `agentic-circuit` CLI remain distinct +from `pycircuit`. AC symbols are not re-exported from `pycircuit.__init__`. +See the [ACIR architecture overview](docs/acir/index.md) and +[migration record](docs/acir/migration.md). + ## First cycle-aware design ```python @@ -108,6 +137,15 @@ bash flows/scripts/run_examples.sh bash flows/scripts/run_sims.sh ``` +Run the Agentic Circuit frontend and contract lane: + +```bash +PYTHONPATH=components/agentic-circuit/src \ +python3 -m unittest discover \ + -s components/agentic-circuit/tests \ + -p 'test_*.py' +``` + System tests require a built toolchain and Verilator: ```bash @@ -124,12 +162,16 @@ pytest tests/system -m system - [IR specification](docs/IR_SPEC.md) - [pyCircuit 6 decisions](docs/rfcs/pyc6-decisions.md) - [pyCircuit 6 evolution plan](docs/pyc6-plan.md) +- [ACIR architecture and frontend](docs/acir/index.md) +- [Agentic Circuit migration](docs/acir/migration.md) ## Repository governance -PTO-ISA owns product decisions, releases, package publication, and the default -branch. Linx integration changes should be developed so they can be reviewed -upstream; the LinxISA fork follows the upstream default branch. +PTO-ISA owns product decisions, both Python distributions, releases, package +publication, and the default branch. Linx integration changes should be +developed so they can be reviewed upstream; the LinxISA fork follows the +upstream default branch. The former standalone Agentic Circuit repository is +retired only after both AC and PYC closure gates pass. - [Contribution workflow](docs/development/contributing-workflow.md) - [Testing and gates](docs/development/testing-and-gates.md) @@ -146,6 +188,7 @@ trace, and gate contracts use `libpyc6_runtime`, `PYC6TRC3`, and pyCircuit/ ├── compiler/frontend/pycircuit/ # Python language frontend ├── compiler/mlir/ # pyc dialect, passes, pycc, and emitters +├── components/agentic-circuit/ # AC frontend, ACIR/ACSim, gfsim, and AC tools ├── runtime/ # C++ simulation and Verilog primitives ├── designs/examples/ # Supported product examples ├── flows/ # Build and validation orchestration @@ -155,4 +198,6 @@ pyCircuit/ ## License -pyCircuit is licensed under the MIT License. See [LICENSE](LICENSE). +pyCircuit, including the integrated Agentic Circuit sources, is licensed under +the BSD 3-Clause License. See [LICENSE](LICENSE) and the +[relicensing record](docs/legal/AC-RELICENSE-BSD-3-CLAUSE.md). diff --git a/components/agentic-circuit/.clang-format b/components/agentic-circuit/.clang-format new file mode 100644 index 00000000..9b3aa8b7 --- /dev/null +++ b/components/agentic-circuit/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: LLVM diff --git a/components/agentic-circuit/.clang-tidy b/components/agentic-circuit/.clang-tidy new file mode 100644 index 00000000..784da8f0 --- /dev/null +++ b/components/agentic-circuit/.clang-tidy @@ -0,0 +1,47 @@ +# clang-tidy gate for Agentic Circuit owned sources. +# +# The gate enforces the static analyzer plus high-signal bugprone/performance +# checks that hold across the entire owned tree. Noise-heavy checks whose +# findings are dominated by MLIR/gtest idioms (macro parentheses, optional +# access heuristics, generated enum sizing, gtest static initialization) are +# intentionally out of scope for the CI gate. +Checks: > + -*, + clang-analyzer-*, + bugprone-assert-side-effect, + bugprone-bool-pointer-implicit-conversion, + bugprone-dangling-handle, + bugprone-exception-escape, + bugprone-implicit-widening-of-multiplication-result, + bugprone-inc-dec-in-conditions, + bugprone-inaccurate-erase, + bugprone-infinite-loop, + bugprone-misplaced-operator-in-strlen-in-alloc, + bugprone-misplaced-widening-cast, + bugprone-not-null-terminated-result, + bugprone-optional-value-conversion, + bugprone-posix-return, + bugprone-string-constructor, + bugprone-stringview-nullptr, + bugprone-suspicious-missing-comma, + bugprone-suspicious-realloc-usage, + bugprone-suspicious-string-compare, + bugprone-swapped-arguments, + bugprone-undelegated-constructor, + bugprone-undefined-memory-manipulation, + bugprone-unhandled-self-assignment, + bugprone-use-after-move, + bugprone-virtual-near-miss, + performance-faster-string-find, + performance-implicit-conversion-in-loop, + performance-inefficient-algorithm, + performance-inefficient-string-concatenation, + performance-move-const-arg, + performance-no-automatic-move, + performance-no-int-to-ptr, + performance-trivially-destructible, + performance-type-promotion-in-math-fn, + performance-unnecessary-copy-initialization, + -clang-analyzer-core.StackAddressEscape +WarningsAsErrors: '*' +HeaderFilterRegex: '.*agentic-circuit/(include|lib|tools|unittests)/.*' diff --git a/components/agentic-circuit/.editorconfig b/components/agentic-circuit/.editorconfig new file mode 100644 index 00000000..2e0d78c1 --- /dev/null +++ b/components/agentic-circuit/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.py] +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/components/agentic-circuit/.gitattributes b/components/agentic-circuit/.gitattributes new file mode 100644 index 00000000..f9080a8d --- /dev/null +++ b/components/agentic-circuit/.gitattributes @@ -0,0 +1,9 @@ +* text=auto eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.pdf binary +references/davincioo-gfsim/upstream/** whitespace=-trailing-space diff --git a/components/agentic-circuit/.github/dependabot.yml b/components/agentic-circuit/.github/dependabot.yml new file mode 100644 index 00000000..3b5ca194 --- /dev/null +++ b/components/agentic-circuit/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" diff --git a/components/agentic-circuit/.github/workflows/ci.yml b/components/agentic-circuit/.github/workflows/ci.yml new file mode 100644 index 00000000..fe833dca --- /dev/null +++ b/components/agentic-circuit/.github/workflows/ci.yml @@ -0,0 +1,327 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + python-frontend: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13"] + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Set up Python + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: requirements-dev.lock + + - name: Install locked development requirements + run: python -m pip install --disable-pip-version-check -r requirements-dev.lock + + - name: Run Python frontend contracts + env: + PYTHONPATH: src + run: | + python -m unittest discover -s tests/python_frontend -v + python -m unittest \ + tests.cli.test_cli_parser.CliParserTest.test_exact_command_inventory \ + tests.cli.test_cli_parser.CliParserTest.test_aliases_and_repeated_singleton_options_are_rejected \ + tests.cli.test_all_commands -v + PYTHONHASHSEED=1 python -m unittest tests.python_frontend.test_determinism -v + PYTHONHASHSEED=99 python -m unittest tests.python_frontend.test_determinism -v + + contracts-and-configure: + runs-on: ubuntu-24.04 + env: + LLVM_RELEASE: "22.1.8" + LLVM_SOURCE_SHA256: "922f1817a0df7b1489272d18134ee0087a8b068828f87ac63b9861b1a9965888" + LLVM_SOURCE_URL: "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/llvm-project-22.1.8.src.tar.xz" + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Set up Python + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 + with: + python-version: "3.12" + cache: "pip" + cache-dependency-path: requirements-dev.lock + + - name: Install locked development requirements + run: python -m pip install --disable-pip-version-check -r requirements-dev.lock + + - name: Install native test dependencies + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends libgtest-dev + + - name: Run repository contracts + run: python -m unittest tests.contracts.test_contracts -v + + - name: Run IR coverage gate + run: | + python -m unittest tests.contracts.test_ir_coverage -v + python scripts/check-ir-coverage.py + + - name: Check clang-format on owned sources + run: | + git ls-files "*.cpp" "*.h" ":!references/**" | + xargs clang-format --dry-run --Werror + + - name: Cache content-addressed LLVM source archive + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 + with: + path: .cache/llvm-project-22.1.8.src.tar.xz + key: llvm-${{ runner.os }}-${{ runner.arch }}-${{ env.LLVM_SOURCE_SHA256 }}-${{ hashFiles('toolchains/llvm.lock.json') }} + + - name: Fetch and verify locked LLVM source + run: | + mkdir -p .cache + if [[ ! -f .cache/llvm-project-${LLVM_RELEASE}.src.tar.xz ]]; then + curl --fail --location --proto '=https' --tlsv1.2 \ + --output .cache/llvm-project-${LLVM_RELEASE}.src.tar.xz \ + "${LLVM_SOURCE_URL}" + fi + echo "${LLVM_SOURCE_SHA256} .cache/llvm-project-${LLVM_RELEASE}.src.tar.xz" | sha256sum --check --strict + tar -C .cache -xf .cache/llvm-project-${LLVM_RELEASE}.src.tar.xz + + - name: Compute LLVM build fingerprint + id: llvm-build-fingerprint + run: | + { + pwd -P + command -v c++ + c++ --version + cmake --version + ninja --version + uname -m + ldd --version 2>&1 | head -n 1 + } | sha256sum | awk '{print "value=" $1}' >> "${GITHUB_OUTPUT}" + + - name: Restore exact LLVM build outputs + id: llvm-build-cache + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 + with: + path: .cache/llvm-build + key: llvm-build-${{ runner.os }}-${{ runner.arch }}-${{ env.LLVM_SOURCE_SHA256 }}-${{ hashFiles('toolchains/llvm.lock.json', 'scripts/ci-build-llvm.sh') }}-${{ steps.llvm-build-fingerprint.outputs.value }} + + - name: Build locked MLIR package + id: llvm-build + if: steps.llvm-build-cache.outputs.cache-hit != 'true' + run: bash scripts/ci-build-llvm.sh + + - name: Save exact LLVM build outputs + # Save even when later steps fail: the LLVM build takes over an hour + # and must never be rebuilt just because a test regressed. Only save + # a fully built tree, never a partial one. + if: always() && steps.llvm-build-cache.outputs.cache-hit != 'true' && steps.llvm-build.conclusion == 'success' + uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 + with: + path: .cache/llvm-build + key: llvm-build-${{ runner.os }}-${{ runner.arch }}-${{ env.LLVM_SOURCE_SHA256 }}-${{ hashFiles('toolchains/llvm.lock.json', 'scripts/ci-build-llvm.sh') }}-${{ steps.llvm-build-fingerprint.outputs.value }} + + - name: Verify exact LLVM build outputs + run: | + test -x .cache/llvm-build/bin/mlir-opt + test -x .cache/llvm-build/bin/FileCheck + test -x .cache/llvm-build/bin/not + test -x .cache/llvm-build/bin/split-file + test -x .cache/llvm-build/bin/count + test -f .cache/llvm-build/lib/cmake/llvm/LLVMConfigVersion.cmake + test -f .cache/llvm-build/lib/cmake/mlir/MLIRConfigVersion.cmake + .cache/llvm-build/bin/mlir-opt --version | grep -F "LLVM version ${LLVM_RELEASE}" + grep -F "set(PACKAGE_VERSION \"${LLVM_RELEASE}\")" .cache/llvm-build/lib/cmake/llvm/LLVMConfigVersion.cmake + grep -F "set(PACKAGE_VERSION \"${LLVM_RELEASE}\")" .cache/llvm-build/lib/cmake/mlir/MLIRConfigVersion.cmake + grep -F "CMAKE_HOME_DIRECTORY:INTERNAL=${GITHUB_WORKSPACE}/.cache/llvm-project-${LLVM_RELEASE}.src/llvm" .cache/llvm-build/CMakeCache.txt + grep -F "CMAKE_CACHEFILE_DIR:INTERNAL=${GITHUB_WORKSPACE}/.cache/llvm-build" .cache/llvm-build/CMakeCache.txt + + - name: Configure development preset + env: + LLVM_PREFIX: "${{ github.workspace }}/.cache/llvm-build" + run: cmake --preset dev-llvm22 -DMLIR_DIR="${LLVM_PREFIX}/lib/cmake/mlir" -DACIR_ENABLE_ASSERTIONS=ON + + - name: Build development preset with assertions + run: cmake --build --preset dev-llvm22 + + - name: Run lit and FileCheck suite + # Run lit directly with -v so failed tests print their command output; + # the add_lit_testsuite default is succinct and hides failure bodies. + run: | + cmake --build --preset dev-llvm22 --target acir-opt acir-opt-internal + lit -v build/dev-llvm22/test + + - name: Run unit tests with assertions + run: ctest --test-dir build/dev-llvm22 --output-on-failure + + - name: Run public CLI and Python frontend tests + env: + PYTHONPATH: src:${{ github.workspace }}/build/dev-llvm22/python + run: | + python -m unittest discover -s tests/cli -v + python -m unittest discover -s tests/python_frontend -v + + - name: Run workspace tool and end-to-end tests + env: + PYTHONPATH: src:${{ github.workspace }}/build/dev-llvm22/python + run: | + python -m unittest discover -s tests/tools -v + python -m unittest \ + tests.e2e.test_workspace_examples \ + tests.e2e.test_workspace_npu -v + + - name: Audit workspace determinism and generated dependencies + env: + PYTHONPATH: src:${{ github.workspace }}/build/dev-llvm22/python + RUNS: "2" + run: scripts/audit-workspace-determinism.sh + + - name: Install clang-tidy + run: sudo apt-get install --yes --no-install-recommends clang-tidy + + - name: Run clang-tidy gate on owned sources + run: | + git ls-files "lib/*.cpp" "tools/*.cpp" "unittests/*.cpp" | + xargs -P 4 -I {} clang-tidy -p build/dev-llvm22 \ + --config-file=.clang-tidy \ + --header-filter='.*agentic-circuit/(include|lib|tools|unittests)/.*' \ + {} + + - name: Configure release preset + env: + LLVM_PREFIX: "${{ github.workspace }}/.cache/llvm-build" + run: cmake --preset release-llvm22 -DMLIR_DIR="${LLVM_PREFIX}/lib/cmake/mlir" + + - name: Build release preset + run: cmake --build --preset release-llvm22 + + - name: Run release test suites + run: | + cmake --build --preset release-llvm22 --target check-acir + ctest --test-dir build/release-llvm22 --output-on-failure + + - name: Install to a temporary prefix + run: cmake --install build/release-llvm22 --prefix "${RUNNER_TEMP}/acir-prefix" + + - name: Build and run the installed-tree consumer + env: + LLVM_PREFIX: "${{ github.workspace }}/.cache/llvm-build" + run: | + cmake -S tests/install-consumer -B "${RUNNER_TEMP}/acir-consumer" \ + -GNinja -DCMAKE_BUILD_TYPE=Release \ + -DAgenticCircuit_DIR="${RUNNER_TEMP}/acir-prefix/lib/cmake/AgenticCircuit" \ + -DLLVM_DIR="${LLVM_PREFIX}/lib/cmake/llvm" \ + -DMLIR_DIR="${LLVM_PREFIX}/lib/cmake/mlir" + cmake --build "${RUNNER_TEMP}/acir-consumer" + "${RUNNER_TEMP}/acir-consumer/process-state-plan-consumer" + + - name: Check whitespace + run: git diff --check + + sanitizers: + name: ${{ matrix.preset }} + needs: contracts-and-configure + runs-on: ubuntu-24.04 + env: + LLVM_RELEASE: "22.1.8" + LLVM_SOURCE_SHA256: "922f1817a0df7b1489272d18134ee0087a8b068828f87ac63b9861b1a9965888" + LLVM_SOURCE_URL: "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/llvm-project-22.1.8.src.tar.xz" + strategy: + fail-fast: false + matrix: + preset: ["asan-llvm22", "ubsan-llvm22"] + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - name: Set up Python + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 + with: + python-version: "3.12" + cache: "pip" + cache-dependency-path: requirements-dev.lock + + - name: Install locked development requirements + run: python -m pip install --disable-pip-version-check -r requirements-dev.lock + + - name: Install native test dependencies + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends libgtest-dev + + - name: Cache content-addressed LLVM source archive + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 + with: + path: .cache/llvm-project-22.1.8.src.tar.xz + key: llvm-${{ runner.os }}-${{ runner.arch }}-${{ env.LLVM_SOURCE_SHA256 }}-${{ hashFiles('toolchains/llvm.lock.json') }} + + - name: Fetch and verify locked LLVM source + run: | + mkdir -p .cache + if [[ ! -f .cache/llvm-project-${LLVM_RELEASE}.src.tar.xz ]]; then + curl --fail --location --proto '=https' --tlsv1.2 \ + --output .cache/llvm-project-${LLVM_RELEASE}.src.tar.xz \ + "${LLVM_SOURCE_URL}" + fi + echo "${LLVM_SOURCE_SHA256} .cache/llvm-project-${LLVM_RELEASE}.src.tar.xz" | sha256sum --check --strict + tar -C .cache -xf .cache/llvm-project-${LLVM_RELEASE}.src.tar.xz + + - name: Compute LLVM build fingerprint + id: llvm-build-fingerprint + run: | + { + pwd -P + command -v c++ + c++ --version + cmake --version + ninja --version + uname -m + ldd --version 2>&1 | head -n 1 + } | sha256sum | awk '{print "value=" $1}' >> "${GITHUB_OUTPUT}" + + - name: Restore exact LLVM build outputs + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 + with: + path: .cache/llvm-build + key: llvm-build-${{ runner.os }}-${{ runner.arch }}-${{ env.LLVM_SOURCE_SHA256 }}-${{ hashFiles('toolchains/llvm.lock.json', 'scripts/ci-build-llvm.sh') }}-${{ steps.llvm-build-fingerprint.outputs.value }} + fail-on-cache-miss: true + + - name: Verify cached LLVM package + run: | + test -x .cache/llvm-build/bin/mlir-opt + test -f .cache/llvm-build/lib/cmake/llvm/LLVMConfigVersion.cmake + test -f .cache/llvm-build/lib/cmake/mlir/MLIRConfigVersion.cmake + .cache/llvm-build/bin/mlir-opt --version | grep -F "LLVM version ${LLVM_RELEASE}" + + - name: Configure sanitizer preset + env: + LLVM_PREFIX: "${{ github.workspace }}/.cache/llvm-build" + run: >- + cmake --preset "${{ matrix.preset }}" + -DMLIR_DIR="${LLVM_PREFIX}/lib/cmake/mlir" + + - name: Build sanitizer targets + run: >- + cmake --build --preset "${{ matrix.preset }}" + --target GfsimTests CodeGenTests + + - name: Run sanitizer tests + env: + ASAN_OPTIONS: "detect_leaks=1:strict_string_checks=1:check_initialization_order=1:allow_user_poisoning=0:halt_on_error=1" + UBSAN_OPTIONS: "print_stacktrace=1:halt_on_error=1" + run: >- + ctest --test-dir "build/${{ matrix.preset }}" + --output-on-failure -R '^(GfsimTests|CodeGenTests)$' diff --git a/components/agentic-circuit/.gitignore b/components/agentic-circuit/.gitignore new file mode 100644 index 00000000..10e8e12a --- /dev/null +++ b/components/agentic-circuit/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +.worktrees/ +.venv/ +build/ +cmake-build-*/ +compile_commands.json +__pycache__/ +*.py[cod] +.coverage +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ diff --git a/components/agentic-circuit/CHANGELOG.md b/components/agentic-circuit/CHANGELOG.md new file mode 100644 index 00000000..05418ff8 --- /dev/null +++ b/components/agentic-circuit/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +All notable changes to Agentic Circuit will be documented here. The project +follows Keep a Changelog and will use Semantic Versioning after its first +release. + +## Unreleased + +### Added + +- Reproducible LLVM/MLIR 22.1.8 repository and development-toolchain baseline. +- A strict repository-local DavinciOO JSONL adapter that emits canonical, + validated `pto-trace@0.1` documents without widening the simulator input + contract. +- Committed runtime statistics and Chrome Trace Event JSONL, plus a + deterministic repository-local Perfetto packer. +- Six complete golden workspaces covering queueing, + backpressure, request/response memory, nested arrays, time-domain bridging, + and suspended processes. +- A hierarchical superscalar NPU showcase with typed decode, dependency-aware + oldest-ready issue, four finite execution-engine classes, memory behavior, + completion, and in-order retirement. +- Workspace replay, equivalent-root, legal Work-permutation, dependency-scan, + installation, sanitizer, and end-to-end CI gates. +- A provenance-locked DavinciOO gfsim C++ reference model under + `references`, with an executable smoke trace and an explicit + ACIR-to-gfsim generation boundary. diff --git a/components/agentic-circuit/CMakeLists.txt b/components/agentic-circuit/CMakeLists.txt new file mode 100644 index 00000000..eb581a93 --- /dev/null +++ b/components/agentic-circuit/CMakeLists.txt @@ -0,0 +1,215 @@ +cmake_minimum_required(VERSION 3.25) + +project(AgenticCircuit VERSION 0.1.0 LANGUAGES C CXX) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules") +include(cmake/LLVMToolchain.cmake) +acir_configure_llvm() + +function(acir_append_llvm_link_flag flag) + set_property(GLOBAL APPEND PROPERTY ACIR_LLVM_LINK_FLAGS_PROPERTY "${flag}") +endfunction() + +function(acir_collect_llvm_link_target target) + get_property(_visited GLOBAL PROPERTY ACIR_LLVM_LINK_TARGETS_PROPERTY) + if("${target}" IN_LIST _visited) + return() + endif() + set_property(GLOBAL APPEND PROPERTY ACIR_LLVM_LINK_TARGETS_PROPERTY "${target}") + + get_target_property(_location "${target}" IMPORTED_LOCATION) + if(NOT _location OR _location MATCHES "-NOTFOUND$") + get_target_property(_configurations "${target}" IMPORTED_CONFIGURATIONS) + foreach(_configuration IN LISTS _configurations) + string(TOUPPER "${_configuration}" _configuration_upper) + get_target_property( + _candidate "${target}" "IMPORTED_LOCATION_${_configuration_upper}" + ) + if(_candidate AND NOT _candidate MATCHES "-NOTFOUND$") + set(_location "${_candidate}") + break() + endif() + endforeach() + endif() + if(_location AND NOT _location MATCHES "-NOTFOUND$") + acir_append_llvm_link_flag("${_location}") + endif() + + get_target_property(_dependencies "${target}" INTERFACE_LINK_LIBRARIES) + if(NOT _dependencies OR _dependencies MATCHES "-NOTFOUND$") + return() + endif() + foreach(_dependency IN LISTS _dependencies) + if(_dependency MATCHES "^\\$]+)>$") + set(_dependency "${CMAKE_MATCH_1}") + elseif(_dependency MATCHES "^\\$<") + message(FATAL_ERROR + "unsupported LLVM link dependency expression: ${_dependency}" + ) + endif() + if(TARGET "${_dependency}") + acir_collect_llvm_link_target("${_dependency}") + elseif(IS_ABSOLUTE "${_dependency}" OR _dependency MATCHES "^-") + acir_append_llvm_link_flag("${_dependency}") + else() + acir_append_llvm_link_flag("-l${_dependency}") + endif() + endforeach() +endfunction() + +set_property(GLOBAL PROPERTY ACIR_LLVM_LINK_FLAGS_PROPERTY "") +set_property(GLOBAL PROPERTY ACIR_LLVM_LINK_TARGETS_PROPERTY "") +if(TARGET LLVM) + acir_collect_llvm_link_target(LLVM) +else() + acir_collect_llvm_link_target(LLVMSupport) +endif() +get_property(ACIR_LLVM_LINK_FLAGS GLOBAL PROPERTY ACIR_LLVM_LINK_FLAGS_PROPERTY) + +list(APPEND CMAKE_MODULE_PATH "${LLVM_DIR}" "${MLIR_DIR}") +include(TableGen) +include(AddLLVM) +include(AddMLIR) +include(AddACIR) + +set(_ACIR_BUILD_TESTING_DEFAULT "${PROJECT_IS_TOP_LEVEL}") +if(DEFINED BUILD_TESTING) + set(_ACIR_BUILD_TESTING_DEFAULT "${BUILD_TESTING}") +endif() +option(ACIR_BUILD_TESTING "Build Agentic Circuit tests" + "${_ACIR_BUILD_TESTING_DEFAULT}") +unset(_ACIR_BUILD_TESTING_DEFAULT) +if(ACIR_BUILD_TESTING) + include(CTest) + enable_testing() + find_package(Python3 3.11 REQUIRED COMPONENTS Interpreter) + find_program(ACIR_LIT_EXECUTABLE NAMES llvm-lit lit + HINTS "${CMAKE_CURRENT_SOURCE_DIR}/.venv/bin" + REQUIRED + ) + set(LLVM_EXTERNAL_LIT "${ACIR_LIT_EXECUTABLE}" CACHE FILEPATH + "lit executable used by Agentic Circuit tests" FORCE + ) +endif() + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + +include_directories(SYSTEM ${LLVM_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS}) +include_directories( + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_BINARY_DIR}/include" +) +add_definitions(${LLVM_DEFINITIONS}) + +option(ACIR_ENABLE_ASSERTIONS "Keep C/C++ assertions enabled" OFF) +option( + ACIR_BUILD_REFERENCE_MODELS + "Build non-installed reference models used as code-generation baselines" + ${ACIR_BUILD_TESTING} +) + +add_library(acir_project_options INTERFACE) +add_library(AgenticCircuit::ProjectOptions ALIAS acir_project_options) +set_target_properties(acir_project_options PROPERTIES EXPORT_NAME ProjectOptions) +target_compile_features(acir_project_options INTERFACE cxx_std_20) +if(ACIR_ENABLE_ASSERTIONS) + if(MSVC) + target_compile_options(acir_project_options INTERFACE /UNDEBUG) + else() + target_compile_options(acir_project_options INTERFACE -UNDEBUG) + endif() +endif() + +include(CMakePackageConfigHelpers) +configure_package_config_file( + cmake/AgenticCircuitConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/AgenticCircuitConfig.cmake" + INSTALL_DESTINATION lib/cmake/AgenticCircuit +) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/AgenticCircuitConfigVersion.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY ExactVersion +) + +install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/AgenticCircuitConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/AgenticCircuitConfigVersion.cmake" + DESTINATION lib/cmake/AgenticCircuit +) +install(TARGETS acir_project_options EXPORT AgenticCircuitTargets) +install( + EXPORT AgenticCircuitTargets + FILE AgenticCircuitTargets.cmake + NAMESPACE AgenticCircuit:: + DESTINATION lib/cmake/AgenticCircuit +) +install(FILES cmake/modules/AddACIR.cmake DESTINATION lib/cmake/AgenticCircuit/modules) +install(DIRECTORY include/acir DESTINATION include) +install(DIRECTORY include/gfsim DESTINATION include) +install(DIRECTORY resources/pyc_runtime DESTINATION share/AgenticCircuit) +install( + DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include/acir" + DESTINATION include + FILES_MATCHING + PATTERN "*.h.inc" +) + +# The Python bridge locates the same installed-style runtime surface in the +# build tree, keeping native compilation independent of source-tree paths. +file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/include/gfsim" + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/include") +add_custom_target(acir_runtime_headers + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${CMAKE_CURRENT_SOURCE_DIR}/include/gfsim" + "${CMAKE_CURRENT_BINARY_DIR}/include/gfsim" + COMMENT "Synchronizing gfsim runtime headers" +) + +add_subdirectory(include) +add_subdirectory(lib) +add_subdirectory(python) +add_subdirectory(tools) +if(ACIR_BUILD_REFERENCE_MODELS) + add_subdirectory(references/davincioo-gfsim) +endif() +if(ACIR_BUILD_TESTING) + add_subdirectory(test) + add_subdirectory(unittests) + add_test( + NAME WorkspaceE2ETests + COMMAND "${Python3_EXECUTABLE}" -m unittest tests.e2e.test_workspace_examples -v + ) + set_tests_properties(WorkspaceE2ETests PROPERTIES + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + TIMEOUT 300 + LABELS "e2e;workspace" + ENVIRONMENT "AGENTIC_CIRCUIT_TEST_BUILD_DIR=${CMAKE_BINARY_DIR}" + ) + add_test( + NAME DavinciOOTraceTests + COMMAND "${Python3_EXECUTABLE}" -m unittest + tests.e2e.test_davincioo_trace -v + ) + set_tests_properties(DavinciOOTraceTests PROPERTIES + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + TIMEOUT 60 + LABELS "e2e;davincioo" + ) + add_test( + NAME PycVerilogBackendTests + COMMAND "${Python3_EXECUTABLE}" -m unittest + tests.tools.test_pyc_verilog_backend -v + ) + set_tests_properties(PycVerilogBackendTests PROPERTIES + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + TIMEOUT 30 + LABELS "pyc;verilog" + ) +endif() diff --git a/components/agentic-circuit/CMakePresets.json b/components/agentic-circuit/CMakePresets.json new file mode 100644 index 00000000..54334543 --- /dev/null +++ b/components/agentic-circuit/CMakePresets.json @@ -0,0 +1,92 @@ +{ + "version": 7, + "cmakeMinimumRequired": { + "major": 3, + "minor": 25, + "patch": 0 + }, + "configurePresets": [ + { + "name": "llvm22-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": true, + "MLIR_DIR": "$env{LLVM_PREFIX}/lib/cmake/mlir" + } + }, + { + "name": "dev-llvm22", + "displayName": "Development with LLVM 22.1.8", + "inherits": "llvm22-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "ACIR_ENABLE_ASSERTIONS": true + }, + "environment": { + "LLVM_PREFIX": "/opt/homebrew/opt/llvm" + } + }, + { + "name": "release-llvm22", + "displayName": "Release with LLVM 22.1.8", + "inherits": "llvm22-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "ACIR_ENABLE_ASSERTIONS": false + }, + "environment": { + "LLVM_PREFIX": "/opt/homebrew/opt/llvm" + } + }, + { + "name": "asan-llvm22", + "displayName": "ASAN with LLVM 22.1.8", + "inherits": "llvm22-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "ACIR_ENABLE_ASSERTIONS": true, + "CMAKE_CXX_FLAGS": "-fsanitize=address -fno-omit-frame-pointer", + "CMAKE_C_FLAGS": "-fsanitize=address -fno-omit-frame-pointer", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address" + }, + "environment": { + "LLVM_PREFIX": "/opt/homebrew/opt/llvm" + } + }, + { + "name": "ubsan-llvm22", + "displayName": "UBSAN with LLVM 22.1.8", + "inherits": "llvm22-base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "ACIR_ENABLE_ASSERTIONS": true, + "CMAKE_CXX_FLAGS": "-fsanitize=undefined -fno-omit-frame-pointer", + "CMAKE_C_FLAGS": "-fsanitize=undefined -fno-omit-frame-pointer", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=undefined" + }, + "environment": { + "LLVM_PREFIX": "/opt/homebrew/opt/llvm" + } + } + ], + "buildPresets": [ + { + "name": "dev-llvm22", + "configurePreset": "dev-llvm22" + }, + { + "name": "release-llvm22", + "configurePreset": "release-llvm22" + }, + { + "name": "asan-llvm22", + "configurePreset": "asan-llvm22" + }, + { + "name": "ubsan-llvm22", + "configurePreset": "ubsan-llvm22" + } + ] +} diff --git a/components/agentic-circuit/CODE_OF_CONDUCT.md b/components/agentic-circuit/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..87ca1918 --- /dev/null +++ b/components/agentic-circuit/CODE_OF_CONDUCT.md @@ -0,0 +1,28 @@ +# Code of Conduct + +## Our pledge + +We pledge to make participation in Agentic Circuit a harassment-free +experience for everyone, regardless of age, body size, disability, ethnicity, +sex characteristics, gender identity and expression, level of experience, +education, socioeconomic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our standards + +Positive behavior includes empathy, constructive feedback, respect for +different viewpoints, and taking responsibility for mistakes. Unacceptable +behavior includes harassment, sexualized language or imagery, personal or +political attacks, trolling, insulting comments, and publishing another +person's private information without permission. + +## Enforcement + +Project maintainers may remove, edit, or reject comments, commits, code, and +other contributions that do not align with this Code of Conduct. Instances of +abusive, harassing, or otherwise unacceptable behavior may be reported using +the private process in `SECURITY.md`. Maintainers will respect the privacy and +security of reporters and will apply corrective action fairly. + +This Code of Conduct applies in all project spaces and whenever an individual +is officially representing the project. diff --git a/components/agentic-circuit/CONTRIBUTING.md b/components/agentic-circuit/CONTRIBUTING.md new file mode 100644 index 00000000..87e5a7bb --- /dev/null +++ b/components/agentic-circuit/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing to Agentic Circuit + +Thank you for helping improve Agentic Circuit. Contributions must preserve the +exact global contract epoch and the reproducible LLVM toolchain boundary. + +## Development setup + +1. Run `scripts/bootstrap-dev.sh` to create `.venv` and install the locked + development requirements. +2. Activate `.venv`. +3. Run `python -m unittest tests.contracts.test_contracts -v`. +4. Configure with `cmake --preset dev-llvm22`. + +Before submitting a change, run the contract tests, the relevant build and +tests, and `git diff --check`. Changes to a public contract must update every +affected normative specification and machine-readable schema in the same +change. + +By contributing, you agree that your contribution is licensed under the +BSD 3-Clause License and that you will follow the Code of Conduct. diff --git a/components/agentic-circuit/LICENSE b/components/agentic-circuit/LICENSE new file mode 100644 index 00000000..cc7bc6d7 --- /dev/null +++ b/components/agentic-circuit/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, PTO-ISA + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/components/agentic-circuit/README.md b/components/agentic-circuit/README.md new file mode 100644 index 00000000..fc941fdb --- /dev/null +++ b/components/agentic-circuit/README.md @@ -0,0 +1,66 @@ +# Agentic Circuit + +Agentic Circuit is a Python and MLIR-based architecture construction system +that generates a structured, pure C++ graph-flow simulator named `gfsim`. +It also emits canonical PYC IR for downstream C++ and Verilog generation. + +The source tree is intentionally release-neutral: product versions belong to +Git tags and GitHub Releases, not directory names, filenames, symbols, or test +names. Serialized artifacts still carry an exact contract epoch because that +field is part of their wire-format compatibility contract. + +## Development baseline + +The repository is locked to LLVM/MLIR 22.1.8. On Apple Silicon, the default +prefix is `/opt/homebrew/opt/llvm`. On another supported host, pass the +equivalent package explicitly with +`-DMLIR_DIR=/path/to/llvm/lib/cmake/mlir`. + +```sh +scripts/bootstrap-dev.sh +source .venv/bin/activate +python -m unittest tests.contracts.test_contracts -v +cmake --preset dev-llvm22 +cmake --build --preset dev-llvm22 +``` + +Use `release-llvm22` for a release configuration. The exact upstream release, +commit, archive digest, supported host triples, and version policy are recorded +in `toolchains/llvm.lock.json`. + +## Documentation + +- [Specification index](docs/spec/README.md) +- [Agentic Circuit specification manual](docs/spec/agentic-circuit.md) +- [Agentic Circuit 团队 Specification 手册](docs/spec/agentic-circuit.zh-CN.md) +- [NDF release-layout decision](docs/spec/decisions/D-RELEASE-LAYOUT-001.md) +- [Historical specification reference](docs/spec/refs/history.md) +- [Examples by semantic role](examples/README.md) + +Canonical machine-readable schemas: + +- [ACPy](schemas/acpy.schema.json) +- [Capabilities](schemas/capabilities.schema.json) +- [ComponentSchema](schemas/component.schema.json) +- [Official opcode catalog schema](schemas/opcode-catalog.schema.json) +- [Official Queue building-block catalog](schemas/opcodes.json) +- [PTO trace](schemas/pto-trace.schema.json) +- [Build manifest](schemas/build-manifest.schema.json) +- [Run manifest](schemas/run-manifest.schema.json) +- [Run result](schemas/run-result.schema.json) +- [Diagnostic](schemas/diagnostic.schema.json) +- [ACSim binding](schemas/acsim-binding.schema.json) +- [ACIR process-state plan](schemas/acir-process-state-plan.schema.json) + +The repository uses a hard-break layout. Removed implementation-phase and +product-version paths have no aliases or compatibility symlinks. Historical +documents remain recoverable from the Git revision recorded by the NDF +historical reference. + +## Project policies + +Agentic Circuit is distributed as part of pyCircuit under the +[BSD 3-Clause License](LICENSE). See +[Contributing](CONTRIBUTING.md), the [Code of Conduct](CODE_OF_CONDUCT.md), +[Security policy](SECURITY.md), and [Support policy](SUPPORT.md) before opening +a change or report. diff --git a/components/agentic-circuit/SECURITY.md b/components/agentic-circuit/SECURITY.md new file mode 100644 index 00000000..9538777e --- /dev/null +++ b/components/agentic-circuit/SECURITY.md @@ -0,0 +1,14 @@ +# Security policy + +## Reporting a vulnerability + +Do not open a public issue for a suspected vulnerability. Use GitHub's private +security advisory reporting for this repository and include affected versions, +reproduction steps, impact, and any proposed mitigation. + +Maintainers will acknowledge a report within seven days, coordinate validation +and remediation privately, and publish an advisory after a fix is available. +Please allow a reasonable remediation window before public disclosure. + +Only the current development branch is supported until the project publishes +its first release. diff --git a/components/agentic-circuit/SUPPORT.md b/components/agentic-circuit/SUPPORT.md new file mode 100644 index 00000000..dabe4d95 --- /dev/null +++ b/components/agentic-circuit/SUPPORT.md @@ -0,0 +1,9 @@ +# Support + +Use GitHub Discussions for design and usage questions. Use GitHub Issues for a +reproducible defect or a scoped feature proposal. Include the host triple, +LLVM/MLIR version, configure preset, command output, and a minimal reproducer. + +Security reports must follow `SECURITY.md` and must not be filed publicly. +The project is maintained on a best-effort basis and does not provide a service +level agreement. diff --git a/components/agentic-circuit/cmake/AgenticCircuitConfig.cmake.in b/components/agentic-circuit/cmake/AgenticCircuitConfig.cmake.in new file mode 100644 index 00000000..62a0cfd7 --- /dev/null +++ b/components/agentic-circuit/cmake/AgenticCircuitConfig.cmake.in @@ -0,0 +1,18 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(LLVM 22.1.8 EXACT CONFIG) +find_dependency(MLIR 22.1.8 EXACT CONFIG) + +include("${CMAKE_CURRENT_LIST_DIR}/AgenticCircuitTargets.cmake") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules") +include(AddACIR) + +set(AgenticCircuit_EXECUTABLE "${PACKAGE_PREFIX_DIR}/bin/agentic-circuit") +if(NOT EXISTS "${AgenticCircuit_EXECUTABLE}") + set(AgenticCircuit_FOUND FALSE) + set(AgenticCircuit_NOT_FOUND_MESSAGE + "Agentic Circuit executable is missing: ${AgenticCircuit_EXECUTABLE}") +endif() + +check_required_components(AgenticCircuit) diff --git a/components/agentic-circuit/cmake/LLVMToolchain.cmake b/components/agentic-circuit/cmake/LLVMToolchain.cmake new file mode 100644 index 00000000..6e2edf3a --- /dev/null +++ b/components/agentic-circuit/cmake/LLVMToolchain.cmake @@ -0,0 +1,43 @@ +include_guard(GLOBAL) + +set(ACIR_REQUIRED_LLVM_VERSION "22.1.8") + +function(acir_configure_llvm) + if(TARGET MLIRIR) + if(NOT LLVM_PACKAGE_VERSION) + message(FATAL_ERROR + "the parent project supplied LLVM targets without LLVM_PACKAGE_VERSION") + endif() + message(STATUS + "Reusing parent LLVM/MLIR ${LLVM_PACKAGE_VERSION} targets for Agentic Circuit") + else() + if(NOT MLIR_DIR) + message(FATAL_ERROR "MLIR_DIR is required and must select MLIR 22.1.8") + endif() + + get_filename_component(_llvm_cmake_dir "${MLIR_DIR}/../llvm" ABSOLUTE) + if(NOT LLVM_DIR) + set(LLVM_DIR "${_llvm_cmake_dir}" CACHE PATH "LLVM CMake package directory") + endif() + + find_package(LLVM ${ACIR_REQUIRED_LLVM_VERSION} EXACT CONFIG REQUIRED) + find_package(MLIR ${ACIR_REQUIRED_LLVM_VERSION} EXACT CONFIG REQUIRED) + endif() + + if(NOT LLVM_PACKAGE_VERSION STREQUAL ACIR_REQUIRED_LLVM_VERSION) + message(FATAL_ERROR + "LLVM ${ACIR_REQUIRED_LLVM_VERSION} is required; found ${LLVM_PACKAGE_VERSION}" + ) + endif() + + foreach(_package_variable + LLVM_DEFINITIONS + LLVM_INCLUDE_DIRS + LLVM_TOOLS_BINARY_DIR + MLIR_INCLUDE_DIRS + MLIR_TABLEGEN_EXE) + set(${_package_variable} "${${_package_variable}}" PARENT_SCOPE) + endforeach() + message(STATUS "Using LLVM ${LLVM_PACKAGE_VERSION} from ${LLVM_DIR}") + message(STATUS "Using MLIR ${ACIR_REQUIRED_LLVM_VERSION} from ${MLIR_DIR}") +endfunction() diff --git a/components/agentic-circuit/cmake/modules/AddACIR.cmake b/components/agentic-circuit/cmake/modules/AddACIR.cmake new file mode 100644 index 00000000..430025a2 --- /dev/null +++ b/components/agentic-circuit/cmake/modules/AddACIR.cmake @@ -0,0 +1,19 @@ +include_guard(GLOBAL) + +function(add_acir_library target) + cmake_parse_arguments(ARG "" "" "SOURCES;LINK_LIBRARIES" ${ARGN}) + if(NOT ARG_SOURCES) + message(FATAL_ERROR "add_acir_library(${target}) requires SOURCES") + endif() + + add_library(${target} ${ARG_SOURCES}) + target_compile_features(${target} PUBLIC cxx_std_20) + set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF) + target_link_libraries( + ${target} + PUBLIC + AgenticCircuit::ProjectOptions + MLIRIR + ${ARG_LINK_LIBRARIES} + ) +endfunction() diff --git a/components/agentic-circuit/contracts/acir.yaml b/components/agentic-circuit/contracts/acir.yaml new file mode 100644 index 00000000..0ad4bceb --- /dev/null +++ b/components/agentic-circuit/contracts/acir.yaml @@ -0,0 +1,130 @@ +# Normative ACIR public inventory. +# +# This manifest is the contract-side source of truth for the public ACIR +# surface. scripts/check-ir-coverage.py requires the ODS definitions to +# match these lists exactly: any added, renamed, or removed public operation +# or type must be reflected here in the same commit, and every entry must +# have observable lit coverage. +schema: acir-ir-inventory +contract_epoch: "0.3" +dialect: acir +operations: + - ac.system + - ac.type_scope + - ac.type_alias + - ac.module + - ac.module.extern + - ac.module.generated + - ac.instance + - ac.array + - ac.instances + - ac.view + - ac.port + - ac.return + - ac.queue + - ac.event_queue + - ac.resource + - ac.address_space + - ac.address_map + - ac.time_domain + - ac.struct + - ac.enum + - ac.union + - ac.packet + - ac.transaction + - ac.interface + - ac.protocol + - ac.role + - ac.state + - ac.event + - ac.transition + - ac.guarantee + - ac.process + - ac.record.create + - ac.record.get + - ac.record.with + - ac.var.constant + - ac.var.add + - ac.var.sub + - ac.var.mul + - ac.var.cmp + - ac.var.get + - ac.var.with + - ac.var.popcount + - ac.packet.serialize + - ac.packet.deserialize + - ac.transform + - ac.transform.yield + - ac.source + - ac.sink + - ac.observe + - ac.expect + - ac.expect.yield + - ac.broadcast + - ac.fork + - ac.route + - ac.route.yield + - ac.select + - ac.select.yield + - ac.merge + - ac.barrier + - ac.credit + - ac.credit.yield + - ac.dependency + - ac.dependency.yield + - ac.memory.instance + - ac.memory.request + - ac.memory.yield + - ac.reorder + - ac.reorder.yield + - ac.feedback + - ac.feedback.yield + - ac.scope + - ac.scope.yield + - ac.firing + - ac.firing.yield + - ac.queue.peek + - ac.queue.pop + - ac.queue.push + - ac.try_send + - ac.try_recv + - ac.schedule + - ac.wait_until + - ac.wait_for + - ac.await_event + - ac.yield_sim + - ac.trace.open + - ac.trace.next + - ac.trace.decode + - ac.trace.eof + - ac.trace.position + - ac.require + - ac.ensure + - ac.assert + - ac.probe + - ac.stat + - ac.stat.add + - ac.instrumentation +types: + - struct + - packet + - transaction + - enum + - union + - optional + - list + - vector + - var + - queue + - array + - map + - set + - flow + - endpoint + - resource_ref + - channel + - duration + - rate + - event + - address + - resource_token diff --git a/components/agentic-circuit/contracts/acsim.yaml b/components/agentic-circuit/contracts/acsim.yaml new file mode 100644 index 00000000..83d526f6 --- /dev/null +++ b/components/agentic-circuit/contracts/acsim.yaml @@ -0,0 +1,48 @@ +# Normative ACSim v0.1 public inventory. +# +# This manifest is the contract-side source of truth for the public ACSim +# surface. The acsim.type inventory includes the closed optional runtime +# metadata contract for time_domain realizations: period, phase, tick_scale, +# and paired parent/bridge attributes. scripts/check-ir-coverage.py requires +# the ODS definitions to +# match these lists exactly: any added, renamed, or removed public operation +# or type must be reflected here in the same commit, and every entry must +# have observable lit coverage. +schema: acir-ir-inventory +contract_epoch: "0.3" +dialect: acsim +operations: + - acsim.model + - acsim.type + - acsim.binding + - acsim.module + - acsim.instance + - acsim.array + - acsim.element + - acsim.port + - acsim.resource + - acsim.bind + - acsim.inline + - acsim.process + - acsim.live.load + - acsim.live.store + - acsim.invoke + - acsim.continue + - acsim.suspend + - acsim.terminate + - acsim.export + - acsim.dispatch + - acsim.activate + - acsim.return +types: + - value + - expr + - owner + - ref + - port + - resource + - array + - object_id + - activation_id + - pc + - wake diff --git a/components/agentic-circuit/docs/spec/00-charter/scope.md b/components/agentic-circuit/docs/spec/00-charter/scope.md new file mode 100644 index 00000000..4ed3156b --- /dev/null +++ b/components/agentic-circuit/docs/spec/00-charter/scope.md @@ -0,0 +1,26 @@ +# Repository and release charter + +## Release tags own product versions {#ARC-RELEASE-001} + + +The checked-out source tree describes one current product. Git releases and +tags assign product versions. Source paths, public type names, test names, and +example groups do not encode a product version or implementation phase. + +Serialized documents may retain their own schema version, and dependency locks +retain exact external tool versions. Those values version protocols and +dependencies, not repository layouts. + +## Current source uses semantic partitions {#ARC-LAYOUT-001} + + +Current source is partitioned by responsibility: specifications under +`docs/spec`, public examples under `examples`, imported upstream material under +`references`, and test-owned golden data under `tests/goldens`. + +## Historical design remains in Git history {#ARC-HISTORY-001} + + +Completed plans, superseded proposals, and release audits are not duplicated in +the current tree. [[REF-HISTORY-001]] pins the last pre-migration tree so each +historical document remains reproducible through Git. diff --git a/components/agentic-circuit/docs/spec/50-verification/ir-coverage.md b/components/agentic-circuit/docs/spec/50-verification/ir-coverage.md new file mode 100644 index 00000000..bb852c01 --- /dev/null +++ b/components/agentic-circuit/docs/spec/50-verification/ir-coverage.md @@ -0,0 +1,179 @@ +# ACIR / ACSim coverage ledger + + + +acir_contract_epoch: 0.3 +acsim_contract_epoch: 0.3 + +## acir operations (96) + +| operation | source symbol | positive coverage | negative coverage | +| --- | --- | --- | --- | +| ac.system | ACIR_SystemOp | test/ACIR/collections-valid.mlir
test/ACIR/hierarchy-valid.mlir
test/Analysis/process-state-pass.mlir
test/Bindings/resolve-ambiguous.mlir
test/Bindings/resolve-empty.mlir
test/Bindings/resolve-missing.mlir
test/Bindings/resolve-valid.mlir
test/CodeGen/arbiter-verilog.mlir
test/CodeGen/arbiter.mlir
test/CodeGen/atomic-transform.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Conversion/atomic-failure.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/process-basic.mlir
test/Conversion/process-control-flow.mlir
test/Conversion/process-live-values.mlir
test/Conversion/pure-results.mlir
test/Conversion/structure.mlir
test/Conversion/time-domains.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Python/frontend-lowering.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/freeze-topology.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/collections-invalid.mlir
test/ACIR/hierarchy-invalid.mlir
test/ACSim/ops-invalid.mlir
test/Conversion/bindings-invalid.mlir
test/Conversion/process-invalid.mlir | +| ac.type_scope | ACIR_TypeScopeOp | test/ACIR/address-selectors-valid.mlir
test/ACIR/declarations-valid.mlir
test/ACIR/memory.mlir
test/ACIR/protocols-valid.mlir
test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir
test/ACIR/select.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/declarations-invalid.mlir
test/ACIR/hierarchy-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/memory-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir
test/ACIR/records-invalid.mlir
test/ACIR/resources-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir
test/ACIR/var-expressions-invalid.mlir | +| ac.type_alias | ACIR_TypeAliasOp | test/ACIR/declarations-valid.mlir | test/ACIR/declarations-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir | +| ac.module | ACIR_ModuleOp | test/ACIR/address-map-canonical-order.mlir
test/ACIR/address-selectors-valid.mlir
test/ACIR/address-time-valid.mlir
test/ACIR/canonical-entrypoint.mlir
test/ACIR/collections-valid.mlir
test/ACIR/contracts-valid.mlir
test/ACIR/hierarchy-valid.mlir
test/ACIR/process-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/trace-valid.mlir
test/ACIR/view-provenance.mlir
test/Analysis/process-state-pass.mlir
test/Analysis/process-state-verifier.mlir
test/Bindings/resolve-ambiguous.mlir
test/Bindings/resolve-empty.mlir
test/Bindings/resolve-missing.mlir
test/Bindings/resolve-valid.mlir
test/Conversion/atomic-failure.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/process-basic.mlir
test/Conversion/process-control-flow.mlir
test/Conversion/process-live-values.mlir
test/Conversion/pure-results.mlir
test/Conversion/structure.mlir
test/Conversion/time-domains.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Python/frontend-lowering.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/freeze-topology.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/address-time-invalid.mlir
test/ACIR/collections-invalid.mlir
test/ACIR/contracts-invalid.mlir
test/ACIR/hierarchy-invalid.mlir
test/ACIR/process-invalid.mlir
test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir
test/ACIR/runtime-references-invalid.mlir
test/ACIR/trace-invalid.mlir
test/Conversion/bindings-invalid.mlir
test/Conversion/process-invalid.mlir | +| ac.module.extern | ACIR_ModuleExternOp | test/ACIR/canonical-entrypoint.mlir
test/ACIR/hierarchy-valid.mlir
test/Bindings/resolve-ambiguous.mlir
test/Bindings/resolve-missing.mlir
test/Bindings/resolve-valid.mlir
test/Conversion/bindings.mlir
test/Conversion/pure-results.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/verify-model.mlir | test/ACIR/collections-invalid.mlir
test/ACIR/hierarchy-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| ac.module.generated | ACIR_ModuleGeneratedOp | test/ACIR/hierarchy-valid.mlir | test/ACIR/hierarchy-invalid.mlir | +| ac.instance | ACIR_InstanceOp | test/ACIR/address-time-valid.mlir
test/ACIR/hierarchy-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/view-provenance.mlir
test/Bindings/resolve-ambiguous.mlir
test/Bindings/resolve-missing.mlir
test/Bindings/resolve-valid.mlir
test/Conversion/bindings.mlir
test/Conversion/hierarchy.mlir
test/Conversion/pure-results.mlir
test/Conversion/time-domains.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Python/frontend-lowering.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/address-time-invalid.mlir
test/ACIR/collections-invalid.mlir
test/ACIR/hierarchy-invalid.mlir
test/ACIR/process-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| ac.array | ACIR_ArrayOp | test/ACIR/collections-valid.mlir
test/ACIR/hierarchy-valid.mlir
test/Conversion/collections.mlir
test/Conversion/whole-model.mlir | test/ACIR/collections-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| ac.instances | ACIR_InstancesOp | test/ACIR/collections-valid.mlir
test/ACIR/hierarchy-valid.mlir | test/ACIR/collections-invalid.mlir | +| ac.view | ACIR_ViewOp | test/ACIR/collections-valid.mlir
test/ACIR/view-provenance.mlir | test/ACIR/collections-invalid.mlir | +| ac.port | ACIR_PortOp | test/ACIR/interfaces-valid.mlir
test/ACIR/port-mapping-review-r2.mlir
test/ACIR/resources-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/deterministic-canonicalization.mlir | test/ACIR/interfaces-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir | +| ac.return | ACIR_ReturnOp | test/ACIR/address-map-canonical-order.mlir
test/ACIR/address-selectors-valid.mlir
test/ACIR/address-time-valid.mlir
test/ACIR/canonical-entrypoint.mlir
test/ACIR/collections-valid.mlir
test/ACIR/contracts-valid.mlir
test/ACIR/hierarchy-valid.mlir
test/ACIR/process-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/trace-valid.mlir
test/ACIR/view-provenance.mlir
test/Analysis/process-state-pass.mlir
test/Analysis/process-state-verifier.mlir
test/Bindings/resolve-ambiguous.mlir
test/Bindings/resolve-empty.mlir
test/Bindings/resolve-missing.mlir
test/Bindings/resolve-valid.mlir
test/Conversion/atomic-failure.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/process-basic.mlir
test/Conversion/process-control-flow.mlir
test/Conversion/process-live-values.mlir
test/Conversion/pure-results.mlir
test/Conversion/structure.mlir
test/Conversion/time-domains.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Python/frontend-lowering.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/freeze-topology.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/address-time-invalid.mlir
test/ACIR/collections-invalid.mlir
test/ACIR/contracts-invalid.mlir
test/ACIR/hierarchy-invalid.mlir
test/ACIR/process-invalid.mlir
test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir
test/ACIR/runtime-references-invalid.mlir
test/ACIR/trace-invalid.mlir
test/Conversion/bindings-invalid.mlir
test/Conversion/process-invalid.mlir | +| ac.queue | ACIR_QueueOp | test/ACIR/contracts-valid.mlir
test/ACIR/process-valid.mlir
test/ACIR/resources-valid.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir
test/ACIR/runtime-references-invalid.mlir | +| ac.event_queue | ACIR_EventQueueOp | test/ACIR/process-valid.mlir
test/ACIR/resources-valid.mlir | test/ACIR/resources-invalid.mlir | +| ac.resource | ACIR_ResourceOp | test/ACIR/process-valid.mlir
test/ACIR/resources-valid.mlir
test/Transforms/verify-model.mlir | test/ACIR/resources-invalid.mlir | +| ac.address_space | ACIR_AddressSpaceOp | test/ACIR/address-map-canonical-order.mlir
test/ACIR/address-selectors-valid.mlir
test/ACIR/address-time-valid.mlir | test/ACIR/address-time-invalid.mlir | +| ac.address_map | ACIR_AddressMapOp | test/ACIR/address-map-canonical-order.mlir
test/ACIR/address-selectors-valid.mlir
test/ACIR/address-time-valid.mlir | test/ACIR/address-time-invalid.mlir | +| ac.time_domain | ACIR_TimeDomainOp | test/ACIR/address-time-valid.mlir
test/ACIR/process-valid.mlir
test/ACIR/resources-valid.mlir
test/Conversion/structure.mlir
test/Conversion/time-domains.mlir | test/ACIR/address-time-invalid.mlir
test/ACIR/resources-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| ac.struct | ACIR_StructOp | test/ACIR/declarations-valid.mlir
test/ACIR/memory.mlir
test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir
test/ACIR/select.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/declarations-invalid.mlir
test/ACIR/memory-invalid.mlir
test/ACIR/review-r1-invalid.mlir | +| ac.enum | ACIR_EnumOp | test/ACIR/declarations-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/declarations-invalid.mlir | +| ac.union | ACIR_UnionOp | test/ACIR/declarations-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir | +| ac.packet | ACIR_PacketOp | test/ACIR/declarations-valid.mlir
test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/records-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir | +| ac.transaction | ACIR_TransactionOp | test/ACIR/address-selectors-valid.mlir
test/ACIR/declarations-valid.mlir
test/ACIR/protocols-valid.mlir
test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/declarations-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/records-invalid.mlir
test/ACIR/resources-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir
test/ACIR/var-expressions-invalid.mlir | +| ac.interface | ACIR_InterfaceOp | test/ACIR/interfaces-valid.mlir
test/ACIR/port-mapping-review-r2.mlir
test/ACIR/resources-valid.mlir
test/ACIR/types-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/deterministic-canonicalization.mlir | test/ACIR/interfaces-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir
test/ACIR/types-invalid.mlir | +| ac.protocol | ACIR_ProtocolOp | test/ACIR/contracts-valid.mlir
test/ACIR/interfaces-valid.mlir
test/ACIR/ownership-order-review-r2.mlir
test/ACIR/port-mapping-review-r2.mlir
test/ACIR/process-valid.mlir
test/ACIR/protocols-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/types-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/guarantees-review-r1-invalid.mlir
test/ACIR/interfaces-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir
test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir | +| ac.role | ACIR_RoleOp | test/ACIR/contracts-valid.mlir
test/ACIR/interfaces-valid.mlir
test/ACIR/ownership-order-review-r2.mlir
test/ACIR/port-mapping-review-r2.mlir
test/ACIR/process-valid.mlir
test/ACIR/protocols-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/types-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/interfaces-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir
test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir | +| ac.state | ACIR_StateOp | test/ACIR/contracts-valid.mlir
test/ACIR/interfaces-valid.mlir
test/ACIR/ownership-order-review-r2.mlir
test/ACIR/port-mapping-review-r2.mlir
test/ACIR/process-valid.mlir
test/ACIR/protocols-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/types-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/guarantees-review-r1-invalid.mlir
test/ACIR/interfaces-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir
test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir | +| ac.event | ACIR_EventOp | test/ACIR/contracts-valid.mlir
test/ACIR/interfaces-valid.mlir
test/ACIR/ownership-order-review-r2.mlir
test/ACIR/port-mapping-review-r2.mlir
test/ACIR/process-valid.mlir
test/ACIR/protocols-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/types-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/interfaces-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir
test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir | +| ac.transition | ACIR_TransitionOp | test/ACIR/contracts-valid.mlir
test/ACIR/interfaces-valid.mlir
test/ACIR/ownership-order-review-r2.mlir
test/ACIR/process-valid.mlir
test/ACIR/protocols-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/types-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir
test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir | +| ac.guarantee | ACIR_GuaranteeOp | test/ACIR/interfaces-valid.mlir
test/ACIR/ownership-order-review-r2.mlir
test/ACIR/protocols-valid.mlir
test/ACIR/resources-valid.mlir | test/ACIR/guarantees-review-r1-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/protocols-review-r1-invalid.mlir
test/ACIR/resources-forward-malformed-invalid.mlir
test/ACIR/resources-invalid.mlir | +| ac.process | ACIR_ProcessOp | test/ACIR/contracts-valid.mlir
test/ACIR/hierarchy-valid.mlir
test/ACIR/process-valid.mlir
test/ACIR/trace-valid.mlir
test/Analysis/process-state-pass.mlir
test/Analysis/process-state-verifier.mlir
test/Bindings/resolve-ambiguous.mlir
test/Bindings/resolve-empty.mlir
test/Bindings/resolve-missing.mlir
test/Bindings/resolve-valid.mlir
test/Conversion/atomic-failure.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/process-basic.mlir
test/Conversion/process-control-flow.mlir
test/Conversion/process-live-values.mlir
test/Conversion/pure-results.mlir
test/Conversion/structure.mlir
test/Conversion/time-domains.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Python/frontend-lowering.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/freeze-topology.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/contracts-invalid.mlir
test/ACIR/process-invalid.mlir
test/ACIR/runtime-references-invalid.mlir
test/ACIR/trace-invalid.mlir
test/Conversion/bindings-invalid.mlir
test/Conversion/process-invalid.mlir | +| ac.record.create | ACIR_RecordCreateOp | test/ACIR/protocols-valid.mlir
test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/records-invalid.mlir
test/ACIR/review-r2-invalid.mlir | +| ac.record.get | ACIR_RecordGetOp | test/ACIR/protocols-valid.mlir
test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/records-invalid.mlir
test/ACIR/review-r1-invalid.mlir | +| ac.record.with | ACIR_RecordWithOp | test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/records-invalid.mlir
test/ACIR/review-r1-invalid.mlir | +| ac.var.constant | ACIR_VarConstantOp | test/ACIR/control-feedback.mlir
test/ACIR/dependency.mlir
test/ACIR/expect.mlir
test/ACIR/var-expressions.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/control-feedback-invalid.mlir
test/ACIR/credit-invalid.mlir
test/ACIR/dependency-invalid.mlir
test/ACIR/expect-invalid.mlir
test/ACIR/reorder-invalid.mlir
test/ACIR/select-invalid.mlir
test/ACIR/var-expressions-invalid.mlir | +| ac.var.add | ACIR_VarAddOp | test/ACIR/control-feedback.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/atomic-transform.mlir | test/ACIR/var-expressions-invalid.mlir | +| ac.var.sub | ACIR_VarSubOp | test/ACIR/var-expressions.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/var-expressions-invalid.mlir | +| ac.var.mul | ACIR_VarMulOp | test/ACIR/var-expressions.mlir | test/ACIR/var-expressions-invalid.mlir | +| ac.var.cmp | ACIR_VarCmpOp | test/ACIR/expect.mlir
test/ACIR/var-expressions.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/var-expressions-invalid.mlir | +| ac.var.get | ACIR_VarGetOp | test/ACIR/memory.mlir
test/ACIR/select.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/memory-invalid.mlir
test/ACIR/var-expressions-invalid.mlir | +| ac.var.with | ACIR_VarWithOp | test/ACIR/var-expressions.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/var-expressions-invalid.mlir | +| ac.var.popcount | ACIR_VarPopcountOp | test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir | test/ACIR/var-expressions-invalid.mlir | +| ac.packet.serialize | ACIR_PacketSerializeOp | test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/records-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir | +| ac.packet.deserialize | ACIR_PacketDeserializeOp | test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir | test/ACIR/records-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir | +| ac.transform | ACIR_TransformOp | test/ACIR/scope.mlir
test/ACIR/transform.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/atomic-transform.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir | test/ACIR/transform-invalid.mlir | +| ac.transform.yield | ACIR_TransformYieldOp | test/ACIR/scope.mlir
test/ACIR/transform.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/atomic-transform.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir | test/ACIR/transform-invalid.mlir | +| ac.source | ACIR_SourceOp | test/ACIR/barrier.mlir
test/ACIR/broadcast.mlir
test/ACIR/control-feedback.mlir
test/ACIR/credit.mlir
test/ACIR/dependency.mlir
test/ACIR/expect.mlir
test/ACIR/fork.mlir
test/ACIR/memory.mlir
test/ACIR/observe.mlir
test/ACIR/reorder.mlir
test/ACIR/route.mlir
test/ACIR/scope.mlir
test/ACIR/select.mlir
test/ACIR/source-sink.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/arbiter-verilog.mlir
test/CodeGen/arbiter.mlir
test/CodeGen/atomic-transform.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/barrier-invalid.mlir
test/ACIR/broadcast-invalid.mlir
test/ACIR/control-feedback-invalid.mlir
test/ACIR/credit-invalid.mlir
test/ACIR/dependency-invalid.mlir
test/ACIR/expect-invalid.mlir
test/ACIR/fork-invalid.mlir
test/ACIR/memory-invalid.mlir
test/ACIR/observe-invalid.mlir
test/ACIR/reorder-invalid.mlir
test/ACIR/route-invalid.mlir
test/ACIR/scope-invalid.mlir
test/ACIR/select-invalid.mlir
test/ACIR/source-sink-invalid.mlir | +| ac.sink | ACIR_SinkOp | test/ACIR/barrier.mlir
test/ACIR/broadcast.mlir
test/ACIR/control-feedback.mlir
test/ACIR/credit.mlir
test/ACIR/dependency.mlir
test/ACIR/expect.mlir
test/ACIR/fork.mlir
test/ACIR/memory.mlir
test/ACIR/observe.mlir
test/ACIR/reorder.mlir
test/ACIR/route.mlir
test/ACIR/scope.mlir
test/ACIR/select.mlir
test/ACIR/source-sink.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/arbiter-verilog.mlir
test/CodeGen/arbiter.mlir
test/CodeGen/atomic-transform.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/memory-invalid.mlir
test/ACIR/source-sink-invalid.mlir | +| ac.observe | ACIR_ObserveOp | test/ACIR/observe.mlir | test/ACIR/observe-invalid.mlir | +| ac.expect | ACIR_ExpectOp | test/ACIR/expect.mlir | test/ACIR/expect-invalid.mlir | +| ac.expect.yield | ACIR_ExpectYieldOp | test/ACIR/expect.mlir | test/ACIR/expect-invalid.mlir | +| ac.broadcast | ACIR_BroadcastOp | test/ACIR/broadcast.mlir | test/ACIR/broadcast-invalid.mlir | +| ac.fork | ACIR_ForkOp | test/ACIR/fork.mlir | test/ACIR/fork-invalid.mlir | +| ac.route | ACIR_RouteOp | test/ACIR/route.mlir | test/ACIR/route-invalid.mlir | +| ac.route.yield | ACIR_RouteYieldOp | test/ACIR/route.mlir | test/ACIR/route-invalid.mlir | +| ac.select | ACIR_SelectOp | test/ACIR/select.mlir | test/ACIR/select-invalid.mlir | +| ac.select.yield | ACIR_SelectYieldOp | test/ACIR/select.mlir | test/ACIR/select-invalid.mlir | +| ac.merge | ACIR_MergeOp | test/ACIR/control-feedback.mlir
test/CodeGen/arbiter-verilog.mlir
test/CodeGen/arbiter.mlir | test/ACIR/control-feedback-invalid.mlir | +| ac.barrier | ACIR_BarrierOp | test/ACIR/barrier.mlir | test/ACIR/barrier-invalid.mlir | +| ac.credit | ACIR_CreditOp | test/ACIR/credit.mlir | test/ACIR/credit-invalid.mlir | +| ac.credit.yield | ACIR_CreditYieldOp | test/ACIR/credit.mlir | test/ACIR/credit-invalid.mlir | +| ac.dependency | ACIR_DependencyOp | test/ACIR/dependency.mlir | test/ACIR/dependency-invalid.mlir | +| ac.dependency.yield | ACIR_DependencyYieldOp | test/ACIR/dependency.mlir | test/ACIR/dependency-invalid.mlir | +| ac.memory.instance | ACIR_MemoryInstanceOp | test/ACIR/memory.mlir | test/ACIR/memory-invalid.mlir | +| ac.memory.request | ACIR_MemoryRequestOp | test/ACIR/memory.mlir | test/ACIR/memory-invalid.mlir | +| ac.memory.yield | ACIR_MemoryYieldOp | test/ACIR/memory.mlir | test/ACIR/memory-invalid.mlir | +| ac.reorder | ACIR_ReorderOp | test/ACIR/reorder.mlir | test/ACIR/reorder-invalid.mlir | +| ac.reorder.yield | ACIR_ReorderYieldOp | test/ACIR/reorder.mlir | test/ACIR/reorder-invalid.mlir | +| ac.feedback | ACIR_FeedbackOp | test/ACIR/control-feedback.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/control-feedback-invalid.mlir | +| ac.feedback.yield | ACIR_FeedbackYieldOp | test/ACIR/control-feedback.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/control-feedback-invalid.mlir | +| ac.scope | ACIR_ScopeOp | test/ACIR/memory.mlir
test/ACIR/scope.mlir | test/ACIR/memory-invalid.mlir
test/ACIR/scope-invalid.mlir | +| ac.scope.yield | ACIR_ScopeYieldOp | test/ACIR/memory.mlir
test/ACIR/scope.mlir | test/ACIR/memory-invalid.mlir
test/ACIR/scope-invalid.mlir | +| ac.firing | ACIR_FiringOp | test/ACIR/firing.mlir | test/ACIR/firing-invalid.mlir | +| ac.firing.yield | ACIR_FiringYieldOp | test/ACIR/firing.mlir | test/ACIR/firing-invalid.mlir | +| ac.queue.peek | ACIR_QueuePeekOp | test/ACIR/firing.mlir | test/ACIR/firing-invalid.mlir | +| ac.queue.pop | ACIR_QueuePopOp | test/ACIR/firing.mlir | test/ACIR/firing-invalid.mlir | +| ac.queue.push | ACIR_QueuePushOp | test/ACIR/firing.mlir | test/ACIR/firing-invalid.mlir | +| ac.try_send | ACIR_TrySendOp | test/ACIR/process-valid.mlir | test/ACIR/contracts-invalid.mlir
test/ACIR/runtime-references-invalid.mlir | +| ac.try_recv | ACIR_TryRecvOp | test/ACIR/process-valid.mlir | test/ACIR/process-invalid.mlir | +| ac.schedule | ACIR_ScheduleOp | test/ACIR/process-valid.mlir | test/ACIR/contracts-invalid.mlir
test/ACIR/process-invalid.mlir
test/ACIR/runtime-references-invalid.mlir | +| ac.wait_until | ACIR_WaitUntilOp | test/ACIR/process-valid.mlir
test/ACIR/trace-valid.mlir
test/Analysis/process-state-pass.mlir
test/Analysis/process-state-verifier.mlir
test/Conversion/process-control-flow.mlir
test/Conversion/process-live-values.mlir
test/Conversion/whole-model.mlir | test/ACIR/process-invalid.mlir | +| ac.wait_for | ACIR_WaitForOp | test/ACIR/process-valid.mlir
test/Transforms/verify-model.mlir | test/ACIR/runtime-references-invalid.mlir | +| ac.await_event | ACIR_AwaitEventOp | test/ACIR/process-valid.mlir | test/ACIR/process-invalid.mlir
test/ACIR/runtime-references-invalid.mlir | +| ac.yield_sim | ACIR_YieldSimOp | test/ACIR/contracts-valid.mlir
test/ACIR/hierarchy-valid.mlir
test/ACIR/process-valid.mlir
test/ACIR/trace-valid.mlir
test/Analysis/process-state-pass.mlir
test/Analysis/process-state-verifier.mlir
test/Bindings/resolve-ambiguous.mlir
test/Bindings/resolve-empty.mlir
test/Bindings/resolve-missing.mlir
test/Bindings/resolve-valid.mlir
test/Conversion/atomic-failure.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/process-basic.mlir
test/Conversion/process-control-flow.mlir
test/Conversion/process-live-values.mlir
test/Conversion/pure-results.mlir
test/Conversion/structure.mlir
test/Conversion/time-domains.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Python/frontend-lowering.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/freeze-topology.mlir
test/Transforms/verify-model.mlir
test/Transforms/zero-delay-cycles.mlir | test/ACIR/contracts-invalid.mlir
test/ACIR/process-invalid.mlir
test/ACIR/runtime-references-invalid.mlir
test/ACIR/trace-invalid.mlir
test/Conversion/bindings-invalid.mlir
test/Conversion/process-invalid.mlir | +| ac.trace.open | ACIR_TraceOpenOp | test/ACIR/process-valid.mlir
test/ACIR/trace-valid.mlir
test/Transforms/freeze-topology.mlir | test/ACIR/trace-invalid.mlir | +| ac.trace.next | ACIR_TraceNextOp | test/ACIR/process-valid.mlir
test/ACIR/trace-valid.mlir | test/ACIR/trace-invalid.mlir | +| ac.trace.decode | ACIR_TraceDecodeOp | test/ACIR/process-valid.mlir
test/ACIR/trace-valid.mlir | test/ACIR/trace-invalid.mlir | +| ac.trace.eof | ACIR_TraceEofOp | test/ACIR/trace-valid.mlir | test/ACIR/trace-invalid.mlir | +| ac.trace.position | ACIR_TracePositionOp | test/ACIR/trace-valid.mlir | test/ACIR/trace-invalid.mlir | +| ac.require | ACIR_RequireOp | test/ACIR/contracts-valid.mlir
test/Transforms/freeze-topology.mlir | test/ACIR/contracts-invalid.mlir | +| ac.ensure | ACIR_EnsureOp | test/ACIR/contracts-valid.mlir
test/Transforms/freeze-topology.mlir | test/ACIR/contracts-invalid.mlir | +| ac.assert | ACIR_AssertOp | test/ACIR/contracts-valid.mlir
test/ACIR/process-valid.mlir
test/Transforms/verify-model.mlir | test/ACIR/contracts-invalid.mlir
test/ACIR/credit-invalid.mlir
test/ACIR/dependency-invalid.mlir
test/ACIR/firing-invalid.mlir
test/ACIR/reorder-invalid.mlir
test/ACIR/transform-invalid.mlir | +| ac.probe | ACIR_ProbeOp | test/ACIR/contracts-valid.mlir
test/ACIR/trace-valid.mlir
test/Transforms/verify-model.mlir | test/ACIR/contracts-invalid.mlir
test/ACIR/runtime-references-invalid.mlir | +| ac.stat | ACIR_StatOp | test/ACIR/contracts-valid.mlir
test/ACIR/trace-valid.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/freeze-topology.mlir
test/Transforms/verify-model.mlir | test/ACIR/contracts-invalid.mlir
test/ACIR/process-invalid.mlir
test/ACIR/runtime-references-invalid.mlir | +| ac.stat.add | ACIR_StatAddOp | test/ACIR/contracts-valid.mlir
test/ACIR/trace-valid.mlir
test/Transforms/freeze-topology.mlir | test/ACIR/runtime-references-invalid.mlir | +| ac.instrumentation | ACIR_InstrumentationOp | test/ACIR/contracts-valid.mlir
test/ACIR/hierarchy-valid.mlir
test/Transforms/freeze-topology.mlir | test/ACIR/contracts-invalid.mlir
test/ACIR/hierarchy-invalid.mlir | + +## acir types (22) + +| type | source symbol | positive coverage | negative coverage | +| --- | --- | --- | --- | +| !ac.struct | ACIR_StructType | test/ACIR/declarations-valid.mlir
test/ACIR/memory.mlir
test/ACIR/queue-var-types.mlir
test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir
test/ACIR/select.mlir
test/ACIR/types-valid.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/deterministic-canonicalization.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/declarations-invalid.mlir
test/ACIR/memory-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/types-invalid.mlir | +| !ac.packet | ACIR_PacketType | test/ACIR/declarations-valid.mlir
test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir
test/ACIR/types-valid.mlir | test/ACIR/records-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir | +| !ac.transaction | ACIR_TransactionType | test/ACIR/protocols-valid.mlir
test/ACIR/records-valid.mlir
test/ACIR/types-valid.mlir | test/ACIR/declarations-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/records-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir
test/ACIR/var-expressions-invalid.mlir | +| !ac.enum | ACIR_EnumType | test/ACIR/declarations-valid.mlir
test/ACIR/review-r1-valid.mlir
test/ACIR/types-valid.mlir | test/ACIR/declarations-invalid.mlir | +| !ac.union | ACIR_UnionType | test/ACIR/declarations-valid.mlir
test/ACIR/review-r1-valid.mlir
test/ACIR/types-valid.mlir | test/ACIR/types-invalid.mlir | +| !ac.optional | ACIR_OptionalType | test/ACIR/types-valid.mlir | test/ACIR/review-r2-invalid.mlir
test/ACIR/route-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir
test/ACIR/types-invalid.mlir
test/ACIR/var-expressions-invalid.mlir | +| !ac.list | ACIR_ListType | test/ACIR/review-r1-valid.mlir
test/ACIR/types-valid.mlir
test/Transforms/verify-model.mlir | test/ACIR/queue-var-types-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir | +| !ac.vector | ACIR_VectorType | test/ACIR/records-valid.mlir
test/ACIR/review-r1-valid.mlir
test/ACIR/types-valid.mlir | test/ACIR/records-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/review-r2-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir
test/ACIR/types-invalid.mlir | +| !ac.var | ACIR_VarType | test/ACIR/control-feedback.mlir
test/ACIR/credit.mlir
test/ACIR/dependency.mlir
test/ACIR/expect.mlir
test/ACIR/firing.mlir
test/ACIR/memory.mlir
test/ACIR/queue-var-types.mlir
test/ACIR/reorder.mlir
test/ACIR/route.mlir
test/ACIR/scope.mlir
test/ACIR/select.mlir
test/ACIR/transform.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/atomic-transform.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/control-feedback-invalid.mlir
test/ACIR/credit-invalid.mlir
test/ACIR/dependency-invalid.mlir
test/ACIR/expect-invalid.mlir
test/ACIR/firing-invalid.mlir
test/ACIR/memory-invalid.mlir
test/ACIR/queue-var-types-invalid.mlir
test/ACIR/reorder-invalid.mlir
test/ACIR/route-invalid.mlir
test/ACIR/select-invalid.mlir
test/ACIR/transform-invalid.mlir
test/ACIR/var-expressions-invalid.mlir | +| !ac.queue | ACIR_QueueType | test/ACIR/barrier.mlir
test/ACIR/broadcast.mlir
test/ACIR/control-feedback.mlir
test/ACIR/credit.mlir
test/ACIR/dependency.mlir
test/ACIR/expect.mlir
test/ACIR/firing.mlir
test/ACIR/fork.mlir
test/ACIR/memory.mlir
test/ACIR/observe.mlir
test/ACIR/queue-var-types.mlir
test/ACIR/reorder.mlir
test/ACIR/route.mlir
test/ACIR/scope.mlir
test/ACIR/select.mlir
test/ACIR/source-sink.mlir
test/ACIR/transform.mlir
test/ACIR/var-expressions.mlir
test/CodeGen/arbiter-verilog.mlir
test/CodeGen/arbiter.mlir
test/CodeGen/atomic-transform.mlir
test/CodeGen/popcount-verilog.mlir
test/CodeGen/popcount.mlir
test/Transforms/queue-var-cse.mlir | test/ACIR/barrier-invalid.mlir
test/ACIR/broadcast-invalid.mlir
test/ACIR/control-feedback-invalid.mlir
test/ACIR/credit-invalid.mlir
test/ACIR/dependency-invalid.mlir
test/ACIR/expect-invalid.mlir
test/ACIR/firing-invalid.mlir
test/ACIR/fork-invalid.mlir
test/ACIR/memory-invalid.mlir
test/ACIR/observe-invalid.mlir
test/ACIR/queue-var-types-invalid.mlir
test/ACIR/reorder-invalid.mlir
test/ACIR/route-invalid.mlir
test/ACIR/scope-invalid.mlir
test/ACIR/select-invalid.mlir
test/ACIR/source-sink-invalid.mlir
test/ACIR/transform-invalid.mlir | +| !ac.array | ACIR_ArrayType | test/ACIR/queue-var-types.mlir | test/ACIR/queue-var-types-invalid.mlir | +| !ac.map | ACIR_MapType | test/ACIR/queue-var-types.mlir | test/ACIR/queue-var-types-invalid.mlir | +| !ac.set | ACIR_SetType | test/ACIR/queue-var-types.mlir | test/ACIR/queue-var-types-invalid.mlir | +| !ac.flow | ACIR_FlowType | test/ACIR/protocols-valid.mlir
test/ACIR/types-valid.mlir
test/Transforms/verify-model.mlir | test/ACIR/interfaces-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir
test/ACIR/types-invalid.mlir | +| !ac.endpoint | ACIR_EndpointType | test/ACIR/interfaces-valid.mlir
test/ACIR/types-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir | test/ACIR/interfaces-invalid.mlir
test/ACIR/protocols-invalid.mlir
test/ACIR/resources-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir | +| !ac.resource_ref | ACIR_ResourceRefType | test/ACIR/types-valid.mlir | test/ACIR/topology-review-r1-invalid.mlir | +| !ac.channel | ACIR_ChannelType | test/ACIR/interfaces-valid.mlir
test/ACIR/port-mapping-review-r2.mlir
test/ACIR/resources-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir
test/Transforms/deterministic-canonicalization.mlir | test/ACIR/interfaces-invalid.mlir
test/ACIR/interfaces-review-r1-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir
test/ACIR/types-invalid.mlir | +| !ac.duration | ACIR_DurationType | test/ACIR/types-valid.mlir | test/ACIR/types-invalid.mlir | +| !ac.rate | ACIR_RateType | test/ACIR/types-valid.mlir | test/ACIR/types-invalid.mlir | +| !ac.event | ACIR_EventType | test/ACIR/process-valid.mlir
test/ACIR/resources-valid.mlir
test/ACIR/types-valid.mlir | test/ACIR/resources-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir | +| !ac.address | ACIR_AddressType | test/ACIR/types-valid.mlir | test/ACIR/types-invalid.mlir | +| !ac.resource_token | ACIR_ResourceTokenType | test/ACIR/process-valid.mlir
test/ACIR/types-valid.mlir | test/ACIR/hierarchy-invalid.mlir
test/ACIR/process-invalid.mlir
test/ACIR/review-r1-invalid.mlir
test/ACIR/topology-review-r1-invalid.mlir | + +## acsim operations (22) + +| operation | source symbol | positive coverage | negative coverage | +| --- | --- | --- | --- | +| acsim.model | ACSim_ModelOp | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/driver-errors.mlir
test/CodeGen/driver-stages.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/structure.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | +| acsim.type | ACSim_TypeOp | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/bindings.mlir
test/Conversion/process-basic.mlir
test/Conversion/structure.mlir
test/Conversion/time-domains.mlir | test/ACSim/ops-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| acsim.binding | ACSim_BindingOp | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/Conversion/bindings.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | +| acsim.module | ACSim_ModuleOp | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/driver-errors.mlir
test/CodeGen/driver-stages.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/pure-results.mlir
test/Conversion/structure.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| acsim.instance | ACSim_InstanceOp | test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/extension-provider.mlir
test/Conversion/bindings.mlir
test/Conversion/hierarchy.mlir
test/Conversion/typed-graph.mlir | test/ACSim/ops-invalid.mlir | +| acsim.array | ACSim_ArrayOp | test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/multidimensional-array.mlir
test/Conversion/collections.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | +| acsim.element | ACSim_ElementOp | test/ACSim/ops-valid.mlir | test/ACSim/ops-invalid.mlir | +| acsim.port | ACSim_PortOp | test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/Conversion/typed-graph.mlir | test/ACSim/ops-invalid.mlir | +| acsim.resource | ACSim_ResourceOp | test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir | test/ACSim/ops-invalid.mlir | +| acsim.bind | ACSim_BindOp | test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | +| acsim.inline | ACSim_InlineOp | test/ACSim/generated-call-contract.mlir
test/ACSim/ops-valid.mlir
test/Conversion/process-live-values.mlir
test/Conversion/pure-results.mlir | test/ACSim/ops-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| acsim.process | ACSim_ProcessOp | test/ACSim/generated-call-contract.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/process-basic.mlir
test/Conversion/process-control-flow.mlir
test/Conversion/process-live-values.mlir
test/Conversion/structure.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir
test/Conversion/process-invalid.mlir | +| acsim.live.load | ACSim_LiveLoadOp | test/ACSim/ops-valid.mlir
test/Conversion/process-live-values.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | +| acsim.live.store | ACSim_LiveStoreOp | test/ACSim/ops-valid.mlir
test/Conversion/process-live-values.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | +| acsim.invoke | ACSim_InvokeOp | test/ACSim/generated-call-contract.mlir
test/ACSim/ops-valid.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/process-basic.mlir
test/Conversion/structure.mlir | test/ACSim/ops-invalid.mlir | +| acsim.continue | ACSim_ContinueOp | test/ACSim/ops-valid.mlir | test/ACSim/ops-invalid.mlir | +| acsim.suspend | ACSim_SuspendOp | test/ACSim/ops-valid.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/process-basic.mlir
test/Conversion/process-control-flow.mlir
test/Conversion/process-live-values.mlir
test/Conversion/structure.mlir | test/ACSim/ops-invalid.mlir | +| acsim.terminate | ACSim_TerminateOp | test/ACSim/generated-call-contract.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir | test/ACSim/ops-invalid.mlir | +| acsim.export | ACSim_ExportOp | test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/Conversion/pure-results.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | +| acsim.dispatch | ACSim_DispatchOp | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/process-basic.mlir
test/Conversion/pure-results.mlir
test/Conversion/structure.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| acsim.activate | ACSim_ActivateOp | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/structure.mlir
test/Conversion/typed-graph.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | +| acsim.return | ACSim_ReturnOp | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/CodeGen/driver-errors.mlir
test/CodeGen/driver-stages.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir
test/Conversion/pure-results.mlir
test/Conversion/structure.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir | + +## acsim types (11) + +| type | source symbol | positive coverage | negative coverage | +| --- | --- | --- | --- | +| !acsim.value | ACSim_ValueType | test/ACSim/generated-call-contract.mlir
test/ACSim/ops-valid.mlir
test/ACSim/types-valid.mlir | test/ACSim/ops-invalid.mlir | +| !acsim.expr | ACSim_ExprType | test/ACSim/generated-call-contract.mlir
test/ACSim/ops-valid.mlir
test/ACSim/types-valid.mlir
test/Conversion/pure-results.mlir | test/ACSim/ops-invalid.mlir
test/Conversion/bindings-invalid.mlir | +| !acsim.owner | ACSim_OwnerType | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/ACSim/types-valid.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/Conversion/bindings.mlir
test/Conversion/collections.mlir
test/Conversion/hierarchy.mlir | test/ACSim/ops-invalid.mlir
test/ACSim/types-invalid.mlir | +| !acsim.ref | ACSim_RefType | test/ACSim/generated-call-contract.mlir
test/ACSim/ops-valid.mlir
test/ACSim/types-valid.mlir | test/ACSim/ops-invalid.mlir
test/ACSim/types-invalid.mlir | +| !acsim.port | ACSim_PortType | test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/types-valid.mlir
test/Conversion/whole-model.mlir | test/ACSim/ops-invalid.mlir
test/ACSim/types-invalid.mlir | +| !acsim.resource | ACSim_ResourceType | test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/types-valid.mlir | test/ACSim/ops-invalid.mlir
test/ACSim/types-invalid.mlir | +| !acsim.array | ACSim_ArrayType | test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/ACSim/types-valid.mlir
test/CodeGen/multidimensional-array.mlir
test/Conversion/collections.mlir | test/ACSim/ops-invalid.mlir
test/ACSim/types-invalid.mlir | +| !acsim.object_id | ACSim_ObjectIdType | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/ACSim/types-valid.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/structure.mlir | test/ACSim/ops-invalid.mlir | +| !acsim.activation_id | ACSim_ActivationIdType | test/ACSim/generated-call-contract.mlir
test/ACSim/generated-wrapper-activation.mlir
test/ACSim/ops-valid.mlir
test/ACSim/reusable-modules.mlir
test/ACSim/types-valid.mlir
test/CodeGen/extension-provider.mlir
test/CodeGen/multidimensional-array.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/structure.mlir | test/ACSim/ops-invalid.mlir | +| !acsim.pc | ACSim_PcType | test/ACSim/generated-call-contract.mlir
test/ACSim/types-valid.mlir | test/ACSim/types-invalid.mlir | +| !acsim.wake | ACSim_WakeType | test/ACSim/generated-call-contract.mlir
test/ACSim/ops-valid.mlir
test/ACSim/types-valid.mlir
test/CodeGen/process-control-flow.mlir
test/Conversion/process-basic.mlir
test/Conversion/structure.mlir | test/ACSim/ops-invalid.mlir | diff --git a/components/agentic-circuit/docs/spec/50-verification/pyc-verilog-backend.md b/components/agentic-circuit/docs/spec/50-verification/pyc-verilog-backend.md new file mode 100644 index 00000000..50f1f558 --- /dev/null +++ b/components/agentic-circuit/docs/spec/50-verification/pyc-verilog-backend.md @@ -0,0 +1,63 @@ +# In-tree PYC-to-Verilog bridge + +## Verify the integrated Verilog bridge {#VER-PYC-VERILOG-001} + + +The `acir-queue-veriloggen.py` entrypoint closes the first integrated backend +slice without introducing a second ACIR lowering: + +```text +frozen ACIR + -> acir-queue-pycgen + -> canonical textual PYC IR + -> PYC compatibility emitter + -> self-contained Verilog (PYC runtime modules embedded) +``` + +The runtime modules under `resources/pyc_runtime/verilog/` are the small +cycle-level primitives migrated from pyCircuit's runtime surface: `pyc_fifo`, +`pyc_reg`, `pyc_popcount`, and `pyc_rr_arbiter`. The bridge currently covers +the operations needed by the golden Popcount and round-robin Queue slices: +FIFO, register, wiring, constants, mux/boolean/integer expressions, concat, +extract, Popcount, and round-robin Arbiter. Unsupported PYC operations fail +explicitly instead of silently producing incomplete RTL. + +Pure Python compute regions spell population count as `ac.popcount(value)`. +The bridge also preserves canonical `pyc.assert` operations as simulation-only +Verilog checks guarded by synthesis translation directives; failure semantics +are not silently removed from Verilator runs. + +Example: + +```bash +python tools/acir-queue-veriloggen.py test/CodeGen/arbiter-verilog.mlir \ + --pycgen build/dev-llvm22/bin/acir-queue-pycgen \ + --emit-pyc /tmp/arbiter.pyc.mlir \ + --output /tmp/arbiter.v +``` + +For a constrained WSL host, build only the producer needed by this bridge and +keep Ninja single-threaded: + +```bash +cmake --build build/dev-llvm22 --target acir-queue-pycgen -j1 +python tools/acir-queue-veriloggen.py test/CodeGen/arbiter-verilog.mlir \ + --pycgen build/dev-llvm22/bin/acir-queue-pycgen --timeout 120 \ + --output /tmp/arbiter.v +``` + +The timeout guards against an accidentally started full build or malformed +input keeping the lowering process alive. It does not start a compiler build; +the bridge consumes an already-built `acir-queue-pycgen` executable. + +The generated Verilog is self-contained, so Verilator/Yosys can consume one +file. This is an integration bridge, not yet the generic registry-driven +`pycc` replacement. The next step is to replace the textual compatibility +emitter with the shared PYC MLIR dialect/emitter once the repositories agree on +one LLVM/MLIR toolchain. + +The same path is covered by `test/CodeGen/popcount-verilog.mlir`; the +fixture exercises packed-field extraction (`i12` to `i8`) before the qualified +`pyc.popcount` primitive. `test/CodeGen/pyc-primitives-smoke.sv` provides a +bounded N=2/N=3/N=4 arbitration, popcount, and FIFO simulation. The smoke is +part of `PycVerilogBackendTests` whenever Verilator is available. diff --git a/components/agentic-circuit/docs/spec/50-verification/repository-layout.md b/components/agentic-circuit/docs/spec/50-verification/repository-layout.md new file mode 100644 index 00000000..26022e30 --- /dev/null +++ b/components/agentic-circuit/docs/spec/50-verification/repository-layout.md @@ -0,0 +1,9 @@ +# Repository layout verification + +## Enforce the release-owned layout {#VER-LAYOUT-001} + + +`scripts/check-release-layout.py` rejects tracked product-version and phase +paths and checks the required semantic roots. `scripts/check-ndf.py` validates +clause metadata, stable IDs, relationship targets, and L1 verification +coverage. Repository contract tests execute both gates. diff --git a/components/agentic-circuit/docs/spec/README.md b/components/agentic-circuit/docs/spec/README.md new file mode 100644 index 00000000..063c88a3 --- /dev/null +++ b/components/agentic-circuit/docs/spec/README.md @@ -0,0 +1,45 @@ +# Agentic Circuit Specifications + +This directory contains the human-readable contracts for Agentic Circuit. +Machine-readable schemas under [`schemas`](../../schemas) and MLIR ODS +definitions under [`include/acir`](../../include/acir) remain the executable +sources of truth. + +The generated [official Queue building-block catalog](../../schemas/opcodes.json) +records the closed opcode roles, arity, constants, backend realizations, and +refinement observations. + +## Current manuals + +- [Agentic Circuit Specification Manual](agentic-circuit.md) defines the + implementation-facing serial Python, ACIR, gfsim, PYC, and refinement + contract. +- [Agentic Circuit 团队 Specification 手册](agentic-circuit.zh-CN.md) + provides a Chinese teammate-facing overview, common patterns, executable + examples, backend differences, and troubleshooting guidance. + +## NDF decision spine + +The repository follows the restricted NDF profile declared in +[`ndf.yaml`](ndf.yaml). Stable clause IDs connect the current architecture, +decisions, historical evidence, and verification: + +- [`ARC-RELEASE-001`, `ARC-LAYOUT-001`, and `ARC-HISTORY-001`](00-charter/scope.md) + define release ownership, semantic source partitions, and history policy. +- [`D-RELEASE-LAYOUT-001`](decisions/D-RELEASE-LAYOUT-001.md) records the + hard-break release-neutral layout decision. +- [`D-BLOCK-MODEL-001`](decisions/D-BLOCK-MODEL-001.md) records the current + Queue/Var building-block model. +- [`REF-HISTORY-001`](refs/history.md) pins removed historical specifications + to an immutable Git revision. +- [`VER-LAYOUT-001`](50-verification/repository-layout.md) binds these rules to + executable repository checks. +- [`VER-PYC-VERILOG-001`](50-verification/pyc-verilog-backend.md) records the + integrated PYC-to-Verilog bridge and its executable fixtures. +- [IR coverage ledger](50-verification/ir-coverage.md) is generated from the + current ACIR/ACSim manifests and lit coverage. + +Product releases are represented by Git tags and GitHub Releases. The source +tree does not retain product-version or implementation-phase paths, aliases, +or compatibility symlinks. Serialized contract epochs and external dependency +pins remain versioned where interoperability and reproducibility require them. diff --git a/components/agentic-circuit/docs/spec/agentic-circuit.md b/components/agentic-circuit/docs/spec/agentic-circuit.md new file mode 100644 index 00000000..31e22192 --- /dev/null +++ b/components/agentic-circuit/docs/spec/agentic-circuit.md @@ -0,0 +1,1412 @@ +# Agentic Circuit Specification Manual + +| Field | Value | +| --- | --- | +| Specification | Serial Python, Queue/Var ACIR, typed gfsim, and PYC refinement | +| Target contract epoch | `0.3` | +| Status | Current implementation contract; serialized epoch `0.3` is active on `main` | +| Public namespace | `ac` | +| Audience | Frontend, compiler, simulator, and RTL contributors | +| Design background | [NDF block-model decision](decisions/D-BLOCK-MODEL-001.md) | +| Executable examples | [Pipeline examples](../../examples/pipelines/README.md) | + +## Purpose + +Agentic Circuit lets an author describe a static circuit as serial-looking +Python. The author names values and lexical scopes; the compiler infers queue +connections, scope boundaries, typed payloads, and common hardware building +blocks. The same frozen ACIR graph can generate: + +- a deterministic typed gfsim C++ model built around `SimQueue`; and +- canonical PYC IR that external pinned `pycc` lowers to PYC C++ and Verilog. + +This manual is the implementation-facing specification for teammates. It +defines the supported programming model, ACIR contracts, backend obligations, +examples, and current limitations. The NDF +[block-model decision](decisions/D-BLOCK-MODEL-001.md) records the architectural +rationale; this document records the executable contract. + +![Agentic Circuit compilation and refinement](images/agentic-circuit-pipeline.svg) + +The editable diagram source is +[`agentic-circuit-pipeline.drawio`](images/agentic-circuit-pipeline.drawio). + +## Status and authority + +The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are +normative requirements for the current contract. + +Producers emit exact serialized epoch `0.3`; consumers reject other epochs +before interpreting the artifact. The toolchain provides no compatibility +alias or best-effort conversion. + +When this manual and implementation disagree, use the following authority +order: + +1. machine-readable schema and MLIR ODS definitions; +2. verifier and conformance tests; +3. this manual; +4. NDF decisions and references. + +The principal machine-readable and executable sources are: + +- [`ACIRTypes.td`](../../include/acir/Dialect/ACIR/ACIRTypes.td) for Queue, Var, + and collection types; +- [`ACIROps.td`](../../include/acir/Dialect/ACIR/ACIROps.td) for operation + signatures; +- [`ACIROps.cpp`](../../lib/Dialect/ACIR/ACIROps.cpp) for semantic verification; +- [`QueueGraphPlan.cpp`](../../lib/CodeGen/QueueGraphPlan.cpp) for the frozen + backend plan; +- [`opcodes.json`](../../schemas/opcodes.json) for the closed official + building-block catalog and backend availability; +- [`queue.h`](../../include/gfsim/queue.h) and + [`queue_blocks.h`](../../include/gfsim/queue_blocks.h) for gfsim behavior; +- [`test_queue_frontend.py`](../../tests/python_frontend/test_queue_frontend.py) + for accepted and rejected Python syntax; +- [`test/ACIR`](../../test/ACIR) for ACIR conformance tests. + +The executable conformance suites and generated +[IR coverage ledger](50-verification/ir-coverage.md) track the live +requirement-by-requirement status. + +## Core mental model + +### Serial Python elaborates a static graph + +An `@ac.system` body is not an imperative program that runs once per simulated +cycle. The frontend parses its Python AST and treats statements as graph +construction in source order. + +```python +@ac.system +def pipeline() -> None: + incoming = ac.source(int) + adjusted = incoming.apply(lambda item: item + 1) + ac.sink(adjusted) +``` + +This source creates two Queue values and one persistent transform block: + +```text +incoming Queue -> transform(item + 1) -> adjusted Queue -> sink +``` + +The frontend does not require explicit module input or output declarations. +`ac.source(...)` and `ac.sink(...)` define the current executable boundary, and +lexical uses determine scope inputs and outputs. + +### Queue is state; Var is combinational value + +`!ac.queue` is a finite, typed, stateful FIFO channel. It has positive depth, +positive latency, occupancy, backpressure, stable identity, and commit-time +effects. + +`!ac.var` is an immutable, zero-latency value. It has no occupancy, capacity, +push, pop, or independent runtime identity. Lambda arguments, constants, +arithmetic results, comparisons, field projections, and immutable field updates +are Vars. + +```text +!ac.queue stateful channel +!ac.var combinational scalar +!ac.queue> stateful typed channel +!ac.var> immutable token value +``` + +A Queue MUST have latency of at least one. A zero-latency Queue is invalid; +zero-latency logic belongs in a Var region. + +### Mutable channel, immutable token + +Queue state changes at commit. Token payloads do not mutate in place. + +```python +# Valid: creates a new immutable token value. +next_item = item.with_fields(remaining=item.remaining - 1) + +# Invalid: mutates the input object. +item.remaining -= 1 +``` + +The frontend and backends MAY copy or move an immutable token internally, but +they MUST NOT expose mutable aliases that change a token already stored in a +Queue. + +### Opcodes are common building blocks + +The public `ac.*` inventory is closed and repository-owned. Users compose +common transport, computation, state, boundary, and observation blocks. They +MUST NOT define private opcodes, C++ providers, PYC providers, or raw Verilog +providers. + +Application stages such as `decode`, `rename`, `dispatch`, and `retire` are +scope names or compositions. They are not generic ACIR opcodes. + +Generate the canonical catalog directly from the shared backend contract table: + +```sh +build/dev-llvm22/bin/acir-opcode-catalog +agentic-circuit schema opcode ac.transform +``` + +## Python authoring contract + +### System declaration + +A Queue/Var system uses `@ac.system` and takes no parameters. Inputs and outputs +are inferred from the body. + +```python +import agentic_circuit as ac + + +@ac.system +def pipeline() -> None: + value = ac.source(int) + ac.sink(value) +``` + +The source file is compiled through AST capture. The queue primitives inside +the system body are syntax markers; ordinary Python execution of the body is +not the compilation path. + +### Payload structures + +Use `@ac.struct` to define a compile-time token layout. + +```python +@ac.struct +class WorkItem: + value: ac.u32 + route: ac.u2 + remaining: ac.u16 + valid: bool +``` + +The current frontend accepts these scalar field spellings: + +| Python spelling | ACIR element type | +| --- | --- | +| `bool` | `i1` | +| `int` | `i64` | +| `ac.u1`, `ac.u2`, `ac.u4` | `i1`, `i2`, `i4` | +| `ac.u8`, `ac.u16`, `ac.u32`, `ac.u64` | corresponding integer width | +| `ac.s8`, `ac.s16`, `ac.s32`, `ac.s64` | corresponding integer width | + +Field order is declaration order. Fields MUST be unique and annotated. The +current ACIR integer type freezes width but not signedness as a distinct type; +do not rely on unsigned comparison semantics until the signedness contract is +made explicit. + +### Source and sink + +`ac.source(T, depth=N, latency=L)` creates a Queue boundary with payload `T`. +`depth` and `latency` default to one and MUST be positive compile-time integers. + +```python +incoming = ac.source(WorkItem, depth=8, latency=1) +ac.sink(incoming) +``` + +`ac.sink(queue)` consumes tokens from a Queue. A system MUST contain at least +one source Queue and at least one sink. + +### Transform with `apply` + +`queue.apply(lambda item: expression, depth=N, latency=L)` creates an +`ac.transform` block and one output Queue. + +```python +updated = incoming.apply( + lambda item: item.with_fields( + value=(item.value + 1) * 2, + remaining=item.remaining - 1, + ), + depth=4, + latency=2, +) +``` + +The current lambda subset supports: + +- the lambda parameter itself; +- integer and Boolean constants; +- structure field reads; +- `+`, `-`, and `*` over identical Var types; +- `==`, `!=`, `<`, `<=`, `>`, and `>=`; +- immutable `with_fields(...)` updates. + +The lambda MUST take exactly one argument and MUST return the Queue payload +type. Function calls other than `with_fields`, mutation, I/O, allocation, +ambient state access, and arbitrary Python expressions are rejected. + +### Lexical scope and inferred boundaries + +`with ac.scope("name"):` defines ownership and hierarchy. It does not declare +ports. + +```python +incoming = ac.source(int) + +with ac.scope("frontend"): + adjusted = incoming.apply(lambda item: item + 1) + with ac.scope("inner"): + completed = adjusted.apply(lambda item: item * 2) + +ac.sink(completed) +``` + +The compiler infers: + +- `incoming` as a borrowed input of `/frontend`; +- `adjusted` as a Queue owned inside `/frontend`; +- `completed` as an exported output of `/frontend/inner` and `/frontend`; +- parent ownership for an interconnect at the lowest common lexical ancestor. + +Scope names MUST be non-empty, and one lexical path MUST NOT be declared twice. + +### Multiple consuming uses + +Queue consumption is destructive. If one Queue variable feeds multiple +`apply` statements, the frontend inserts `ac.broadcast` at the lexical lowest +common ancestor. + +```python +incoming = ac.source(int) +left = incoming.apply(lambda item: item + 1) +right = incoming.apply(lambda item: item + 2) +``` + +The inserted broadcast is strict and atomic: it pops the input only when every +output can accept the token. It has no hidden per-output progress state. + +### Explicit decoupled fork + +Use `fork` when outputs may accept the token on different cycles. + +```python +left, right = incoming.fork(outputs=2, depth=2, latency=1) +``` + +`ac.fork` retains one token and a per-output delivered mask until every output +has accepted that token. Each output receives the token exactly once. The input +is popped only after delivery to all outputs completes. + +This distinction is normative: + +| Block | Acceptance rule | Hidden progress state | +| --- | --- | --- | +| `ac.broadcast` | all outputs accept in one atomic firing | none | +| `ac.fork` | outputs may accept independently | per-token delivered mask | + +### Route + +`route` sends one token to exactly one statically declared output. + +```python +scalar, vector, cube, tma = prepared.route( + outputs=4, + key=lambda item: item.route, + depth=2, + latency=1, +) +``` + +The output tuple arity MUST equal `outputs`. The selector lambda returns an +integer or enum-like Var. A selector outside `[0, outputs)` is a deterministic +runtime failure named `route_selector_out_of_range`; it is not wrapped or +clamped. + +### Merge + +`merge` combines two or more Queues with identical payload types. + +```python +completed = scalar_done.merge( + vector_done, + cube_done, + tma_done, + policy="round_robin", + depth=8, + latency=1, +) +``` + +Supported policies are: + +- `priority`: select the first ready input in source order; +- `round_robin`: begin from a committed cursor and advance the cursor after a + successful transfer. + +The output Queue applies ordinary capacity and latency rules. + +### Dependency scheduling + +`depend` is the generic bounded dependency window. It admits typed tokens, +starts a token when its predecessor is complete, counts its declared execution +cost, and emits tokens in completion order. + +```python +completed = issued.depend( + key=lambda item: item.sequence_id, + waits_for=lambda item: item.waits_for, + resource=lambda item: item.route, + cost=lambda item: item.cycles, + capacity=8, + resources=4, + no_dependency=255, + depth=8, + latency=1, +) +``` + +The three lambdas are pure Var regions. `key` and `waits_for` MUST return the +same integer Var type, no wider than 64 bits. `no_dependency` MUST fit that +type. `resource` selects one of the statically declared resources; each resource +admits at most one executing token at a time. `cost` MUST return a positive +integer at runtime. Dependencies refer to tokens retained in the bounded +window; a missing predecessor blocks the token and can participate in deadlock +diagnostics. + +### Credit scheduling + +`credit` is a bounded parallel completion window. It admits at most `credits` +tokens, evaluates a pure per-token cost, advances every occupied slot once per +epoch, and returns the slot when the completed token transfers to the output +Queue. + +```python +completed = issued.credit( + cost=lambda item: item.cycles, + credits=2, + depth=4, + latency=1, +) +``` + +`credits`, output `depth`, and output `latency` are positive compile-time +constants. The cost lambda MUST return an integer Var no wider than 64 bits. +Every accepted runtime cost MUST be positive; zero or negative cost produces +the deterministic `credit_nonpositive_cost` failure/assertion. + +Each slot counts down independently, so completion order may differ from input +order. When multiple slots are complete, the block chooses the lowest canonical +slot index and emits at most one token per epoch. Admission and retirement may +occur in the same epoch when they use different committed slots. A slot retired +in the current Xfer is not reused until a later epoch. + +### Barrier synchronization + +`barrier` synchronizes two or more Queue heads and publishes a positionally +matching output tuple as one atomic firing. Input payload types may differ. + +```python +left_ready, right_ready = left.barrier( + right, + depth=2, + latency=1, +) +``` + +All input Queues MUST be distinct. The output count MUST equal the input count, +and each output payload type MUST match its corresponding input. The barrier +waits until every input can pop and every output can accept; then all pops and +pushes commit together. Before that Xfer, it publishes no partial result. + +The output Queues are ordinary independent Queues after the atomic transfer. +Downstream consumers may therefore drain them on different later epochs without +changing the barrier firing contract. + +### Typed memory + +`memory` declares a physical single-read/single-write state instance. One or +more logical request endpoints connect to it; requests and responses use one +shared structure type. Three pure lambdas select the address, write enable, and +write data; `result_field` names the response field replaced with old data. + +```python +@ac.struct +class MemoryRequest: + address: ac.u4 + write: ac.u1 + data: ac.u16 + tag: ac.u8 + + +sram = ac.memory(ac.u16, entries=16, init=0, latency=3) +requests = ac.source(MemoryRequest, depth=4, latency=1) +responses = sram.request( + requests, + address=lambda item: item.address, + write=lambda item: item.write, + data=lambda item: item.data, + result_field="data", + depth=4, +) +ac.sink(responses) +``` + +`entries`, `init`, instance `latency`, `result_field`, and `depth` are +compile-time constants. `entries`, `depth`, and instance `latency` MUST be +positive. The response Queue has fixed latency one. The address MUST +be an integer Var no wider than 64 bits and wide enough to represent every +entry. The write policy MUST return `!ac.var`. The data policy and result +field MUST have the same integer type, no wider than 64 bits. The current contract supports +deterministic zero initialization only, so `init` MUST equal zero. + +Every accepted request performs a read. A request accepted in cycle `T` can +offer its response no earlier than `T + latency`; the instance remains busy +throughout that interval and while the response Queue is blocked. A response +preserves the request's other fields and replaces `result_field` with the +pre-transfer memory value. A +write commits at Xfer. Therefore a read and write to the same address in one +request returns old data and makes the new data visible to a later request. + +### Reorder + +`reorder` accepts out-of-order completions and releases them in monotonically +increasing key order. It is the generic ordering primitive used to compose a +ROB-like retirement path; `retire` itself remains an application scope. + +```python +retired = completed.reorder( + key=lambda item: item.sequence_id, + capacity=64, + start=0, + depth=8, + latency=1, +) +``` + +The key lambda MUST return an integer Var no wider than 64 bits. `capacity`, +`start`, output `depth`, and output `latency` are compile-time constants. The +non-negative `start` value MUST fit the key width. The +block backpressures when every entry is occupied and emits only the token whose +key equals the committed next key. Duplicate, negative, or already retired keys +are invalid. + +### Observation + +`ac.observe(queue)` reads the committed Queue head without consuming it and +without participating in backpressure. + +```python +ac.observe(completed) +ac.sink(completed) +``` + +An observation-only use does not cause broadcast insertion. Observations may +record a new head when the token or committed pop count changes, but MUST NOT +alter functional state. + +### Verification expectation + +`ac.expect` is a non-consuming verification leaf for gfsim and PYC testbench +boundaries. + +```python +ac.expect( + completed, + predicate=lambda item: item.value > 0, + message="value must be positive", +) +``` + +The predicate MUST be pure and return bool. gfsim evaluates each new committed +head and reports `expectation_failed` without consuming or backpressuring the +Queue. `ac.expect` is verification-role, not design-role: PYC design emission +rejects it with an explicit instruction to place the check at the testbench +boundary. `ac.observe` remains observation-role and may enter design lowering +because it cannot change functional state. + +### Atomic group + +Each ordinary `apply` is an atomic input-pop/output-push firing. Use +`with ac.atomic():` to group at least two independent direct Queue transforms. + +```python +left = ac.source(int) +right = ac.source(int) + +with ac.atomic(): + left_next = left.apply(lambda item: item + 1) + right_next = right.apply(lambda item: item * 2) +``` + +All input Queues in the group MUST be unique. The grouped transform fires only +when every input can pop and every output can push; all effects commit or none +commit. + +### Explicit Python firing effects + +Use `firing` when a low-level algorithm is clearer as explicit `peek`, `pop`, +and `push` effects while keeping the destination Queue implicit. + +```python +outgoing = incoming.firing( + lambda queue: queue.push( + queue.pop().with_fields( + value=queue.peek().value + 1, + ) + ) +) +``` + +One Python firing MUST contain exactly one `pop` and one outer `push`; it MAY +contain repeated non-consuming `peek` calls. `peek` and `pop` return immutable +token Vars, and `push` requires the unchanged Queue payload type. Queue effects +are rejected inside ordinary `apply` lambdas. + +The frontend normalizes this one-input/one-output form to the standard atomic +`ac.transform` building block. The lower-level `ac.firing` and +`ac.queue.peek/pop/push` operations remain the normative ACIR effect contract +for future multi-Queue/state-effect normalization; generated hot paths do not +interpret Python effect objects. + +### Bounded feedback + +The current runtime-loop form is one Queue rebinding through one `apply`. + +```python +current = ac.source(WorkItem) + +while current.remaining > 0: + current = current.apply( + lambda item: item.with_fields( + value=item.value + 1, + remaining=item.remaining - 1, + ), + depth=2, + latency=1, + ) + +ac.sink(current) +``` + +The frontend lowers this form to `ac.feedback` with a stateful feedback Queue. +The current compiler freezes `max_iterations = 1024`. When the condition is +false, the current token exits unchanged. When it is true, the immutable update +is recirculated. Exceeding the bound reports `feedback_iteration_limit`. + +A bounded loop may place one runtime `break` guard before its Queue update and +one runtime `continue` guard at the tail: + +```python +while current.remaining > 0: + if current.stop: + break + current = current.apply(step) + if current.skip: + continue +``` + +The leading `break` becomes part of the explicit feedback continuation +condition; a matching token exits unchanged. A tail `continue` targets the same +feedback edge as normal loop fallthrough and is normalized to that edge. Other +statement placement, loop `else`, and more than one Queue update remain +deterministic errors. + +### Static collections + +Queue collections have compile-time shape and membership. + +```python +lanes = ac.array( + 2, + lambda lane: ac.source(int, depth=lane + 1), +) +named = ac.map({"right": lanes[1], "left": lanes[0]}) +active = ac.set({named["right"], named["left"]}) + +for lane in active: + ac.sink(lane) +``` + +Runtime selection from a flat Queue collection uses one explicit control Queue +and lowers to the official `ac.select` mux. It never creates a runtime Queue +handle. + +```python +control = ac.source(SelectControl) +lanes = ac.array(2, lambda index: ac.source(int)) +selected = lanes.select( + control, + key=lambda item: item.route, +) +ac.sink(selected) +``` + +The control token and exactly one selected data token transfer atomically. An +out-of-range selector produces `select_selector_out_of_range`. Nested +collections MUST first be statically flattened to a flat collection. + +The current frontend supports: + +- `ac.array(extent, lambda index: ...)` with positive static extent; +- `ac.map({...})` with unique compile-time `bool`, `int`, or non-empty `str` + keys; +- `ac.set({...})` over unique Queue or nested collection members; +- nested collections; +- static indexing; +- compile-time iteration over a collection. + +Map keys and set members are canonicalized. Frozen QueueGraph planning flattens +collections into statically named Queue members; it never creates a runtime +Queue pointer or host-order container dependency. + +### Static control + +`if True` and `if False` are elaborated statically. `for` over +`range(constant)` or a static Queue collection is expanded at compile time. + +```python +if True: + selected = incoming.apply(lambda item: item + 1) + +for index in range(2): + ac.sink(lanes[index]) +``` + +One structurally decreasing Queue helper may recurse at compile time: + +```python +def add_stages(queue, count): + if count == 0: + return queue + return add_stages( + queue.apply(lambda item: item + 1), + count - 1, + ) + +outgoing = add_stages(incoming, 3) +``` + +The helper MUST have exactly one Queue parameter and one integer count, a +`count == 0` identity base case, and one self-call whose count is `count - 1`. +The call-site depth MUST be a compile-time integer in `[0, 1024]`. The frontend +expands the helper before ACIR publication; no recursion, call stack, or dynamic +module creation remains in either backend. + +A runtime Queue condition may use the symmetric form below. The condition MUST +lower to `ac.var`, both branches MUST consume the same Queue through one +`apply`, and both branches MUST assign the same fresh result name. + +```python +if incoming.route == 0: + selected = incoming.apply( + lambda item: item.with_fields(value=item.value + 10) + ) +else: + selected = incoming.apply( + lambda item: item.with_fields(value=item.value + 20) + ) +``` + +The frontend lowers this statement to an official two-way `ac.route`, two +branch transforms, and a mutually exclusive priority `ac.merge`. More complex +runtime Queue control remains explicit through `route`/`merge`. Runtime topology +allocation is forbidden. + +## ACIR type contract + +### Immutable payload types + +`!ac.var` and `!ac.queue` require an immutable ACIR payload type. They +MUST NOT recursively carry Queue, mutable list, function, channel, endpoint, or +other runtime-reference types. + +Valid examples: + +```mlir +!ac.var +!ac.queue +!ac.var> +!ac.queue> +``` + +Invalid examples: + +```mlir +!ac.queue> +!ac.var> +!ac.queue> +!ac.queue<(i32) -> i32> +``` + +### Static collection types + +ACIR provides statically shaped collection types: + +```mlir +!ac.array<4 x !ac.queue> +!ac.map<["cube", "scalar", "vector"], !ac.queue> +!ac.set<4 x !ac.var> +``` + +Array and set lengths MUST be positive. ACIR map keys are non-empty unique +strings in strict lexicographic order. Collection elements MUST be Queue, Var, +or another supported static collection with a valid fixed shape. + +## ACIR operation contract + +### Implemented common building blocks + +The official graph-level catalog contains exactly these operations. Every +design entry has both a typed gfsim realization and a PYC realization; +verification entries declare their permitted boundary explicitly. + +| Operation | Role | Queue arity | Static parameters | Core behavior | +| --- | --- | --- | --- | --- | +| `ac.source` | design | none to one | `depth`, `latency` | boundary producer | +| `ac.sink` | design | one to none | none | consuming boundary | +| `ac.observe` | observation | one to none | `name` | non-consuming, non-backpressuring probe | +| `ac.expect` | verification | one to none | `message` | non-consuming predicate check; PYC testbench only | +| `ac.transform` | design | one or more to one or more | output depths and latencies | pure Var region plus atomic Queue transfer | +| `ac.broadcast` | design | one to two or more | output depths and latencies | strict atomic fanout | +| `ac.fork` | design | one to two or more | output depths and latencies | decoupled exactly-once fanout | +| `ac.route` | design | one to two or more | output depths and latencies | selector-controlled demultiplexing | +| `ac.select` | design | one control plus two or more data inputs to one | `depth`, `latency` | selector-controlled data Queue mux | +| `ac.merge` | design | two or more to one | `policy`, `depth`, `latency` | priority or round-robin arbitration | +| `ac.barrier` | design | two or more to the same count | output depths and latencies | positionally typed atomic synchronization | +| `ac.credit` | design | one to one | `credits`, `depth`, `latency` | bounded parallel cost countdown and completion | +| `ac.memory.instance` / `ac.memory.request` | design | shared instance, one-to-one endpoint | instance identity, ordinal, `entries`, `init`, instance `latency`, `result_field`, `depth` | fixed-priority single-outstanding old-data memory | +| `ac.dependency` | design | one to one | `capacity`, `resources`, `no_dependency`, `depth`, `latency` | bounded predecessor tracking, resource reservation, and execution countdown | +| `ac.reorder` | design | one to one | `capacity`, `start`, `depth`, `latency` | bounded key-ordered retirement | +| `ac.feedback` | design | one to one | `depth`, `latency`, `max_iterations` | bounded stateful loop | +| `ac.scope` | design | variadic to variadic | symbol name | hierarchy boundary; PYC elaboration flattens it | + +`ac.firing`, `ac.queue.peek`, `ac.queue.pop`, and `ac.queue.push` are lower-level +transactional primitives. They are normative ACIR operations, but they are not +independent QueueGraph building blocks and therefore do not appear in the +graph-level opcode catalog. + +The closed inventory will grow with other common hardware blocks. New +application-specific opcodes and private provider identities are not an +extension mechanism. + +### Transform example + +The following excerpt is the canonical shape produced for a structure update: + +```mlir +%output = ac.transform %input depths [2] latencies [1] { +^transform(%item: !ac.var>): + %value = ac.var.get %item field "value" + : !ac.var> -> !ac.var + %one = ac.var.constant 1 : i64 as !ac.var + %next_value = ac.var.add %value, %one : !ac.var + %next = ac.var.with %item, %next_value field "value" + : !ac.var>, !ac.var + -> !ac.var> + ac.transform.yield %next : !ac.var> +} {ac.name = "output"} + : (!ac.queue>) + -> !ac.queue> +``` + +The region MUST have one Var block argument for each input Queue. All body +operations before `ac.transform.yield` MUST be pure. Yielded Var types MUST +match the payload types of the corresponding output Queues. + +Input and output arity are independent. This two-input, one-output transform +consumes both heads and publishes the sum as one atomic transaction: + +```mlir +%sum = ac.transform %left, %right depths [2] latencies [1] { +^transform(%left_item: !ac.var, %right_item: !ac.var): + %value = ac.var.add %left_item, %right_item : !ac.var + ac.transform.yield %value : !ac.var +} {ac.output_names = ["sum"]} + : (!ac.queue, !ac.queue) -> !ac.queue +``` + +Neither backend may consume only one input or publish a partial output set. +The transform fires only when every input is valid and every output can accept +its corresponding result. + +### Credit example + +The Python credit call lowers to one stateful `ac.credit` and one pure cost +region. + +```mlir +%completed = ac.credit %issued credits 2 depth 4 latency 1 cost { +^cost(%item: !ac.var>): + %cycles = ac.var.get %item field "cycles" + : !ac.var> -> !ac.var + ac.credit.yield %cycles : !ac.var +} : !ac.queue> + -> !ac.queue> +``` + +The cost region MUST contain one argument matching the Queue payload, contain +only pure Var operations, and terminate with exactly one `ac.credit.yield`. + +### Barrier example + +The barrier has no Var policy region. Its positional Queue types and static +output parameters completely define the operation. + +```mlir +%left_ready, %right_ready = ac.barrier %left, %right + depths [2, 2] latencies [1, 1] + : (!ac.queue, !ac.queue) + -> (!ac.queue, !ac.queue) +``` + +The input operands MUST be unique. Output depth and latency arrays MUST match +the output count and contain only positive values. + +### Memory example + +The declaration lowers to `ac.memory.instance`; every endpoint lowers to an +`ac.memory.request` with a frozen ordinal and three pure policy regions. + +```mlir +ac.memory.instance @sram data i16 entries 16 init 0 latency 3 + owner "/" stable_id "memory/sram" +%response = ac.memory.request @sram, %request ordinal 0 + result_field "data" depth 4 + address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" + : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var +} write { + ^write(%item: !ac.var>): + %write = ac.var.get %item field "write" + : !ac.var> -> !ac.var + ac.memory.yield %write : !ac.var +} data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" + : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var +} {ac.endpoint_path = "/response", ac.name = "response"} + : !ac.queue> + -> !ac.queue> +``` + +The three regions MUST each contain one block argument matching the request +payload, contain only pure Var operations, and terminate with exactly one +`ac.memory.yield`. + +An instance is visible only from its declaration scope and descendants. Its +endpoints use fixed ordinal priority. While one transaction is outstanding all +request endpoints are backpressured; `busy` is released only when the selected +response Queue accepts the response, and no request is reaccepted in that same +epoch. Every backend realizes exactly one physical memory per instance. + +The Python frontend may statically elaborate a homogeneous memory array without +adding a frozen operation. `banks.select(requests, key=...).request(...)` +lowers to one `ac.route`, one ordinary memory instance and request per bank, +and one response `ac.merge`. The route key selects exactly one bank. Banks have +independent outstanding state, so responses from different banks may be +reordered; callers that require request order retain a tag and use `reorder`. +Memory arrays are one-dimensional in epoch 0.3 and require identical data type, +entry count, and initialization across all banks. + +### Explicit firing example + +Low-level Queue effects are legal only inside `ac.firing`. + +```mlir +ac.firing(%input, %output) { + %head = ac.queue.peek %input + : !ac.queue -> !ac.var + %value = ac.queue.pop %input + : !ac.queue -> !ac.var + ac.queue.push %output, %value + : !ac.queue, !ac.var + ac.firing.yield +} : (!ac.queue, !ac.queue) +``` + +Firing Queue operands MUST be unique. Every Queue effect MUST reference a +listed operand. One Queue may be popped at most once and pushed at most once in +one firing. A firing MUST contain at least one pop or push. `peek` reads the +same committed head without consuming it. + +### Frozen logical identity + +Every Queue-producing operation MUST carry exact frozen logical output names +before QueueGraph extraction: + +- one result uses non-empty `ac.name`; +- multiple results use exact `ac.output_names` in result order; +- names are unique across the system; +- each Queue records payload type, scope path, depth, and latency. + +The canonical QueueGraph JSON uses schema +`agentic-circuit-queue-graph-plan`, version `0.2`. Its ordering and bytes MUST +not depend on host addresses, hash iteration, allocation order, or checkout +path. + +### Queue graph verification + +A backend-ready Queue graph MUST satisfy: + +- every Queue has exactly one producer; +- every Queue has exactly one consuming block; +- `ac.observe` does not count as a consuming block; +- fanout is represented by `ac.broadcast` or `ac.fork`; +- merge is represented by `ac.merge`, not multiple producers on one Queue; +- key-ordered retirement is represented by bounded `ac.reorder` state; +- every Queue depth and latency is positive; +- Queue logical identities are non-empty and unique; +- every block input and output references a known Queue identity; +- raw QueueGraph cycles are rejected; a stateful loop MUST use `ac.feedback`; +- every Var region uses only supported pure operations and structured yields; +- topology and collection shape are compile-time fixed. + +An otherwise unused Queue is a static no-progress risk and is rejected with an +actionable `connect ac.sink` diagnostic. The same verifier runs after ACIR +extraction and again at both backend entry points, so hand-constructed plans +cannot bypass the producer, consumer, reference, or cycle checks. + +## Runtime execution contract + +### Snapshot, proposal, arbitration, and Xfer + +At one active epoch, gfsim follows this state discipline: + +1. blocks read committed Queue state; +2. blocks propose pushes and pops without publishing them; +3. Queue-local arbitration resolves deterministic FIFO proposals; +4. Xfer commits accepted changes; +5. consumers observe committed results no earlier than the required later + epoch. + +One block MUST NOT observe another block's uncommitted proposal in the same +epoch. Independent Work order MUST NOT change architectural results, +diagnostics, committed statistics, or refinement observations. + +### Capacity and latency + +`SimQueue` counts committed entries, delayed entries, and push proposals +against capacity. It rejects a push proposal that would exceed entry or byte +capacity. + +Latency is exact and positive. A token accepted at epoch `t` by a Queue with +latency `L` becomes visible to downstream committed-state reads no earlier than +the boundary corresponding to `t + L`. Latency one is still stateful; it is not +a combinational wire. + +### Atomic transfer + +A transform fires only when all required input pops and output pushes can be +proposed. It computes output Vars from immutable input heads, proposes every +output, proposes every input pop, and commits the complete transaction through +Xfer. + +No legal lowering may commit an input pop while a required output push is +rejected. + +### Credit transfer + +Credit admission pops one input token into a free committed slot. Active slots +decrement at Xfer, and a zero-remaining slot may propose one output token. The +slot becomes free only when that output push commits. Output backpressure keeps +the completed token and its credit occupied. + +The gfsim and PYC implementations use the same lowest-slot tie break, the same +one-admission/one-completion bandwidth, and the same no-same-Xfer slot reuse +rule. Refinement compares accepted and completed transactions and derives the +active credit count from their committed difference. + +### Barrier transfer + +A barrier reads every committed input head, checks every output proposal slot, +and proposes the complete positional transfer. If any precondition fails, it +proposes no pop or push. Xfer commits all accepted Queue effects together. + +gfsim implements this as `QueueBarrier>`. PYC implements the +same contract with an all-input-valid conjunction, per-input ready gating, and +per-output valid gating; no backend retains a dynamic Queue pointer. + +### Memory transfer + +Memory reads observe committed state before the current Xfer. The memory block +stages an optional write only after its response push and request pop are both +accepted. Xfer commits the staged write and the Queue effects together. A +rejected response push MUST leave the request and memory unchanged. + +Model construction and replay start from the deterministic zero image. +gfsim `reset()` restores that image so the same model instance can replay +deterministically. The raw PYC primitive does not clear memory on its reset +port, so mid-run reset of memory contents is outside the shared refinement +contract; a PYC replay MUST instantiate a fresh model. + +## Typed gfsim C++ lowering + +The C++ backend MUST generate statically typed, queue-wired code. + +```cpp +struct WorkItem { + std::uint32_t value; + std::uint8_t route; + std::uint16_t remaining; +}; + +gfsim::SimQueue input_queue_; +gfsim::SimQueue output_queue_; +``` + +The generated system owns interconnect Queues. Child scope modules and common +blocks borrow typed Queue references. Sibling blocks MUST NOT own duplicate +instances of the same interconnect. + +The implementation currently provides reusable templates for transform, +atomic transform, sink, observe, broadcast, fork, route, merge, barrier, credit, +memory, dependency, reorder, and feedback. +Generated dispatch is static; generated runtime code MUST NOT discover opcodes +by strings, walk schemas, construct topology dynamically, or depend on Python +or MLIR libraries. + +## PYC and Verilog lowering + +Agentic Circuit owns `frozen ACIR -> canonical PYC IR`. A pinned external +`pycc` owns PYC verification, C++ emission, and Verilog emission. The pin is +recorded in [`pyc.lock.json`](../../toolchains/pyc.lock.json). + +The current hardware lowering maps: + +| ACIR concept | PYC/RTL realization | +| --- | --- | +| scalar or structure Var | combinational value or packed bundle | +| Queue | valid/data/ready channel with fixed storage | +| Queue depth and latency | fixed register/FIFO stages | +| transform | combinational data logic plus atomic handshakes | +| broadcast | all-output ready conjunction | +| fork | delivered-mask registers and independent output handshakes | +| route | selector decoder, valid demultiplexing, ready multiplexing | +| select | selector mux, selected-input ready, and control/data atomic handshake | +| priority merge | fixed-priority selection | +| round-robin merge | selection plus committed cursor register | +| barrier | all-input-valid/all-output-ready atomic handshake | +| credit | fixed slot register bank, parallel countdown, and deterministic completion selection | +| memory | `pyc.sync_mem` plus an aligned pending request/valid register | +| dependency | fixed register window, predecessor wakeup, countdown, and resource grants | +| reorder | fixed register window and committed next-key register | +| bounded feedback | committed valid/data/iteration registers and limit assertion | +| scope | static module hierarchy | +| observe | non-functional probe boundary | +| expect | rejected in design hierarchy; permitted only at the PYC testbench boundary | + +PYC C++ and Verilog generated from the same PYC IR MUST be cycle equivalent. +Memory uses the PYC synchronous 1R1W primitive. The lowering retains the +request until its registered read data is aligned and accepted, drives writes +only when the request handshake fires, and enables every byte lane of the +integer data word. The primitive's read-during-write rule is old data. +Feedback uses explicit sequential state in PYC IR; it is not a combinational +unroll or a backend-specific loop. + +Before lowering, role placement is checked against the shared opcode catalog. +A verification-only leaf in the design graph fails deterministically; an +observation leaf remains non-state-changing and cannot affect ready/valid. + +## Cross-backend refinement + +Typed gfsim and PYC/Verilog have different internal IR and may have different +internal cycle structures. Cross-backend validation compares a declared +semantic projection, including: + +- input transaction sequence; +- accepted and completed transaction identities; +- output transaction sequence; +- architectural state and memory-visible effects when present; +- declared assertions and runtime failures. + +Cross-backend refinement does not require equality of: + +- internal Queue implementation; +- gfsim deltas; +- PYC registers and wires; +- every internal stage cycle; +- abstract versus detailed pipeline latency that is outside the declared + observation contract. + +## End-to-end example + +The executable +[`davincioo_queue_model.py`](../../examples/pipelines/davincioo_queue_model.py) +uses only serial Python and common building blocks. + +```python +import agentic_circuit as ac + + +@ac.struct +class WorkItem: + value: int + route: int + remaining: int + + +@ac.system +def davincioo_queue_model() -> None: + trace = ac.source(WorkItem, depth=8, latency=1) + + with ac.scope("frontend"): + prepared = trace.apply( + lambda item: item.with_fields(value=item.value + 1), + depth=4, + latency=1, + ) + + with ac.scope("dispatch"): + scalar, vector, cube, tma = prepared.route( + outputs=4, + key=lambda item: item.route, + depth=2, + latency=1, + ) + + scalar_done = scalar.apply(lambda item: item.with_fields(value=item.value + 1)) + vector_done = vector.apply(lambda item: item.with_fields(value=item.value + 2)) + cube_done = cube.apply(lambda item: item.with_fields(value=item.value + 3)) + tma_done = tma.apply(lambda item: item.with_fields(value=item.value + 4)) + + completed = scalar_done.merge( + vector_done, + cube_done, + tma_done, + policy="round_robin", + depth=8, + latency=1, + ) + + ac.sink(completed) +``` + +The checked-in executable adds explicit engine and retirement scopes. The +inferred graph is: + +```text +trace -> frontend transform -> four-way route + | scalar engine + | vector engine + | cube engine + | tma engine + round-robin merge -> retire -> sink +``` + +### Generate canonical ACIR, QueueGraph JSON, and gfsim C++ + +Configure and build the native tools first: + +```sh +scripts/bootstrap-dev.sh +cmake --preset dev-llvm22 +cmake --build --preset dev-llvm22 +``` + +Generate all canonical Queue artifacts: + +```sh +PYTHONPATH=src .venv/bin/python tools/ac-queue-cxxgen.py \ + examples/pipelines/davincioo_queue_model.py \ + --system davincioo_queue_model \ + --acir-output build/davincioo_queue_model.ac.mlir \ + --plan-output build/davincioo_queue_model.queue-plan.json \ + --acir-opt build/dev-llvm22/bin/acir-opt \ + --queue-plan-tool build/dev-llvm22/bin/acir-queue-plan \ + --queue-cxxgen-tool build/dev-llvm22/bin/acir-queue-cxxgen \ + --output build/davincioo_queue_model.cpp +``` + +Check that the generated C++ is valid for the local compiler: + +```sh +c++ -std=c++20 -I include -fsyntax-only build/davincioo_queue_model.cpp +``` + +### Generate PYC, PYC C++, and Verilog + +Use the exact pyCircuit commit and LLVM version in the toolchain lock. Given a +matching local pyCircuit installation, run the canonical bundle command: + +```sh +PYC_TOOLCHAIN_ROOT=/path/to/pycircuit/toolchain/install + +.venv/bin/python tools/ac-queue-pyc-build.py \ + build/davincioo_queue_model.ac.mlir \ + --pycgen-tool build/dev-llvm22/bin/acir-queue-pycgen \ + --pycc "$PYC_TOOLCHAIN_ROOT/bin/pycc" \ + --toolchain-lock toolchains/pyc.lock.json \ + --toolchain-metadata \ + "$PYC_TOOLCHAIN_ROOT/share/pycircuit/toolchain-metadata.json" \ + --cxx "$(command -v c++)" \ + --verilator "$(command -v verilator)" \ + --pyc-output build/davincioo_queue_model.pyc \ + --cpp-output-dir build/davincioo_queue_model-pyc-cpp \ + --verilog-output-dir build/davincioo_queue_model-verilog \ + --manifest build/davincioo_queue_model-pyc-manifest.json +``` + +The command validates the toolchain lock, emits PYC C++ and Verilog, runs C++ +syntax checking and Verilator lint, and records deterministic artifact hashes. +Output paths MUST not already exist. + +## Rejected examples + +### Explicit system ports + +```python +@ac.system +def illegal(input_queue): + ... +``` + +Queue/Var system boundaries are inferred. A system with parameters is rejected. + +### Zero-latency Queue + +```python +incoming = ac.source(int, latency=0) +``` + +Use Var computation inside a lambda for latency-zero logic. + +### Runtime topology + +```python +for _ in range(item.value): + queues.append(ac.source(int)) +``` + +Queue count and collection shape MUST be known during AST elaboration. + +### Runtime Queue condition + +```python +if incoming: + selected = incoming.apply(lambda item: item + 1) +``` + +Use `route` for runtime token selection. + +### Dynamic Queue handle + +```python +selected_queue = queues[item.route] +``` + +Runtime selection MUST lower to `route`, select, or arbitration logic. It MUST +NOT materialize a runtime Queue pointer. + +### Private opcode or backend + +```python +@ac.opcode +def private_scheduler(...): + ... + +ac.raw_verilog("assign ...") +``` + +Both forms are forbidden. Add a reusable common building block to the +repository-owned inventory with ACIR, gfsim, PYC, and conformance definitions. + +## Diagnostics + +Frontend Queue diagnostics use the `ACPY-QUEUE-*` family. They SHOULD identify +the source construct, violated static rule, and repair. Important current codes +include: + +| Code | Meaning | +| --- | --- | +| `ACPY-QUEUE-001` | invalid system, assignment, statement, or positive constant | +| `ACPY-QUEUE-002` | unsupported payload or structure declaration | +| `ACPY-QUEUE-003` | invalid lambda or Var expression | +| `ACPY-QUEUE-004` | duplicate scope path | +| `ACPY-QUEUE-005` | invalid static collection or reference | +| `ACPY-QUEUE-006` | invalid route declaration | +| `ACPY-QUEUE-007` | invalid bounded feedback loop | +| `ACPY-QUEUE-008` | invalid merge | +| `ACPY-QUEUE-009` | invalid atomic group | +| `ACPY-QUEUE-010` | forbidden user opcode or backend provider | +| `ACPY-QUEUE-011` | runtime `if` is not a symmetric Boolean Queue branch | +| `ACPY-QUEUE-012` | invalid fork | + +Native QueueGraph/backend diagnostics use the `ACLOWER-QUEUE-*` family and +MUST reject an invalid graph before emitting partial backend artifacts. + +## Determinism requirements + +Canonical ACIR, QueueGraph JSON, generated C++, PYC IR, manifests, and +observations MUST NOT depend on: + +- Python hash iteration order; +- host pointer values or allocation order; +- unordered C++ traversal order; +- ambient checkout path; +- process ID or wall-clock time; +- runtime plugin discovery; +- arbitrary Python or Verilog execution. + +Canonical ordering uses source occurrence, static collection order, frozen +logical identity, and declared arbitration policy. + +## Current implementation boundary + +The following slices are implemented and tested: + +- AST-based serial Python capture; +- immutable scalar and structure payloads; +- Queue/Var types and pure Var expressions; +- scopes with inferred Queue boundaries; +- transform, strict broadcast, decoupled fork, route, merge, atomic barrier, + bounded credit, typed memory, dependency, reorder, observe, sink, explicit + atomic transform, and bounded feedback; +- static arrays, maps, sets, runtime flat-collection selection, static `if`, + static loops, and symmetric runtime Queue `if` lowering through + route/transform/merge; +- canonical QueueGraph extraction; +- typed gfsim C++ generation; +- PYC/Verilog lowering for transform, broadcast, fork, route, select, merge, + atomic barrier, bounded credit, typed synchronous memory, dependency, + reorder, bounded feedback, elaboration-time scope flattening, packed + structures, atomic handshakes, and exact Queue latency; +- PYC C++ versus Verilog cycle equivalence and gfsim/PYC projected transaction + comparison. + +Issues [#9](https://github.com/PTO-ISA/agentic-circuit/issues/9), +[#10](https://github.com/PTO-ISA/agentic-circuit/issues/10), and +[#11](https://github.com/PTO-ISA/agentic-circuit/issues/11) are closed. The +requirement matrix contains no partial or missing rows. Future public +building blocks, richer signedness semantics, and additional refinement +projections are new contract work rather than incomplete requirements of these +issues. + +The checked-in DavinciOO-like model now proves topology, typed payloads, finite +Queues, backpressure, deterministic C++ generation, the 15-record softmax +opcode/completion/retirement projection, and the 453-cycle bounded oracle. The +same frozen ACIR produces PYC C++ and Verilog with cycle-identical hardware +observations and the same projected output transactions. Dependency readiness +resource reservation, and execution countdown are now explicit `ac.dependency` +state, while bounded parallel in-flight work is explicit `ac.credit` state. The +projection +retains only a fixed 5-cycle ingress and 4-cycle drain compensation for the +different model boundaries. The checked occupancy projection records dependency +window peak 8, per-resource executing peaks `[1, 1, 0, 1]`, and reorder window +peak 8 through stable generated-model accessors. + +## Contributor checklist + +A change to the public contract is complete only when it updates all +affected layers: + +- Python accepted and rejected syntax; +- ACIR ODS type or operation definition; +- verifier and diagnostic; +- QueueGraph canonical plan; +- gfsim runtime semantics and typed C++ emission; +- PYC lowering or an explicit backend rejection; +- positive, negative, determinism, and round-trip tests; +- this manual and the relevant machine-readable schema; +- exact contract epoch and capability declarations when public syntax changes. + +Do not document a backend-specific behavior as shared ACIR semantics. Do not +add a compatibility alias for removed public source surfaces. Git history, +release tags, and [`REF-HISTORY-001`](refs/history.md) preserve prior contracts. diff --git a/components/agentic-circuit/docs/spec/agentic-circuit.zh-CN.md b/components/agentic-circuit/docs/spec/agentic-circuit.zh-CN.md new file mode 100644 index 00000000..93dcd44f --- /dev/null +++ b/components/agentic-circuit/docs/spec/agentic-circuit.zh-CN.md @@ -0,0 +1,685 @@ +# Agentic Circuit 团队 Specification 手册 + +| 字段 | 内容 | +| --- | --- | +| 目标版本 | Explicit Memory contract epoch `0.3` | +| 状态 | 已在 `main` 实现;本文是团队阅读入口 | +| 适用读者 | Python 前端、ACIR、gfsim、PYC/Verilog 和模型验证开发者 | +| 规范主文档 | [Agentic Circuit Specification Manual](agentic-circuit.md) | +| 机器可读清单 | [`opcodes.json`](../../schemas/opcodes.json) | +| 可执行示例 | [`examples/pipelines`](../../examples/pipelines/README.md) | + +## 文档定位 + +本文帮助团队成员快速理解和使用 Agentic Circuit。它解释编程模型、 +常用积木、后端差异、验证方法和常见错误,并给出与仓内测试一致的示例。 + +本文不复制所有 ODS 签名和 verifier 条件。发生差异时,按以下顺序判断: + +1. JSON Schema、opcode catalog 和 MLIR ODS; +2. verifier 与 conformance test; +3. [英文规范](agentic-circuit.md); +4. 本文和设计提案。 + +规范中的 **MUST**、**MUST NOT**、**SHOULD**、**SHOULD NOT** 和 **MAY** +具有 RFC 风格的约束含义。本文使用“必须”“禁止”“应该”和“可以”表达同一 +含义。 + +## 一句话理解 + +用户编写串行风格的 Python。编译器读取 AST,把 Python 变量解释成静态连接的 +`ac.queue`,把 lambda 内的值解释成零延迟 `ac.var`,再从同一份冻结 ACIR +生成两种内部结构不同的后端: + +```text +串行 Python + | + v +AST capture / ACPy + | + v +Queue/Var ACIR + | + +------------------------------+ + | | + v v +typed gfsim C++ canonical PYC IR +SimQueue 模型 | + v + pinned pycc + | | + v v + PYC C++ Verilog +``` + +Python 看起来按顺序书写,但运行时不是逐行解释 Python。系统体只在编译期完成 +拓扑 elaboration;生成的 Queue、积木和作用域在运行期保持静态。 + +## 核心对象 + +### Queue 是带时序的状态 + +`ac.queue` 是有限深度、带类型、带反压的 FIFO 通道。它具有: + +- 编译期确定的 payload 类型 `T`; +- 正数 `depth` 和正数 `latency`; +- 稳定的逻辑身份和层级路径; +- `peek`、`pop`、`push` 和 commit-time 状态变化; +- gfsim 中的 `SimQueue` 实例; +- PYC/RTL 中的 valid/data/ready 与固定存储。 + +Queue 的 latency 禁止为零。需要零延迟组合逻辑时使用 Var 表达式。 + +### Var 是不可变组合值 + +`ac.var` 没有容量、占用率和运行时对象身份。以下对象都属于 Var: + +- lambda 参数; +- 常量和算术结果; +- 结构体字段投影; +- 比较结果; +- `with_fields(...)` 产生的新 token。 + +Queue 可以改变占用状态,Queue 中的 token 不可原地修改。 + +```python +# 正确:创建一个新 token。 +next_item = item.with_fields(remaining=item.remaining - 1) + +# 错误:原地修改输入 token。 +item.remaining -= 1 +``` + +### Opcode 是公共硬件积木 + +公开积木统一使用 `ac.*` 命名空间。不存在 `ac.std.*`,也不允许用户定义私有 +opcode、C++ provider、PYC provider 或直接插入 Verilog。 + +`decode`、`dispatch`、`rename`、`retire` 等名称属于具体架构的 scope 或积木组合, +不是通用 opcode。公共积木描述的是 transform、route、merge、memory、barrier、 +credit、dependency 和 reorder 等可复用硬件行为。 + +查看当前闭集: + +```bash +build/dev-llvm22/bin/acir-opcode-catalog +agentic-circuit schema opcode ac.transform +``` + +## 最小示例 + +下面的系统没有显式输入输出声明。`source` 和 `sink` 定义可执行边界,变量的定义和 +使用关系定义 Queue 连接。 + +```python +import agentic_circuit as ac + + +@ac.system +def pipeline() -> None: + incoming = ac.source(int, depth=4, latency=1) + outgoing = incoming.apply( + lambda item: item + 1, + depth=2, + latency=1, + ) + ac.sink(outgoing) +``` + +它 elaboration 成: + +```text +incoming Queue -> ac.transform(item + 1) -> outgoing Queue -> ac.sink +``` + +系统函数必须无参数。`source`、`apply` 和 `sink` 是 AST marker,不能通过普通 +Python 调用系统体来模拟电路。 + +## 定义结构化 payload + +使用 `@ac.struct` 冻结字段顺序、位宽和结构身份。 + +```python +import agentic_circuit as ac + + +@ac.struct +class WorkItem: + sequence: ac.u64 + value: ac.u32 + route: ac.u2 + remaining: ac.u16 + valid: bool + + +@ac.system +def pipeline() -> None: + incoming = ac.source(WorkItem) + updated = incoming.apply( + lambda item: item.with_fields( + value=item.value + 1, + remaining=item.remaining - 1, + ) + ) + ac.sink(updated) +``` + +当前常用字段类型包括 `bool`、`int`、`ac.u1/u2/u4/u8/u16/u32/u64` 和 +`ac.s8/s16/s32/s64`。当前 ACIR 契约已冻结整数宽度,但尚未把有符号性作为完全独立的 +类型语义;不要假设所有比较都自动采用 Python 的有符号规则。 + +可执行示例: +[`pyc_struct_pipeline.py`](../../examples/pipelines/pyc_struct_pipeline.py)。 + +## 使用 scope 表达层级 + +`with ac.scope("name"):` 表达所有权和层级,不声明端口。编译器根据跨 scope 的 +def-use 自动推导输入、输出和 interconnect 所属的最低公共祖先。 + +```python +@ac.system +def pipeline() -> None: + incoming = ac.source(int) + + with ac.scope("frontend"): + prepared = incoming.apply(lambda item: item + 1) + + with ac.scope("backend"): + completed = prepared.apply(lambda item: item * 2) + + ac.sink(completed) +``` + +scope 名必须非空,同一路径不能重复声明。跨 scope 的 Queue 不会被两个子模块重复 +拥有;生成系统拥有 interconnect,子模块只借用类型化 Queue 引用。 + +## 数据通路积木 + +### Transform + +`apply` 生成一个 `ac.transform`。一次 firing 原子地 pop 输入并 push 输出。 + +```python +updated = incoming.apply( + lambda item: item.with_fields(value=(item.value + 1) * 2), + depth=4, + latency=2, +) +``` + +lambda 必须是纯 Var 表达式,且返回类型与输出 Queue payload 一致。当前支持字段读取、 +常量、`+`、`-`、`*`、比较和不可变 `with_fields(...)` 更新。 + +### Broadcast 与 Fork + +同一 Queue 被多个消费点使用时,前端自动插入严格原子的 `ac.broadcast`:所有输出 +必须在同一 firing 接收 token。 + +需要各输出在不同 cycle 接收时,显式使用 `fork`: + +```python +left, right = incoming.fork(outputs=2, depth=2, latency=1) +``` + +`fork` 为当前 token 保存 delivered mask,每个输出恰好收到一次,全部完成后才 pop +输入。两者不能互换,因为反压和内部状态不同。 + +可执行示例: +[`pyc_broadcast_pipeline.py`](../../examples/pipelines/pyc_broadcast_pipeline.py) 和 +[`pyc_fork_pipeline.py`](../../examples/pipelines/pyc_fork_pipeline.py)。 + +### Route 与 Merge + +`route` 根据一个 Var selector 把 token 发送到一个静态输出,`merge` 把同类型 Queue +合并回来。 + +```python +scalar, vector = prepared.route( + outputs=2, + key=lambda item: item.route, + depth=2, + latency=1, +) + +scalar_done = scalar.apply(lambda item: item.with_fields(value=item.value + 1)) +vector_done = vector.apply(lambda item: item.with_fields(value=item.value + 2)) + +completed = scalar_done.merge( + vector_done, + policy="round_robin", + depth=4, + latency=1, +) +``` + +selector 越界产生确定性 `route_selector_out_of_range` 失败,不会回绕。merge policy +仅支持 `priority` 和 `round_robin`。 + +可执行示例: +[`pyc_route_merge_pipeline.py`](../../examples/pipelines/pyc_route_merge_pipeline.py)。 + +## 静态集合与运行时选择 + +Queue 和 Var 可以放入编译期确定形状的 array、map 和 set。 + +```python +lanes = ac.array(4, lambda index: ac.source(int, depth=index + 1)) +named = ac.map({"scalar": lanes[0], "vector": lanes[1]}) +active = ac.set({named["scalar"], named["vector"]}) +``` + +静态索引和编译期遍历会被展开。运行时从 flat Queue collection 选择一个成员时,必须 +提供显式 control Queue;编译器生成 `ac.select`,而不是动态 Queue 指针。 + +```python +@ac.struct +class Control: + route: ac.u2 + + +@ac.system +def selected_pipeline() -> None: + control = ac.source(Control) + lanes = ac.array(4, lambda index: ac.source(int)) + selected = lanes.select( + control, + key=lambda item: item.route, + depth=2, + latency=1, + ) + ac.sink(selected) +``` + +control token 与被选中的 data token 原子传输。越界产生 +`select_selector_out_of_range`。嵌套集合必须先静态展开,不能在运行时保存或返回 +Queue handle。 + +可执行示例: +[`pyc_select_pipeline.py`](../../examples/pipelines/pyc_select_pipeline.py)。 + +## 状态和调度积木 + +### Typed Memory + +`memory` 是单读单写的类型化状态积木。一次请求总会读取;写入在 Xfer commit;同一 +请求对同地址读写时返回旧值,新值对后续请求可见。 + +```python +@ac.struct +class Request: + address: ac.u8 + write: bool + data: ac.u16 + + +@ac.system +def memory_pipeline() -> None: + sram = ac.memory(ac.u16, entries=16, init=0, latency=3) + requests = ac.source(Request) + responses = sram.request( + requests, + address=lambda item: item.address, + write=lambda item: item.write, + data=lambda item: item.data, + result_field="data", + depth=4, + ) + ac.sink(responses) +``` + +只允许 `init=0`,实例 `latency` 必须为正数。周期 `T` 接受的请求最早在 +`T + latency` 提交响应;request 的 response Queue latency 固定为 1。一个实例可以 +连接多个逻辑 endpoint,但只有一个物理端口和一个 outstanding request。endpoint 按 +冻结 ordinal 固定优先级仲裁;访问延迟期间及 response Queue 阻塞时全部反压,直到 +选中 response Queue 接纳响应。PYC 每个实例只生成一个 `pyc.sync_mem`。 + +Python 前端允许用同构 `ac.array` 静态声明 memory banks,并以 +`banks.select(requests, key=...).request(...)` 明确选择 bank。该写法只在前端展开: +冻结 ACIR 包含一个 `ac.route`、每个 bank 各一个普通 memory instance/request,以及 +一个 response `ac.merge`,不会引入新的 primitive。各 bank 的 outstanding 状态独立, +因此跨 bank response 可能乱序;需要保序时应在 payload 中保留 tag 并显式接 +`reorder`。epoch 0.3 仅支持一维、data type、entries、init 和 latency 完全相同的 memory +array。 + +可执行示例: +[`pyc_memory_pipeline.py`](../../examples/pipelines/pyc_memory_pipeline.py)。 + +### Credit + +`credit` 表达固定数量的并行 in-flight slot。每个 slot 独立倒计时,完成顺序可以和 +输入顺序不同。 + +```python +completed = issued.credit( + cost=lambda item: item.cycles, + credits=4, + depth=4, + latency=1, +) +``` + +`credits` 必须为正,运行时 cost 必须为正。多个 slot 同时完成时,选择最低 canonical +slot index,并且每个 epoch 最多输出一个 token。 + +可执行示例: +[`pyc_credit_pipeline.py`](../../examples/pipelines/pyc_credit_pipeline.py)。 + +### Barrier + +`barrier` 等待所有输入可 pop 且所有输出可 push,然后一次性提交所有效果。各输入 +payload 类型可以不同,但输出必须按位置匹配。 + +```python +left_ready, right_ready = left.barrier( + right, + depth=2, + latency=1, +) +``` + +输入 Queue 必须互不相同。barrier firing 前禁止发布部分结果。 + +可执行示例: +[`pyc_barrier_pipeline.py`](../../examples/pipelines/pyc_barrier_pipeline.py)。 + +### Dependency 与 Reorder + +`depend` 表达有界依赖窗口、资源占用和执行 cost;`reorder` 按连续 key 恢复顺序。 +它们可以组合成 issue/execute/retire 风格架构,但这些应用阶段仍是 scope,而不是新 +opcode。 + +```python +completed = issued.depend( + key=lambda item: item.sequence, + waits_for=lambda item: item.waits_for, + resource=lambda item: item.route, + cost=lambda item: item.cycles, + capacity=16, + resources=4, + no_dependency=255, + depth=8, + latency=1, +) + +retired = completed.reorder( + key=lambda item: item.sequence, + capacity=16, + start=0, + depth=4, + latency=1, +) +``` + +完整参考: +[`davincioo_queue_model.py`](../../examples/pipelines/davincioo_queue_model.py)。该模型用公共 +积木构造 DavinciOO-like 拓扑,并验证 15 条记录、out-of-order completion、in-order +retirement、Queue occupancy 和 453-cycle 投影。 + +## 串行控制流 + +### 编译期控制 + +`if True/False`、`range(constant)`、静态集合遍历和结构递减的有限递归在编译期展开。 + +```python +def add_stages(queue, count): + if count == 0: + return queue + return add_stages( + queue.apply(lambda item: item + 1), + count - 1, + ) + + +@ac.system +def recursive_pipeline() -> None: + incoming = ac.source(int) + outgoing = add_stages(incoming, 3) + ac.sink(outgoing) +``` + +递归深度必须是 `[0, 1024]` 范围内的编译期整数。后端中不会留下递归或动态模块创建。 + +可执行示例: +[`pyc_recursive_pipeline.py`](../../examples/pipelines/pyc_recursive_pipeline.py)。 + +### 运行时 if + +当前高层语法支持对同一 Queue 的对称二分支更新。它会 lowering 成 route、两个 +transform 和互斥 priority merge。 + +```python +if incoming.route == 0: + selected = incoming.apply( + lambda item: item.with_fields(value=item.value + 10) + ) +else: + selected = incoming.apply( + lambda item: item.with_fields(value=item.value + 20) + ) +``` + +两个分支必须消费同一 Queue、各执行一次 apply,并赋值给同一新变量。更复杂的控制 +使用显式 route/merge 组合。 + +可执行示例: +[`pyc_conditional_pipeline.py`](../../examples/pipelines/pyc_conditional_pipeline.py)。 + +### 有界 while + +Queue rebinding 的串行 `while` lowering 成带状态 feedback Queue 的 `ac.feedback`。 + +```python +while current.remaining > 0: + if current.stop: + break + current = current.apply( + lambda item: item.with_fields(remaining=item.remaining - 1) + ) + if current.skip: + continue +``` + +当前允许 update 前一个 break guard 和尾部一个 continue guard。最大迭代次数为 +1024,超出时产生 `feedback_iteration_limit`。 + +可执行示例: +[`pyc_feedback_pipeline.py`](../../examples/pipelines/pyc_feedback_pipeline.py) 和 +[`pyc_loop_control_pipeline.py`](../../examples/pipelines/pyc_loop_control_pipeline.py)。 + +## 显式 Queue effect + +需要强调 pop 的副作用和 peek 的非消费读取时,使用 `firing`: + +```python +outgoing = incoming.firing( + lambda queue: queue.push( + queue.pop().with_fields( + value=queue.peek().value + 1, + ) + ) +) +``` + +一次 Python firing 必须恰好包含一次 `pop` 和一次最外层 `push`,可以重复 `peek`。 +`peek` 和 `pop` 返回不可变 Var。当前单输入单输出形式会规范化为标准 +`ac.transform`,不会在生成的 hot path 中解释 Python effect 对象。 + +可执行示例: +[`pyc_firing_pipeline.py`](../../examples/pipelines/pyc_firing_pipeline.py)。 + +## Observation 与 Verification + +`ac.observe(queue)` 非消费地观察 committed head,不参与反压,可以进入设计 lowering。 + +`ac.expect(...)` 也是非消费 leaf,但角色是 verification: + +```python +ac.expect( + completed, + predicate=lambda item: item.value > 0, + message="value must be positive", +) +``` + +gfsim 对每个新 committed head 检查 predicate,失败时报告 `expectation_failed`。 +PYC design hierarchy 明确拒绝 `ac.expect`;对应检查必须放到 PYC testbench boundary。 +这不是后端缺失,而是 design role 与 verification role 的边界。 + +可执行示例: +[`gfsim_expect_pipeline.py`](../../examples/pipelines/gfsim_expect_pipeline.py)。 + +## gfsim 与 PYC/Verilog 的差异 + +两种后端共享冻结 ACIR 语义,但内部 IR 和状态结构不要求相同。 + +| 方面 | typed gfsim | PYC/Verilog | +| --- | --- | --- | +| Queue | `SimQueue` 对象 | valid/data/ready + FIFO/register | +| Var | C++ 局部值或纯表达式 | wire、packed value、组合 op | +| 层级 | `gfsim::Module` 对象层级 | 静态 module hierarchy | +| 调度 | snapshot/proposal/Xfer/commit | 时钟边沿和 ready/valid handshake | +| Memory | 类型化 QueueMemory 状态 | `pyc.sync_mem` + 对齐寄存器 | +| Verification | `ac.expect` 可执行 | design 中拒绝,testbench 中实现 | +| 输出 | 类型化 C++ simulator | PYC C++ 与 Verilog | + +跨后端 refinement 比较声明过的语义投影:输入/输出 transaction、接受与完成身份、 +架构状态、memory-visible effect 和明确的 assertion。它不比较 gfsim delta、内部 Queue +布局、PYC 寄存器名称或每个未声明的内部 cycle。 + +同一 PYC IR 生成的 PYC C++ 和 Verilog 必须 cycle equivalent。gfsim 与 PYC 可以有 +不同内部 latency,但必须满足选定的 observation/refinement contract。 + +## 编译和验证示例 + +先配置 LLVM 22.1.8 开发环境: + +```bash +scripts/bootstrap-dev.sh +source .venv/bin/activate +cmake --preset dev-llvm22 +cmake --build --preset dev-llvm22 +``` + +生成 frozen ACIR、QueueGraph plan 和 typed gfsim C++: + +```bash +PYTHONPATH=src .venv/bin/python tools/ac-queue-cxxgen.py \ + examples/pipelines/davincioo_queue_model.py \ + --system davincioo_queue_model \ + --acir-output build/davincioo_queue_model.ac.mlir \ + --plan-output build/davincioo_queue_model.queue-plan.json \ + --acir-opt build/dev-llvm22/bin/acir-opt \ + --queue-plan-tool build/dev-llvm22/bin/acir-queue-plan \ + --queue-cxxgen-tool build/dev-llvm22/bin/acir-queue-cxxgen \ + --output build/davincioo_queue_model.cpp +``` + +验证生成 C++: + +```bash +c++ -std=c++20 -I include -fsyntax-only build/davincioo_queue_model.cpp +``` + +使用锁定的 pyCircuit toolchain 生成 PYC C++ 与 Verilog: + +```bash +PYC_TOOLCHAIN_ROOT=/path/to/pycircuit/toolchain/install + +.venv/bin/python tools/ac-queue-pyc-build.py \ + build/davincioo_queue_model.ac.mlir \ + --pycgen-tool build/dev-llvm22/bin/acir-queue-pycgen \ + --pycc "$PYC_TOOLCHAIN_ROOT/bin/pycc" \ + --toolchain-lock toolchains/pyc.lock.json \ + --toolchain-metadata \ + "$PYC_TOOLCHAIN_ROOT/share/pycircuit/toolchain-metadata.json" \ + --cxx "$(command -v c++)" \ + --verilator "$(command -v verilator)" \ + --pyc-output build/davincioo_queue_model.pyc \ + --cpp-output-dir build/davincioo_queue_model-pyc-cpp \ + --verilog-output-dir build/davincioo_queue_model-verilog \ + --manifest build/davincioo_queue_model-pyc-manifest.json +``` + +命令会检查 [`pyc.lock.json`](../../toolchains/pyc.lock.json),执行 PYC +verification、C++ syntax check、Verilator lint,并记录确定性 artifact hash。目标输出 +路径必须不存在,防止覆盖旧证据。 + +## 明确禁止的写法 + +### 显式系统端口 + +```python +# 禁止:系统边界由 source/sink 和 def-use 推导。 +@ac.system +def pipeline(input_queue, output_queue): + ... +``` + +### 零延迟 Queue + +```python +# 禁止:Queue latency 必须为正。 +incoming = ac.source(int, latency=0) +``` + +### 运行时拓扑和动态 Queue handle + +```python +# 禁止:运行时不能构造、保存或返回 Queue 指针。 +selected_queue = lanes[token.route] +``` + +应改用 `lanes.select(control, key=...)`。 + +### 私有 opcode 或后端代码 + +```python +# 禁止:用户不能从 Python 注册私有实现。 +ac.register_opcode("my.dispatch", cpp_provider=..., verilog=...) +``` + +需要新能力时,先把它定义成通用硬件积木,并同步更新 Python、ACIR、verifier、 +QueueGraph、gfsim、PYC、测试和 opcode catalog。 + +## 常见问题 + +| 现象 | 原因 | 处理方法 | +| --- | --- | --- | +| Queue 被两个消费者使用 | pop 是消费 effect | 需要原子复制时依赖自动 broadcast;需要解耦时显式 fork | +| `latency=0` 被拒绝 | Queue 是状态,不是 wire | 把逻辑写入 lambda,让它 lowering 成 Var | +| selector 越界 | route/select 拓扑是静态闭集 | 修正 selector 位宽或在上游保证合法范围 | +| loop 被拒绝 | 不是受支持的单 Queue 有界 feedback 形状 | 简化为一次 Queue update,或显式组合 route/merge/feedback | +| PYC 拒绝 `ac.expect` | verification leaf 不能进入 design | 把 assertion 放入 PYC testbench boundary | +| 后端结果内部 cycle 不同 | gfsim 与 RTL IR 不同 | 比较声明的 transaction/state/refinement projection | +| artifact epoch 不匹配 | serialized epoch 是 hard break | 重新生成 exact epoch `0.3` artifact,不使用兼容 shim | + +## 修改公共契约的完成条件 + +新增或修改一个公共积木时,必须同步完成: + +- Python 正向和拒绝语法; +- ACIR ODS 类型或 operation; +- verifier、错误码和诊断文本; +- QueueGraph canonical plan; +- gfsim runtime 语义与类型化 C++ 生成; +- PYC lowering,或明确的 backend-role 拒绝; +- 正向、负向、determinism、round-trip 和 cross-backend 测试; +- [`opcodes.json`](../../schemas/opcodes.json); +- [英文规范](agentic-circuit.md) 和本文对应入口。 + +不要把某个后端的实现细节提升成共享 ACIR 语义,也不要为已移除的公共表面增加兼容 +别名。旧契约由 Git 历史、release tag 和 +[`REF-HISTORY-001`](refs/history.md) 保存。 + +## 推荐阅读顺序 + +新同学可以按以下顺序阅读和动手: + +1. 本文的“核心对象”和“最小示例”; +2. [`examples/pipelines/README.md`](../../examples/pipelines/README.md); +3. [`davincioo_queue_model.py`](../../examples/pipelines/davincioo_queue_model.py); +4. [英文规范](agentic-circuit.md); +5. [`opcodes.json`](../../schemas/opcodes.json) 和 + [`test/ACIR`](../../test/ACIR); +6. [NDF 仓库布局验证](50-verification/repository-layout.md)。 diff --git a/components/agentic-circuit/docs/spec/decisions/D-BLOCK-MODEL-001.md b/components/agentic-circuit/docs/spec/decisions/D-BLOCK-MODEL-001.md new file mode 100644 index 00000000..6d59fa7e --- /dev/null +++ b/components/agentic-circuit/docs/spec/decisions/D-BLOCK-MODEL-001.md @@ -0,0 +1,15 @@ +# Queue and block model + +## Use one current compositional block model {#D-BLOCK-MODEL-001} + + +**Context.** Queue/Var, explicit memory, parameterized blocks, and end-to-end +workspaces were developed in successive release phases. Keeping those phase +names in source paths made them look like competing APIs. + +**Decision.** `ac.queue`, `ac.var`, explicit memory, parameterized blocks, and +workspace examples form one current model. Examples are grouped by purpose: +pipelines, memory, blocks, architecture models, and complete workspaces. + +**Consequence.** New work extends the existing semantic group. It does not add +a versioned or phase-numbered sibling. diff --git a/components/agentic-circuit/docs/spec/decisions/D-RELEASE-LAYOUT-001.md b/components/agentic-circuit/docs/spec/decisions/D-RELEASE-LAYOUT-001.md new file mode 100644 index 00000000..4f743a91 --- /dev/null +++ b/components/agentic-circuit/docs/spec/decisions/D-RELEASE-LAYOUT-001.md @@ -0,0 +1,16 @@ +# Release-owned repository layout + +## Remove product versions and phases from source paths {#D-RELEASE-LAYOUT-001} + + +**Context.** Versioned directories caused several generations of examples, +tests, and contracts to appear active at the same time. Phase plans and current +implementation documentation were also indistinguishable. + +**Decision.** The repository uses semantic paths only. Releases identify +versions. A new release changes files in place and does not create a parallel +versioned source tree. The migration is a hard break: old paths have no aliases, +redirect files, or compatibility symlinks. + +**Consequence.** Consumers must use the paths shipped by the selected release. +Repository gates reject new product-version or phase tokens in tracked paths. diff --git a/components/agentic-circuit/docs/spec/images/agentic-circuit-pipeline.drawio b/components/agentic-circuit/docs/spec/images/agentic-circuit-pipeline.drawio new file mode 100644 index 00000000..25c016c6 --- /dev/null +++ b/components/agentic-circuit/docs/spec/images/agentic-circuit-pipeline.drawio @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/agentic-circuit/docs/spec/images/agentic-circuit-pipeline.svg b/components/agentic-circuit/docs/spec/images/agentic-circuit-pipeline.svg new file mode 100644 index 00000000..98dd1073 --- /dev/null +++ b/components/agentic-circuit/docs/spec/images/agentic-circuit-pipeline.svg @@ -0,0 +1,4 @@ + + + +
Authoring and canonicalization
Serial Python
@ac.system, scopes, lambdas
AST capture and inference
Queue identities, scope I/O, static control
Queue/Var ACIR
typed topology and pure Var regions
Frozen QueueGraph plan
stable names, depths, latencies, policies
Canonical hashes and manifests
Backend realization
ACIR Queue C++ generator
Typed gfsim C++
SimQueue and common blocks
ACIR to PYC generator
Repo-local pycc
pyc6, LLVM 22
PYC C++ and Verilog
valid/data/ready channels
Validation
gfsim observations
accepted, completed, outputs
Refinement comparator
semantic projection, not internal cycles
PYC/Verilog observations
plus cycle equivalence
lower
freeze
record
generate
generate
C++
canonical PYC IR
emit
observe
observe
project
compare
Legend
Frontend
Compiler/backend
Frozen contract
Generated artifact
Validation
Text is not SVG - cannot display
\ No newline at end of file diff --git a/components/agentic-circuit/docs/spec/ndf.yaml b/components/agentic-circuit/docs/spec/ndf.yaml new file mode 100644 index 00000000..e18a2727 --- /dev/null +++ b/components/agentic-circuit/docs/spec/ndf.yaml @@ -0,0 +1,5 @@ +project: agentic-circuit +release_model: git-release +layers: L0,L1,L2,L3 +clause_heading: "## Title {#CLAUSE-ID}" +metadata_comment: "" diff --git a/components/agentic-circuit/docs/spec/refs/history.md b/components/agentic-circuit/docs/spec/refs/history.md new file mode 100644 index 00000000..fbbfd03d --- /dev/null +++ b/components/agentic-circuit/docs/spec/refs/history.md @@ -0,0 +1,12 @@ +# Historical source reference + +## Pre-migration plans and audits {#REF-HISTORY-001} + + + +Commit `5514f886f9967d3f06551f030b74d1fcaccd383e` is the final tree before +the release-layout hard break. It contains the complete dated implementation +plans, superseded design proposals, phase audits, and their original paths. + +NDF clauses in the current tree retain stable decisions and verification +relationships. Git history retains the full historical prose. diff --git a/components/agentic-circuit/examples/README.md b/components/agentic-circuit/examples/README.md new file mode 100644 index 00000000..bad3ccce --- /dev/null +++ b/components/agentic-circuit/examples/README.md @@ -0,0 +1,13 @@ +# Examples + +Examples are grouped by purpose. Product releases version the repository; the +source tree does not keep parallel versioned or phase-numbered example sets. + +- [`pipelines`](pipelines/README.md): Queue/Var composition and control flow. +- [`memory`](memory/README.md): explicit memory, banking, latency, and DMA. +- [`blocks`](blocks/README.md): parameterized reusable building blocks. +- [`architecture`](architecture/README.md): complete generated architecture models. +- [`workspaces`](workspaces/README.md): CLI build/run/replay workspaces with goldens. + +Imported upstream source is not an example. It lives under [`references`](../references/README.md). +Test-owned generated output lives under [`tests/goldens`](../tests/goldens/README.md). diff --git a/components/agentic-circuit/examples/architecture/README.md b/components/agentic-circuit/examples/architecture/README.md new file mode 100644 index 00000000..ae6c4c74 --- /dev/null +++ b/components/agentic-circuit/examples/architecture/README.md @@ -0,0 +1,9 @@ +# Architecture models + +This directory contains complete architecture-scale inputs and native harnesses. +`davincioo_jit.py` specializes the current block catalog for the canonical +DavinciOO trace, while `davincioo_trace_harness.cpp` executes the generated +gfsim model. + +Byte-exact reports and visual goldens belong to +[`tests/goldens/davincioo`](../../tests/goldens/davincioo/README.md). diff --git a/components/agentic-circuit/examples/architecture/davincioo_jit.py b/components/agentic-circuit/examples/architecture/davincioo_jit.py new file mode 100644 index 00000000..0105c031 --- /dev/null +++ b/components/agentic-circuit/examples/architecture/davincioo_jit.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import agentic_circuit as ac + + +@ac.config +class CoreConfig: + engines: int + schedule_entries: int + rob_entries: int + + +@ac.struct +class PTOInst: + sequence: ac.u8 + waits_for: ac.u8 + engine: ac.u2 + cycles: ac.u16 + value: ac.u32 + + +@ac.system +def davincioo(cfg: ac.const[CoreConfig]) -> None: + incoming = ac.source(PTOInst, depth=4, latency=1) + + with ac.scope("decode"): + decoded = ac.compute( + incoming, + lambda item: item.with_fields(value=item.value + 1), + depth=4, + latency=1, + ) + + with ac.scope("pipeline"): + pipelined = ac.pipeline(decoded, stages=2, depth=4) + + with ac.scope("dispatch"): + scalar, vector, cube, tma = ac.route( + pipelined, + by=PTOInst.engine, + outputs=cfg.engines, + depth=8, + latency=1, + ) + dispatched = ac.merge( + scalar, + vector, + cube, + tma, + policy=ac.round_robin, + depth=8, + latency=1, + ) + + with ac.scope("schedule"): + completed = ac.schedule( + dispatched, + by=PTOInst.sequence, + waits_for=PTOInst.waits_for, + resource=PTOInst.engine, + cost=PTOInst.cycles, + entries=cfg.schedule_entries, + resources=cfg.engines, + no_dependency=255, + depth=16, + latency=1, + ) + + with ac.scope("retire"): + retired = ac.reorder( + completed, + by=PTOInst.sequence, + entries=cfg.rob_entries, + start=0, + depth=8, + latency=1, + ) + + ac.observe(retired) + ac.sink(retired) + + +specialization = ac.jit( + davincioo, + cfg=CoreConfig( + engines=4, + schedule_entries=16, + rob_entries=64, + ), +) diff --git a/components/agentic-circuit/examples/architecture/davincioo_trace_harness.cpp b/components/agentic-circuit/examples/architecture/davincioo_trace_harness.cpp new file mode 100644 index 00000000..3a6286f2 --- /dev/null +++ b/components/agentic-circuit/examples/architecture/davincioo_trace_harness.cpp @@ -0,0 +1,200 @@ +#include "davincioo.generated.cpp" +#include "davincioo_trace_fixture.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Span { + std::size_t sequence = 0; + std::string stage; + std::uint64_t begin = 0; + std::uint64_t end = 0; +}; + +class Tracker { +public: + explicit Tracker(std::size_t count) + : current_(count, "source_wait"), begin_(count, 0), seen_(count, false), + completionSeen_(count, false), retirementSeen_(count, false) {} + + void transition(std::size_t sequence, std::string_view stage, + std::uint64_t cycle) { + if (sequence >= current_.size() || current_[sequence] == stage) + return; + close(sequence, cycle); + current_[sequence] = stage; + begin_[sequence] = cycle; + seen_[sequence] = true; + } + + void infer(std::size_t sequence, std::uint64_t cycle) { + if (!seen_[sequence]) + return; + const std::string &stage = current_[sequence]; + if (stage == "completed" || stage == "rob_wait") + transition(sequence, "rob_wait", cycle); + else if (stage == "dispatched" || stage == "schedule_execute") + transition(sequence, "schedule_execute", cycle); + } + + void completion(std::size_t sequence) { + if (sequence < completionSeen_.size() && !completionSeen_[sequence]) { + completionSeen_[sequence] = true; + completionOrder_.push_back(sequence); + } + } + + void retirement(std::size_t sequence) { + if (sequence < retirementSeen_.size() && !retirementSeen_[sequence]) { + retirementSeen_[sequence] = true; + retirementOrder_.push_back(sequence); + } + } + + bool isRetired(std::size_t sequence) const { + return sequence < retirementSeen_.size() && retirementSeen_[sequence]; + } + + void finish(std::uint64_t cycle) { + for (std::size_t sequence = 0; sequence < current_.size(); ++sequence) + close(sequence, cycle); + } + + const std::vector &spans() const { return spans_; } + const std::vector &completionOrder() const { + return completionOrder_; + } + const std::vector &retirementOrder() const { + return retirementOrder_; + } + +private: + void close(std::size_t sequence, std::uint64_t cycle) { + if (current_[sequence] != "done" && cycle > begin_[sequence]) + spans_.push_back({sequence, current_[sequence], begin_[sequence], cycle}); + } + + std::vector current_; + std::vector begin_; + std::vector seen_; + std::vector completionSeen_; + std::vector retirementSeen_; + std::vector completionOrder_; + std::vector retirementOrder_; + std::vector spans_; +}; + +template +void observeQueue(const Queue &queue, std::string_view stage, + std::vector> &observed, + Tracker *tracker = nullptr, bool completion = false, + bool retirement = false) { + for (const auto &item : queue.committedValues()) { + const std::size_t sequence = item.sequence; + if (sequence >= observed.size()) + continue; + observed[sequence] = std::string(stage); + if (tracker && completion) + tracker->completion(sequence); + if (tracker && retirement) + tracker->retirement(sequence); + } +} + +void printOrder(std::string_view name, const std::vector &order) { + std::cout << name; + for (std::size_t sequence : order) + std::cout << ' ' << sequence; + std::cout << '\n'; +} + +} // namespace + +int main() { + ac_generated::Davincioo model; + auto rows = model.dispatch_rows(); + Tracker tracker(davincioo_fixture::kTokens.size()); + std::size_t nextInput = 0; + std::uint64_t cycles = 0; + + for (std::uint64_t tick = 0; tick < 4096; ++tick) { + while (nextInput < davincioo_fixture::kTokens.size() && + model.incoming().canProposePush()) { + if (!model.incoming().proposePush(davincioo_fixture::kTokens[nextInput])) + break; + ++nextInput; + } + + const gfsim::Epoch epoch{tick, 0}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + + std::vector> observed( + davincioo_fixture::kTokens.size()); + observeQueue(model.incoming(), "incoming", observed); + observeQueue(model.decoded(), "decoded", observed); + observeQueue(model.pipelined(), "pipelined", observed); + observeQueue(model.scalar(), "dispatch_scalar", observed); + observeQueue(model.vector(), "dispatch_vector", observed); + observeQueue(model.cube(), "dispatch_cube", observed); + observeQueue(model.tma(), "dispatch_tma", observed); + observeQueue(model.dispatched(), "dispatched", observed); + observeQueue(model.completed(), "completed", observed, &tracker, true); + for (const auto &item : model.retired().committedValues()) { + if (item.sequence >= observed.size()) + continue; + const bool alreadyRetired = tracker.isRetired(item.sequence); + tracker.retirement(item.sequence); + observed[item.sequence] = alreadyRetired ? "done" : "retired"; + } + for (const auto &item : model.sink_0_values()) { + if (item.sequence < observed.size()) { + const bool alreadyRetired = tracker.isRetired(item.sequence); + tracker.retirement(item.sequence); + observed[item.sequence] = alreadyRetired ? "done" : "retired"; + } + } + + for (std::size_t sequence = 0; sequence < observed.size(); ++sequence) { + if (observed[sequence]) + tracker.transition(sequence, *observed[sequence], tick); + else + tracker.infer(sequence, tick); + } + + if (model.sink_0_values().size() == davincioo_fixture::kTokens.size()) { + cycles = tick + 1; + break; + } + } + + if (cycles == 0) + return 2; + tracker.finish(cycles); + + std::cout << "SUMMARY " << cycles << ' ' << model.sink_0_values().size() + << '\n'; + printOrder("COMPLETION", tracker.completionOrder()); + printOrder("RETIREMENT", tracker.retirementOrder()); + std::cout << "VALUES"; + for (const auto &item : model.sink_0_values()) + std::cout << ' ' << item.value; + std::cout << '\n'; + for (const Span &span : tracker.spans()) + std::cout << "SPAN " << span.sequence << ' ' + << davincioo_fixture::kOpcodes[span.sequence] << ' ' << span.stage + << ' ' << span.begin << ' ' << span.end << '\n'; + return 0; +} diff --git a/components/agentic-circuit/examples/blocks/README.md b/components/agentic-circuit/examples/blocks/README.md new file mode 100644 index 00000000..206ff344 --- /dev/null +++ b/components/agentic-circuit/examples/blocks/README.md @@ -0,0 +1,5 @@ +# Parameterized blocks + +This directory contains small examples of reusable, statically specialized +building blocks. `multirate_compute.py` demonstrates a compute block with a +compile-time rate and latency contract. diff --git a/components/agentic-circuit/examples/blocks/multirate_compute.py b/components/agentic-circuit/examples/blocks/multirate_compute.py new file mode 100644 index 00000000..c4e5f1f6 --- /dev/null +++ b/components/agentic-circuit/examples/blocks/multirate_compute.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import agentic_circuit as ac + + +@ac.config +class Config: + rate: int + + +@ac.system +def multirate_compute(cfg: ac.const[Config]) -> None: + incoming = ac.source(int, depth=8, rate=cfg.rate) + computed = ac.compute( + incoming, + lambda item: item + 1, + depth=8, + rate=cfg.rate, + ) + pipelined = ac.pipeline( + computed, + stages=2, + depth=8, + rate=cfg.rate, + ) + ac.sink(pipelined) + + +specialization = ac.jit(multirate_compute, cfg=Config(rate=4)) diff --git a/components/agentic-circuit/examples/memory/README.md b/components/agentic-circuit/examples/memory/README.md new file mode 100644 index 00000000..db5512ef --- /dev/null +++ b/components/agentic-circuit/examples/memory/README.md @@ -0,0 +1,216 @@ +# Agentic Circuit memory examples + +## Parameterized high-level blocks + +`davincioo_jit.py` keeps every dynamic connection as a typed Queue SSA edge, +freezes `CoreConfig` through `ac.jit`, and uses the simple high-level block +names `compute`, `route`, `pipeline`, `merge`, `schedule`, and `reorder`. +Only `compute` carries a lambda; other blocks select typed payload fields with +compile-time descriptors and bind optimized gfsim and PYC/Verilog providers. + +```bash +PYTHONPATH=src python - <<'PY' +from examples.architecture.davincioo_jit import specialization + +open("build/davincioo.ac.mlir", "w").write(specialization.lower_acir()) +open("build/davincioo.cpp", "w").write(specialization.lower_cpp()) +PY + +c++ -std=c++20 -I include -fsyntax-only build/davincioo.cpp +``` + +`specialization.materialize_cpp(cache_root)` compiles the generated C++ on +first use and returns the content-addressed cached object later. +`materialize_pyc(...)` publishes the corresponding PYC/C++/Verilog bundle. + +`multirate_compute.py` freezes `rate=4` into the Queue and C++ template +identities. Native QueueGraph/gfsim supports it; PYC deliberately rejects +`rate>1` until shared ordered FIFO lane lowering is implemented. + +### DavinciOO canonical PTO trace + +The trace runner converts the locked DavinciOO JSONL trace to canonical PTO +trace JSON, specializes the high-level Python model, compiles generated gfsim, +and checks completion order, retirement order, architectural values, and the +453-cycle reference contract. It also emits a deterministic instruction +swimlane. + +```bash +.venv/bin/python tools/run-davincioo.py +``` + +Checked-in evidence: + +- [`davincioo-softmax-run.json`](../../tests/goldens/davincioo/davincioo-softmax-run.json) +- [`davincioo-softmax-swimlane.svg`](../../tests/goldens/davincioo/davincioo-softmax-swimlane.svg) + +## Explicit memory (epoch 0.3) + +`memory_simple.py` declares one root-owned, 16-entry `u16` memory and connects +two typed logical endpoints from child scopes. Writer endpoint ordinal 0 has +fixed priority over reader endpoint ordinal 1. + +The harness makes both endpoints valid together. Two writes to address `3` +return old values `0` and `42`; only after those responses complete can the +reader run, and it observes the final value `99`. This exercises: + +- one physical memory shared by multiple logical request endpoints; +- root-to-descendant scope visibility; +- canonical fixed-priority arbitration; +- one outstanding request and global busy backpressure; +- endpoint-specific write policies and response demultiplexing; +- old-data write responses. + +From the repository root: + +```bash +PYTHONPATH=src \ + .venv/bin/python \ + tools/ac-queue-cxxgen.py examples/memory/memory_simple.py \ + --system memory_simple \ + --acir-output /tmp/memory_simple.mlir \ + --plan-output /tmp/memory_simple.plan.json \ + --acir-opt build/dev-llvm22/bin/acir-opt \ + --queue-plan-tool build/dev-llvm22/bin/acir-queue-plan \ + --queue-cxxgen-tool build/dev-llvm22/bin/acir-queue-cxxgen \ + --output examples/memory/memory_simple.generated.cpp + +c++ \ + -std=c++20 -Iinclude -Iexamples/memory \ + examples/memory/memory_simple_harness.cpp \ + -o /tmp/memory_simple_sim + +/tmp/memory_simple_sim +``` + +Expected output: + +```text +cycles=8 write_old_values=0,42 read_after_priority=99 +``` + +The generated `memory_simple.generated.cpp` is a build artifact and is not +checked in. + +## Statically expanded memory banks + +`memory_banks.py` uses a homogeneous `ac.array` of four memories. The +Python-only `banks.select(...).request(...)` expansion makes bank selection +explicit while lowering entirely to the existing `ac.route`, four +`ac.memory.instance`/`ac.memory.request` pairs, and one `ac.merge`. Each bank +has independent storage and one outstanding request; responses from different +banks may be reordered, so the harness checks them by tag. + +```bash +PYTHONPATH=src \ + .venv/bin/python \ + tools/ac-queue-cxxgen.py examples/memory/memory_banks.py \ + --system memory_banks \ + --acir-output /tmp/memory_banks.mlir \ + --plan-output /tmp/memory_banks.plan.json \ + --acir-opt build/dev-llvm22/bin/acir-opt \ + --queue-plan-tool build/dev-llvm22/bin/acir-queue-plan \ + --queue-cxxgen-tool build/dev-llvm22/bin/acir-queue-cxxgen \ + --output examples/memory/memory_banks.generated.cpp + +c++ \ + -std=c++20 -Iinclude -Iexamples/memory \ + examples/memory/memory_banks_harness.cpp \ + -o /tmp/memory_banks_sim + +/tmp/memory_banks_sim +``` + +Expected output has the following values; the cycle count is deterministic but +is intentionally not part of the example contract: + +```text +cycles= bank0=41 bank1=91 bank2_initial=0 +``` + +## Single-memory busy backpressure + +`memory_busy.py` uses `ac.memory(..., latency=3)` and issues two reads at +different simulated times. Its harness prints the public request Queue +occupancy after every epoch: the second request remains queued throughout the +physical access latency, then is accepted only after the first response +releases `busy`. + +```bash +PYTHONPATH=src \ + .venv/bin/python \ + tools/ac-queue-cxxgen.py examples/memory/memory_busy.py \ + --system memory_busy \ + --acir-output /tmp/memory_busy.mlir \ + --plan-output /tmp/memory_busy.plan.json \ + --acir-opt build/dev-llvm22/bin/acir-opt \ + --queue-plan-tool build/dev-llvm22/bin/acir-queue-plan \ + --queue-cxxgen-tool build/dev-llvm22/bin/acir-queue-cxxgen \ + --output examples/memory/memory_busy.generated.cpp + +c++ \ + -std=c++20 -Iinclude -Iexamples/memory \ + examples/memory/memory_busy_harness.cpp \ + -o /tmp/memory_busy_sim + +/tmp/memory_busy_sim +``` + +Expected output: + +```text +epoch req_q received event + 0 1 0 request A queued + 1 1 0 A accepted; request B queued + 2 1 0 B blocked by memory latency + 3 1 0 B blocked by memory latency + 4 1 0 A response accepted; busy released + 5 0 1 B accepted + 6 0 1 B response pending + 7 0 1 B response pending + 8 0 1 B response accepted; busy released + 9 0 2 sink received B +latency_blocked=1 accepted_after_release=1 responses=2 +``` + +## DMA-style DRAM-to-SRAM copy + +`dma.py` declares root-owned DRAM and SRAM instances. Inside +`with ac.scope("dma")`, the DRAM response Queue directly drives an SRAM write +endpoint. The harness first seeds `DRAM[5]` with `0x1234`, runs one DMA copy to +`SRAM[3]`, then reads SRAM back to verify the transferred value. + +```bash +PYTHONPATH=src \ + .venv/bin/python \ + tools/ac-queue-cxxgen.py examples/memory/dma.py \ + --system dma \ + --acir-output /tmp/dma.mlir \ + --plan-output /tmp/dma.plan.json \ + --acir-opt build/dev-llvm22/bin/acir-opt \ + --queue-plan-tool build/dev-llvm22/bin/acir-queue-plan \ + --queue-cxxgen-tool build/dev-llvm22/bin/acir-queue-cxxgen \ + --output examples/memory/dma.generated.cpp + +c++ \ + -std=c++20 -Iinclude -Iexamples/memory \ + examples/memory/dma_harness.cpp \ + -o /tmp/dma_sim + +/tmp/dma_sim +``` + +Expected output: + +```text +seed_tick=5 copy_tick=14 verify_tick=19 dram_value=0x1234 copy_old_sram=0 +``` + +## Current boundary + +The 0.3 contract intentionally supports one physical port and one outstanding +request per memory instance. It does not provide true multi-port access, +round-robin fairness, response reordering, byte enables, or non-zero +initialization. A continuously valid higher-priority endpoint can starve lower +ordinals. All endpoints of an instance must use the same payload struct and +the memory data/address widths are limited to 64 bits. diff --git a/components/agentic-circuit/examples/memory/dma.py b/components/agentic-circuit/examples/memory/dma.py new file mode 100644 index 00000000..021631ab --- /dev/null +++ b/components/agentic-circuit/examples/memory/dma.py @@ -0,0 +1,75 @@ +"""DMA-style DRAM-to-SRAM copy built from explicit memory endpoints. + +The memories are owned by the root scope. The ``dma`` scope connects a +request Queue to a DRAM read endpoint and feeds that response Queue directly +into an SRAM write endpoint: + + requests -> DRAM read -> dram_responses -> SRAM write -> copy_responses + +Each DMA token carries both addresses. The DRAM response replaces ``data`` +with the value read from DRAM; the following SRAM endpoint uses that value as +its write data. The SRAM response contains SRAM old-data, as required by the +memory contract. +""" + +import agentic_circuit as ac + + +@ac.struct +class DmaRequest: + dram_address: ac.u4 + sram_address: ac.u4 + data: ac.u16 + tag: ac.u8 + + +@ac.system +def dma() -> None: + dram = ac.memory(ac.u16, entries=16, init=0, latency=3) + sram = ac.memory(ac.u16, entries=16, init=0, latency=2) + + # Test/host endpoint used by the harness to put non-zero data in DRAM. + with ac.scope("host"): + dram_seed = ac.source(DmaRequest, depth=2, latency=1) + dram_seed_responses = dram.request( + dram_seed, + address=lambda request: request.dram_address, + write=lambda request: True, + data=lambda request: request.data, + result_field="data", + depth=2, + ) + ac.sink(dram_seed_responses) + + with ac.scope("dma"): + requests = ac.source(DmaRequest, depth=4, latency=1) + dram_responses = dram.request( + requests, + address=lambda request: request.dram_address, + write=lambda request: False, + data=lambda request: request.data, + result_field="data", + depth=2, + ) + copy_responses = sram.request( + dram_responses, + address=lambda response: response.sram_address, + write=lambda response: True, + data=lambda response: response.data, + result_field="data", + depth=2, + ) + ac.sink(copy_responses) + + # Independent read endpoint used to verify the copied SRAM value. + with ac.scope("check"): + sram_reads = ac.source(DmaRequest, depth=2, latency=1) + sram_read_responses = sram.request( + sram_reads, + address=lambda request: request.sram_address, + write=lambda request: False, + data=lambda request: request.data, + result_field="data", + depth=2, + ) + ac.sink(sram_read_responses) diff --git a/components/agentic-circuit/examples/memory/dma_harness.cpp b/components/agentic-circuit/examples/memory/dma_harness.cpp new file mode 100644 index 00000000..550aff0d --- /dev/null +++ b/components/agentic-circuit/examples/memory/dma_harness.cpp @@ -0,0 +1,69 @@ +#include "dma.generated.cpp" + +#include +#include +#include + +namespace { + +void step(ac_generated::Dma &model, std::size_t tick) { + const gfsim::Epoch epoch{tick, 0}; + auto rows = model.dispatch_rows(); + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); +} + +template +std::size_t runUntil(ac_generated::Dma &model, std::size_t &nextTick, + Done done) { + while (nextTick < 64) { + const std::size_t current = nextTick++; + step(model, current); + if (done()) + return current; + } + return 64; +} + +} // namespace + +int main() { + ac_generated::Dma model; + std::size_t nextTick = 0; + + // Host writes 0x1234 to DRAM[5]. + if (!model.dram_seed().proposePush({5, 3, 0x1234, 1})) + return 1; + const std::size_t seedTick = runUntil( + model, nextTick, [&] { return model.sink_0_values().size() == 1; }); + if (seedTick == 64 || model.sink_0_values()[0].data != 0) + return 2; + + // DMA reads DRAM[5] and writes the returned value into SRAM[3]. + if (!model.requests().proposePush({5, 3, 0, 2})) + return 3; + const std::size_t copyTick = runUntil( + model, nextTick, [&] { return model.sink_1_values().size() == 1; }); + if (copyTick == 64 || model.sink_1_values()[0].tag != 2 || + model.sink_1_values()[0].data != 0) + return 4; + + // Read SRAM[3]. The response proves that the DMA copied the DRAM value. + if (!model.sram_reads().proposePush({0, 3, 0, 3})) + return 5; + const std::size_t verifyTick = runUntil( + model, nextTick, [&] { return model.sink_2_values().size() == 1; }); + if (verifyTick == 64 || model.sink_2_values()[0].tag != 3 || + model.sink_2_values()[0].data != 0x1234) + return 6; + + std::cout << "seed_tick=" << seedTick << " copy_tick=" << copyTick + << " verify_tick=" << verifyTick << " dram_value=0x" << std::hex + << model.sink_2_values()[0].data << std::dec + << " copy_old_sram=" << model.sink_1_values()[0].data << "\n"; + return 0; +} diff --git a/components/agentic-circuit/examples/memory/memory_banks.py b/components/agentic-circuit/examples/memory/memory_banks.py new file mode 100644 index 00000000..278d45d0 --- /dev/null +++ b/components/agentic-circuit/examples/memory/memory_banks.py @@ -0,0 +1,66 @@ +"""Statically expanded banked-memory example for contract epoch 0.3. + +The companion harness injects five requests before tick 0: + + tag operation + 1 bank0[3] = 41 (returns old data 0) + 2 bank1[3] = 91 (returns old data 0) + 3 read bank0[3] (returns 41) + 4 read bank1[3] (returns 91) + 5 read bank2[3] (returns 0) + +Current gfsim event pipeline (R=route, A=bank accept, P=bank response, +M=priority merge, S=sink): + + tick 0 1 2 3 4 5 6 7 8 9 10 11 + input t1 Q R A . P M S . . . . . + input t2 Q . R A . P M S . . . . + input t3 Q . . R W A . P M S . . + input t4 Q . . . R W A . P M S . + input t5 Q . . . . R A . P W M S + +Q commits the initially injected token, and W marks waiting caused by either +a busy bank or priority-merge serialization. Notice that bank1 accepts tag 2 +while bank0 is still processing tag 1, and bank1/bank2 accept tags 4/5 in the +same tick. Requests to one bank remain single-outstanding. +""" + +import agentic_circuit as ac + + +@ac.struct +class BankRequest: + bank: ac.u2 + offset: ac.u4 + write: ac.u1 + data: ac.u16 + tag: ac.u8 + + +@ac.system +def memory_banks() -> None: + requests = ac.source(BankRequest, depth=8, latency=1) + + with ac.scope("sram"): + banks = ac.array( + 4, + lambda _: ac.memory(ac.u16, entries=16, init=0, latency=2), + ) + selected = banks.select( + requests, + key=lambda request: request.bank, + depth=2, + latency=1, + ) + responses = selected.request( + address=lambda request: request.offset, + write=lambda request: request.write, + data=lambda request: request.data, + result_field="data", + depth=2, + merge_policy="priority", + merge_depth=2, + merge_latency=1, + ) + + ac.sink(responses) diff --git a/components/agentic-circuit/examples/memory/memory_banks_harness.cpp b/components/agentic-circuit/examples/memory/memory_banks_harness.cpp new file mode 100644 index 00000000..31acbeb5 --- /dev/null +++ b/components/agentic-circuit/examples/memory/memory_banks_harness.cpp @@ -0,0 +1,55 @@ +#include "memory_banks.generated.cpp" + +#include +#include +#include +#include + +int main() { + ac_generated::MemoryBanks model; + const std::array requests{{ + ac_generated::BankRequest{0, 3, 1, 41, 1}, + ac_generated::BankRequest{1, 3, 1, 91, 2}, + ac_generated::BankRequest{0, 3, 0, 0, 3}, + ac_generated::BankRequest{1, 3, 0, 0, 4}, + ac_generated::BankRequest{2, 3, 0, 0, 5}, + }}; + + for (const auto &request : requests) + if (!model.requests().proposePush(request)) + return 1; + + auto rows = model.dispatch_rows(); + std::size_t cycles = 0; + for (std::size_t tick = 0; tick < 64; ++tick) { + const gfsim::Epoch epoch{tick, 0}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + if (model.sink_0_values().size() == requests.size()) { + cycles = tick + 1; + break; + } + } + + std::array oldDataByTag{}; + const auto &responses = model.sink_0_values(); + if (responses.size() != requests.size()) + return 2; + for (const auto &response : responses) { + if (response.tag == 0 || response.tag >= oldDataByTag.size()) + return 3; + oldDataByTag[response.tag] = response.data; + } + if (oldDataByTag[1] != 0 || oldDataByTag[2] != 0 || oldDataByTag[3] != 41 || + oldDataByTag[4] != 91 || oldDataByTag[5] != 0) + return 4; + + std::cout << "cycles=" << cycles << " bank0=" << oldDataByTag[3] + << " bank1=" << oldDataByTag[4] + << " bank2_initial=" << oldDataByTag[5] << "\n"; + return 0; +} diff --git a/components/agentic-circuit/examples/memory/memory_busy.py b/components/agentic-circuit/examples/memory/memory_busy.py new file mode 100644 index 00000000..fbeba17b --- /dev/null +++ b/components/agentic-circuit/examples/memory/memory_busy.py @@ -0,0 +1,42 @@ +"""Single-memory busy/backpressure example for contract epoch 0.3. + +The companion harness injects read A (tag 1) before tick 0 and read B (tag 2) +before tick 1. Both access zero-initialized locations through one memory with +``latency=3``. + +Current gfsim event pipeline (Q=request Queue commit, A=memory accept, +W=waiting while the memory is busy, P=response Queue commit/busy release, +S=sink): + + tick 0 1 2 3 4 5 6 7 8 9 + read A Q A . . P S . . . . + read B . Q W W W A . . P S + +Read B remains at the head of the request Queue while A is outstanding. A's +response releases ``busy`` at tick 4, but the no-same-epoch-reaccept rule means +B can be accepted only at tick 5. Both responses return old data 0. +""" + +import agentic_circuit as ac + + +@ac.struct +class ReadRequest: + address: ac.u4 + data: ac.u16 + tag: ac.u8 + + +@ac.system +def memory_busy() -> None: + sram = ac.memory(ac.u16, entries=16, init=0, latency=3) + requests = ac.source(ReadRequest, depth=4, latency=1) + responses = sram.request( + requests, + address=lambda request: request.address, + write=lambda request: False, + data=lambda request: request.data, + result_field="data", + depth=1, + ) + ac.sink(responses) diff --git a/components/agentic-circuit/examples/memory/memory_busy_harness.cpp b/components/agentic-circuit/examples/memory/memory_busy_harness.cpp new file mode 100644 index 00000000..e8f2b794 --- /dev/null +++ b/components/agentic-circuit/examples/memory/memory_busy_harness.cpp @@ -0,0 +1,75 @@ +#include "memory_busy.generated.cpp" + +#include +#include +#include + +namespace { + +void step(ac_generated::MemoryBusy &model, std::size_t tick) { + const gfsim::Epoch epoch{tick, 0}; + auto rows = model.dispatch_rows(); + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); +} + +} // namespace + +int main() { + ac_generated::MemoryBusy model; + const ac_generated::ReadRequest first{3, 0, 1}; + const ac_generated::ReadRequest second{7, 0, 2}; + + std::cout << "epoch req_q received event\n"; + auto record = [&](std::size_t tick, const char *event) { + step(model, tick); + std::cout << std::setw(5) << tick << " " << std::setw(5) + << model.requests().committedSize() << " " << std::setw(8) + << model.sink_0_values().size() << " " << event << "\n"; + }; + + // Epoch 0 publishes the first request into the source Queue. + if (!model.requests().proposePush(first)) + return 1; + record(0, "request A queued"); + if (model.requests().committedSize() != 1) + return 2; + + // Offer the second request at a later simulated time. Epoch 1 accepts the + // first request and commits the second, leaving exactly one queued request. + if (!model.requests().proposePush(second)) + return 3; + record(1, "A accepted; request B queued"); + if (model.requests().committedSize() != 1) + return 4; + + // With instance latency 3, B stays queued throughout epochs 2 and 3. A's + // response matures in epoch 4, and the no-same-epoch rule still keeps B. + record(2, "B blocked by memory latency"); + record(3, "B blocked by memory latency"); + record(4, "A response accepted; busy released"); + if (model.requests().committedSize() != 1) + return 5; + + // B can fire only in the epoch after A releases busy. + record(5, "B accepted"); + if (model.requests().committedSize() != 0) + return 6; + + record(6, "B response pending"); + record(7, "B response pending"); + record(8, "B response accepted; busy released"); + record(9, "sink received B"); + const auto &responses = model.sink_0_values(); + if (responses.size() != 2 || responses[0].tag != 1 || responses[1].tag != 2 || + responses[0].data != 0 || responses[1].data != 0) + return 7; + + std::cout << "latency_blocked=1 accepted_after_release=1 responses=" + << responses.size() << "\n"; + return 0; +} diff --git a/components/agentic-circuit/examples/memory/memory_simple.py b/components/agentic-circuit/examples/memory/memory_simple.py new file mode 100644 index 00000000..38fa3cba --- /dev/null +++ b/components/agentic-circuit/examples/memory/memory_simple.py @@ -0,0 +1,43 @@ +"""Shared explicit-memory Queue frontend example for contract epoch 0.3.""" + +import agentic_circuit as ac + + +@ac.struct +class MemoryRequest: + address: ac.u4 + data: ac.u16 + tag: ac.u8 + + +@ac.system +def memory_simple() -> None: + # One physical SRAM is owned by the root scope. Both child scopes borrow it. + sram = ac.memory(ac.u16, entries=16, init=0, latency=1) + + # Canonical endpoint ordinal 0: fixed-priority writer. + with ac.scope("a_writer"): + writes = ac.source(MemoryRequest, depth=4, latency=1) + write_responses = sram.request( + writes, + address=lambda request: request.address, + write=lambda request: True, + data=lambda request: request.data, + result_field="data", + depth=2, + ) + ac.sink(write_responses) + + # Canonical endpoint ordinal 1: reader, accepted only when the SRAM is idle + # and no writer request is valid. + with ac.scope("b_reader"): + reads = ac.source(MemoryRequest, depth=4, latency=1) + read_responses = sram.request( + reads, + address=lambda request: request.address, + write=lambda request: False, + data=lambda request: request.data, + result_field="data", + depth=2, + ) + ac.sink(read_responses) diff --git a/components/agentic-circuit/examples/memory/memory_simple_harness.cpp b/components/agentic-circuit/examples/memory/memory_simple_harness.cpp new file mode 100644 index 00000000..387345b8 --- /dev/null +++ b/components/agentic-circuit/examples/memory/memory_simple_harness.cpp @@ -0,0 +1,53 @@ +#include "memory_simple.generated.cpp" + +#include +#include +#include + +int main() { + ac_generated::MemorySimple model; + const std::array writes{{ + ac_generated::MemoryRequest{3, 42, 1}, + ac_generated::MemoryRequest{3, 99, 2}, + }}; + const ac_generated::MemoryRequest read{3, 0, 3}; + + // Both logical endpoints are valid before the first simulated epoch. Fixed + // priority drains both writer requests before accepting the reader request. + for (const auto &request : writes) + if (!model.writes().proposePush(request)) + return 1; + if (!model.reads().proposePush(read)) + return 1; + + auto rows = model.dispatch_rows(); + std::size_t cycles = 0; + for (std::size_t tick = 0; tick < 32; ++tick) { + const gfsim::Epoch epoch{tick, 0}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + if (model.sink_0_values().size() == writes.size() && + model.sink_1_values().size() == 1) { + cycles = tick + 1; + break; + } + } + + const auto &write_responses = model.sink_0_values(); + const auto &read_responses = model.sink_1_values(); + if (write_responses.size() != 2 || write_responses[0].data != 0 || + write_responses[0].tag != 1 || write_responses[1].data != 42 || + write_responses[1].tag != 2 || read_responses.size() != 1 || + read_responses[0].data != 99 || read_responses[0].tag != 3) + return 2; + + std::cout << "cycles=" << cycles + << " write_old_values=" << write_responses[0].data << "," + << write_responses[1].data + << " read_after_priority=" << read_responses[0].data << "\n"; + return 0; +} diff --git a/components/agentic-circuit/examples/pipelines/README.md b/components/agentic-circuit/examples/pipelines/README.md new file mode 100644 index 00000000..1cb3b310 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/README.md @@ -0,0 +1,110 @@ +# Queue/Var DavinciOO-like model + +The [Agentic Circuit Specification Manual](../../docs/spec/agentic-circuit.md) +defines the authoring, ACIR, runtime, backend, and refinement contracts exercised +by these examples. + +`davincioo_queue_model.py` is the first executable topology generated +from serial Python. It uses only repository-owned common building blocks: +`ac.source`, `ac.transform`, `ac.dependency`, `ac.route`, `ac.merge`, +`ac.observe`, `ac.reorder`, and `ac.sink`, connected by typed `ac.queue` values. + +Generate one canonical typed C++ model: + +```bash +PYTHONPATH=src tools/ac-queue-cxxgen.py \ + examples/pipelines/davincioo_queue_model.py \ + --system davincioo_queue_model \ + --acir-output build/davincioo_queue_model.ac.mlir \ + --plan-output build/davincioo_queue_model.queue-plan.json \ + --acir-opt build/dev-llvm22/bin/acir-opt \ + --queue-plan-tool build/dev-llvm22/bin/acir-queue-plan \ + --queue-cxxgen-tool build/dev-llvm22/bin/acir-queue-cxxgen \ + -o build/davincioo_queue_model.cpp +``` + +The generated class owns every interconnect as a typed `gfsim::SimQueue`. +Lexical scopes become `gfsim::Module` hierarchy nodes. Engine blocks borrow +Queue references; they do not allocate or own sibling interconnect. + +The example models the reference shape at building-block level: + +```text +trace -> frontend -> dependency window -> 4-way dispatch + | scalar + | vector + | cube + | tma + merge -> reorder -> retire -> sink +``` + +The checked-in +[`softmax-projection.json`](../../tests/goldens/davincioo/softmax-projection.json) binds +this generated topology to the provenance-locked 15-record softmax trace. It +records opcode identities, engine routes, reference execution costs, explicit +predecessors, fixed boundary-cycle compensation, out-of-order completion order, +in-order retirement, architectural values, and the 453-cycle oracle. + +The generated model uses the official bounded `ac.dependency` window with four +reserved resource classes, round-robin merge, committed observations, and the +official `ac.reorder` block. The same serial Python and frozen ACIR now pass all +of these gates: + +- typed gfsim consumes all 15 projected records and finishes in 453 cycles; +- opcode counts and completion/retirement order match the reference projection; +- copied-source generation remains byte-identical across unrelated roots; +- the same frozen ACIR builds with pinned `pycc` as PYC C++ and Verilog; +- PYC C++ and Verilator produce cycle-identical ready/valid/data observations; +- gfsim, PYC C++, and Verilog produce the same projected output transactions. + +Dependency wait is no longer folded into token latency. `ac.dependency` tracks +predecessor completion explicitly and counts the reference execution cost. The +projection applies only a documented 5-cycle ingress and 4-cycle drain +compensation for the different Queue boundaries. It checks dependency-window +peak occupancy, per-resource executing peaks, and reorder-window peak occupancy +through stable generated-model accessors; raw reference rename-table structure +remains outside the declared observation projection. + +## PYC and Verilog slice + +`pyc_queue_pipeline.py` exercises the initial scalar hardware lowering. Generate +frozen ACIR first, then run `acir-queue-pycgen` or the bundled +`tools/ac-queue-pyc-build.py` command. The bundle command validates the pinned +toolchain lock, invokes external `pycc` for C++ and Verilog, compiles the C++ +source, runs Verilator lint, and writes a canonical hash manifest. + +The pinned toolchain is recorded in `toolchains/pyc.lock.json`. Build that +exact pyCircuit commit with LLVM 19 before running the PYC gate. + +`pyc_struct_pipeline.py` verifies deterministic packed struct layout. +`pyc_route_merge_pipeline.py` verifies static selector demux and priority merge +logic, including forward valid and backward ready paths. +`pyc_select_pipeline.py` verifies runtime selection from a statically shaped +Queue collection without dynamic Queue pointers. +`pyc_atomic_pipeline.py` verifies multi-source/multi-sink atomic firing. +`pyc_firing_pipeline.py` verifies explicit Python `peek`/`pop`/`push` effects +normalize to the standard atomic transform. +`gfsim_expect_pipeline.py` verifies a verification-role leaf executes in gfsim +and is rejected from PYC design hierarchy with testbench-boundary guidance. +`pyc_fork_pipeline.py` verifies decoupled fanout with per-output delivered state. +`pyc_conditional_pipeline.py` verifies that a serial runtime `if` becomes an +official route, two branch transforms, and a mutually exclusive priority merge. +`pyc_feedback_pipeline.py` verifies a bounded serial `while` as sequential +feedback data, valid, and iteration state shared by PYC C++ and Verilog. +`pyc_loop_control_pipeline.py` verifies a leading runtime `break` and tail +`continue` normalize to explicit bounded feedback conditions. +`pyc_recursive_pipeline.py` verifies bounded compile-time recursion expands to +a frozen three-stage Queue chain before ACIR publication. +`pyc_reorder_pipeline.py` verifies bounded key-ordered retirement with the same +register-bank and handshake semantics in typed gfsim, PYC C++, and Verilog. +`pyc_dependency_pipeline.py` verifies predecessor wakeup, execution countdown, +out-of-order completion, and PYC C++/Verilator cycle equivalence. +`pyc_barrier_pipeline.py` verifies heterogeneous positional payloads and an +all-input/all-output atomic synchronization firing shared by typed gfsim, PYC +C++, and Verilog. +`pyc_credit_pipeline.py` verifies bounded parallel in-flight slots, independent +cost countdown, deterministic completion selection, automatic credit return, +and PYC C++/Verilator cycle equivalence. +`pyc_memory_pipeline.py` verifies an explicit typed memory instance, old-data +read-during-write behavior, aligned request/response state, and exactly one +`pyc.sync_mem` realization per instance in PYC C++ and Verilog. diff --git a/components/agentic-circuit/examples/pipelines/davincioo_queue_model.py b/components/agentic-circuit/examples/pipelines/davincioo_queue_model.py new file mode 100644 index 00000000..e3e0d319 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/davincioo_queue_model.py @@ -0,0 +1,91 @@ +import agentic_circuit as ac + + +@ac.struct +class WorkItem: + sequence_id: ac.u8 + opcode: ac.u8 + route: ac.u2 + waits_for: ac.u8 + cycles: ac.u16 + value: ac.u64 + + +@ac.system +def davincioo_queue_model() -> None: + trace = ac.source(WorkItem, depth=16, latency=1) + + with ac.scope("frontend"): + prepared = trace.apply( + lambda item: item.with_fields(value=item.value + 1), + depth=4, + latency=1, + ) + + with ac.scope("dependency"): + scheduled = prepared.depend( + key=lambda item: item.sequence_id, + waits_for=lambda item: item.waits_for, + resource=lambda item: item.route, + cost=lambda item: item.cycles, + capacity=8, + resources=4, + no_dependency=255, + depth=16, + latency=1, + ) + ac.observe(scheduled) + + with ac.scope("dispatch"): + scalar, vector, cube, tma = scheduled.route( + outputs=4, + key=lambda item: item.route, + depth=8, + latency=1, + ) + + with ac.scope("scalar_engine"): + scalar_done = scalar.apply( + lambda item: item.with_fields(value=item.value + 1) + ) + with ac.scope("vector_engine"): + vector_done = vector.apply( + lambda item: item.with_fields(value=item.value + 2) + ) + ac.observe(vector_done) + with ac.scope("cube_engine"): + cube_done = cube.apply( + lambda item: item.with_fields(value=item.value + 3) + ) + with ac.scope("tma_engine"): + tma_done = tma.apply( + lambda item: item.with_fields(value=item.value + 4) + ) + ac.observe(tma_done) + + completed = scalar_done.merge( + vector_done, + cube_done, + tma_done, + policy="round_robin", + depth=8, + latency=1, + ) + ac.observe(completed) + + ordered = completed.reorder( + key=lambda item: item.sequence_id, + capacity=64, + start=0, + depth=8, + latency=1, + ) + + with ac.scope("retire"): + retired = ordered.apply( + lambda item: item.with_fields( + value=item.value + 100, + ) + ) + + ac.sink(retired) diff --git a/components/agentic-circuit/examples/pipelines/gfsim_expect_pipeline.py b/components/agentic-circuit/examples/pipelines/gfsim_expect_pipeline.py new file mode 100644 index 00000000..3658a1eb --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/gfsim_expect_pipeline.py @@ -0,0 +1,17 @@ +import agentic_circuit as ac + + +@ac.struct +class ExpectedToken: + value: ac.u16 + + +@ac.system +def gfsim_expect_pipeline() -> None: + incoming = ac.source(ExpectedToken, depth=2, latency=1) + ac.expect( + incoming, + predicate=lambda item: item.value > 0, + message="value must be positive", + ) + ac.sink(incoming) diff --git a/components/agentic-circuit/examples/pipelines/pyc_atomic_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_atomic_pipeline.py new file mode 100644 index 00000000..5cf57d70 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_atomic_pipeline.py @@ -0,0 +1,12 @@ +import agentic_circuit as ac + + +@ac.system +def pyc_atomic_pipeline() -> None: + left = ac.source(int) + right = ac.source(int) + with ac.atomic(): + left_next = left.apply(lambda item: item + 1) + right_next = right.apply(lambda item: item * 2) + ac.sink(left_next) + ac.sink(right_next) diff --git a/components/agentic-circuit/examples/pipelines/pyc_barrier_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_barrier_pipeline.py new file mode 100644 index 00000000..349aec9b --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_barrier_pipeline.py @@ -0,0 +1,20 @@ +import agentic_circuit as ac + + +@ac.struct +class LeftToken: + value: ac.u16 + + +@ac.struct +class RightToken: + value: ac.u32 + + +@ac.system +def pyc_barrier_pipeline() -> None: + left = ac.source(LeftToken, depth=2, latency=1) + right = ac.source(RightToken, depth=2, latency=1) + left_ready, right_ready = left.barrier(right, depth=2, latency=1) + ac.sink(left_ready) + ac.sink(right_ready) diff --git a/components/agentic-circuit/examples/pipelines/pyc_broadcast_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_broadcast_pipeline.py new file mode 100644 index 00000000..ceeb53a7 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_broadcast_pipeline.py @@ -0,0 +1,10 @@ +import agentic_circuit as ac + + +@ac.system +def pyc_broadcast_pipeline() -> None: + input_queue = ac.source(int, depth=2, latency=1) + left = input_queue.apply(lambda item: item + 1) + right = input_queue.apply(lambda item: item + 2) + merged = left.merge(right, policy="priority", depth=2, latency=1) + ac.sink(merged) diff --git a/components/agentic-circuit/examples/pipelines/pyc_conditional_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_conditional_pipeline.py new file mode 100644 index 00000000..9bd8f77d --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_conditional_pipeline.py @@ -0,0 +1,21 @@ +import agentic_circuit as ac + + +@ac.struct +class Item: + value: ac.u32 + route: ac.u1 + + +@ac.system +def pyc_conditional_pipeline() -> None: + input_queue = ac.source(Item, depth=2, latency=1) + if input_queue.route == 0: + output_queue = input_queue.apply( + lambda item: item.with_fields(value=item.value + 10) + ) + else: + output_queue = input_queue.apply( + lambda item: item.with_fields(value=item.value + 20) + ) + ac.sink(output_queue) diff --git a/components/agentic-circuit/examples/pipelines/pyc_credit_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_credit_pipeline.py new file mode 100644 index 00000000..e4e6639f --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_credit_pipeline.py @@ -0,0 +1,20 @@ +import agentic_circuit as ac + + +@ac.struct +class CreditToken: + sequence: ac.u4 + cycles: ac.u4 + value: ac.u16 + + +@ac.system +def pyc_credit_pipeline() -> None: + issued = ac.source(CreditToken, depth=4, latency=1) + completed = issued.credit( + cost=lambda item: item.cycles, + credits=2, + depth=4, + latency=1, + ) + ac.sink(completed) diff --git a/components/agentic-circuit/examples/pipelines/pyc_dependency_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_dependency_pipeline.py new file mode 100644 index 00000000..ec410549 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_dependency_pipeline.py @@ -0,0 +1,27 @@ +import agentic_circuit as ac + + +@ac.struct +class Token: + sequence: ac.u4 + waits_for: ac.u4 + resource: ac.u1 + cycles: ac.u4 + value: ac.u16 + + +@ac.system +def pyc_dependency_pipeline() -> None: + issued = ac.source(Token, depth=4, latency=1) + completed = issued.depend( + key=lambda item: item.sequence, + waits_for=lambda item: item.waits_for, + resource=lambda item: item.resource, + cost=lambda item: item.cycles, + capacity=4, + resources=2, + no_dependency=15, + depth=4, + latency=1, + ) + ac.sink(completed) diff --git a/components/agentic-circuit/examples/pipelines/pyc_feedback_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_feedback_pipeline.py new file mode 100644 index 00000000..5be83465 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_feedback_pipeline.py @@ -0,0 +1,22 @@ +import agentic_circuit as ac + + +@ac.struct +class Item: + value: ac.u32 + remaining: ac.u4 + + +@ac.system +def pyc_feedback_pipeline() -> None: + current = ac.source(Item, depth=2, latency=1) + while current.remaining > 0: + current = current.apply( + lambda item: item.with_fields( + value=item.value + 1, + remaining=item.remaining - 1, + ), + depth=1, + latency=1, + ) + ac.sink(current) diff --git a/components/agentic-circuit/examples/pipelines/pyc_firing_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_firing_pipeline.py new file mode 100644 index 00000000..351b111e --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_firing_pipeline.py @@ -0,0 +1,19 @@ +import agentic_circuit as ac + + +@ac.struct +class FiringToken: + value: ac.u16 + + +@ac.system +def pyc_firing_pipeline() -> None: + incoming = ac.source(FiringToken, depth=2, latency=1) + outgoing = incoming.firing( + lambda queue: queue.push( + queue.pop().with_fields(value=queue.peek().value + 1) + ), + depth=2, + latency=1, + ) + ac.sink(outgoing) diff --git a/components/agentic-circuit/examples/pipelines/pyc_fork_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_fork_pipeline.py new file mode 100644 index 00000000..3e895c9d --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_fork_pipeline.py @@ -0,0 +1,9 @@ +import agentic_circuit as ac + + +@ac.system +def pyc_fork_pipeline() -> None: + input_queue = ac.source(int) + left, right = input_queue.fork(outputs=2, depth=2, latency=1) + ac.sink(left) + ac.sink(right) diff --git a/components/agentic-circuit/examples/pipelines/pyc_latency_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_latency_pipeline.py new file mode 100644 index 00000000..8645eb10 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_latency_pipeline.py @@ -0,0 +1,12 @@ +import agentic_circuit as ac + + +@ac.system +def pyc_latency_pipeline() -> None: + input_queue = ac.source(int, depth=2, latency=1) + output_queue = input_queue.apply( + lambda item: item + 1, + depth=4, + latency=3, + ) + ac.sink(output_queue) diff --git a/components/agentic-circuit/examples/pipelines/pyc_loop_control_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_loop_control_pipeline.py new file mode 100644 index 00000000..9dc12304 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_loop_control_pipeline.py @@ -0,0 +1,24 @@ +import agentic_circuit as ac + + +@ac.struct +class LoopToken: + remaining: ac.u4 + stop: bool + skip: bool + + +@ac.system +def pyc_loop_control_pipeline() -> None: + current = ac.source(LoopToken, depth=2, latency=1) + while current.remaining > 0: + if current.stop: + break + current = current.apply( + lambda item: item.with_fields(remaining=item.remaining - 1), + depth=1, + latency=1, + ) + if current.skip: + continue + ac.sink(current) diff --git a/components/agentic-circuit/examples/pipelines/pyc_memory_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_memory_pipeline.py new file mode 100644 index 00000000..ef66542f --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_memory_pipeline.py @@ -0,0 +1,24 @@ +import agentic_circuit as ac + + +@ac.struct +class MemoryRequest: + address: ac.u4 + write: ac.u1 + data: ac.u16 + tag: ac.u8 + + +@ac.system +def pyc_memory_pipeline() -> None: + sram = ac.memory(ac.u16, entries=16, init=0, latency=3) + requests = ac.source(MemoryRequest, depth=4, latency=1) + responses = sram.request( + requests, + address=lambda item: item.address, + write=lambda item: item.write, + data=lambda item: item.data, + result_field="data", + depth=4, + ) + ac.sink(responses) diff --git a/components/agentic-circuit/examples/pipelines/pyc_queue_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_queue_pipeline.py new file mode 100644 index 00000000..14a7b65c --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_queue_pipeline.py @@ -0,0 +1,12 @@ +import agentic_circuit as ac + + +@ac.system +def pyc_queue_pipeline() -> None: + input_queue = ac.source(int, depth=2, latency=1) + output_queue = input_queue.apply( + lambda item: item + 1, + depth=2, + latency=1, + ) + ac.sink(output_queue) diff --git a/components/agentic-circuit/examples/pipelines/pyc_recursive_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_recursive_pipeline.py new file mode 100644 index 00000000..19c5a853 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_recursive_pipeline.py @@ -0,0 +1,17 @@ +import agentic_circuit as ac + + +def add_stages(queue, count): + if count == 0: + return queue + return add_stages( + queue.apply(lambda item: item + 1, depth=2, latency=1), + count - 1, + ) + + +@ac.system +def pyc_recursive_pipeline() -> None: + incoming = ac.source(int, depth=2, latency=1) + outgoing = add_stages(incoming, 3) + ac.sink(outgoing) diff --git a/components/agentic-circuit/examples/pipelines/pyc_reorder_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_reorder_pipeline.py new file mode 100644 index 00000000..0b076dee --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_reorder_pipeline.py @@ -0,0 +1,20 @@ +import agentic_circuit as ac + + +@ac.struct +class Token: + sequence: ac.u32 + value: ac.u32 + + +@ac.system +def pyc_reorder_pipeline() -> None: + completed = ac.source(Token, depth=8, latency=1) + retired = completed.reorder( + key=lambda item: item.sequence, + capacity=16, + start=0, + depth=4, + latency=1, + ) + ac.sink(retired) diff --git a/components/agentic-circuit/examples/pipelines/pyc_route_merge_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_route_merge_pipeline.py new file mode 100644 index 00000000..0ea91aba --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_route_merge_pipeline.py @@ -0,0 +1,16 @@ +import agentic_circuit as ac + + +@ac.system +def pyc_route_merge_pipeline() -> None: + input_queue = ac.source(int, depth=2, latency=1) + left, right = input_queue.route( + outputs=2, + key=lambda item: item, + depth=2, + latency=1, + ) + left_done = left.apply(lambda item: item + 10) + right_done = right.apply(lambda item: item + 20) + merged = left_done.merge(right_done, policy="priority", depth=2, latency=1) + ac.sink(merged) diff --git a/components/agentic-circuit/examples/pipelines/pyc_select_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_select_pipeline.py new file mode 100644 index 00000000..96b46e36 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_select_pipeline.py @@ -0,0 +1,19 @@ +import agentic_circuit as ac + + +@ac.struct +class SelectControl: + route: ac.u1 + + +@ac.system +def pyc_select_pipeline() -> None: + control = ac.source(SelectControl, depth=2, latency=1) + lanes = ac.array(2, lambda index: ac.source(int, depth=2, latency=1)) + selected = lanes.select( + control, + key=lambda item: item.route, + depth=2, + latency=1, + ) + ac.sink(selected) diff --git a/components/agentic-circuit/examples/pipelines/pyc_struct_pipeline.py b/components/agentic-circuit/examples/pipelines/pyc_struct_pipeline.py new file mode 100644 index 00000000..4f67d750 --- /dev/null +++ b/components/agentic-circuit/examples/pipelines/pyc_struct_pipeline.py @@ -0,0 +1,21 @@ +import agentic_circuit as ac + + +@ac.struct +class Item: + value: int + remaining: int + + +@ac.system +def pyc_struct_pipeline() -> None: + input_queue = ac.source(Item, depth=2, latency=1) + output_queue = input_queue.apply( + lambda item: item.with_fields( + value=item.value + 1, + remaining=item.remaining - 1, + ), + depth=2, + latency=1, + ) + ac.sink(output_queue) diff --git a/components/agentic-circuit/examples/workspaces/README.md b/components/agentic-circuit/examples/workspaces/README.md new file mode 100644 index 00000000..39fd8024 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/README.md @@ -0,0 +1,9 @@ +# Complete workspaces + +Each child directory is a self-contained public CLI workspace. A workspace owns +its component schemas, bindings, canonical input trace, and expected build/run +artifacts so it can be copied and replayed independently. + +The workspace contract intentionally duplicates a small fixture set. Shared +generated artifacts that are not part of workspace portability belong under +`tests/goldens` instead. diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/agentic-circuit.toml b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/agentic-circuit.toml new file mode 100644 index 00000000..1afdb03d --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/agentic-circuit.toml @@ -0,0 +1,27 @@ +contract_epoch = "0.3" + +[project] +name = "workspace-backpressured-pipeline" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["."] +default_trace = "trace.pto.json" +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/architecture.py b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/architecture.py new file mode 100644 index 00000000..3050a386 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/architecture.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from agentic_circuit import module, process, scope, system + + +@module +def top() -> None: + with scope("pipeline"): + Showcase(scenario=1, name="runtime") + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/components/showcase.binding.json b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/components/showcase.binding.json new file mode 100644 index 00000000..27001455 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/components/showcase.binding.json @@ -0,0 +1,142 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "unknown-unknown", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 1 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:bcb524cf922c8470ec7e6210d785c22442de250f1a0ae51b505dde33ef715974", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 1 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "native", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 1 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:bcb524cf922c8470ec7e6210d785c22442de250f1a0ae51b505dde33ef715974", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 1 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> ()", + "parameters": [ + { + "acir_type": "i64", + "name": "scenario", + "ordinal": 0, + "value": 1 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resolution_key": "@Showcase", + "resources": [], + "results": [] + } + ] +} diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/components/showcase.component.json b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/components/showcase.component.json new file mode 100644 index 00000000..e57d3748 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/components/showcase.component.json @@ -0,0 +1,50 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "workspace.Showcase", + "family": "source", + "provider_namespace": "workspace.provider", + "stability": "experimental", + "cpp_binding": { + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [{ + "name": "scenario", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "0 <= value < 6", + "cpp_mapping": "constructor_constant" + }], + "bindings": [], + "results": [], + "resources": [], + "address_behavior": null, + "protocol_contracts": [], + "effect": { + "kind": "stateful", + "requirements": ["one validated PTO trace document"], + "guarantees": ["one deterministic showcase execution"], + "observable_effects": ["architectural_values", "runtime_events"], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [], + "predicate": "loaded and not committed", + "wakeup_contract": [], + "quiescence": "showcase committed" + }, + "observation": { + "probes": [], + "counters": ["trace_records", "showcase_events"], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270" +} diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-events.jsonl b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-events.jsonl new file mode 100644 index 00000000..5ac10d08 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-events.jsonl @@ -0,0 +1,6 @@ +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":0,"gfsim_object_id":0,"pending_offers":1,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"stall","name":"backpressure","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":1,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":2,"gfsim_object_id":0,"pending_offers":1,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"stall","name":"backpressure","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":3,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":4,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":4},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":5,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":5},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-hierarchy.json b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-hierarchy.json new file mode 100644 index 00000000..f4cf77e2 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-hierarchy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","kind":"hierarchy","path":null,"records":[{"id":"e1","kind":"module","path":"top","schema":null},{"id":"e2","kind":"call","path":"top.runtime","schema":"workspace.Showcase"},{"id":"e3","kind":"process","path":"top.workload","schema":null}],"schema":"agentic-circuit-inspection","system":"main","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-result.json b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-result.json new file mode 100644 index 00000000..49404b9f --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-result.json @@ -0,0 +1 @@ +{"schema":"workspace-showcase-result","version":"0.1","status":"completed","termination_reason":"trace_drained","simulated_ticks":3,"trace_position":{"last_committed_sequence_id":1,"next_record_index":2},"architectural_values":{"consumed_count":2,"consumed_sum":55,"transfer_count":2}} diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-stats.json b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-stats.json new file mode 100644 index 00000000..b26c9c0e --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/expected-stats.json @@ -0,0 +1 @@ +[{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_consumed_count","object_path":"/generated/root-model/runtime","sum":0,"value":2},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_consumed_sum","object_path":"/generated/root-model/runtime","sum":0,"value":55},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_transfer_count","object_path":"/generated/root-model/runtime","sum":0,"value":2},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"showcase_events","object_path":"/generated/root-model/runtime","sum":0,"value":6},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"trace_records","object_path":"/generated/root-model/runtime","sum":0,"value":2}] diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/protocols/.gitkeep b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/protocols/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/protocols/.gitkeep @@ -0,0 +1 @@ + diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/source.davincioo.jsonl b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/source.davincioo.jsonl new file mode 100644 index 00000000..d060f19a --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/source.davincioo.jsonl @@ -0,0 +1,2 @@ +{"block_idx":0,"sequence_id":0,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"0"}],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"outer=col_major,inner=row_major,fractal_size=512","dtype":"float32"}]} +{"block_idx":0,"sequence_id":1,"opcode":"TLOAD","input_tiles":[{"address":"0xfffd835fd000","shape":[1,1,1,64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"outer=col_major,inner=row_major,fractal_size=512","dtype":"float32"}]} diff --git a/components/agentic-circuit/examples/workspaces/backpressured_pipeline/trace.pto.json b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/trace.pto.json new file mode 100644 index 00000000..0cc918ce --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/backpressured_pipeline/trace.pto.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"content_hash":"sha256:4ad35c1b1106c2cae0b56e0c7233a019d85e3387653e6e886f3765e0a966f179","data_layout":"davincioo-tile-address-v1","producer":"davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb","pto_identity":"pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3","record_count":2,"source_program":"examples/beginner_matmul"},"records":[{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["scalar_input","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[{"dtype":"uint64","value":"0"}]}},"dependencies":[],"opcode":"TASSIGN","operands":[{"kind":"immediate","type":"uint64","value":"0"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":0},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0xfffd835fd000","dtype":"float32","layout":"ND","shape":[1,1,1,64,256]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TLOAD","operands":[{"id":"block/0/tile/0xfffd835fd000","kind":"tile"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":1}],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/agentic-circuit.toml b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/agentic-circuit.toml new file mode 100644 index 00000000..096e1b51 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/agentic-circuit.toml @@ -0,0 +1,27 @@ +contract_epoch = "0.3" + +[project] +name = "workspace-multi-time-domain-bridge" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["."] +default_trace = "trace.pto.json" +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/architecture.py b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/architecture.py new file mode 100644 index 00000000..efdc8ca9 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/architecture.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from agentic_circuit import module, process, scope, system + + +@module +def top() -> None: + with scope("domains"): + Showcase(scenario=4, name="runtime") + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/components/showcase.binding.json b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/components/showcase.binding.json new file mode 100644 index 00000000..d32e8404 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/components/showcase.binding.json @@ -0,0 +1,142 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "unknown-unknown", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 4 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:9c1b546207ae0685de2ccda7f1e5c89cfff2239fa3dfe022b20734b5d6fd7099", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 4 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "native", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 4 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:9c1b546207ae0685de2ccda7f1e5c89cfff2239fa3dfe022b20734b5d6fd7099", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 4 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> ()", + "parameters": [ + { + "acir_type": "i64", + "name": "scenario", + "ordinal": 0, + "value": 4 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resolution_key": "@Showcase", + "resources": [], + "results": [] + } + ] +} diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/components/showcase.component.json b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/components/showcase.component.json new file mode 100644 index 00000000..e57d3748 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/components/showcase.component.json @@ -0,0 +1,50 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "workspace.Showcase", + "family": "source", + "provider_namespace": "workspace.provider", + "stability": "experimental", + "cpp_binding": { + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [{ + "name": "scenario", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "0 <= value < 6", + "cpp_mapping": "constructor_constant" + }], + "bindings": [], + "results": [], + "resources": [], + "address_behavior": null, + "protocol_contracts": [], + "effect": { + "kind": "stateful", + "requirements": ["one validated PTO trace document"], + "guarantees": ["one deterministic showcase execution"], + "observable_effects": ["architectural_values", "runtime_events"], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [], + "predicate": "loaded and not committed", + "wakeup_contract": [], + "quiescence": "showcase committed" + }, + "observation": { + "probes": [], + "counters": ["trace_records", "showcase_events"], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270" +} diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-events.jsonl b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-events.jsonl new file mode 100644 index 00000000..c8de8c0c --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-events.jsonl @@ -0,0 +1,9 @@ +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":0,"gfsim_object_id":0,"pending_offers":1,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"stall","name":"backpressure","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":1,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":2,"gfsim_object_id":0,"pending_offers":1,"showcase_epoch_delta":0,"showcase_epoch_time":4},"cat":"stall","name":"backpressure","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":3,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":4},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":4,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":6},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":5,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":7},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":6,"gfsim_object_id":0,"pending_offers":1,"showcase_epoch_delta":0,"showcase_epoch_time":8},"cat":"stall","name":"backpressure","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":7,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":9},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":8,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":10},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-hierarchy.json b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-hierarchy.json new file mode 100644 index 00000000..f4cf77e2 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-hierarchy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","kind":"hierarchy","path":null,"records":[{"id":"e1","kind":"module","path":"top","schema":null},{"id":"e2","kind":"call","path":"top.runtime","schema":"workspace.Showcase"},{"id":"e3","kind":"process","path":"top.workload","schema":null}],"schema":"agentic-circuit-inspection","system":"main","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-result.json b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-result.json new file mode 100644 index 00000000..a85bc3c0 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-result.json @@ -0,0 +1 @@ +{"schema":"workspace-showcase-result","version":"0.1","status":"completed","termination_reason":"trace_drained","simulated_ticks":3,"trace_position":{"last_committed_sequence_id":1,"next_record_index":2},"architectural_values":{"bridged_count":3,"bridged_sum":19,"last_transfer_tick":9}} diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-stats.json b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-stats.json new file mode 100644 index 00000000..73af9c43 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/expected-stats.json @@ -0,0 +1 @@ +[{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_bridged_count","object_path":"/generated/root-model/runtime","sum":0,"value":3},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_bridged_sum","object_path":"/generated/root-model/runtime","sum":0,"value":19},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_last_transfer_tick","object_path":"/generated/root-model/runtime","sum":0,"value":9},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"showcase_events","object_path":"/generated/root-model/runtime","sum":0,"value":9},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"trace_records","object_path":"/generated/root-model/runtime","sum":0,"value":2}] diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/protocols/.gitkeep b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/protocols/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/protocols/.gitkeep @@ -0,0 +1 @@ + diff --git a/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/trace.pto.json b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/trace.pto.json new file mode 100644 index 00000000..0cc918ce --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/multi_time_domain_bridge/trace.pto.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"content_hash":"sha256:4ad35c1b1106c2cae0b56e0c7233a019d85e3387653e6e886f3765e0a966f179","data_layout":"davincioo-tile-address-v1","producer":"davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb","pto_identity":"pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3","record_count":2,"source_program":"examples/beginner_matmul"},"records":[{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["scalar_input","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[{"dtype":"uint64","value":"0"}]}},"dependencies":[],"opcode":"TASSIGN","operands":[{"kind":"immediate","type":"uint64","value":"0"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":0},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0xfffd835fd000","dtype":"float32","layout":"ND","shape":[1,1,1,64,256]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TLOAD","operands":[{"id":"block/0/tile/0xfffd835fd000","kind":"tile"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":1}],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/agentic-circuit.toml b/components/agentic-circuit/examples/workspaces/nested_arrays/agentic-circuit.toml new file mode 100644 index 00000000..97d6f840 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/agentic-circuit.toml @@ -0,0 +1,27 @@ +contract_epoch = "0.3" + +[project] +name = "workspace-nested-arrays" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["."] +default_trace = "trace.pto.json" +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/architecture.py b/components/agentic-circuit/examples/workspaces/nested_arrays/architecture.py new file mode 100644 index 00000000..d662b33f --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/architecture.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from agentic_circuit import module, process, scope, system + + +@module +def top() -> None: + with scope("lanes"): + Showcase(scenario=3, name="runtime") + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/components/showcase.binding.json b/components/agentic-circuit/examples/workspaces/nested_arrays/components/showcase.binding.json new file mode 100644 index 00000000..06045036 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/components/showcase.binding.json @@ -0,0 +1,142 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "unknown-unknown", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 3 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:9c2220e4555a613737912eb5d889906b649345e9d8db66075b9814d3809d45e5", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 3 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "native", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 3 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:9c2220e4555a613737912eb5d889906b649345e9d8db66075b9814d3809d45e5", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 3 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> ()", + "parameters": [ + { + "acir_type": "i64", + "name": "scenario", + "ordinal": 0, + "value": 3 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resolution_key": "@Showcase", + "resources": [], + "results": [] + } + ] +} diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/components/showcase.component.json b/components/agentic-circuit/examples/workspaces/nested_arrays/components/showcase.component.json new file mode 100644 index 00000000..e57d3748 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/components/showcase.component.json @@ -0,0 +1,50 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "workspace.Showcase", + "family": "source", + "provider_namespace": "workspace.provider", + "stability": "experimental", + "cpp_binding": { + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [{ + "name": "scenario", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "0 <= value < 6", + "cpp_mapping": "constructor_constant" + }], + "bindings": [], + "results": [], + "resources": [], + "address_behavior": null, + "protocol_contracts": [], + "effect": { + "kind": "stateful", + "requirements": ["one validated PTO trace document"], + "guarantees": ["one deterministic showcase execution"], + "observable_effects": ["architectural_values", "runtime_events"], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [], + "predicate": "loaded and not committed", + "wakeup_contract": [], + "quiescence": "showcase committed" + }, + "observation": { + "probes": [], + "counters": ["trace_records", "showcase_events"], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270" +} diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/expected-events.jsonl b/components/agentic-circuit/examples/workspaces/nested_arrays/expected-events.jsonl new file mode 100644 index 00000000..96065a87 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/expected-events.jsonl @@ -0,0 +1,27 @@ +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":0,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":1,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":2,"gfsim_object_id":0,"occupancy":2,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":3,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":4,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":5,"gfsim_object_id":0,"occupancy":2,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":6,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":7,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":8,"gfsim_object_id":0,"occupancy":2,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":9,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":10,"gfsim_object_id":0,"occupancy":1,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":11,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":12,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":13,"gfsim_object_id":0,"occupancy":1,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":14,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":15,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":16,"gfsim_object_id":0,"occupancy":1,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":17,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":18,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":19,"gfsim_object_id":0,"occupancy":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":20,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":21,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":22,"gfsim_object_id":0,"occupancy":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":23,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":24,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":25,"gfsim_object_id":0,"occupancy":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":26,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/expected-hierarchy.json b/components/agentic-circuit/examples/workspaces/nested_arrays/expected-hierarchy.json new file mode 100644 index 00000000..f4cf77e2 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/expected-hierarchy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","kind":"hierarchy","path":null,"records":[{"id":"e1","kind":"module","path":"top","schema":null},{"id":"e2","kind":"call","path":"top.runtime","schema":"workspace.Showcase"},{"id":"e3","kind":"process","path":"top.workload","schema":null}],"schema":"agentic-circuit-inspection","system":"main","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/expected-result.json b/components/agentic-circuit/examples/workspaces/nested_arrays/expected-result.json new file mode 100644 index 00000000..2eeb2df9 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/expected-result.json @@ -0,0 +1 @@ +{"schema":"workspace-showcase-result","version":"0.1","status":"completed","termination_reason":"trace_drained","simulated_ticks":3,"trace_position":{"last_committed_sequence_id":1,"next_record_index":2},"architectural_values":{"lane_0_sum":3,"lane_1_sum":30,"lane_2_sum":300}} diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/expected-stats.json b/components/agentic-circuit/examples/workspaces/nested_arrays/expected-stats.json new file mode 100644 index 00000000..37ba82a0 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/expected-stats.json @@ -0,0 +1 @@ +[{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_lane_0_sum","object_path":"/generated/root-model/runtime","sum":0,"value":3},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_lane_1_sum","object_path":"/generated/root-model/runtime","sum":0,"value":30},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_lane_2_sum","object_path":"/generated/root-model/runtime","sum":0,"value":300},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"showcase_events","object_path":"/generated/root-model/runtime","sum":0,"value":27},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"trace_records","object_path":"/generated/root-model/runtime","sum":0,"value":2}] diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/protocols/.gitkeep b/components/agentic-circuit/examples/workspaces/nested_arrays/protocols/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/protocols/.gitkeep @@ -0,0 +1 @@ + diff --git a/components/agentic-circuit/examples/workspaces/nested_arrays/trace.pto.json b/components/agentic-circuit/examples/workspaces/nested_arrays/trace.pto.json new file mode 100644 index 00000000..0cc918ce --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/nested_arrays/trace.pto.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"content_hash":"sha256:4ad35c1b1106c2cae0b56e0c7233a019d85e3387653e6e886f3765e0a966f179","data_layout":"davincioo-tile-address-v1","producer":"davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb","pto_identity":"pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3","record_count":2,"source_program":"examples/beginner_matmul"},"records":[{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["scalar_input","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[{"dtype":"uint64","value":"0"}]}},"dependencies":[],"opcode":"TASSIGN","operands":[{"kind":"immediate","type":"uint64","value":"0"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":0},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0xfffd835fd000","dtype":"float32","layout":"ND","shape":[1,1,1,64,256]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TLOAD","operands":[{"id":"block/0/tile/0xfffd835fd000","kind":"tile"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":1}],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/npu/agentic-circuit.toml b/components/agentic-circuit/examples/workspaces/npu/agentic-circuit.toml new file mode 100644 index 00000000..c04d0085 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/agentic-circuit.toml @@ -0,0 +1,27 @@ +contract_epoch = "0.3" + +[project] +name = "workspace-npu" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["traces"] +default_trace = "traces/pto-trace.json" +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/examples/workspaces/npu/architecture.py b/components/agentic-circuit/examples/workspaces/npu/architecture.py new file mode 100644 index 00000000..63fa746b --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/architecture.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from agentic_circuit import module, process, scope, system + + +@module +def top() -> None: + Npu(name="trace_source") + with scope("frontend"): + NpuNode(name="frontend_decode") + NpuNode(name="frontend_dispatch") + with scope("backend"): + NpuNode(name="backend_dependencies") + NpuNode(name="backend_issue_scalar") + NpuNode(name="backend_issue_vector") + NpuNode(name="backend_issue_cube") + NpuNode(name="backend_issue_tma") + with scope("execution"): + NpuNode(name="execution_scalar_unit_0") + NpuNode(name="execution_vector_unit_0") + NpuNode(name="execution_vector_unit_1") + NpuNode(name="execution_cube_unit_0") + NpuNode(name="execution_tma_unit_0") + with scope("memory"): + NpuNode(name="memory_load_store") + NpuNode(name="memory_scratchpad") + NpuNode(name="memory_controller") + NpuNode(name="completion") + NpuNode(name="retirement") + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/examples/workspaces/npu/components/npu.binding.json b/components/agentic-circuit/examples/workspaces/npu/components/npu.binding.json new file mode 100644 index 00000000..36a445e0 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/components/npu.binding.json @@ -0,0 +1,196 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "unknown-unknown", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Npu_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Npu", + "component_schema_fingerprint": "sha256:d4db2ec8813016dce7fc4e03edddbeb45057bbf9d58081c265a27ecc4a7f6968", + "construction": {"arguments": [], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_npu_reset", + "validate": "gfsim::workspace_npu_validate", + "work": "gfsim::workspace_npu_work", + "xfer": "gfsim::workspace_npu_xfer" + }, + "header": "gfsim/npu.h", + "symbol": "gfsim::NpuTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_npu_type", + "effect": "stateful", + "fingerprint": "sha256:29db918e3fc58e1dcf8bfa1281e6c3f424b0025e78b4e3c28e270126d1db080e", + "implementation": "gfsim.NpuTraceSource", + "ownership": {"kind": "unique", "placement": "member_or_array"}, + "parameters": [], + "ports": [], + "provider": "workspace_npu_source", + "provider_implementation_fingerprint": "sha256:ca6c3fdd699cec3a02697f3714e1b5f70df101b28929cb6ddb118b8fa30c9180", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "native", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Npu_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Npu", + "component_schema_fingerprint": "sha256:d4db2ec8813016dce7fc4e03edddbeb45057bbf9d58081c265a27ecc4a7f6968", + "construction": {"arguments": [], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_npu_reset", + "validate": "gfsim::workspace_npu_validate", + "work": "gfsim::workspace_npu_work", + "xfer": "gfsim::workspace_npu_xfer" + }, + "header": "gfsim/npu.h", + "symbol": "gfsim::NpuTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_npu_type", + "effect": "stateful", + "fingerprint": "sha256:29db918e3fc58e1dcf8bfa1281e6c3f424b0025e78b4e3c28e270126d1db080e", + "implementation": "gfsim.NpuTraceSource", + "ownership": {"kind": "unique", "placement": "member_or_array"}, + "parameters": [], + "ports": [], + "provider": "workspace_npu_source", + "provider_implementation_fingerprint": "sha256:ca6c3fdd699cec3a02697f3714e1b5f70df101b28929cb6ddb118b8fa30c9180", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "unknown-unknown", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_NpuNode_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.NpuNode", + "component_schema_fingerprint": "sha256:3b96b9435f28a5fa08f41dc396618678cede4071171fe56628b11f14071205e3", + "construction": {"arguments": [], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_npu_node_reset", + "validate": "gfsim::workspace_npu_node_validate", + "work": "gfsim::workspace_npu_node_work", + "xfer": "gfsim::workspace_npu_node_xfer" + }, + "header": "gfsim/npu.h", + "symbol": "gfsim::NpuNode", + "target": "gfsim" + }, + "cpp_type": "workspace_npu_node_type", + "effect": "stateful", + "fingerprint": "sha256:3222d9417a28be5dc2b1b7566f784d780d0bdfeac7a7877beac5cbfa93484d7c", + "implementation": "gfsim.NpuNode", + "ownership": {"kind": "unique", "placement": "member_or_array"}, + "parameters": [], + "ports": [], + "provider": "workspace_npu_node", + "provider_implementation_fingerprint": "sha256:ca6c3fdd699cec3a02697f3714e1b5f70df101b28929cb6ddb118b8fa30c9180", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "native", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_NpuNode_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.NpuNode", + "component_schema_fingerprint": "sha256:3b96b9435f28a5fa08f41dc396618678cede4071171fe56628b11f14071205e3", + "construction": {"arguments": [], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_npu_node_reset", + "validate": "gfsim::workspace_npu_node_validate", + "work": "gfsim::workspace_npu_node_work", + "xfer": "gfsim::workspace_npu_node_xfer" + }, + "header": "gfsim/npu.h", + "symbol": "gfsim::NpuNode", + "target": "gfsim" + }, + "cpp_type": "workspace_npu_node_type", + "effect": "stateful", + "fingerprint": "sha256:3222d9417a28be5dc2b1b7566f784d780d0bdfeac7a7877beac5cbfa93484d7c", + "implementation": "gfsim.NpuNode", + "ownership": {"kind": "unique", "placement": "member_or_array"}, + "parameters": [], + "ports": [], + "provider": "workspace_npu_node", + "provider_implementation_fingerprint": "sha256:ca6c3fdd699cec3a02697f3714e1b5f70df101b28929cb6ddb118b8fa30c9180", + "resources": [], + "results": [] + } + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "workspace_Npu_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Npu", + "component_schema_fingerprint": "sha256:d4db2ec8813016dce7fc4e03edddbeb45057bbf9d58081c265a27ecc4a7f6968", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> ()", + "parameters": [], + "ports": [], + "provider": "workspace_npu_source", + "provider_implementation_fingerprint": "sha256:ca6c3fdd699cec3a02697f3714e1b5f70df101b28929cb6ddb118b8fa30c9180", + "resolution_key": "@Npu", + "resources": [], + "results": [] + }, + { + "activation_sources": [], + "binding": "workspace_NpuNode_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.NpuNode", + "component_schema_fingerprint": "sha256:3b96b9435f28a5fa08f41dc396618678cede4071171fe56628b11f14071205e3", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> ()", + "parameters": [], + "ports": [], + "provider": "workspace_npu_node", + "provider_implementation_fingerprint": "sha256:ca6c3fdd699cec3a02697f3714e1b5f70df101b28929cb6ddb118b8fa30c9180", + "resolution_key": "@NpuNode", + "resources": [], + "results": [] + } + ] +} diff --git a/components/agentic-circuit/examples/workspaces/npu/components/npu.component.json b/components/agentic-circuit/examples/workspaces/npu/components/npu.component.json new file mode 100644 index 00000000..e853c0be --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/components/npu.component.json @@ -0,0 +1,43 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "workspace.Npu", + "family": "source", + "provider_namespace": "workspace.npu.provider", + "stability": "experimental", + "cpp_binding": { + "header": "gfsim/npu.h", + "symbol": "gfsim::NpuTraceSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [], + "bindings": [], + "results": [], + "resources": [], + "address_behavior": null, + "protocol_contracts": [], + "effect": { + "kind": "stateful", + "requirements": ["one validated canonical PTO trace document"], + "guarantees": ["deterministic dependency-aware NPU execution"], + "observable_effects": ["architectural_values", "runtime_events"], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [], + "predicate": "loaded and not committed", + "wakeup_contract": [], + "quiescence": "trace exhausted and all instructions retired" + }, + "observation": { + "probes": [], + "counters": ["trace_records", "npu_events", "architectural_retired_instructions", "architectural_digest"], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:d4db2ec8813016dce7fc4e03edddbeb45057bbf9d58081c265a27ecc4a7f6968" +} diff --git a/components/agentic-circuit/examples/workspaces/npu/components/npu_node.component.json b/components/agentic-circuit/examples/workspaces/npu/components/npu_node.component.json new file mode 100644 index 00000000..aaa8578a --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/components/npu_node.component.json @@ -0,0 +1,43 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "workspace.NpuNode", + "family": "compute", + "provider_namespace": "workspace.npu.provider", + "stability": "experimental", + "cpp_binding": { + "header": "gfsim/npu.h", + "symbol": "gfsim::NpuNode", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [], + "bindings": [], + "results": [], + "resources": [], + "address_behavior": null, + "protocol_contracts": [], + "effect": { + "kind": "stateful", + "requirements": [], + "guarantees": ["stable generated hierarchy placement"], + "observable_effects": [], + "failure_behavior": "none" + }, + "activation": { + "sources": [], + "predicate": "never", + "wakeup_contract": [], + "quiescence": "always" + }, + "observation": { + "probes": [], + "counters": [], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:3b96b9435f28a5fa08f41dc396618678cede4071171fe56628b11f14071205e3" +} diff --git a/components/agentic-circuit/examples/workspaces/npu/components/npu_provider.cpp b/components/agentic-circuit/examples/workspaces/npu/components/npu_provider.cpp new file mode 100644 index 00000000..247883ae --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/components/npu_provider.cpp @@ -0,0 +1,6 @@ +#include "npu_provider.h" + +#include "gfsim/components.h" + +static_assert(gfsim::Component); +static_assert(gfsim::Component); diff --git a/components/agentic-circuit/examples/workspaces/npu/components/npu_provider.h b/components/agentic-circuit/examples/workspaces/npu/components/npu_provider.h new file mode 100644 index 00000000..989d9162 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/components/npu_provider.h @@ -0,0 +1,10 @@ +#pragma once + +#include "gfsim/npu.h" + +namespace workspace::npu::provider { + +using NpuProvider = gfsim::NpuTraceSource; +using NpuNodeProvider = gfsim::NpuNode; + +} // namespace workspace::npu::provider diff --git a/components/agentic-circuit/examples/workspaces/npu/expected-events.jsonl b/components/agentic-circuit/examples/workspaces/npu/expected-events.jsonl new file mode 100644 index 00000000..911e18d6 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/expected-events.jsonl @@ -0,0 +1,42 @@ +{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":0,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":0,"stable_object_id":23},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"scalar","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":1,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":1,"stable_object_id":20},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":2,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":2,"stable_object_id":21},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":3,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":2,"producer_sequence_id":0,"tile_identity":"block/0/tile/0x0"},"cat":"dependency","id":5047684461776682,"name":"tile","ph":"s","pid":0,"tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":4,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":2,"producer_sequence_id":1,"tile_identity":"block/0/tile/0x10"},"cat":"dependency","id":7276611561329711,"name":"tile","ph":"s","pid":0,"tid":17,"ts":0} +{"args":{"engine":"cube","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":5,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":3,"stable_object_id":22},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":6,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":3,"producer_sequence_id":2,"tile_identity":"block/0/tile/0x20"},"cat":"dependency","id":1432032477267923,"name":"tile","ph":"s","pid":0,"tid":17,"ts":0} +{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":7,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":4,"stable_object_id":21},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":8,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":5,"stable_object_id":23},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":9,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":5,"producer_sequence_id":3,"tile_identity":"block/0/tile/0x40"},"cat":"dependency","id":3575161440751033,"name":"tile","ph":"s","pid":0,"tid":17,"ts":0} +{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":10,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":6,"stable_object_id":23},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"scalar","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":11,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":6,"stable_object_id":20},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":12,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":6,"stable_object_id":21},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":13,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":7,"unit_index":0},"cat":"instruction","dur":3,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0} +{"args":{"engine":"scalar","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":14,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":7,"unit_index":0},"cat":"instruction","dur":1,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0} +{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":15,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":7,"unit_index":0},"cat":"instruction","dur":2,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":16,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":8,"producer_sequence_id":1},"cat":"dependency","name":"ready","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":17,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":8},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":18,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":9},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":19,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":10,"producer_sequence_id":0},"cat":"dependency","name":"ready","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":20,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":10},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":21,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":10},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":22,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":10},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":23,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":11,"stable_object_id":21},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":24,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":11,"producer_sequence_id":0,"tile_identity":"block/0/tile/0x0"},"bp":"e","cat":"dependency","id":5047684461776682,"name":"tile","ph":"f","pid":0,"tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":25,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":11,"producer_sequence_id":1,"tile_identity":"block/0/tile/0x10"},"bp":"e","cat":"dependency","id":7276611561329711,"name":"tile","ph":"f","pid":0,"tid":17,"ts":0} +{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":26,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":12,"unit_index":0},"cat":"instruction","dur":2,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":27,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":14,"producer_sequence_id":2},"cat":"dependency","name":"ready","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":28,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":14},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":29,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":14},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"cube","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":30,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":15,"stable_object_id":22},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":31,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":15,"producer_sequence_id":2,"tile_identity":"block/0/tile/0x20"},"bp":"e","cat":"dependency","id":1432032477267923,"name":"tile","ph":"f","pid":0,"tid":17,"ts":0} +{"args":{"engine":"cube","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":32,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":16,"unit_index":0},"cat":"instruction","dur":4,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":33,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":20,"producer_sequence_id":3},"cat":"dependency","name":"ready","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":34,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":20},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":35,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":20},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":36,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":20},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":37,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":21,"stable_object_id":23},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":38,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":21,"producer_sequence_id":3,"tile_identity":"block/0/tile/0x40"},"bp":"e","cat":"dependency","id":3575161440751033,"name":"tile","ph":"f","pid":0,"tid":17,"ts":0} +{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":39,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":22,"unit_index":0},"cat":"instruction","dur":4,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":40,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":26},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":41,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":26},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0} diff --git a/components/agentic-circuit/examples/workspaces/npu/expected-hierarchy.json b/components/agentic-circuit/examples/workspaces/npu/expected-hierarchy.json new file mode 100644 index 00000000..a7375c20 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/expected-hierarchy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","kind":"hierarchy","path":null,"records":[{"id":"e1","kind":"module","path":"top","schema":null},{"id":"e5","kind":"call","path":"top.backend_dependencies","schema":"workspace.NpuNode"},{"id":"e8","kind":"call","path":"top.backend_issue_cube","schema":"workspace.NpuNode"},{"id":"e6","kind":"call","path":"top.backend_issue_scalar","schema":"workspace.NpuNode"},{"id":"e9","kind":"call","path":"top.backend_issue_tma","schema":"workspace.NpuNode"},{"id":"e7","kind":"call","path":"top.backend_issue_vector","schema":"workspace.NpuNode"},{"id":"e18","kind":"call","path":"top.completion","schema":"workspace.NpuNode"},{"id":"e13","kind":"call","path":"top.execution_cube_unit_0","schema":"workspace.NpuNode"},{"id":"e10","kind":"call","path":"top.execution_scalar_unit_0","schema":"workspace.NpuNode"},{"id":"e14","kind":"call","path":"top.execution_tma_unit_0","schema":"workspace.NpuNode"},{"id":"e11","kind":"call","path":"top.execution_vector_unit_0","schema":"workspace.NpuNode"},{"id":"e12","kind":"call","path":"top.execution_vector_unit_1","schema":"workspace.NpuNode"},{"id":"e3","kind":"call","path":"top.frontend_decode","schema":"workspace.NpuNode"},{"id":"e4","kind":"call","path":"top.frontend_dispatch","schema":"workspace.NpuNode"},{"id":"e17","kind":"call","path":"top.memory_controller","schema":"workspace.NpuNode"},{"id":"e15","kind":"call","path":"top.memory_load_store","schema":"workspace.NpuNode"},{"id":"e16","kind":"call","path":"top.memory_scratchpad","schema":"workspace.NpuNode"},{"id":"e19","kind":"call","path":"top.retirement","schema":"workspace.NpuNode"},{"id":"e2","kind":"call","path":"top.trace_source","schema":"workspace.Npu"},{"id":"e20","kind":"process","path":"top.workload","schema":null}],"schema":"agentic-circuit-inspection","system":"main","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/npu/expected-perfetto.json b/components/agentic-circuit/examples/workspaces/npu/expected-perfetto.json new file mode 100644 index 00000000..ffdd01a8 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/expected-perfetto.json @@ -0,0 +1 @@ +{"traceEvents":[{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":0,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":0,"stable_object_id":23},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"scalar","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":1,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":1,"stable_object_id":20},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":2,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":2,"stable_object_id":21},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":3,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":2,"producer_sequence_id":0,"tile_identity":"block/0/tile/0x0"},"cat":"dependency","id":5047684461776682,"name":"tile","ph":"s","pid":0,"tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":4,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":2,"producer_sequence_id":1,"tile_identity":"block/0/tile/0x10"},"cat":"dependency","id":7276611561329711,"name":"tile","ph":"s","pid":0,"tid":17,"ts":0},{"args":{"engine":"cube","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":5,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":3,"stable_object_id":22},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":6,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":3,"producer_sequence_id":2,"tile_identity":"block/0/tile/0x20"},"cat":"dependency","id":1432032477267923,"name":"tile","ph":"s","pid":0,"tid":17,"ts":0},{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":7,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":4,"stable_object_id":21},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":8,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":5,"stable_object_id":23},"cat":"instruction","name":"dispatch","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":9,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":5,"producer_sequence_id":3,"tile_identity":"block/0/tile/0x40"},"cat":"dependency","id":3575161440751033,"name":"tile","ph":"s","pid":0,"tid":17,"ts":0},{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":10,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":6,"stable_object_id":23},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"scalar","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":11,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":6,"stable_object_id":20},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":12,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":6,"stable_object_id":21},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":13,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":7,"unit_index":0},"cat":"instruction","dur":3,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0},{"args":{"engine":"scalar","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":14,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":7,"unit_index":0},"cat":"instruction","dur":1,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0},{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":15,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":7,"unit_index":0},"cat":"instruction","dur":2,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":16,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":8,"producer_sequence_id":1},"cat":"dependency","name":"ready","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":17,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":8},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":18,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":9},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":19,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":10,"producer_sequence_id":0},"cat":"dependency","name":"ready","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":20,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":10},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":21,"gfsim_object_id":17,"gfsim_root_sequence_id":0,"npu_epoch_delta":0,"npu_epoch_time":10},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":22,"gfsim_object_id":17,"gfsim_root_sequence_id":1,"npu_epoch_delta":0,"npu_epoch_time":10},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":23,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":11,"stable_object_id":21},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":24,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":11,"producer_sequence_id":0,"tile_identity":"block/0/tile/0x0"},"bp":"e","cat":"dependency","id":5047684461776682,"name":"tile","ph":"f","pid":0,"tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":25,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":11,"producer_sequence_id":1,"tile_identity":"block/0/tile/0x10"},"bp":"e","cat":"dependency","id":7276611561329711,"name":"tile","ph":"f","pid":0,"tid":17,"ts":0},{"args":{"engine":"vector","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":26,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":12,"unit_index":0},"cat":"instruction","dur":2,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":27,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":14,"producer_sequence_id":2},"cat":"dependency","name":"ready","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":28,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":14},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":29,"gfsim_object_id":17,"gfsim_root_sequence_id":2,"npu_epoch_delta":0,"npu_epoch_time":14},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"cube","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":30,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":15,"stable_object_id":22},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":31,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":15,"producer_sequence_id":2,"tile_identity":"block/0/tile/0x20"},"bp":"e","cat":"dependency","id":1432032477267923,"name":"tile","ph":"f","pid":0,"tid":17,"ts":0},{"args":{"engine":"cube","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":32,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":16,"unit_index":0},"cat":"instruction","dur":4,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":33,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":20,"producer_sequence_id":3},"cat":"dependency","name":"ready","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":34,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":20},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":35,"gfsim_object_id":17,"gfsim_root_sequence_id":3,"npu_epoch_delta":0,"npu_epoch_time":20},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":36,"gfsim_object_id":17,"gfsim_root_sequence_id":4,"npu_epoch_delta":0,"npu_epoch_time":20},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":37,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":21,"stable_object_id":23},"cat":"instruction","name":"issue","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":38,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":21,"producer_sequence_id":3,"tile_identity":"block/0/tile/0x40"},"bp":"e","cat":"dependency","id":3575161440751033,"name":"tile","ph":"f","pid":0,"tid":17,"ts":0},{"args":{"engine":"tma","gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":39,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":22,"unit_index":0},"cat":"instruction","dur":4,"name":"execute","ph":"X","pid":0,"tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":40,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":26},"cat":"instruction","name":"complete","ph":"i","pid":0,"s":"t","tid":17,"ts":0},{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":41,"gfsim_object_id":17,"gfsim_root_sequence_id":5,"npu_epoch_delta":0,"npu_epoch_time":26},"cat":"instruction","name":"retire","ph":"i","pid":0,"s":"t","tid":17,"ts":0}]} diff --git a/components/agentic-circuit/examples/workspaces/npu/expected-result.json b/components/agentic-circuit/examples/workspaces/npu/expected-result.json new file mode 100644 index 00000000..c7537ff1 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/expected-result.json @@ -0,0 +1 @@ +{"architectural_values":{"digest":11635184385557860000,"retired_instructions":6},"schema":"workspace-npu-result","simulated_ticks":3,"status":"completed","termination_reason":"trace_drained","trace_position":{"last_committed_sequence_id":5,"next_record_index":6},"version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/npu/expected-stats.json b/components/agentic-circuit/examples/workspaces/npu/expected-stats.json new file mode 100644 index 00000000..5d98bea8 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/expected-stats.json @@ -0,0 +1 @@ +[{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_digest","object_path":"/generated/root-model/trace_source","sum":0,"value":11635184385557860000},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_retired_instructions","object_path":"/generated/root-model/trace_source","sum":0,"value":6},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"npu_events","object_path":"/generated/root-model/trace_source","sum":0,"value":42},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"trace_records","object_path":"/generated/root-model/trace_source","sum":0,"value":6}] diff --git a/components/agentic-circuit/examples/workspaces/npu/protocols/.gitkeep b/components/agentic-circuit/examples/workspaces/npu/protocols/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/protocols/.gitkeep @@ -0,0 +1 @@ + diff --git a/components/agentic-circuit/examples/workspaces/npu/traces/davincioo.jsonl b/components/agentic-circuit/examples/workspaces/npu/traces/davincioo.jsonl new file mode 100644 index 00000000..187ca141 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/traces/davincioo.jsonl @@ -0,0 +1,6 @@ +{"block_idx":0,"sequence_id":0,"opcode":"TLOAD","input_tiles":[{"address":"0x100","shape":[2,4],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[2,4],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":1,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"7"}],"output_tiles":[{"address":"0x10","shape":[2,4],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":2,"opcode":"TADD","input_tiles":[{"address":"0x0","shape":[2,4],"layout":"ND","dtype":"float32"},{"address":"0x10","shape":[2,4],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x20","shape":[2,4],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":3,"opcode":"TMATMUL","input_tiles":[{"address":"0x20","shape":[2,4],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x40","shape":[2,4],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":4,"opcode":"TADD","input_tiles":[],"scalar_inputs":[],"output_tiles":[{"address":"0x50","shape":[2,4],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":5,"opcode":"TSTORE","input_tiles":[{"address":"0x40","shape":[2,4],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x200","shape":[2,4],"layout":"ND","dtype":"float32"}]} diff --git a/components/agentic-circuit/examples/workspaces/npu/traces/pto-trace.json b/components/agentic-circuit/examples/workspaces/npu/traces/pto-trace.json new file mode 100644 index 00000000..40b2c318 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/npu/traces/pto-trace.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"content_hash":"sha256:2ca5496aff126de3532252186dda9c25ec0a1a4a7c7b558311d93fe26d42be8e","data_layout":"davincioo-tile-address-v1","producer":"davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb","pto_identity":"pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3","record_count":6,"source_program":"examples/workspaces/npu"},"records":[{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0x100","dtype":"float32","layout":"ND","shape":[2,4]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"ND","shape":[2,4]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TLOAD","operands":[{"id":"block/0/tile/0x100","kind":"tile"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":0},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["scalar_input","output_tile"],"output_tiles":[{"address":"0x10","dtype":"float32","layout":"ND","shape":[2,4]}],"scalar_inputs":[{"dtype":"uint64","value":"7"}]}},"dependencies":[],"opcode":"TASSIGN","operands":[{"kind":"immediate","type":"uint64","value":"7"},{"id":"block/0/tile/0x10","kind":"tile"}],"sequence_id":1},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0x0","dtype":"float32","layout":"ND","shape":[2,4]},{"address":"0x10","dtype":"float32","layout":"ND","shape":[2,4]}],"operand_roles":["input_tile","input_tile","output_tile"],"output_tiles":[{"address":"0x20","dtype":"float32","layout":"ND","shape":[2,4]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TADD","operands":[{"id":"block/0/tile/0x0","kind":"tile"},{"id":"block/0/tile/0x10","kind":"tile"},{"id":"block/0/tile/0x20","kind":"tile"}],"sequence_id":2},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0x20","dtype":"float32","layout":"ND","shape":[2,4]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x40","dtype":"float32","layout":"ND","shape":[2,4]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TMATMUL","operands":[{"id":"block/0/tile/0x20","kind":"tile"},{"id":"block/0/tile/0x40","kind":"tile"}],"sequence_id":3},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["output_tile"],"output_tiles":[{"address":"0x50","dtype":"float32","layout":"ND","shape":[2,4]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TADD","operands":[{"id":"block/0/tile/0x50","kind":"tile"}],"sequence_id":4},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0x40","dtype":"float32","layout":"ND","shape":[2,4]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x200","dtype":"float32","layout":"ND","shape":[2,4]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TSTORE","operands":[{"id":"block/0/tile/0x40","kind":"tile"},{"id":"block/0/tile/0x200","kind":"tile"}],"sequence_id":5}],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/agentic-circuit.toml b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/agentic-circuit.toml new file mode 100644 index 00000000..1dfd1e74 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/agentic-circuit.toml @@ -0,0 +1,27 @@ +contract_epoch = "0.3" + +[project] +name = "workspace-producer-queue-consumer" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["."] +default_trace = "trace.pto.json" +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/architecture.py b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/architecture.py new file mode 100644 index 00000000..c11af1fb --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/architecture.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from agentic_circuit import module, process, scope, system + + +@module +def top() -> None: + with scope("pipeline"): + Showcase(scenario=0, name="runtime") + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/components/showcase.binding.json b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/components/showcase.binding.json new file mode 100644 index 00000000..2e33ec75 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/components/showcase.binding.json @@ -0,0 +1,113 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "unknown-unknown", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": {"arguments": [0], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:32e7a810b0f32c03a2c2121ec89e038cf6e10282d674553b7ed55d379069acc6", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": {"kind": "unique", "placement": "member_or_array"}, + "parameters": [{ + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 0 + }], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "native", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": {"arguments": [0], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:32e7a810b0f32c03a2c2121ec89e038cf6e10282d674553b7ed55d379069acc6", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": {"kind": "unique", "placement": "member_or_array"}, + "parameters": [{ + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 0 + }], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + } + ], + "requests": [{ + "activation_sources": [], + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> ()", + "parameters": [{"acir_type": "i64", "name": "scenario", "ordinal": 0, "value": 0}], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resolution_key": "@Showcase", + "resources": [], + "results": [] + }] +} diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/components/showcase.component.json b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/components/showcase.component.json new file mode 100644 index 00000000..e57d3748 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/components/showcase.component.json @@ -0,0 +1,50 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "workspace.Showcase", + "family": "source", + "provider_namespace": "workspace.provider", + "stability": "experimental", + "cpp_binding": { + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [{ + "name": "scenario", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "0 <= value < 6", + "cpp_mapping": "constructor_constant" + }], + "bindings": [], + "results": [], + "resources": [], + "address_behavior": null, + "protocol_contracts": [], + "effect": { + "kind": "stateful", + "requirements": ["one validated PTO trace document"], + "guarantees": ["one deterministic showcase execution"], + "observable_effects": ["architectural_values", "runtime_events"], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [], + "predicate": "loaded and not committed", + "wakeup_contract": [], + "quiescence": "showcase committed" + }, + "observation": { + "probes": [], + "counters": ["trace_records", "showcase_events"], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270" +} diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-events.jsonl b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-events.jsonl new file mode 100644 index 00000000..d3a68209 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-events.jsonl @@ -0,0 +1,13 @@ +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":0,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":1,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":2,"gfsim_object_id":0,"occupancy":2,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":3,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":4,"gfsim_object_id":0,"occupancy":1,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":5,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":6,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":7,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":8,"gfsim_object_id":0,"occupancy":1,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":9,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":10,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":4},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":11,"gfsim_object_id":0,"occupancy":0,"showcase_epoch_delta":0,"showcase_epoch_time":4},"cat":"queue","name":"occupancy","ph":"C","pid":0,"tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":12,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":4},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-hierarchy.json b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-hierarchy.json new file mode 100644 index 00000000..f4cf77e2 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-hierarchy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","kind":"hierarchy","path":null,"records":[{"id":"e1","kind":"module","path":"top","schema":null},{"id":"e2","kind":"call","path":"top.runtime","schema":"workspace.Showcase"},{"id":"e3","kind":"process","path":"top.workload","schema":null}],"schema":"agentic-circuit-inspection","system":"main","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-result.json b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-result.json new file mode 100644 index 00000000..3b49a917 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-result.json @@ -0,0 +1 @@ +{"schema":"workspace-showcase-result","version":"0.1","status":"completed","termination_reason":"trace_drained","simulated_ticks":3,"trace_position":{"last_committed_sequence_id":1,"next_record_index":2},"architectural_values":{"consumed_count":3,"consumed_sum":16}} diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-stats.json b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-stats.json new file mode 100644 index 00000000..ebb2ba74 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/expected-stats.json @@ -0,0 +1 @@ +[{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_consumed_count","object_path":"/generated/root-model/runtime","sum":0,"value":3},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_consumed_sum","object_path":"/generated/root-model/runtime","sum":0,"value":16},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"showcase_events","object_path":"/generated/root-model/runtime","sum":0,"value":13},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"trace_records","object_path":"/generated/root-model/runtime","sum":0,"value":2}] diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/protocols/.gitkeep b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/protocols/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/protocols/.gitkeep @@ -0,0 +1 @@ + diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/source.davincioo.jsonl b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/source.davincioo.jsonl new file mode 100644 index 00000000..d060f19a --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/source.davincioo.jsonl @@ -0,0 +1,2 @@ +{"block_idx":0,"sequence_id":0,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"0"}],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"outer=col_major,inner=row_major,fractal_size=512","dtype":"float32"}]} +{"block_idx":0,"sequence_id":1,"opcode":"TLOAD","input_tiles":[{"address":"0xfffd835fd000","shape":[1,1,1,64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"outer=col_major,inner=row_major,fractal_size=512","dtype":"float32"}]} diff --git a/components/agentic-circuit/examples/workspaces/producer_queue_consumer/trace.pto.json b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/trace.pto.json new file mode 100644 index 00000000..0cc918ce --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/producer_queue_consumer/trace.pto.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"content_hash":"sha256:4ad35c1b1106c2cae0b56e0c7233a019d85e3387653e6e886f3765e0a966f179","data_layout":"davincioo-tile-address-v1","producer":"davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb","pto_identity":"pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3","record_count":2,"source_program":"examples/beginner_matmul"},"records":[{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["scalar_input","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[{"dtype":"uint64","value":"0"}]}},"dependencies":[],"opcode":"TASSIGN","operands":[{"kind":"immediate","type":"uint64","value":"0"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":0},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0xfffd835fd000","dtype":"float32","layout":"ND","shape":[1,1,1,64,256]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TLOAD","operands":[{"id":"block/0/tile/0xfffd835fd000","kind":"tile"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":1}],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/agentic-circuit.toml b/components/agentic-circuit/examples/workspaces/request_response_memory/agentic-circuit.toml new file mode 100644 index 00000000..5aad2a56 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/agentic-circuit.toml @@ -0,0 +1,27 @@ +contract_epoch = "0.3" + +[project] +name = "workspace-request-response-memory" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["."] +default_trace = "trace.pto.json" +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/architecture.py b/components/agentic-circuit/examples/workspaces/request_response_memory/architecture.py new file mode 100644 index 00000000..c6392e93 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/architecture.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from agentic_circuit import module, process, scope, system + + +@module +def top() -> None: + with scope("memory_path"): + Showcase(scenario=2, name="runtime") + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/components/showcase.binding.json b/components/agentic-circuit/examples/workspaces/request_response_memory/components/showcase.binding.json new file mode 100644 index 00000000..98eab3a3 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/components/showcase.binding.json @@ -0,0 +1,142 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "unknown-unknown", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 2 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:04d4bb6c35f30c3202e0d90d520dcd731882ac806e8725490319c8caae770e44", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 2 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "native", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 2 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:04d4bb6c35f30c3202e0d90d520dcd731882ac806e8725490319c8caae770e44", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 2 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> ()", + "parameters": [ + { + "acir_type": "i64", + "name": "scenario", + "ordinal": 0, + "value": 2 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resolution_key": "@Showcase", + "resources": [], + "results": [] + } + ] +} diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/components/showcase.component.json b/components/agentic-circuit/examples/workspaces/request_response_memory/components/showcase.component.json new file mode 100644 index 00000000..e57d3748 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/components/showcase.component.json @@ -0,0 +1,50 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "workspace.Showcase", + "family": "source", + "provider_namespace": "workspace.provider", + "stability": "experimental", + "cpp_binding": { + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [{ + "name": "scenario", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "0 <= value < 6", + "cpp_mapping": "constructor_constant" + }], + "bindings": [], + "results": [], + "resources": [], + "address_behavior": null, + "protocol_contracts": [], + "effect": { + "kind": "stateful", + "requirements": ["one validated PTO trace document"], + "guarantees": ["one deterministic showcase execution"], + "observable_effects": ["architectural_values", "runtime_events"], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [], + "predicate": "loaded and not committed", + "wakeup_contract": [], + "quiescence": "showcase committed" + }, + "observation": { + "probes": [], + "counters": ["trace_records", "showcase_events"], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270" +} diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/expected-events.jsonl b/components/agentic-circuit/examples/workspaces/request_response_memory/expected-events.jsonl new file mode 100644 index 00000000..4ce5d5f3 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/expected-events.jsonl @@ -0,0 +1,10 @@ +{"args":{"correlation_id":10,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":0,"gfsim_object_id":0,"gfsim_root_sequence_id":10,"showcase_epoch_delta":0,"showcase_epoch_time":1},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"correlation_id":10,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":1,"gfsim_object_id":0,"gfsim_root_sequence_id":10,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"transaction","name":"request","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"address":1,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":2,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":2},"cat":"memory","name":"write","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"correlation_id":10,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":3,"gfsim_object_id":0,"gfsim_root_sequence_id":10,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"correlation_id":10,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":4,"gfsim_object_id":0,"gfsim_root_sequence_id":10,"showcase_epoch_delta":0,"showcase_epoch_time":4},"cat":"transaction","name":"response","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"correlation_id":11,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":5,"gfsim_object_id":0,"gfsim_root_sequence_id":11,"showcase_epoch_delta":0,"showcase_epoch_time":5},"cat":"transaction","name":"accepted","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"correlation_id":11,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":6,"gfsim_object_id":0,"gfsim_root_sequence_id":11,"showcase_epoch_delta":0,"showcase_epoch_time":6},"cat":"transaction","name":"request","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"address":3,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":7,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":6},"cat":"memory","name":"write","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"correlation_id":11,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":8,"gfsim_object_id":0,"gfsim_root_sequence_id":11,"showcase_epoch_delta":0,"showcase_epoch_time":7},"cat":"transaction","name":"completed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"correlation_id":11,"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":9,"gfsim_object_id":0,"gfsim_root_sequence_id":11,"showcase_epoch_delta":0,"showcase_epoch_time":8},"cat":"transaction","name":"response","ph":"i","pid":0,"s":"t","tid":0,"ts":0} diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/expected-hierarchy.json b/components/agentic-circuit/examples/workspaces/request_response_memory/expected-hierarchy.json new file mode 100644 index 00000000..f4cf77e2 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/expected-hierarchy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","kind":"hierarchy","path":null,"records":[{"id":"e1","kind":"module","path":"top","schema":null},{"id":"e2","kind":"call","path":"top.runtime","schema":"workspace.Showcase"},{"id":"e3","kind":"process","path":"top.workload","schema":null}],"schema":"agentic-circuit-inspection","system":"main","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/expected-result.json b/components/agentic-circuit/examples/workspaces/request_response_memory/expected-result.json new file mode 100644 index 00000000..1d9a5ba5 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/expected-result.json @@ -0,0 +1 @@ +{"schema":"workspace-showcase-result","version":"0.1","status":"completed","termination_reason":"trace_drained","simulated_ticks":3,"trace_position":{"last_committed_sequence_id":1,"next_record_index":2},"architectural_values":{"completed_responses":2,"memory_0":0,"memory_1":17,"memory_2":0,"memory_3":29}} diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/expected-stats.json b/components/agentic-circuit/examples/workspaces/request_response_memory/expected-stats.json new file mode 100644 index 00000000..1b6679e3 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/expected-stats.json @@ -0,0 +1 @@ +[{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_completed_responses","object_path":"/generated/root-model/runtime","sum":0,"value":2},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_memory_0","object_path":"/generated/root-model/runtime","sum":0,"value":0},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_memory_1","object_path":"/generated/root-model/runtime","sum":0,"value":17},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_memory_2","object_path":"/generated/root-model/runtime","sum":0,"value":0},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_memory_3","object_path":"/generated/root-model/runtime","sum":0,"value":29},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"showcase_events","object_path":"/generated/root-model/runtime","sum":0,"value":10},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"trace_records","object_path":"/generated/root-model/runtime","sum":0,"value":2}] diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/protocols/.gitkeep b/components/agentic-circuit/examples/workspaces/request_response_memory/protocols/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/protocols/.gitkeep @@ -0,0 +1 @@ + diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/source.davincioo.jsonl b/components/agentic-circuit/examples/workspaces/request_response_memory/source.davincioo.jsonl new file mode 100644 index 00000000..d060f19a --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/source.davincioo.jsonl @@ -0,0 +1,2 @@ +{"block_idx":0,"sequence_id":0,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"0"}],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"outer=col_major,inner=row_major,fractal_size=512","dtype":"float32"}]} +{"block_idx":0,"sequence_id":1,"opcode":"TLOAD","input_tiles":[{"address":"0xfffd835fd000","shape":[1,1,1,64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"outer=col_major,inner=row_major,fractal_size=512","dtype":"float32"}]} diff --git a/components/agentic-circuit/examples/workspaces/request_response_memory/trace.pto.json b/components/agentic-circuit/examples/workspaces/request_response_memory/trace.pto.json new file mode 100644 index 00000000..0cc918ce --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/request_response_memory/trace.pto.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"content_hash":"sha256:4ad35c1b1106c2cae0b56e0c7233a019d85e3387653e6e886f3765e0a966f179","data_layout":"davincioo-tile-address-v1","producer":"davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb","pto_identity":"pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3","record_count":2,"source_program":"examples/beginner_matmul"},"records":[{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["scalar_input","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[{"dtype":"uint64","value":"0"}]}},"dependencies":[],"opcode":"TASSIGN","operands":[{"kind":"immediate","type":"uint64","value":"0"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":0},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0xfffd835fd000","dtype":"float32","layout":"ND","shape":[1,1,1,64,256]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TLOAD","operands":[{"id":"block/0/tile/0xfffd835fd000","kind":"tile"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":1}],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/agentic-circuit.toml b/components/agentic-circuit/examples/workspaces/suspended_process/agentic-circuit.toml new file mode 100644 index 00000000..82716d7b --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/agentic-circuit.toml @@ -0,0 +1,27 @@ +contract_epoch = "0.3" + +[project] +name = "workspace-suspended-process" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["."] +default_trace = "trace.pto.json" +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/architecture.py b/components/agentic-circuit/examples/workspaces/suspended_process/architecture.py new file mode 100644 index 00000000..12d17190 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/architecture.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from agentic_circuit import module, process, scope, system + + +@module +def top() -> None: + with scope("controller"): + Showcase(scenario=5, name="runtime") + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/components/showcase.binding.json b/components/agentic-circuit/examples/workspaces/suspended_process/components/showcase.binding.json new file mode 100644 index 00000000..0e97ee34 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/components/showcase.binding.json @@ -0,0 +1,142 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "unknown-unknown", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 5 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:a2791e95a8814f8319e29ce010e24ac1a4e090e560d0721a5cbcada111bf1d95", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 5 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + }, + { + "available": true, + "profile": "fast", + "target": "native", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "construction": { + "arguments": [ + 5 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::Component", + "entry_points": { + "pure": "", + "reset": "gfsim::workspace_reset", + "validate": "gfsim::workspace_validate", + "work": "gfsim::workspace_work", + "xfer": "gfsim::workspace_xfer" + }, + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "target": "gfsim" + }, + "cpp_type": "workspace_showcase_type", + "effect": "stateful", + "fingerprint": "sha256:a2791e95a8814f8319e29ce010e24ac1a4e090e560d0721a5cbcada111bf1d95", + "implementation": "gfsim.ShowcaseTraceSource", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::uint64_t", + "mapping": "constructor_constant", + "name": "scenario", + "ordinal": 0, + "value": 5 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resources": [], + "results": [] + } + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "workspace_Showcase_binding", + "binding_schema": "acsim-binding-0.1", + "component_schema": "workspace.Showcase", + "component_schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> ()", + "parameters": [ + { + "acir_type": "i64", + "name": "scenario", + "ordinal": 0, + "value": 5 + } + ], + "ports": [], + "provider": "workspace", + "provider_implementation_fingerprint": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "resolution_key": "@Showcase", + "resources": [], + "results": [] + } + ] +} diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/components/showcase.component.json b/components/agentic-circuit/examples/workspaces/suspended_process/components/showcase.component.json new file mode 100644 index 00000000..e57d3748 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/components/showcase.component.json @@ -0,0 +1,50 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "workspace.Showcase", + "family": "source", + "provider_namespace": "workspace.provider", + "stability": "experimental", + "cpp_binding": { + "header": "gfsim/showcase.h", + "symbol": "gfsim::ShowcaseTraceSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [{ + "name": "scenario", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "0 <= value < 6", + "cpp_mapping": "constructor_constant" + }], + "bindings": [], + "results": [], + "resources": [], + "address_behavior": null, + "protocol_contracts": [], + "effect": { + "kind": "stateful", + "requirements": ["one validated PTO trace document"], + "guarantees": ["one deterministic showcase execution"], + "observable_effects": ["architectural_values", "runtime_events"], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [], + "predicate": "loaded and not committed", + "wakeup_contract": [], + "quiescence": "showcase committed" + }, + "observation": { + "probes": [], + "counters": ["trace_records", "showcase_events"], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:3dd113d76227679688dc514e8bbe401a8225925cf592e362891a8857b6f2a270" +} diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/expected-events.jsonl b/components/agentic-circuit/examples/workspaces/suspended_process/expected-events.jsonl new file mode 100644 index 00000000..4be10f1f --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/expected-events.jsonl @@ -0,0 +1,3 @@ +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":0,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":0},"cat":"process","name":"suspended","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":1,"gfsim_object_id":0,"showcase_epoch_delta":0,"showcase_epoch_time":3},"cat":"process","name":"wake","ph":"i","pid":0,"s":"t","tid":0,"ts":0} +{"args":{"gfsim_epoch_delta":0,"gfsim_epoch_time":0,"gfsim_local_committed_index":2,"gfsim_object_id":0,"showcase_epoch_delta":1,"showcase_epoch_time":3},"cat":"process","name":"resumed","ph":"i","pid":0,"s":"t","tid":0,"ts":0} diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/expected-hierarchy.json b/components/agentic-circuit/examples/workspaces/suspended_process/expected-hierarchy.json new file mode 100644 index 00000000..f4cf77e2 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/expected-hierarchy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","kind":"hierarchy","path":null,"records":[{"id":"e1","kind":"module","path":"top","schema":null},{"id":"e2","kind":"call","path":"top.runtime","schema":"workspace.Showcase"},{"id":"e3","kind":"process","path":"top.workload","schema":null}],"schema":"agentic-circuit-inspection","system":"main","version":"0.1"} diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/expected-result.json b/components/agentic-circuit/examples/workspaces/suspended_process/expected-result.json new file mode 100644 index 00000000..6fb56bad --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/expected-result.json @@ -0,0 +1 @@ +{"schema":"workspace-showcase-result","version":"0.1","status":"completed","termination_reason":"trace_drained","simulated_ticks":3,"trace_position":{"last_committed_sequence_id":1,"next_record_index":2},"architectural_values":{"process_result":42,"resume_count":1,"wake_tick":3}} diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/expected-stats.json b/components/agentic-circuit/examples/workspaces/suspended_process/expected-stats.json new file mode 100644 index 00000000..64cda148 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/expected-stats.json @@ -0,0 +1 @@ +[{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_process_result","object_path":"/generated/root-model/runtime","sum":0,"value":42},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_resume_count","object_path":"/generated/root-model/runtime","sum":0,"value":1},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"architectural_wake_tick","object_path":"/generated/root-model/runtime","sum":0,"value":3},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"showcase_events","object_path":"/generated/root-model/runtime","sum":0,"value":3},{"buckets":[],"count":0,"kind":"counter","last_update":{"delta":0,"time":0},"maximum":0,"minimum":0,"name":"trace_records","object_path":"/generated/root-model/runtime","sum":0,"value":2}] diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/protocols/.gitkeep b/components/agentic-circuit/examples/workspaces/suspended_process/protocols/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/protocols/.gitkeep @@ -0,0 +1 @@ + diff --git a/components/agentic-circuit/examples/workspaces/suspended_process/trace.pto.json b/components/agentic-circuit/examples/workspaces/suspended_process/trace.pto.json new file mode 100644 index 00000000..0cc918ce --- /dev/null +++ b/components/agentic-circuit/examples/workspaces/suspended_process/trace.pto.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"content_hash":"sha256:4ad35c1b1106c2cae0b56e0c7233a019d85e3387653e6e886f3765e0a966f179","data_layout":"davincioo-tile-address-v1","producer":"davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb","pto_identity":"pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3","record_count":2,"source_program":"examples/beginner_matmul"},"records":[{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["scalar_input","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[{"dtype":"uint64","value":"0"}]}},"dependencies":[],"opcode":"TASSIGN","operands":[{"kind":"immediate","type":"uint64","value":"0"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":0},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0xfffd835fd000","dtype":"float32","layout":"ND","shape":[1,1,1,64,256]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TLOAD","operands":[{"id":"block/0/tile/0xfffd835fd000","kind":"tile"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":1}],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/include/CMakeLists.txt b/components/agentic-circuit/include/CMakeLists.txt new file mode 100644 index 00000000..4ee86ae6 --- /dev/null +++ b/components/agentic-circuit/include/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(acir) diff --git a/components/agentic-circuit/include/acir/Analysis/ModelAnalysis.h b/components/agentic-circuit/include/acir/Analysis/ModelAnalysis.h new file mode 100644 index 00000000..dd4fd4a8 --- /dev/null +++ b/components/agentic-circuit/include/acir/Analysis/ModelAnalysis.h @@ -0,0 +1,46 @@ +#ifndef ACIR_ANALYSIS_MODELANALYSIS_H +#define ACIR_ANALYSIS_MODELANALYSIS_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Support/LogicalResult.h" + +#include +namespace acir { + +/// Fixed capability limits keep hostile or malformed models diagnosable and +/// make the whole-model analyses independent of allocator failure behavior. +inline constexpr uint64_t kMaxModelAnalysisNodes = 1U << 20; +inline constexpr uint64_t kMaxModelAnalysisEdges = 1U << 22; +/// The top-level builtin.module has depth 0; each contained operation adds 1. +inline constexpr uint64_t kMaxModelRegionNesting = 512; +inline constexpr uint64_t kMaxPureCallFunctions = 1U << 16; +inline constexpr uint64_t kMaxPureCallEdges = 1U << 18; +inline constexpr uint64_t kMaxPureCallDepth = 1024; + +class ModelAnalysis { +public: + explicit ModelAnalysis(mlir::ModuleOp model) : model(model) {} + + /// Runs the complete IR-stage semantic closure. Existing operation + /// verifiers remain authoritative for local contracts; this analysis adds + /// deterministic whole-model selection, purity, dependency, and frozen + /// integrity checks. + mlir::LogicalResult verify(); + + /// Verifies module-level require/ensure conditions at topology freeze. + mlir::LogicalResult verifyFreezeContracts(); + +private: + mlir::LogicalResult verifyPureProcessCalls(); + mlir::LogicalResult verifyZeroDelayDependencies(); + mlir::LogicalResult verifyFrozenIntegrity(); + + mlir::ModuleOp model; +}; + +mlir::LogicalResult verifyModel(mlir::ModuleOp model); +bool isTopologyFrozen(mlir::ModuleOp model); + +} // namespace acir + +#endif // ACIR_ANALYSIS_MODELANALYSIS_H diff --git a/components/agentic-circuit/include/acir/Analysis/ProcessStatePlan.h b/components/agentic-circuit/include/acir/Analysis/ProcessStatePlan.h new file mode 100644 index 00000000..abc52b67 --- /dev/null +++ b/components/agentic-circuit/include/acir/Analysis/ProcessStatePlan.h @@ -0,0 +1,719 @@ +#ifndef ACIR_ANALYSIS_PROCESSSTATEPLAN_H +#define ACIR_ANALYSIS_PROCESSSTATEPLAN_H + +#include "acir/Dialect/ACIR/ACIROps.h" + +#include "mlir/IR/Block.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Types.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" + +#include +#include +#include +#include +#include +#include + +namespace acir { +namespace detail { +class PlanSetBuilder; +} + +struct ProcessStateLimits { + uint64_t maxProcesses = 1U << 20; + uint64_t maxProgramCounters = 1U << 20; + uint64_t maxLiveSlots = 1U << 20; + uint64_t maxWakeRecords = 1U << 20; + uint64_t maxCalleeDescriptors = 1U << 20; + uint64_t maxPlannedOperations = 1U << 20; + uint64_t maxFairnessWork = 1U << 20; + uint64_t maxTransitions = 1U << 22; + uint64_t maxNestedRegionDepth = 512; + uint64_t maxCanonicalReportBytes = 1U << 24; +}; + +#define ACIR_DECLARE_PROCESS_ID(Name) \ + class Name { \ + public: \ + uint32_t value() const { return value_; } \ + auto operator<=>(const Name &) const = default; \ + \ + private: \ + explicit Name(uint32_t value) : value_(value) {} \ + uint32_t value_; \ + friend class detail::PlanSetBuilder; \ + } + +ACIR_DECLARE_PROCESS_ID(ProcessCalleeId); +ACIR_DECLARE_PROCESS_ID(ProcessValueTypeId); +ACIR_DECLARE_PROCESS_ID(ProcessCaptureId); +ACIR_DECLARE_PROCESS_ID(ProcessPcId); +ACIR_DECLARE_PROCESS_ID(ProcessBlockId); +ACIR_DECLARE_PROCESS_ID(ProcessLiveSlotId); +ACIR_DECLARE_PROCESS_ID(ProcessWakeId); +ACIR_DECLARE_PROCESS_ID(ProcessTransitionId); +#undef ACIR_DECLARE_PROCESS_ID + +enum class ProcessWakeKind { Condition, Resource, EventQueue, NextDelta }; +enum class ProcessSubscriptionSourceKind { Capture, Value, Symbol }; +enum class ProcessActionKind { + Original, + Constant, + ForInitialize, + ForCondition, + ForIncrement, + ScalarWrap, + ScalarUnwrap +}; +enum class ProcessEmissionClass { + CopyScalar, + Inline, + Invoke, + Wrap, + Unwrap, + ForwardOnly +}; +enum class ProcessOccurrenceKind { + Original, + SyntheticLoop, + SyntheticWrapper, + SyntheticConstant +}; +enum class ProcessLoopPhase { Initialize, Condition, Increment }; +enum class ProcessWrapperDirection { Wrap, Unwrap }; +enum class ProcessFrameKind { Entry, ScfIf, ScfFor, ScfWhile }; +enum class ProcessFramePhase { + Entry, + Then, + Else, + Merge, + Header, + Body, + Before, + After, + Exit +}; +enum class ProcessPlannedValueKind { + Original, + Capture, + LiveSlot, + Synthetic, + Constant +}; +enum class ProcessValueCoordinateKind { Result, BlockArgument }; +enum class ProcessControlEdgeKind { Branch, LocalContinue, Suspend, Terminate }; +enum class ProcessTerminateStatus { Success, Failure }; +enum class ProcessEffectKind { Pure, Stateful }; +enum class ProcessValueTypeKind { Value, Packet }; +enum class ProcessHelperRole { + RecordCreate, + RecordGet, + RecordWith, + PacketSerialize, + PacketDeserialize, + TraceDecode, + QueueTrySend, + QueueTryRecv, + EventSchedule, + TraceOpen, + TraceNext, + TraceEof, + TracePosition, + ContractRequire, + ContractEnsure, + ContractAssert, + Probe, + StatAdd, + WakeCondition, + WakeResource, + WakeEventQueue, + WakeNextDelta, + ScalarWrap, + ScalarUnwrap +}; +enum class ProcessValueTypeMemberKind { Field, Element }; +enum class ProcessStorageSignedness { Signless, Signed, Unsigned }; + +#define ACIR_PROCESS_PIMPL(Name) \ + struct Impl; \ + explicit Name(std::shared_ptr impl) : impl_(std::move(impl)) {} \ + std::shared_ptr impl_; \ + friend class detail::PlanSetBuilder + +class ProcessCallSitePlan { +public: + mlir::Operation *operation() const; + llvm::StringRef operationPath() const; + llvm::ArrayRef iterationVector() const; + +private: + ACIR_PROCESS_PIMPL(ProcessCallSitePlan); +}; + +class ProcessOccurrenceId; +class ProcessOriginalOccurrence { +public: + mlir::Operation *operation() const; + llvm::StringRef operationPath() const; + llvm::ArrayRef callSites() const; + llvm::ArrayRef iterationVector() const; + +private: + ACIR_PROCESS_PIMPL(ProcessOriginalOccurrence); +}; +class ProcessSyntheticLoopOccurrence { +public: + const ProcessOccurrenceId &anchor() const; + ProcessLoopPhase phase() const; + +private: + ACIR_PROCESS_PIMPL(ProcessSyntheticLoopOccurrence); +}; +class ProcessSyntheticWrapperOccurrence { +public: + const ProcessOccurrenceId &anchor() const; + ProcessTransitionId transition() const; + ProcessLiveSlotId slot() const; + ProcessWrapperDirection direction() const; + +private: + ACIR_PROCESS_PIMPL(ProcessSyntheticWrapperOccurrence); +}; +class ProcessSyntheticConstantOccurrence { +public: + const ProcessOccurrenceId &anchor() const; + uint32_t constant() const; + +private: + ACIR_PROCESS_PIMPL(ProcessSyntheticConstantOccurrence); +}; +class ProcessOccurrenceId { +public: + ProcessOccurrenceKind kind() const; + const ProcessOriginalOccurrence &original() const; + const ProcessSyntheticLoopOccurrence &syntheticLoop() const; + const ProcessSyntheticWrapperOccurrence &syntheticWrapper() const; + const ProcessSyntheticConstantOccurrence &syntheticConstant() const; + +private: + ACIR_PROCESS_PIMPL(ProcessOccurrenceId); +}; + +class ProcessValueCoordinate { +public: + ProcessValueCoordinateKind kind() const; + llvm::StringRef ownerPath() const; + uint32_t index() const; + +private: + ACIR_PROCESS_PIMPL(ProcessValueCoordinate); +}; +class ProcessOriginalPlannedValue { +public: + mlir::Value value() const; + const ProcessOccurrenceId &occurrence() const; + const ProcessValueCoordinate &coordinate() const; + llvm::StringRef path() const; + +private: + ACIR_PROCESS_PIMPL(ProcessOriginalPlannedValue); +}; +class ProcessCapturePlannedValue { +public: + ProcessCaptureId capture() const; + +private: + ACIR_PROCESS_PIMPL(ProcessCapturePlannedValue); +}; +class ProcessLiveSlotPlannedValue { +public: + ProcessLiveSlotId slot() const; + +private: + ACIR_PROCESS_PIMPL(ProcessLiveSlotPlannedValue); +}; +class ProcessSyntheticPlannedValue { +public: + const ProcessOccurrenceId &occurrence() const; + const ProcessValueCoordinate &coordinate() const; + +private: + ACIR_PROCESS_PIMPL(ProcessSyntheticPlannedValue); +}; +class ProcessConstantPlannedValue { +public: + llvm::StringRef value() const; + +private: + ACIR_PROCESS_PIMPL(ProcessConstantPlannedValue); +}; +class ProcessPlannedValue { +public: + ProcessPlannedValueKind kind() const; + mlir::Type type() const; + const ProcessOriginalPlannedValue &original() const; + const ProcessCapturePlannedValue &capture() const; + const ProcessLiveSlotPlannedValue &liveSlot() const; + const ProcessSyntheticPlannedValue &synthetic() const; + const ProcessConstantPlannedValue &constant() const; + +private: + ACIR_PROCESS_PIMPL(ProcessPlannedValue); +}; + +class ProcessScalarAttribute { +public: + llvm::StringRef name() const; + llvm::StringRef value() const; + +private: + ACIR_PROCESS_PIMPL(ProcessScalarAttribute); +}; +class ProcessScalarOperationPlan { +public: + llvm::StringRef name() const; + llvm::ArrayRef attributes() const; + llvm::StringRef properties() const; + +private: + ACIR_PROCESS_PIMPL(ProcessScalarOperationPlan); +}; + +class ProcessCapturePlan { +public: + ProcessCaptureId id() const; + llvm::StringRef name() const; + mlir::Value operand() const; + mlir::Value entryArgument() const; + mlir::Type type() const; + llvm::StringRef operandPath() const; + llvm::StringRef argumentPath() const; + +private: + ACIR_PROCESS_PIMPL(ProcessCapturePlan); +}; +class ProcessActionPlan { +public: + uint32_t id() const; + ProcessActionKind kind() const; + ProcessEmissionClass emission() const; + const ProcessOccurrenceId &occurrence() const; + mlir::Operation *sourceOperation() const; + llvm::ArrayRef iterationVector() const; + llvm::ArrayRef operands() const; + llvm::ArrayRef results() const; + uint32_t cost() const; + llvm::ArrayRef resultTypes() const; + std::optional callee() const; + const ProcessScalarOperationPlan *scalarOp() const; + +private: + ACIR_PROCESS_PIMPL(ProcessActionPlan); +}; +class ProcessLiveSlotPlan { +public: + ProcessLiveSlotId id() const; + llvm::StringRef name() const; + mlir::Type type() const; + ProcessValueTypeId storageType() const; + llvm::ArrayRef memberValues() const; + std::optional wrapCallee() const; + std::optional unwrapCallee() const; + +private: + ACIR_PROCESS_PIMPL(ProcessLiveSlotPlan); +}; +class ProcessSubscriptionSourcePlan { +public: + ProcessSubscriptionSourceKind kind() const; + mlir::Value value() const; + mlir::Operation *owner() const; + mlir::Operation *declaration() const; + std::optional capture() const; + llvm::StringRef symbol() const; + llvm::StringRef path() const; + llvm::StringRef ownerPath() const; + +private: + ACIR_PROCESS_PIMPL(ProcessSubscriptionSourcePlan); +}; +class ProcessWakePlan { +public: + ProcessWakeId id() const; + ProcessWakeKind kind() const; + mlir::Operation *operation() const; + mlir::Value triggeringValue() const; + mlir::Operation *declaration() const; + ProcessCalleeId callee() const; + llvm::StringRef typeKey() const; + llvm::StringRef operationPath() const; + llvm::StringRef target() const; + const ProcessOccurrenceId &occurrence() const; + llvm::ArrayRef iterationVector() const; + llvm::ArrayRef sources() const; + +private: + ACIR_PROCESS_PIMPL(ProcessWakePlan); +}; +class ProcessTransitionStorePlan { +public: + ProcessLiveSlotId slot() const; + const ProcessPlannedValue &source() const; + mlir::Value sourceValue() const; + +private: + ACIR_PROCESS_PIMPL(ProcessTransitionStorePlan); +}; +class ProcessTransitionLoadPlan { +public: + ProcessLiveSlotId slot() const; + llvm::ArrayRef replacements() const; + +private: + ACIR_PROCESS_PIMPL(ProcessTransitionLoadPlan); +}; +class ProcessTransitionPlan { +public: + ProcessTransitionId id() const; + ProcessPcId sourcePc() const; + ProcessPcId targetPc() const; + ProcessWakeId wake() const; + llvm::ArrayRef iterationVector() const; + llvm::ArrayRef stores() const; + llvm::ArrayRef loads() const; + +private: + ACIR_PROCESS_PIMPL(ProcessTransitionPlan); +}; +class ProcessForwardingBindingPlan { +public: + const ProcessPlannedValue &from() const; + const ProcessPlannedValue &to() const; + +private: + ACIR_PROCESS_PIMPL(ProcessForwardingBindingPlan); +}; +class ProcessControlFramePlan { +public: + ProcessFrameKind kind() const; + ProcessFramePhase phase() const; + mlir::Operation *operation() const; + llvm::StringRef operationPath() const; + llvm::ArrayRef bindings() const; + +private: + ACIR_PROCESS_PIMPL(ProcessControlFramePlan); +}; +class ProcessControlEdgePlan { +public: + ProcessControlEdgeKind kind() const; + const ProcessPlannedValue &condition() const; + ProcessBlockId trueBlock() const; + ProcessBlockId falseBlock() const; + llvm::ArrayRef trueBindings() const; + llvm::ArrayRef falseBindings() const; + ProcessBlockId targetBlock() const; + llvm::ArrayRef bindings() const; + ProcessTransitionId transition() const; + ProcessTerminateStatus status() const; + +private: + ACIR_PROCESS_PIMPL(ProcessControlEdgePlan); +}; +class ProcessBlockPlan { +public: + ProcessBlockId id() const; + ProcessPcId pc() const; + mlir::Region *originRegion() const; + mlir::Block *originBlock() const; + llvm::StringRef path() const; + llvm::ArrayRef frames() const; + llvm::ArrayRef loads() const; + llvm::ArrayRef actions() const; + const ProcessControlEdgePlan &edge() const; + uint64_t cost() const; + +private: + ACIR_PROCESS_PIMPL(ProcessBlockPlan); +}; +class ProcessPcPlan { +public: + ProcessPcId id() const; + llvm::StringRef name() const; + llvm::StringRef entryPath() const; + llvm::ArrayRef blocks() const; + +private: + ACIR_PROCESS_PIMPL(ProcessPcPlan); +}; +class ProcessStatePlan { +public: + llvm::StringRef definitionKey() const; + ac::ProcessOp process() const; + llvm::ArrayRef captures() const; + ProcessPcId entryPc() const; + llvm::ArrayRef pcs() const; + llvm::ArrayRef blocks() const; + llvm::ArrayRef liveSlots() const; + llvm::ArrayRef wakes() const; + llvm::ArrayRef transitions() const; + uint32_t pcBitWidth() const; + uint64_t fairnessWork() const; + +private: + ACIR_PROCESS_PIMPL(ProcessStatePlan); +}; + +class ProcessRecordFieldDescriptor { +public: + llvm::StringRef name() const; + llvm::StringRef typeKey() const; + +private: + ACIR_PROCESS_PIMPL(ProcessRecordFieldDescriptor); +}; +#define ACIR_DECLARE_STRING_PAYLOAD(Name, ...) \ + class Name { \ + public: \ + __VA_ARGS__ private : ACIR_PROCESS_PIMPL(Name); \ + } +class ProcessRecordCreatePayload { +public: + llvm::ArrayRef fields() const; + llvm::StringRef recordType() const; + +private: + ACIR_PROCESS_PIMPL(ProcessRecordCreatePayload); +}; +ACIR_DECLARE_STRING_PAYLOAD(ProcessRecordGetPayload, + llvm::StringRef field() const; + llvm::StringRef record() const; + llvm::StringRef result() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessRecordWithPayload, + llvm::StringRef field() const; + llvm::StringRef record() const; + llvm::StringRef value() const;); +class ProcessPacketSerializePayload { +public: + uint64_t bytes() const; + llvm::StringRef packet() const; + llvm::StringRef packetType() const; + +private: + ACIR_PROCESS_PIMPL(ProcessPacketSerializePayload); +}; +class ProcessPacketDeserializePayload { +public: + uint64_t bytes() const; + llvm::StringRef packet() const; + llvm::StringRef packetType() const; + +private: + ACIR_PROCESS_PIMPL(ProcessPacketDeserializePayload); +}; +ACIR_DECLARE_STRING_PAYLOAD(ProcessTraceDecodePayload, + llvm::StringRef entry() const; + llvm::StringRef result() const; + llvm::StringRef source() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessQueueTrySendPayload, + llvm::StringRef element() const; + llvm::StringRef queue() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessQueueTryRecvPayload, + llvm::StringRef element() const; + llvm::StringRef queue() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessEventSchedulePayload, + llvm::StringRef delay() const; + llvm::StringRef target() const; + llvm::StringRef value() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessTraceOpenPayload, + llvm::StringRef source() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessTraceNextPayload, + llvm::StringRef entry() const; + llvm::StringRef source() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessTraceEofPayload, + llvm::StringRef source() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessTracePositionPayload, + llvm::StringRef source() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessContractRequirePayload, + llvm::StringRef message() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessContractEnsurePayload, + llvm::StringRef message() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessContractAssertPayload, + llvm::StringRef message() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessProbePayload, llvm::StringRef kind() const; + llvm::StringRef result() const; + llvm::StringRef target() const;); +ACIR_DECLARE_STRING_PAYLOAD(ProcessStatAddPayload, llvm::StringRef stat() const; + llvm::StringRef valueType() const;); +#undef ACIR_DECLARE_STRING_PAYLOAD +#define ACIR_DECLARE_WAKE_PAYLOAD(Name) \ + class Name { \ + public: \ + ProcessWakeKind wakeKind() const; \ + llvm::StringRef wakeType() const; \ + \ + private: \ + ACIR_PROCESS_PIMPL(Name); \ + } +ACIR_DECLARE_WAKE_PAYLOAD(ProcessWakeConditionPayload); +ACIR_DECLARE_WAKE_PAYLOAD(ProcessWakeResourcePayload); +ACIR_DECLARE_WAKE_PAYLOAD(ProcessWakeEventQueuePayload); +ACIR_DECLARE_WAKE_PAYLOAD(ProcessWakeNextDeltaPayload); +#undef ACIR_DECLARE_WAKE_PAYLOAD +class ProcessScalarWrapPayload { +public: + ProcessWrapperDirection direction() const; + llvm::StringRef scalar() const; + llvm::StringRef valueType() const; + +private: + ACIR_PROCESS_PIMPL(ProcessScalarWrapPayload); +}; +class ProcessScalarUnwrapPayload { +public: + ProcessWrapperDirection direction() const; + llvm::StringRef scalar() const; + llvm::StringRef valueType() const; + +private: + ACIR_PROCESS_PIMPL(ProcessScalarUnwrapPayload); +}; + +class ProcessGeneratedCalleePayload { +public: + ProcessHelperRole role() const; + const ProcessRecordCreatePayload &recordCreate() const; + const ProcessRecordGetPayload &recordGet() const; + const ProcessRecordWithPayload &recordWith() const; + const ProcessPacketSerializePayload &packetSerialize() const; + const ProcessPacketDeserializePayload &packetDeserialize() const; + const ProcessTraceDecodePayload &traceDecode() const; + const ProcessQueueTrySendPayload &queueTrySend() const; + const ProcessQueueTryRecvPayload &queueTryRecv() const; + const ProcessEventSchedulePayload &eventSchedule() const; + const ProcessTraceOpenPayload &traceOpen() const; + const ProcessTraceNextPayload &traceNext() const; + const ProcessTraceEofPayload &traceEof() const; + const ProcessTracePositionPayload &tracePosition() const; + const ProcessContractRequirePayload &contractRequire() const; + const ProcessContractEnsurePayload &contractEnsure() const; + const ProcessContractAssertPayload &contractAssert() const; + const ProcessProbePayload &probe() const; + const ProcessStatAddPayload &statAdd() const; + const ProcessWakeConditionPayload &wakeCondition() const; + const ProcessWakeResourcePayload &wakeResource() const; + const ProcessWakeEventQueuePayload &wakeEventQueue() const; + const ProcessWakeNextDeltaPayload &wakeNextDelta() const; + const ProcessScalarWrapPayload &scalarWrap() const; + const ProcessScalarUnwrapPayload &scalarUnwrap() const; + +private: + ACIR_PROCESS_PIMPL(ProcessGeneratedCalleePayload); +}; + +class ProcessValueTypeMemberPlan { +public: + ProcessValueTypeMemberKind kind() const; + llvm::StringRef name() const; + std::optional index() const; + uint64_t offsetBits() const; + uint64_t widthBits() const; + std::optional signedness() const; + llvm::StringRef encoding() const; + llvm::StringRef typeKey() const; + +private: + ACIR_PROCESS_PIMPL(ProcessValueTypeMemberPlan); +}; +class ProcessStorageValuePayload { +public: + llvm::ArrayRef members() const; + uint64_t widthBits() const; + llvm::StringRef encoding() const; + +private: + ACIR_PROCESS_PIMPL(ProcessStorageValuePayload); +}; +class ProcessStoragePacketPayload { +public: + llvm::ArrayRef members() const; + uint64_t widthBits() const; + uint64_t bytes() const; + llvm::StringRef encoding() const; + +private: + ACIR_PROCESS_PIMPL(ProcessStoragePacketPayload); +}; +class ProcessValueTypePayload { +public: + ProcessValueTypeKind kind() const; + const ProcessStorageValuePayload &value() const; + const ProcessStoragePacketPayload &packet() const; + +private: + ACIR_PROCESS_PIMPL(ProcessValueTypePayload); +}; +class ProcessGeneratedCalleePlan { +public: + ProcessCalleeId id() const; + llvm::StringRef symbol() const; + llvm::StringRef cpp() const; + llvm::StringRef kind() const; + llvm::StringRef fingerprint() const; + ProcessEffectKind effect() const; + llvm::ArrayRef inputTypeKeys() const; + llvm::ArrayRef resultTypeKeys() const; + ProcessHelperRole role() const; + const ProcessGeneratedCalleePayload &payload() const; + llvm::ArrayRef sourceOperations() const; + llvm::ArrayRef declarations() const; + llvm::ArrayRef sourcePaths() const; + +private: + ACIR_PROCESS_PIMPL(ProcessGeneratedCalleePlan); +}; +class ProcessValueTypePlan { +public: + ProcessValueTypeId id() const; + llvm::StringRef symbol() const; + llvm::StringRef cpp() const; + ProcessValueTypeKind kind() const; + llvm::StringRef fingerprint() const; + mlir::Type acirType() const; + const ProcessValueTypePayload &payload() const; + +private: + ACIR_PROCESS_PIMPL(ProcessValueTypePlan); +}; +class ProcessStatePlanSet { +public: + llvm::ArrayRef processes() const; + llvm::ArrayRef callees() const; + llvm::ArrayRef valueTypes() const; + const ProcessStatePlan * + lookupByDefinitionKey(llvm::StringRef definitionKey) const; + +private: + ACIR_PROCESS_PIMPL(ProcessStatePlanSet); +}; + +#undef ACIR_PROCESS_PIMPL + +mlir::LogicalResult +verifyProcessStatePlan(const ProcessStatePlanSet &plans, + const ProcessStateLimits &limits = ProcessStateLimits()); +mlir::FailureOr +planProcessState(mlir::ModuleOp module, + const ProcessStateLimits &limits = ProcessStateLimits()); +llvm::Expected serializeProcessStatePlan( + const ProcessStatePlanSet &plans, + const ProcessStateLimits &limits = ProcessStateLimits()); + +} // namespace acir + +#endif diff --git a/components/agentic-circuit/include/acir/Bindings/Binding.h b/components/agentic-circuit/include/acir/Bindings/Binding.h new file mode 100644 index 00000000..a7caeb70 --- /dev/null +++ b/components/agentic-circuit/include/acir/Bindings/Binding.h @@ -0,0 +1,184 @@ +#ifndef ACIR_BINDINGS_BINDING_H +#define ACIR_BINDINGS_BINDING_H + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" + +#include +#include +#include +#include +#include + +namespace acir::bindings { + +inline constexpr llvm::StringLiteral BindingSchema = "acsim-binding-0.1"; +inline constexpr llvm::StringLiteral ContractEpoch = "0.3"; + +struct JsonParseLimits { + size_t maxInputBytes = 1U << 20; + size_t maxDepth = 64; + size_t maxStructuralWork = 100000; + size_t maxStringBytes = 1U << 18; + size_t maxTotalStringBytes = 1U << 20; + size_t maxArrayElements = 65536; + size_t maxObjectMembers = 4096; +}; + +/// Parses the accepted RFC 8785/I-JSON subset while retaining lexical checks +/// that ordinary JSON DOM parsers lose (duplicate keys and negative zero). +llvm::Expected +parseIJson(llvm::StringRef input, + const JsonParseLimits &limits = JsonParseLimits()); + +/// Emits RFC 8785 canonical UTF-8 bytes. Object names are ordered by unsigned +/// UTF-16 code units recursively; arrays retain their input order. Constructed +/// DOMs are preflighted against the same deterministic resource limits as text. +llvm::Expected +canonicalizeJson(const llvm::json::Value &value, + const JsonParseLimits &limits = JsonParseLimits()); +llvm::Expected +canonicalizeJsonText(llvm::StringRef input, + const JsonParseLimits &limits = JsonParseLimits()); + +std::string sha256Fingerprint(llvm::StringRef canonicalBytes); + +struct CppEntryPoints { + std::string pure; + std::string reset; + std::string validate; + std::string work; + std::string xfer; + + bool operator==(const CppEntryPoints &) const = default; +}; + +struct CppBinding { + std::string conceptName; + CppEntryPoints entryPoints; + std::string header; + std::string symbol; + std::string target; + + bool operator==(const CppBinding &) const = default; +}; + +struct ConstructionBinding { + std::vector arguments; + std::string kind; + + bool operator==(const ConstructionBinding &) const = default; +}; + +struct OwnershipBinding { + std::string kind; + std::string placement; + + bool operator==(const OwnershipBinding &) const = default; +}; + +struct ParameterBinding { + std::string acirType; + std::string cppType; + std::string mapping; + std::string name; + int64_t ordinal = 0; + llvm::json::Value value = nullptr; + + bool operator==(const ParameterBinding &) const = default; +}; + +struct PortBinding { + std::string accessor; + std::string cardinality; + std::string delegation; + std::string direction; + std::string interface; + std::string ownership; + std::string payload; + std::string protocol; + std::string role; + std::string timeDomain; + + bool operator==(const PortBinding &) const = default; +}; + +struct ResourceBinding { + std::string accessor; + std::string delegation; + std::string mode; + std::string ownership; + std::string resource; + std::string role; + std::string timeDomain; + + bool operator==(const ResourceBinding &) const = default; +}; + +struct ResultBinding { + std::string cppType; + std::string name; + + bool operator==(const ResultBinding &) const = default; +}; + +struct ActivationSourceBinding { + std::string kind; + std::string name; + + bool operator==(const ActivationSourceBinding &) const = default; +}; + +/// Immutable, typed form of the exact 20-field acsim.binding consumer record. +class BindingRecord { +public: + BindingRecord(const BindingRecord &) = default; + BindingRecord(BindingRecord &&) noexcept = default; + BindingRecord &operator=(const BindingRecord &) = default; + BindingRecord &operator=(BindingRecord &&) noexcept = default; + ~BindingRecord(); + + static llvm::Expected + parse(const llvm::json::Object &object, + const JsonParseLimits &limits = JsonParseLimits()); + + llvm::StringRef bindingSchema() const; + llvm::StringRef contractEpoch() const; + llvm::StringRef binding() const; + llvm::StringRef componentSchema() const; + llvm::StringRef componentSchemaFingerprint() const; + llvm::StringRef availability() const; + llvm::StringRef effect() const; + llvm::StringRef cppType() const; + llvm::StringRef implementation() const; + llvm::StringRef provider() const; + llvm::StringRef providerImplementationFingerprint() const; + llvm::StringRef fingerprint() const; + const CppBinding &cpp() const; + const ConstructionBinding &construction() const; + const OwnershipBinding &ownership() const; + llvm::ArrayRef parameters() const; + llvm::ArrayRef ports() const; + llvm::ArrayRef resources() const; + llvm::ArrayRef results() const; + llvm::ArrayRef activationSources() const; + const llvm::json::Object &json() const; + + llvm::Expected canonicalJson() const; + llvm::Expected canonicalJsonForFingerprint() const; + llvm::Error validateFingerprint() const; + +private: + struct Storage; + explicit BindingRecord(std::shared_ptr storage); + std::shared_ptr storage; +}; + +llvm::Expected +computeBindingRecordFingerprint(const BindingRecord &record); + +} // namespace acir::bindings + +#endif // ACIR_BINDINGS_BINDING_H diff --git a/components/agentic-circuit/include/acir/Bindings/Registry.h b/components/agentic-circuit/include/acir/Bindings/Registry.h new file mode 100644 index 00000000..0c0bcee5 --- /dev/null +++ b/components/agentic-circuit/include/acir/Bindings/Registry.h @@ -0,0 +1,179 @@ +#ifndef ACIR_BINDINGS_REGISTRY_H +#define ACIR_BINDINGS_REGISTRY_H + +#include "acir/Bindings/Binding.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include + +namespace acir::bindings { + +/// Candidate-only selection metadata is intentionally outside the exact lock +/// record and is never emitted into acsim.binding. +class BindingCandidate { +public: + BindingCandidate(const BindingCandidate &) = default; + BindingCandidate(BindingCandidate &&) noexcept = default; + BindingCandidate &operator=(const BindingCandidate &) = default; + BindingCandidate &operator=(BindingCandidate &&) noexcept = default; + ~BindingCandidate(); + + static llvm::Expected + parse(const llvm::json::Object &object, + const JsonParseLimits &limits = JsonParseLimits()); + + llvm::StringRef profile() const; + llvm::StringRef target() const; + bool available() const; + const BindingRecord &record() const; + llvm::Expected deterministicKey() const; + +private: + struct Storage; + explicit BindingCandidate(std::shared_ptr storage); + std::shared_ptr storage; +}; + +struct ParameterRequirement { + std::string acirType; + std::string name; + int64_t ordinal = 0; + llvm::json::Value value = nullptr; + + bool operator==(const ParameterRequirement &) const = default; +}; + +struct PortRequirement { + std::string cardinality; + std::string delegation; + std::string direction; + std::string interface; + std::string ownership; + std::string payload; + std::string protocol; + std::string role; + std::string timeDomain; + + bool operator==(const PortRequirement &) const = default; +}; + +struct ResourceRequirement { + std::string delegation; + std::string mode; + std::string ownership; + std::string resource; + std::string role; + std::string timeDomain; + + bool operator==(const ResourceRequirement &) const = default; +}; + +struct ResultRequirement { + std::string acirType; + std::string name; + + bool operator==(const ResultRequirement &) const = default; +}; + +struct BindingRequest { + /// Independent frozen-architecture authority. Candidate-only C++ realization + /// fields (accessors, mappings, construction, symbols, and entry points) are + /// intentionally absent and must never be synthesized into this request. + std::string resolutionKey; + std::string functionType; + std::string bindingSchema; + std::string contractEpoch; + std::string binding; + std::string componentSchema; + std::string componentSchemaFingerprint; + std::string effect; + std::string provider; + std::string providerImplementationFingerprint; + std::vector parameters; + std::vector ports; + std::vector resources; + std::vector results; + std::vector activationSources; +}; + +struct BindingRegistryDocument { + /// The two arrays have distinct provenance: candidates cannot author or + /// supply defaults for requests. + std::vector candidates; + std::vector requests; +}; + +llvm::Expected +parseBindingRegistry(llvm::StringRef input, + const JsonParseLimits &limits = JsonParseLimits()); + +class ResolvedBinding { +public: + ResolvedBinding(std::string resolutionKey, BindingCandidate candidate); + + llvm::StringRef resolutionKey() const { return key; } + const BindingRecord &record() const { return selected.record(); } + const BindingCandidate &candidate() const { return selected; } + +private: + std::string key; + BindingCandidate selected; +}; + +class BindingRegistry { +public: + static llvm::Expected + create(std::vector candidates); + + llvm::Expected resolve(const BindingRequest &request, + llvm::StringRef profile, + llvm::StringRef target) const; + llvm::ArrayRef candidates() const; + +private: + explicit BindingRegistry(std::vector candidates); + std::vector orderedCandidates; +}; + +class BindingResolutionResult { +public: + BindingResolutionResult(std::vector selections, + std::string canonicalLock, + std::string lockFingerprint); + + llvm::ArrayRef selections() const { return selected; } + const ResolvedBinding * + selectionForResolutionKey(llvm::StringRef resolutionKey) const; + llvm::StringRef canonicalLock() const { return lock; } + llvm::StringRef lockFingerprint() const { return fingerprint; } + +private: + std::vector selected; + std::string lock; + std::string fingerprint; +}; + +llvm::Expected +resolveBindings(llvm::ArrayRef candidates, + llvm::ArrayRef requests, + llvm::StringRef profile, llvm::StringRef target); + +llvm::Error emitBindingLock(const BindingResolutionResult &result, + llvm::raw_ostream &output); +llvm::Error emitBindingLockAtomically(const BindingResolutionResult &result, + llvm::StringRef outputPath); +llvm::Error +resolveAndWriteBindingLock(llvm::ArrayRef candidates, + llvm::ArrayRef requests, + llvm::StringRef profile, llvm::StringRef target, + llvm::StringRef outputPath); + +} // namespace acir::bindings + +#endif // ACIR_BINDINGS_REGISTRY_H diff --git a/components/agentic-circuit/include/acir/CMakeLists.txt b/components/agentic-circuit/include/acir/CMakeLists.txt new file mode 100644 index 00000000..557daa84 --- /dev/null +++ b/components/agentic-circuit/include/acir/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(Dialect) +add_subdirectory(Transforms) diff --git a/components/agentic-circuit/include/acir/CodeGen/Build.h b/components/agentic-circuit/include/acir/CodeGen/Build.h new file mode 100644 index 00000000..0821b2b8 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/Build.h @@ -0,0 +1,131 @@ +#ifndef ACIR_CODEGEN_BUILD_H +#define ACIR_CODEGEN_BUILD_H + +#include "acir/CodeGen/Generator.h" + +#include "mlir/IR/BuiltinOps.h" +#include "llvm/Support/Error.h" + +#include +#include + +namespace acir::codegen { + +struct ToolchainIdentity { + std::string compilerPath; + std::string compilerName; + std::string compilerBuildId; + std::string targetTriple; + std::string standardLibrary; + std::string abiMode; + std::string objectFormat; + std::vector contractFlags; + Fingerprint fingerprint; +}; + +struct PrebuiltProvenance { + std::string compilerBuildId; + std::string targetTriple; + std::string standardLibrary; + std::string abiMode; + std::string objectFormat; + std::string contractEpoch = "0.3"; + std::vector contractFlags; + Fingerprint toolchainFingerprint; + Fingerprint sourceFingerprint; +}; + +struct PrebuiltInput { + std::string path; + std::string kind; + PrebuiltProvenance provenance; + bool sourceAvailable = false; +}; + +struct CompileCommand { + std::vector arguments; + std::string output; +}; + +struct LinkInputIdentity { + std::string path; + Fingerprint fingerprint; +}; + +struct FrontendProvenance { + // Empty only for the internal canonical-ACSim driver. Frontend-originated + // builds populate this record completely and are validated atomically. + std::vector sourceFiles; + Artifact acpy; + std::string acpyBytes; + Artifact canonicalAcir; + std::string canonicalAcirBytes; + std::string pythonVersion; + std::vector helperIdentities; +}; + +struct CompilePlan { + std::string schema = "acsim-compile-plan-0.1"; + std::vector sourceUnits; + std::vector objectOutputs; + std::vector includeRoots; + std::vector definitions; + std::vector compilerFlags; + std::vector linkerFlags; + std::vector linkInputs; + std::vector prebuiltInputs; + std::vector compileCommands; + CompileCommand linkCommand; + std::string executablePath; + Fingerprint sourceFingerprint; + Fingerprint toolchainFingerprint; + Fingerprint fingerprint; + + llvm::Error validate() const; + llvm::Expected canonicalJson() const; +}; + +struct BuildRequest { + Identity project; + Identity system; + FrontendProvenance frontend; + mlir::ModuleOp canonicalACSim; + std::string frozenAcirBytes; + std::string canonicalACSimBytes; + std::string bindingLockBytes; + std::string profile; + std::vector passPipeline; + std::vector instrumentationLayers; + std::vector providerInputs; + ToolchainIdentity toolchain; + std::vector prebuiltInputs; + std::vector includeRoots; + std::vector definitions; + std::vector compilerFlags; + std::vector linkerFlags; + std::vector linkInputs; + std::string outputRoot; +}; + +struct BuildResult { + std::string buildDirectory; + std::string executable; + Fingerprint buildFingerprint; + bool cacheHit = false; +}; + +llvm::Expected +identifyToolchain(std::string compilerPath, std::string standardLibrary, + std::string abiMode, std::string objectFormat, + std::vector contractFlags); + +llvm::Error preflightBuildRequest(const BuildRequest &request); + +llvm::Expected createCompilePlan(const BuildRequest &request, + const SourceBundle &bundle); + +llvm::Expected buildGeneratedModel(const BuildRequest &request); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_BUILD_H diff --git a/components/agentic-circuit/include/acir/CodeGen/Emitter.h b/components/agentic-circuit/include/acir/CodeGen/Emitter.h new file mode 100644 index 00000000..16a9c8d4 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/Emitter.h @@ -0,0 +1,129 @@ +#ifndef ACIR_CODEGEN_EMITTER_H +#define ACIR_CODEGEN_EMITTER_H + +#include "acir/CodeGen/Manifest.h" + +#include +#include +#include +#include + +namespace acir::codegen { + +// ── Structured C++ emitter ──────────────────────────────────────────── + +/// Deterministic, indentation-aware C++ code emitter. +/// Produces reproducible output: no timestamps, no pointer values, +/// no host paths, no non-deterministic ordering. +class CppEmitter { +public: + explicit CppEmitter(std::ostream &os) : os_(os) {} + + // ── File structure ────────────────────────────────────────────────── + + void emitInclude(const std::string &header, bool system = false); + void emitPragmaOnce(); + void emitNewline(); + void emitComment(const std::string &text); + + // ── Namespace ─────────────────────────────────────────────────────── + + void beginNamespace(const std::string &name); + void endNamespace(); + + // ── Class ─────────────────────────────────────────────────────────── + + void beginClass(const std::string &name, const std::string &base = "", + bool isStruct = false, bool isFinal = false); + void endClass(); + + // ── Access ───────────────────────────────────────────────────────── + + void emitPublic(); + void emitPrivate(); + void emitProtected(); + + // ── Members ───────────────────────────────────────────────────────── + + void emitMember(const std::string &type, const std::string &name, + const std::string &init = ""); + void emitStaticMember(const std::string &type, const std::string &name, + const std::string &init = ""); + + // ── Methods ───────────────────────────────────────────────────────── + + void emitConstructor( + const std::string &className, + const std::vector> ¶ms, + const std::vector &initializers, + const std::string &body = ""); + void + emitMethod(const std::string &returnType, const std::string &name, + const std::vector> ¶ms, + bool isConst = false, bool isOverride = false, + bool isVirtual = false, bool isStatic = false); + void beginMethodBody(); + void endMethodBody(); + void emitReturn(const std::string &expr = ""); + void emitStatement(const std::string &stmt); + + // ── Enums ─────────────────────────────────────────────────────────── + + void emitEnum(const std::string &name, const std::vector &values, + const std::string &underlyingType = "uint8_t"); + + // ── Switch ────────────────────────────────────────────────────────── + + void emitSwitch(const std::string &expr); + void emitCase(const std::string &value); + void emitDefault(); + void emitBreak(); + void endSwitch(); + + // ── Template ──────────────────────────────────────────────────────── + + void emitTemplate(const std::string ¶ms); + + // ── Raw ───────────────────────────────────────────────────────────── + + void emitRaw(const std::string &code); + std::ostream &raw() { return os_; } + + // ── Indentation ───────────────────────────────────────────────────── + + void indent(); + void dedent(); + int depth() const { return indent_; } + +private: + void writeIndent(); + std::ostream &os_; + int indent_ = 0; + bool atLineStart_ = true; +}; + +// ── Deterministic code generation ───────────────────────────────────── + +/// One generated static-dispatch row. Object expressions are C++ lvalues +/// rooted at the model parameter, for example `model.compute`. +struct DispatchEntry { + uint32_t objectId = 0; + std::string objectExpression; +}; + +struct ActivationEdge { + uint32_t sourceId = 0; + uint32_t targetId = 0; +}; + +/// Generate the deterministic dense gfsim dispatch-table factory for a model. +/// Entries may arrive in any order, but their IDs must be exactly [0, N). +SourceFile +generateDispatchHeader(const std::string &namespaceName, + const std::string &modelType, + std::vector entries, + std::vector activationEdges = {}); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_EMITTER_H diff --git a/components/agentic-circuit/include/acir/CodeGen/Generator.h b/components/agentic-circuit/include/acir/CodeGen/Generator.h new file mode 100644 index 00000000..51584857 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/Generator.h @@ -0,0 +1,31 @@ +#ifndef ACIR_CODEGEN_GENERATOR_H +#define ACIR_CODEGEN_GENERATOR_H + +#include "acir/CodeGen/ModelPlan.h" + +#include "llvm/Support/Error.h" + +#include +#include + +namespace acir::codegen { + +struct GeneratedFile { + std::string relativePath; + std::string content; + Fingerprint fingerprint; +}; + +struct SourceBundle { + Fingerprint sourceFingerprint; + Fingerprint buildFingerprint; + std::vector files; +}; + +llvm::Expected generateModelSources(const ModelPlan &plan); +llvm::Error validateSourceBundle(const ModelPlan &plan, + const SourceBundle &bundle); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_GENERATOR_H diff --git a/components/agentic-circuit/include/acir/CodeGen/Manifest.h b/components/agentic-circuit/include/acir/CodeGen/Manifest.h new file mode 100644 index 00000000..c6c47795 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/Manifest.h @@ -0,0 +1,125 @@ +#ifndef ACIR_CODEGEN_MANIFEST_H +#define ACIR_CODEGEN_MANIFEST_H + +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" + +#include +#include +#include + +namespace acir::codegen { + +/// A normative SHA-256 fingerprint: `sha256:` followed by 64 lowercase hex +/// digits. +using Fingerprint = std::string; + +bool isValidFingerprint(llvm::StringRef value); +Fingerprint computeFingerprint(llvm::StringRef content); +llvm::Expected +fingerprintCanonicalJson(const llvm::json::Value &value); + +/// Transitional source record used by the low-level emitter. Structured source +/// bundles replace it at the ModelPlan boundary. +struct SourceFile { + std::string relativePath; + std::string content; + Fingerprint fingerprint; +}; + +struct Identity { + std::string name; + std::string identity; +}; + +struct FileHash { + std::string path; + Fingerprint sha256; +}; + +struct CompilerIdentity { + std::string name; + std::string buildId; + std::string toolchainTarget; +}; + +struct ProviderIdentity { + std::string nameSpace; + Fingerprint schemaFingerprint; + Fingerprint implementationFingerprint; +}; + +struct ComponentSpecialization { + std::string canonicalName; + Fingerprint schemaFingerprint; + Fingerprint specializationFingerprint; +}; + +struct NamedFingerprint { + std::string name; + Fingerprint fingerprint; +}; + +enum class ArtifactKind { + Acpy, + Acir, + Acsim, + CppSource, + CppHeader, + Executable, + Report, +}; + +struct Artifact { + std::string path; + ArtifactKind kind; + Fingerprint sha256; +}; + +enum class ValidationStatus { Passed, Failed }; + +struct ValidationGate { + std::string name; + ValidationStatus status; + std::optional reportSha256; +}; + +struct SpecializationInput { + std::string name; + std::string acirType; + llvm::json::Value canonicalValue = nullptr; +}; + +/// Typed representation of schemas/build-manifest.schema.json. +struct BuildManifest { + std::string schema = "agentic-circuit-build-manifest"; + std::string version = "0.1"; + std::string contractEpoch = "0.3"; + Identity project; + Identity system; + std::vector sourceFiles; + Fingerprint normalizedAcirSha256; + CompilerIdentity compiler; + std::vector passPipeline; + std::vector providers; + std::vector componentSpecializations; + std::vector protocolIdentities; + std::vector artifacts; + std::vector validationGates; + std::string buildProfile; + std::vector instrumentationLayers; + std::vector specializationInputs; + Fingerprint buildFingerprint; + + llvm::Error validate() const; + llvm::Expected canonicalJson() const; + + /// Fingerprint the closed versioned build preimage and store the result in + /// buildFingerprint. + llvm::Error finalizeBuildFingerprint(); +}; + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_MANIFEST_H diff --git a/components/agentic-circuit/include/acir/CodeGen/ModelPlan.h b/components/agentic-circuit/include/acir/CodeGen/ModelPlan.h new file mode 100644 index 00000000..eb4554c7 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/ModelPlan.h @@ -0,0 +1,359 @@ +#ifndef ACIR_CODEGEN_MODELPLAN_H +#define ACIR_CODEGEN_MODELPLAN_H + +#include "acir/CodeGen/Manifest.h" + +#include "mlir/IR/BuiltinOps.h" +#include "llvm/Support/Error.h" + +#include +#include +#include +#include +#include + +namespace acir::codegen { + +enum class TypeKind { + Accessor, + Implementation, + Interface, + Packet, + Policy, + Protocol, + Provider, + Resource, + Role, + Schema, + TimeDomain, + Value, + Wake, + Payload, +}; + +struct TypePlan { + std::string symbol; + TypeKind kind; + std::string cppType; + Fingerprint fingerprint; +}; + +struct TimeDomainPlan { + std::string name; + uint64_t period = 1; + uint64_t phase = 0; + uint64_t tickScale = 1; +}; + +enum class RuntimeObjectKind { External, Process }; + +struct RuntimeObjectPlan { + uint32_t objectId = 0; + uint32_t activationId = 0; + std::string targetSymbol; + std::string hierarchyPath; + std::vector indices; + RuntimeObjectKind objectKind = RuntimeObjectKind::External; + bool traceOwner = false; + std::string workThunk; + std::string xferThunk; + std::string resetThunk; + std::string validateThunk; +}; + +struct ActivationEdgePlan { + uint32_t sourceId = 0; + uint32_t targetId = 0; + + auto operator<=>(const ActivationEdgePlan &) const = default; +}; + +struct SourceMapPlan { + std::string stableIdentity; + std::string source; +}; + +enum class BindingEffect { Pure, Stateful }; +enum class ParameterMappingKind { + TemplateArgument, + ConstexprArgument, + ConstructorConstant, +}; + +struct ParameterPlan { + std::string name; + std::string acirType; + std::string cppType; + llvm::json::Value canonicalValue = nullptr; + uint32_t ordinal = 0; + ParameterMappingKind mapping = ParameterMappingKind::ConstructorConstant; +}; + +struct EntryPointPlan { + std::string pure; + std::string reset; + std::string validate; + std::string work; + std::string xfer; +}; + +struct PortBindingPlan { + std::string accessor; + std::string cardinality; + std::string delegation; + std::string direction; + std::string interfaceType; + std::string ownership; + std::string payload; + std::string protocol; + std::string role; + std::string timeDomain; +}; + +struct ResourceBindingPlan { + std::string accessor; + std::string delegation; + std::string mode; + std::string ownership; + std::string resource; + std::string role; + std::string timeDomain; +}; + +struct ResultBindingPlan { + std::string name; + std::string cppType; +}; + +struct ActivationSourcePlan { + std::string name; + std::string kind; +}; + +struct BindingPlan { + std::string symbol; + std::string bindingId; + BindingEffect effect = BindingEffect::Pure; + std::string header; + std::string target; + std::string cppSymbol; + std::string conceptName; + std::string cppType; + std::string implementation; + std::string provider; + std::string componentSchema; + Fingerprint recordFingerprint; + Fingerprint componentSchemaFingerprint; + Fingerprint providerImplementationFingerprint; + EntryPointPlan entryPoints; + std::vector constructorArguments; + std::string ownershipKind; + std::string ownershipPlacement; + std::vector parameters; + std::vector ports; + std::vector resources; + std::vector results; + std::vector activationSources; +}; + +enum class PlacementKind { + GeneratedModule, + ExternalStateful, + HomogeneousArray, +}; + +struct PlacementPlan { + PlacementKind kind = PlacementKind::ExternalStateful; + std::string symbol; + std::string memberName; + std::string target; + std::string resultValue; + Fingerprint specializationFingerprint; + std::vector shape; + std::vector staticArguments; +}; + +enum class ProjectionKind { Element, Port, Resource }; + +struct ProjectionPlan { + ProjectionKind kind = ProjectionKind::Element; + std::string resultValue; + std::string baseValue; + std::vector indices; + std::string accessor; + std::string resultType; +}; + +struct BindPlan { + std::string sourceValue; + std::string targetValue; + std::string kind; +}; + +struct ExpressionPlan { + std::string resultValue; + std::string callee; + std::vector arguments; + std::string resultType; +}; + +struct ExportPlan { + std::string symbol; + std::string sourceValue; + std::string resultValue; + std::string role; + std::string resultType; +}; + +struct CapturePlan { + std::string name; + std::string sourceValue; + std::string type; +}; + +struct LiveSlotPlan { + std::string name; + std::string type; +}; + +struct LiveLoadPlan { + std::string resultValue; + std::string slot; + std::string type; +}; +struct LiveStorePlan { + std::string sourceValue; + std::string slot; +}; +struct ConstantPlan { + std::string resultValue; + std::string resultType; + llvm::json::Value canonicalValue; +}; +struct ArithmeticPlan { + std::string operationName; + std::vector arguments; + std::vector results; + std::vector resultTypes; + std::string predicate; +}; +struct IndexPlan { + std::string operationName; + std::vector arguments; + std::vector results; + std::vector resultTypes; + std::string predicate; +}; +struct InlineCallPlan { + std::string callee; + std::vector arguments; + std::vector results; + std::vector resultTypes; +}; +struct InvokePlan { + std::string callee; + std::vector arguments; + std::vector results; + std::vector resultTypes; +}; +using ProcessOperationPlan = + std::variant; + +struct ContinuePlan { + std::string targetPc; +}; +struct SuspendPlan { + std::string wakeValue; + std::string targetPc; +}; +struct TerminatePlan { + std::string status; +}; +struct BlockArgumentPlan { + std::string name; + std::string type; +}; +struct BranchPlan { + uint32_t targetBlock = 0; + std::vector arguments; +}; +struct ConditionalBranchPlan { + std::string condition; + uint32_t trueBlock = 0; + std::vector trueArguments; + uint32_t falseBlock = 0; + std::vector falseArguments; +}; +using ProcessTerminatorPlan = + std::variant; +using BlockTerminatorPlan = + std::variant; + +struct PcBlockPlan { + uint32_t ordinal = 0; + std::vector arguments; + std::vector operations; + BlockTerminatorPlan terminator = BranchPlan{}; +}; + +struct PcStatePlan { + uint32_t ordinal = 0; + std::string name; + std::vector operations; + ProcessTerminatorPlan terminator = ContinuePlan{}; + std::vector blocks; +}; + +struct ProcessPlan { + std::string symbol; + std::string className; + Fingerprint specializationFingerprint; + std::string entryPc; + uint64_t fairnessWork = 0; + std::vector captures; + std::vector liveSlots; + std::vector states; +}; + +struct ModulePlan { + std::string symbol; + std::string className; + Fingerprint specializationFingerprint; + std::vector placements; + std::vector projections; + std::vector binds; + std::vector expressions; + std::vector processes; + std::vector exports; + std::vector returnValues; +}; + +struct ModelPlan { + std::string modelSymbol; + std::string rootSymbol; + std::string contractEpoch; + Fingerprint frozenAcirFingerprint; + Fingerprint bindingLockFingerprint; + Fingerprint providerFingerprint; + Fingerprint profileFingerprint; + Fingerprint toolchainFingerprint; + Fingerprint schemaSetFingerprint; + std::vector constructionOrder; + std::vector destructionOrder; + std::vector types; + std::vector timeDomains; + std::vector bindings; + std::vector modules; + std::vector runtimeObjects; + std::vector activationEdges; + std::vector sourceMap; +}; + +llvm::Expected buildModelPlan(mlir::ModuleOp canonicalACSim); +llvm::Error validateModelPlan(const ModelPlan &plan); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_MODELPLAN_H diff --git a/components/agentic-circuit/include/acir/CodeGen/QueueBlockContract.h b/components/agentic-circuit/include/acir/CodeGen/QueueBlockContract.h new file mode 100644 index 00000000..ffe53702 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/QueueBlockContract.h @@ -0,0 +1,39 @@ +#ifndef ACIR_CODEGEN_QUEUEBLOCKCONTRACT_H +#define ACIR_CODEGEN_QUEUEBLOCKCONTRACT_H + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" + +#include +#include +#include + +namespace acir::codegen { + +struct QueueBlockContract { + std::string kind; + std::string operation; + std::string category; + std::string role; + std::string effect; + int64_t minimumInputs = 0; + int64_t maximumInputs = 0; + int64_t minimumOutputs = 0; + int64_t maximumOutputs = 0; + std::string payloadRelation; + std::vector constants; + bool gfsimAvailable = false; + std::string gfsimRealization; + bool pycAvailable = false; + std::string pycRealization; + std::vector refinementObservations; +}; + +llvm::ArrayRef officialQueueBlockContracts(); +const QueueBlockContract *findQueueBlockContract(llvm::StringRef kind); +llvm::Expected canonicalQueueBlockCatalogJson(); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_QUEUEBLOCKCONTRACT_H diff --git a/components/agentic-circuit/include/acir/CodeGen/QueueGraphGenerator.h b/components/agentic-circuit/include/acir/CodeGen/QueueGraphGenerator.h new file mode 100644 index 00000000..8ec48e86 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/QueueGraphGenerator.h @@ -0,0 +1,15 @@ +#ifndef ACIR_CODEGEN_QUEUEGRAPHGENERATOR_H +#define ACIR_CODEGEN_QUEUEGRAPHGENERATOR_H + +#include "acir/CodeGen/QueueGraphPlan.h" +#include "llvm/Support/Error.h" + +#include + +namespace acir::codegen { + +llvm::Expected generateQueueGraphCpp(const QueueGraphPlan &plan); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_QUEUEGRAPHGENERATOR_H diff --git a/components/agentic-circuit/include/acir/CodeGen/QueueGraphPlan.h b/components/agentic-circuit/include/acir/CodeGen/QueueGraphPlan.h new file mode 100644 index 00000000..d8c69de6 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/QueueGraphPlan.h @@ -0,0 +1,107 @@ +#ifndef ACIR_CODEGEN_QUEUEGRAPHPLAN_H +#define ACIR_CODEGEN_QUEUEGRAPHPLAN_H + +#include "mlir/IR/BuiltinOps.h" +#include "llvm/Support/Error.h" + +#include +#include +#include + +namespace acir::codegen { + +struct QueuePayloadFieldPlan { + std::string name; + std::string type; +}; + +struct QueuePayloadPlan { + std::string name; + std::vector fields; +}; + +struct QueueExpressionPlan { + std::string result; + std::string kind; + std::string type; + std::vector operands; + std::string field; + std::string predicate; + std::string literal; +}; + +struct QueuePlan { + std::string name; + std::string payloadType; + std::string scope; + uint64_t depth = 1; + uint64_t latency = 1; + uint64_t rate = 1; +}; + +struct QueueBlockPlan { + std::string kind; + std::string name; + std::string scope; + std::vector inputs; + std::vector outputs; + std::vector depths; + std::vector latencies; + std::string policy; + uint64_t maxIterations = 0; + std::string region; + std::vector expressions; + std::vector yields; + uint64_t capacity = 0; + uint64_t start = 0; + uint64_t noDependency = 0; + uint64_t resources = 0; + uint64_t credits = 0; + uint64_t entries = 0; + uint64_t init = 0; + std::string resultField; + std::string memoryInstance; + uint64_t endpointOrdinal = 0; + std::string message; +}; + +struct MemoryInstancePlan { + std::string name; + std::string dataType; + uint64_t entries = 0; + uint64_t init = 0; + uint64_t latency = 1; + std::string stableId; + std::string ownerPath; +}; + +struct MemoryRequestPlan { + std::string instance; + std::string name; + std::string scope; + std::string input; + std::string output; + uint64_t ordinal = 0; + uint64_t depth = 1; + std::string resultField; +}; + +struct QueueGraphPlan { + std::string system; + std::string specializationFingerprint; + std::vector payloads; + std::vector scopes; + std::vector queues; + std::vector blocks; + std::vector memoryInstances; + std::vector memoryRequests; + + llvm::Expected canonicalJson() const; +}; + +llvm::Expected buildQueueGraphPlan(mlir::ModuleOp module); +llvm::Error verifyQueueGraphPlan(const QueueGraphPlan &plan); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_QUEUEGRAPHPLAN_H diff --git a/components/agentic-circuit/include/acir/CodeGen/QueueGraphPyc.h b/components/agentic-circuit/include/acir/CodeGen/QueueGraphPyc.h new file mode 100644 index 00000000..a3ea4434 --- /dev/null +++ b/components/agentic-circuit/include/acir/CodeGen/QueueGraphPyc.h @@ -0,0 +1,15 @@ +#ifndef ACIR_CODEGEN_QUEUEGRAPHPYC_H +#define ACIR_CODEGEN_QUEUEGRAPHPYC_H + +#include "acir/CodeGen/QueueGraphPlan.h" +#include "llvm/Support/Error.h" + +#include + +namespace acir::codegen { + +llvm::Expected generateQueueGraphPyc(const QueueGraphPlan &plan); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_QUEUEGRAPHPYC_H diff --git a/components/agentic-circuit/include/acir/Compiler/Driver.h b/components/agentic-circuit/include/acir/Compiler/Driver.h new file mode 100644 index 00000000..b6bf4462 --- /dev/null +++ b/components/agentic-circuit/include/acir/Compiler/Driver.h @@ -0,0 +1,114 @@ +#ifndef ACIR_COMPILER_DRIVER_H +#define ACIR_COMPILER_DRIVER_H + +#include "acir/CodeGen/Build.h" +#include "acir/CodeGen/Manifest.h" + +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" + +#include +#include +#include +#include +#include + +namespace acir::compiler { + +enum class CompilerStage { + AcirParse, + AcirVerify, + AcirNormalize, + AcirFreeze, + AcsimLower, + AcsimVerify, + CxxEmit, + CxxContract, + Compile, + Link, + Publish, +}; + +enum class CompilerProfile { Fast, Validated, Custom }; + +struct SourceLocation { + std::string file; + uint64_t line = 0; + uint64_t column = 0; +}; + +struct CompilerRelated { + std::string message; + std::optional source; + std::optional objectPath; +}; + +struct CompilerFixIt { + std::string message; +}; + +struct CompilerDiagnostic { + std::string stage; + std::string code; + std::string severity; + std::string message; + std::optional source; + std::optional objectPath; + llvm::json::Value expected = nullptr; + llvm::json::Value actual = nullptr; + std::vector related; + std::vector fixits; +}; + +struct CompilerRequest { + std::string acirBytes; + std::string bindingLockBytes; + std::string bindingRegistryBytes; + CompilerProfile profile = CompilerProfile::Fast; + std::optional stopAfter; + std::vector emits; + std::vector dumpBefore; + std::vector dumpAfter; + bool dumpAfterEach = false; + bool verifyAfterEach = false; + std::optional customPipeline; + codegen::BuildRequest build; +}; + +struct CompilerArtifact { + std::string logicalPath; + codegen::ArtifactKind kind = codegen::ArtifactKind::Report; + std::string bytes; + codegen::Fingerprint sha256; +}; + +struct CompilerResult { + std::vector artifacts; + std::vector diagnostics; + std::optional build; +}; + +class CompilerError : public llvm::ErrorInfo { +public: + static char ID; + + explicit CompilerError(std::vector diagnostics); + + const std::vector &diagnostics() const { + return diagnostics_; + } + + void log(llvm::raw_ostream &output) const override; + std::error_code convertToErrorCode() const override; + +private: + std::vector diagnostics_; +}; + +llvm::StringRef compilerStageName(CompilerStage stage); +llvm::Expected runCompiler(const CompilerRequest &request); + +} // namespace acir::compiler + +#endif // ACIR_COMPILER_DRIVER_H diff --git a/components/agentic-circuit/include/acir/Conversion/ACIRToACSim/ACIRToACSim.h b/components/agentic-circuit/include/acir/Conversion/ACIRToACSim/ACIRToACSim.h new file mode 100644 index 00000000..5ea3c6e0 --- /dev/null +++ b/components/agentic-circuit/include/acir/Conversion/ACIRToACSim/ACIRToACSim.h @@ -0,0 +1,49 @@ +#ifndef ACIR_CONVERSION_ACIRTOACSIM_ACIRTOACSIM_H +#define ACIR_CONVERSION_ACIRTOACSIM_ACIRTOACSIM_H + +#include "acir/Bindings/Registry.h" + +#include +#include +#include + +namespace mlir { +class Pass; +} // namespace mlir + +namespace acir { + +/// Options for the atomic ACIR-to-ACSim lowering. The pass resolves exact +/// gfsim bindings in memory through the same closed candidate/request +/// registry contract used by ac-resolve-gfsim-bindings; no lock file +/// round-trip is involved. +struct ACIRToACSimPassOptions { + std::vector candidates; + std::vector requests; + std::string profile; + std::string target; + /// Test hook: when non-zero, replaces the built-in v0.1 expanded-row + /// capability bound so the ACLOWER-DISPATCH overflow path is observable + /// without a million-row input. Production drivers leave this at zero. + uint64_t maxExpandedRows = 0; +}; + +/// Create the atomic `ac-lower-to-acsim` pass. +/// +/// The pass converts one frozen, verified ACIR file (contract epoch "0.3", +/// `ac.topology_frozen = true`, exactly one selected ac.system) into one +/// canonical ACSim model in a single transaction. Every validation failure +/// is diagnosed with an ACLOWER-* code before any mutation, so a failed +/// lowering never publishes a partial acsim.model. +/// +/// The lowering covers generated ownership, homogeneous arrays, exact +/// pure/stateful bindings, typed endpoint graph edges and exports, and the +/// complete public ProcessStatePlan state machine. Unsupported topology kinds +/// and heterogeneous specializations fail with an explicit diagnostic and are +/// never silently dropped. +std::unique_ptr +createACIRToACSimPass(ACIRToACSimPassOptions options); + +} // namespace acir + +#endif // ACIR_CONVERSION_ACIRTOACSIM_ACIRTOACSIM_H diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRAttributes.td b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRAttributes.td new file mode 100644 index 00000000..c2aea7ec --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRAttributes.td @@ -0,0 +1,44 @@ +#ifndef ACIR_DIALECT_ACIR_ACIRATTRIBUTES_TD +#define ACIR_DIALECT_ACIR_ACIRATTRIBUTES_TD + +include "acir/Dialect/ACIR/ACIRDialect.td" +include "mlir/IR/AttrTypeBase.td" +include "mlir/IR/EnumAttr.td" + +def ACIR_Unit_Ticks : I32EnumAttrCase<"Ticks", 0, "ticks">; +def ACIR_Unit_Cycles : I32EnumAttrCase<"Cycles", 1, "cycles">; +def ACIR_Unit_Seconds : I32EnumAttrCase<"Seconds", 2, "seconds">; +def ACIR_Unit_Milliseconds + : I32EnumAttrCase<"Milliseconds", 3, "milliseconds">; +def ACIR_Unit_Microseconds + : I32EnumAttrCase<"Microseconds", 4, "microseconds">; +def ACIR_Unit_Nanoseconds + : I32EnumAttrCase<"Nanoseconds", 5, "nanoseconds">; +def ACIR_Unit_Picoseconds + : I32EnumAttrCase<"Picoseconds", 6, "picoseconds">; +def ACIR_Unit_Bytes : I32EnumAttrCase<"Bytes", 7, "bytes">; +def ACIR_Unit_Bits : I32EnumAttrCase<"Bits", 8, "bits">; +def ACIR_Unit_Entries : I32EnumAttrCase<"Entries", 9, "entries">; +def ACIR_Unit_Packets : I32EnumAttrCase<"Packets", 10, "packets">; +def ACIR_Unit_Transactions + : I32EnumAttrCase<"Transactions", 11, "transactions">; + +def ACIR_Unit : I32EnumAttr<"Unit", "ACIR exact measurement unit", [ + ACIR_Unit_Ticks, + ACIR_Unit_Cycles, + ACIR_Unit_Seconds, + ACIR_Unit_Milliseconds, + ACIR_Unit_Microseconds, + ACIR_Unit_Nanoseconds, + ACIR_Unit_Picoseconds, + ACIR_Unit_Bytes, + ACIR_Unit_Bits, + ACIR_Unit_Entries, + ACIR_Unit_Packets, + ACIR_Unit_Transactions +]> { + let genSpecializedAttr = 0; + let cppNamespace = "::acir::ac"; +} + +#endif // ACIR_DIALECT_ACIR_ACIRATTRIBUTES_TD diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRDialect.h b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRDialect.h new file mode 100644 index 00000000..3e30ab7d --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRDialect.h @@ -0,0 +1,11 @@ +#ifndef ACIR_DIALECT_ACIR_ACIRDIALECT_H +#define ACIR_DIALECT_ACIR_ACIRDIALECT_H + +#include "mlir/IR/Dialect.h" + +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/ACIRTypes.h" + +#include "acir/Dialect/ACIR/ACIRDialect.h.inc" + +#endif // ACIR_DIALECT_ACIR_ACIRDIALECT_H diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRDialect.td b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRDialect.td new file mode 100644 index 00000000..2a75340f --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRDialect.td @@ -0,0 +1,8 @@ +include "mlir/IR/DialectBase.td" + +def ACIR_Dialect : Dialect { + let name = "ac"; + let cppNamespace = "::acir::ac"; + let summary = "Agentic Circuit architecture dialect"; + let useDefaultTypePrinterParser = 1; +} diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRInterfaces.td b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRInterfaces.td new file mode 100644 index 00000000..ea616546 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRInterfaces.td @@ -0,0 +1,58 @@ +#ifndef ACIR_DIALECT_ACIR_ACIRINTERFACES_TD +#define ACIR_DIALECT_ACIR_ACIRINTERFACES_TD + +include "mlir/IR/OpBase.td" + +def ACIR_RecordLike : OpInterface<"RecordLikeOpInterface"> { + let cppNamespace = "::acir::ac"; + let methods = [ + InterfaceMethod<"Returns ordered field dictionaries.", "::mlir::ArrayAttr", + "getRecordFields", (ins), [{ + return $_op.getFields(); + }]> + ]; +} + +def ACIR_InterfaceContainer : OpInterface<"InterfaceContainerOpInterface"> { + let cppNamespace = "::acir::ac"; + let methods = [ + InterfaceMethod<"Returns the declaration region containing roles and ports.", + "::mlir::Region &", "getInterfaceBody", (ins), [{ + return $_op.getBody(); + }]> + ]; +} + +def ACIR_ProtocolContainer : OpInterface<"ProtocolContainerOpInterface"> { + let cppNamespace = "::acir::ac"; + let methods = [ + InterfaceMethod<"Returns the finite-state protocol declaration region.", + "::mlir::Region &", "getProtocolBody", (ins), [{ + return $_op.getBody(); + }]> + ]; +} + +def ACIR_ProcessRegion : OpInterface<"ProcessRegionOpInterface"> { + let cppNamespace = "::acir::ac"; + let methods = [ + InterfaceMethod<"Returns the resumable SSACFG process region.", + "::mlir::Region &", "getProcessBody", (ins), [{ + return $_op.getBody(); + }]>, + InterfaceMethod<"Returns the process execution kind.", + "::llvm::StringRef", "getProcessKind", (ins), [{ + return $_op.getKind(); + }]> + ]; +} + +def ACIR_Observation : OpInterface<"ObservationOpInterface"> { + let cppNamespace = "::acir::ac"; + let methods = [ + InterfaceMethod<"Returns true for operations barred from functional dataflow.", + "bool", "isObservationOnly", (ins), [{ return true; }]> + ]; +} + +#endif diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIROps.h b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIROps.h new file mode 100644 index 00000000..7f8a40f9 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIROps.h @@ -0,0 +1,29 @@ +#ifndef ACIR_DIALECT_ACIR_ACIROPS_H +#define ACIR_DIALECT_ACIR_ACIROPS_H + +#include "acir/Dialect/ACIR/ACIRTypes.h" +#include "mlir/Bytecode/BytecodeOpInterface.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/RegionKindInterface.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/Interfaces/ControlFlowInterfaces.h" +#include "mlir/Interfaces/DataLayoutInterfaces.h" +#include "mlir/Interfaces/FunctionInterfaces.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "llvm/ADT/StringMap.h" + +#include "acir/Dialect/ACIR/ACIROpInterfaces.h.inc" + +#define GET_OP_CLASSES +#include "acir/Dialect/ACIR/ACIROps.h.inc" + +namespace acir::ac { + +/// Verifies symbol resolution and linear-use rules for ACIR topology types on +/// an arbitrary operation. This is called by the whole-file ACIR verifier. +mlir::LogicalResult verifyTopologyTypeUses(mlir::Operation *operation); + +} // namespace acir::ac + +#endif diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIROps.td b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIROps.td new file mode 100644 index 00000000..abf39aab --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIROps.td @@ -0,0 +1,989 @@ +#ifndef ACIR_DIALECT_ACIR_ACIROPS_TD +#define ACIR_DIALECT_ACIR_ACIROPS_TD + +include "acir/Dialect/ACIR/ACIRInterfaces.td" +include "acir/Dialect/ACIR/ACIRTypes.td" +include "mlir/IR/SymbolInterfaces.td" +include "mlir/IR/RegionKindInterface.td" +include "mlir/Interfaces/DataLayoutInterfaces.td" +include "mlir/Interfaces/ControlFlowInterfaces.td" +include "mlir/Interfaces/FunctionInterfaces.td" +include "mlir/Interfaces/SideEffectInterfaces.td" + +class ACIR_Op traits = []> + : Op; + +def ACIR_TypeScopeOp : ACIR_Op<"type_scope", [ + Symbol, SymbolTable, SingleBlock, NoTerminator, + DeclareOpInterfaceMethods + ]> { + let arguments = (ins SymbolNameAttr:$sym_name); + let regions = (region SizedRegion<1>:$body); + let builders = [OpBuilder<(ins "::llvm::StringRef":$name), [{ + $_state.addAttribute(::mlir::SymbolTable::getSymbolAttrName(), + $_builder.getStringAttr(name)); + $_state.addRegion(); + }]>]; + let assemblyFormat = "$sym_name $body attr-dict"; +} + +def ACIR_TypeAliasOp : ACIR_Op<"type_alias", [Symbol]> { + let arguments = (ins SymbolNameAttr:$sym_name, TypeAttr:$target); + let hasVerifier = 1; +} + +class ACIR_RecordDeclarationOp + : ACIR_Op { + let arguments = (ins SymbolNameAttr:$sym_name, ArrayAttr:$fields); + let hasVerifier = 1; + let assemblyFormat = "$sym_name `fields` $fields attr-dict"; +} + +def ACIR_StructOp : ACIR_RecordDeclarationOp<"struct">; + +def ACIR_EnumOp : ACIR_Op<"enum", [Symbol]> { + let arguments = (ins SymbolNameAttr:$sym_name, StrArrayAttr:$enumerants); + let hasVerifier = 1; +} + +def ACIR_UnionOp : ACIR_Op<"union", [Symbol]> { + let arguments = (ins SymbolNameAttr:$sym_name, ArrayAttr:$fields, + StrAttr:$discriminator); + let hasVerifier = 1; +} + +def ACIR_PacketOp : ACIR_Op<"packet", [Symbol, ACIR_RecordLike]> { + let arguments = (ins SymbolNameAttr:$sym_name, ArrayAttr:$fields); + let hasVerifier = 1; +} + +def ACIR_TransactionOp : ACIR_RecordDeclarationOp<"transaction">; + +def ACIR_RecordCreateOp : ACIR_Op<"record.create", [Pure]> { + let arguments = (ins Variadic:$values, StrArrayAttr:$field_names); + let results = (outs AnyType:$result); + let hasVerifier = 1; +} + +def ACIR_RecordGetOp : ACIR_Op<"record.get", [Pure]> { + let arguments = (ins AnyType:$record, StrAttr:$field); + let results = (outs AnyType:$result); + let hasVerifier = 1; +} + +def ACIR_RecordWithOp : ACIR_Op<"record.with", [Pure]> { + let arguments = (ins AnyType:$record, AnyType:$value, StrAttr:$field); + let results = (outs AnyType:$result); + let hasVerifier = 1; +} + +def ACIR_VarConstantOp : ACIR_Op<"var.constant", [Pure]> { + let arguments = (ins AnyAttr:$value); + let results = (outs ACIR_VarType:$result); + let hasVerifier = 1; + let assemblyFormat = "$value attr-dict `as` qualified(type($result))"; +} + +class ACIR_VarBinaryOp : ACIR_Op + ]> { + let arguments = (ins ACIR_VarType:$lhs, ACIR_VarType:$rhs); + let results = (outs ACIR_VarType:$result); + let hasVerifier = 1; + let assemblyFormat = [{ + $lhs `,` $rhs attr-dict `:` qualified(type($result)) + }]; +} + +def ACIR_VarAddOp : ACIR_VarBinaryOp<"var.add">; +def ACIR_VarSubOp : ACIR_VarBinaryOp<"var.sub">; +def ACIR_VarMulOp : ACIR_VarBinaryOp<"var.mul">; + +def ACIR_VarPopcountOp : ACIR_Op<"var.popcount", [Pure]> { + let arguments = (ins ACIR_VarType:$in); + let results = (outs ACIR_VarType:$result); + let hasVerifier = 1; + let assemblyFormat = "$in attr-dict `:` qualified(type($in)) `->` qualified(type($result))"; +} + +def ACIR_VarCmpOp : ACIR_Op<"var.cmp", [ + Pure, AllTypesMatch<["lhs", "rhs"]> + ]> { + let arguments = (ins StrAttr:$predicate, ACIR_VarType:$lhs, + ACIR_VarType:$rhs); + let results = (outs ACIR_VarType:$result); + let hasVerifier = 1; + let assemblyFormat = [{ + $predicate $lhs `,` $rhs attr-dict `:` qualified(type($lhs)) `->` + qualified(type($result)) + }]; +} + +def ACIR_VarGetOp : ACIR_Op<"var.get", [Pure]> { + let arguments = (ins ACIR_VarType:$record, StrAttr:$field); + let results = (outs ACIR_VarType:$result); + let hasVerifier = 1; + let assemblyFormat = [{ + $record `field` $field attr-dict `:` qualified(type($record)) `->` + qualified(type($result)) + }]; +} + +def ACIR_VarWithOp : ACIR_Op<"var.with", [Pure]> { + let arguments = (ins ACIR_VarType:$record, ACIR_VarType:$value, + StrAttr:$field); + let results = (outs ACIR_VarType:$result); + let hasVerifier = 1; + let assemblyFormat = [{ + $record `,` $value `field` $field attr-dict `:` + qualified(type($record)) `,` qualified(type($value)) `->` + qualified(type($result)) + }]; +} + +def ACIR_PacketSerializeOp : ACIR_Op<"packet.serialize", [Pure]> { + let arguments = (ins AnyType:$packet_value, SymbolRefAttr:$packet); + let results = (outs AnyType:$bytes); + let hasVerifier = 1; +} + +def ACIR_PacketDeserializeOp : ACIR_Op<"packet.deserialize", [Pure]> { + let arguments = (ins AnyType:$bytes, SymbolRefAttr:$packet); + let results = (outs AnyType:$packet_value); + let hasVerifier = 1; +} + +def ACIR_TransformOp : ACIR_Op<"transform", [SingleBlock]> { + let arguments = (ins Variadic:$inputs, + DenseI64ArrayAttr:$output_depths, + DenseI64ArrayAttr:$output_latencies); + let results = (outs Variadic:$outputs); + let regions = (region SizedRegion<1>:$body); + let hasVerifier = 1; + let assemblyFormat = [{ + $inputs `depths` $output_depths `latencies` $output_latencies + $body attr-dict `:` functional-type($inputs, $outputs) + }]; +} + +def ACIR_TransformYieldOp : ACIR_Op<"transform.yield", [ + Pure, Terminator, HasParent<"TransformOp"> + ]> { + let arguments = (ins Variadic:$values); + let assemblyFormat = "attr-dict ($values^ `:` type($values))?"; +} + +def ACIR_SourceOp : ACIR_Op<"source", [ + DeclareOpInterfaceMethods + ]> { + let arguments = (ins I64Attr:$depth, I64Attr:$latency); + let results = (outs ACIR_QueueType:$output); + let hasVerifier = 1; + let assemblyFormat = [{ + `depth` $depth `latency` $latency attr-dict `:` + qualified(type($output)) + }]; +} + +def ACIR_SinkOp : ACIR_Op<"sink", [ + DeclareOpInterfaceMethods + ]> { + let arguments = (ins ACIR_QueueType:$input); + let assemblyFormat = "$input attr-dict `:` qualified(type($input))"; +} + +def ACIR_ObserveOp : ACIR_Op<"observe"> { + let arguments = (ins ACIR_QueueType:$input, StrAttr:$name); + let hasVerifier = 1; + let assemblyFormat = "$input `name` $name attr-dict `:` qualified(type($input))"; +} + +def ACIR_ExpectOp : ACIR_Op<"expect", [SingleBlock]> { + let arguments = (ins ACIR_QueueType:$input, StrAttr:$message); + let regions = (region SizedRegion<1>:$predicate); + let hasVerifier = 1; + let assemblyFormat = [{ + $input `message` $message $predicate attr-dict `:` qualified(type($input)) + }]; +} + +def ACIR_ExpectYieldOp : ACIR_Op<"expect.yield", [ + Pure, Terminator, HasParent<"ExpectOp"> + ]> { + let arguments = (ins ACIR_VarType:$condition); + let assemblyFormat = "$condition attr-dict `:` qualified(type($condition))"; +} + +def ACIR_BroadcastOp : ACIR_Op<"broadcast"> { + let arguments = (ins ACIR_QueueType:$input, + DenseI64ArrayAttr:$output_depths, + DenseI64ArrayAttr:$output_latencies); + let results = (outs Variadic:$outputs); + let hasVerifier = 1; + let assemblyFormat = [{ + $input `depths` $output_depths `latencies` $output_latencies attr-dict + `:` qualified(type($input)) `->` `(` type($outputs) `)` + }]; +} + +def ACIR_ForkOp : ACIR_Op<"fork"> { + let arguments = (ins ACIR_QueueType:$input, + DenseI64ArrayAttr:$output_depths, + DenseI64ArrayAttr:$output_latencies); + let results = (outs Variadic:$outputs); + let hasVerifier = 1; + let assemblyFormat = [{ + $input `depths` $output_depths `latencies` $output_latencies attr-dict + `:` qualified(type($input)) `->` `(` type($outputs) `)` + }]; +} + +def ACIR_RouteOp : ACIR_Op<"route", [SingleBlock]> { + let arguments = (ins ACIR_QueueType:$input, + DenseI64ArrayAttr:$output_depths, + DenseI64ArrayAttr:$output_latencies); + let results = (outs Variadic:$outputs); + let regions = (region SizedRegion<1>:$selector); + let hasVerifier = 1; + let assemblyFormat = [{ + $input `depths` $output_depths `latencies` $output_latencies $selector + attr-dict `:` qualified(type($input)) `->` `(` type($outputs) `)` + }]; +} + +def ACIR_RouteYieldOp : ACIR_Op<"route.yield", [ + Pure, Terminator, HasParent<"RouteOp"> + ]> { + let arguments = (ins ACIR_VarType:$selector); + let assemblyFormat = "$selector attr-dict `:` qualified(type($selector))"; +} + +def ACIR_SelectOp : ACIR_Op<"select", [SingleBlock]> { + let arguments = (ins Variadic:$inputs, + I64Attr:$depth, I64Attr:$latency); + let results = (outs ACIR_QueueType:$output); + let regions = (region SizedRegion<1>:$key); + let hasVerifier = 1; + let assemblyFormat = [{ + $inputs `depth` $depth `latency` $latency `key` $key attr-dict `:` + functional-type($inputs, $output) + }]; +} + +def ACIR_SelectYieldOp : ACIR_Op<"select.yield", [ + Pure, Terminator, HasParent<"SelectOp"> + ]> { + let arguments = (ins ACIR_VarType:$selector); + let assemblyFormat = "$selector attr-dict `:` qualified(type($selector))"; +} + +def ACIR_MergeOp : ACIR_Op<"merge"> { + let arguments = (ins Variadic:$inputs, StrAttr:$policy, + I64Attr:$depth, I64Attr:$latency); + let results = (outs ACIR_QueueType:$output); + let hasVerifier = 1; + let assemblyFormat = [{ + $inputs `policy` $policy `depth` $depth `latency` $latency attr-dict + `:` functional-type($inputs, $output) + }]; +} + +def ACIR_BarrierOp : ACIR_Op<"barrier"> { + let arguments = (ins Variadic:$inputs, + DenseI64ArrayAttr:$output_depths, + DenseI64ArrayAttr:$output_latencies); + let results = (outs Variadic:$outputs); + let hasVerifier = 1; + let assemblyFormat = [{ + $inputs `depths` $output_depths `latencies` $output_latencies attr-dict + `:` functional-type($inputs, $outputs) + }]; +} + +def ACIR_ReorderOp : ACIR_Op<"reorder", [SingleBlock]> { + let arguments = (ins ACIR_QueueType:$input, I64Attr:$capacity, + I64Attr:$start, I64Attr:$depth, I64Attr:$latency); + let results = (outs ACIR_QueueType:$output); + let regions = (region SizedRegion<1>:$key); + let hasVerifier = 1; + let assemblyFormat = [{ + $input `capacity` $capacity `start` $start `depth` $depth + `latency` $latency $key attr-dict `:` qualified(type($input)) `->` + qualified(type($output)) + }]; +} + +def ACIR_ReorderYieldOp : ACIR_Op<"reorder.yield", [ + Pure, Terminator, HasParent<"ReorderOp"> + ]> { + let arguments = (ins ACIR_VarType:$key); + let assemblyFormat = "$key attr-dict `:` qualified(type($key))"; +} + +def ACIR_DependencyOp : ACIR_Op<"dependency", [SingleBlock]> { + let arguments = (ins ACIR_QueueType:$input, I64Attr:$capacity, + I64Attr:$resources, I64Attr:$no_dependency, + I64Attr:$depth, I64Attr:$latency); + let results = (outs ACIR_QueueType:$output); + let regions = (region SizedRegion<1>:$key, + SizedRegion<1>:$waits_for, + SizedRegion<1>:$resource, + SizedRegion<1>:$cost); + let hasVerifier = 1; + let assemblyFormat = [{ + $input `capacity` $capacity `resources` $resources + `no_dependency` $no_dependency + `depth` $depth `latency` $latency `key` $key + `waits_for` $waits_for `resource` $resource `cost` $cost attr-dict `:` + qualified(type($input)) `->` qualified(type($output)) + }]; +} + +def ACIR_DependencyYieldOp : ACIR_Op<"dependency.yield", [ + Pure, Terminator, HasParent<"DependencyOp"> + ]> { + let arguments = (ins ACIR_VarType:$value); + let assemblyFormat = "$value attr-dict `:` qualified(type($value))"; +} + +def ACIR_CreditOp : ACIR_Op<"credit", [SingleBlock]> { + let arguments = (ins ACIR_QueueType:$input, I64Attr:$credits, + I64Attr:$depth, I64Attr:$latency); + let results = (outs ACIR_QueueType:$output); + let regions = (region SizedRegion<1>:$cost); + let hasVerifier = 1; + let assemblyFormat = [{ + $input `credits` $credits `depth` $depth `latency` $latency `cost` $cost + attr-dict `:` qualified(type($input)) `->` qualified(type($output)) + }]; +} + +def ACIR_CreditYieldOp : ACIR_Op<"credit.yield", [ + Pure, Terminator, HasParent<"CreditOp"> + ]> { + let arguments = (ins ACIR_VarType:$cost); + let assemblyFormat = "$cost attr-dict `:` qualified(type($cost))"; +} + +def ACIR_MemoryInstanceOp : ACIR_Op<"memory.instance", [Symbol]> { + let arguments = (ins SymbolNameAttr:$sym_name, TypeAttr:$data_type, + I64Attr:$entries, I64Attr:$init, I64Attr:$latency, + StrAttr:$owner, StrAttr:$stable_id); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `data` $data_type `entries` $entries `init` $init + `latency` $latency `owner` $owner `stable_id` $stable_id attr-dict + }]; +} + +def ACIR_MemoryRequestOp : ACIR_Op<"memory.request", [SingleBlock]> { + let arguments = (ins FlatSymbolRefAttr:$instance, ACIR_QueueType:$input, + I64Attr:$ordinal, StrAttr:$result_field, + I64Attr:$depth); + let results = (outs ACIR_QueueType:$output); + let regions = (region SizedRegion<1>:$address, + SizedRegion<1>:$write, + SizedRegion<1>:$data); + let hasVerifier = 1; + let assemblyFormat = [{ + $instance `,` $input `ordinal` $ordinal `result_field` $result_field + `depth` $depth `address` $address + `write` $write `data` $data attr-dict `:` qualified(type($input)) `->` + qualified(type($output)) + }]; +} + +def ACIR_MemoryYieldOp : ACIR_Op<"memory.yield", [ + Pure, Terminator, HasParent<"MemoryRequestOp"> + ]> { + let arguments = (ins ACIR_VarType:$value); + let assemblyFormat = "$value attr-dict `:` qualified(type($value))"; +} + +def ACIR_FeedbackOp : ACIR_Op<"feedback", [SingleBlock]> { + let arguments = (ins ACIR_QueueType:$input, I64Attr:$depth, + I64Attr:$latency, I64Attr:$max_iterations); + let results = (outs ACIR_QueueType:$output); + let regions = (region SizedRegion<1>:$body); + let hasVerifier = 1; + let assemblyFormat = [{ + $input `depth` $depth `latency` $latency + `max_iterations` $max_iterations $body attr-dict `:` + qualified(type($input)) `->` qualified(type($output)) + }]; +} + +def ACIR_FeedbackYieldOp : ACIR_Op<"feedback.yield", [ + Pure, Terminator, HasParent<"FeedbackOp"> + ]> { + let arguments = (ins ACIR_VarType:$value, ACIR_VarType:$continue_value); + let assemblyFormat = [{ + $value `continue` $continue_value attr-dict `:` + qualified(type($value)) `,` qualified(type($continue_value)) + }]; +} + +def ACIR_ScopeOp : ACIR_Op<"scope", [SingleBlock, RecursiveMemoryEffects]> { + let arguments = (ins SymbolNameAttr:$sym_name, + Variadic:$inputs); + let results = (outs Variadic:$outputs); + let regions = (region SizedRegion<1>:$body); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `(` $inputs `)` $body attr-dict `:` + functional-type($inputs, $outputs) + }]; +} + +def ACIR_ScopeYieldOp : ACIR_Op<"scope.yield", [ + Pure, Terminator, HasParent<"ScopeOp"> + ]> { + let arguments = (ins Variadic:$queues); + let assemblyFormat = "attr-dict ($queues^ `:` type($queues))?"; +} + +def ACIR_FiringOp : ACIR_Op<"firing", [ + SingleBlock, RecursiveMemoryEffects + ]> { + let arguments = (ins Variadic:$queues); + let regions = (region SizedRegion<1>:$body); + let hasVerifier = 1; + let assemblyFormat = [{ + `(` $queues `)` $body attr-dict `:` `(` type($queues) `)` + }]; +} + +def ACIR_FiringYieldOp : ACIR_Op<"firing.yield", [ + Pure, Terminator, HasParent<"FiringOp"> + ]> { + let assemblyFormat = "attr-dict"; +} + +class ACIR_QueueEffectOp + : ACIR_Op, + DeclareOpInterfaceMethods + ]>; + +def ACIR_QueuePeekOp : ACIR_QueueEffectOp<"queue.peek"> { + let arguments = (ins ACIR_QueueType:$queue); + let results = (outs ACIR_VarType:$value); + let hasVerifier = 1; + let assemblyFormat = [{ + $queue attr-dict `:` qualified(type($queue)) `->` + qualified(type($value)) + }]; +} + +def ACIR_QueuePopOp : ACIR_QueueEffectOp<"queue.pop"> { + let arguments = (ins ACIR_QueueType:$queue); + let results = (outs ACIR_VarType:$value); + let hasVerifier = 1; + let assemblyFormat = [{ + $queue attr-dict `:` qualified(type($queue)) `->` + qualified(type($value)) + }]; +} + +def ACIR_QueuePushOp : ACIR_QueueEffectOp<"queue.push"> { + let arguments = (ins ACIR_QueueType:$queue, ACIR_VarType:$value); + let hasVerifier = 1; + let assemblyFormat = [{ + $queue `,` $value attr-dict `:` qualified(type($queue)) `,` + qualified(type($value)) + }]; +} + +def ACIR_InterfaceOp : ACIR_Op<"interface", [ + Symbol, SymbolTable, SingleBlock, NoTerminator, IsolatedFromAbove, + RegionKindInterface, HasOnlyGraphRegion, ACIR_InterfaceContainer + ]> { + let arguments = (ins SymbolNameAttr:$sym_name); + let regions = (region SizedRegion<1>:$body); + let builders = [OpBuilder<(ins "::llvm::StringRef":$name), [{ + $_state.addAttribute(::mlir::SymbolTable::getSymbolAttrName(), + $_builder.getStringAttr(name)); + $_state.addRegion(); + }]>]; + let hasVerifier = 1; + let assemblyFormat = "$sym_name $body attr-dict"; +} + +def ACIR_ProtocolOp : ACIR_Op<"protocol", [ + Symbol, SymbolTable, SingleBlock, NoTerminator, IsolatedFromAbove, + RegionKindInterface, HasOnlyGraphRegion, ACIR_ProtocolContainer + ]> { + let arguments = (ins SymbolNameAttr:$sym_name); + let regions = (region SizedRegion<1>:$body); + let builders = [OpBuilder<(ins "::llvm::StringRef":$name), [{ + $_state.addAttribute(::mlir::SymbolTable::getSymbolAttrName(), + $_builder.getStringAttr(name)); + $_state.addRegion(); + }]>]; + let hasVerifier = 1; + let assemblyFormat = "$sym_name $body attr-dict"; +} + +def ACIR_RoleOp : ACIR_Op<"role", [Symbol, Pure]> { + let arguments = (ins SymbolNameAttr:$sym_name, FlatSymbolRefAttr:$dual, + StrAttr:$cardinality); + let hasVerifier = 1; + let assemblyFormat = "$sym_name `dual` $dual `cardinality` $cardinality attr-dict"; +} + +def ACIR_StateOp : ACIR_Op<"state", [Symbol, Pure]> { + let arguments = (ins SymbolNameAttr:$sym_name, BoolAttr:$initial, + BoolAttr:$terminal); + let hasVerifier = 1; + let assemblyFormat = "$sym_name `initial` $initial `terminal` $terminal attr-dict"; +} + +def ACIR_EventOp : ACIR_Op<"event", [Symbol, Pure]> { + let arguments = (ins SymbolNameAttr:$sym_name, FlatSymbolRefAttr:$from, + FlatSymbolRefAttr:$to, TypeAttr:$payload, + StrAttr:$action); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `from` $from `to` $to `payload` $payload `action` $action + attr-dict + }]; +} + +def ACIR_TransitionOp : ACIR_Op<"transition", [SingleBlock, NoTerminator]> { + let arguments = (ins FlatSymbolRefAttr:$source, FlatSymbolRefAttr:$target, + FlatSymbolRefAttr:$event, + OptionalAttr:$priority, + DefaultValuedAttr:$transfer, + DefaultValuedAttr:$retain); + let regions = (region AnyRegion:$guard); + let hasVerifier = 1; + let assemblyFormat = [{ + `from` $source `to` $target `on` $event (`priority` $priority^)? + `transfer` $transfer `retain` $retain `guard` $guard attr-dict + }]; +} + +def ACIR_GuaranteeOp : ACIR_Op<"guarantee", [Pure]> { + let arguments = (ins StrAttr:$kind, AnyAttr:$value); + let hasVerifier = 1; + let assemblyFormat = "$kind `=` $value attr-dict"; +} + +def ACIR_PortOp : ACIR_Op<"port", [Symbol, Pure]> { + let arguments = (ins SymbolNameAttr:$sym_name, TypeAttr:$type, + FlatSymbolRefAttr:$from, FlatSymbolRefAttr:$to, + FlatSymbolRefAttr:$protocol_from, + FlatSymbolRefAttr:$protocol_to); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `:` $type `from` $from `to` $to + `protocol_roles` $protocol_from `to` $protocol_to attr-dict + }]; +} + +def ACIR_SystemOp : ACIR_Op<"system", [Symbol]> { + let arguments = (ins SymbolNameAttr:$sym_name, FlatSymbolRefAttr:$root, + StrAttr:$root_name, I64Attr:$tick_epoch, + StrAttr:$tick_unit, + OptionalAttr:$primary_workload, + DictionaryAttr:$seed_policy, + ArrayAttr:$instrumentation, + DictionaryAttr:$result_schema, + DefaultValuedAttr:$selected); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `root` $root `as` $root_name `tick` $tick_epoch $tick_unit + (`workload` $primary_workload^)? `seed` $seed_policy + `instrumentation` $instrumentation `results` $result_schema + (`selected` $selected^)? attr-dict + }]; +} + +def ACIR_ModuleOp : ACIR_Op<"module", [ + Symbol, FunctionOpInterface, IsolatedFromAbove, SingleBlock, + RegionKindInterface, HasOnlyGraphRegion + ]> { + let arguments = (ins SymbolNameAttr:$sym_name, + TypeAttrOf:$function_type, + OptionalAttr:$sym_visibility, + OptionalAttr:$arg_attrs, + OptionalAttr:$res_attrs, + DictionaryAttr:$static_params); + let regions = (region AnyRegion:$body); + let builders = [OpBuilder<(ins "::llvm::StringRef":$name, + "::mlir::FunctionType":$type, + CArg<"::mlir::DictionaryAttr", "{}">:$staticParams), [{ + $_state.addAttribute(::mlir::SymbolTable::getSymbolAttrName(), + $_builder.getStringAttr(name)); + $_state.addAttribute(getFunctionTypeAttrName($_state.name), + ::mlir::TypeAttr::get(type)); + $_state.addAttribute("static_params", + staticParams ? staticParams : $_builder.getDictionaryAttr({})); + $_state.addRegion(); + }]>]; + let extraClassDeclaration = [{ + ::mlir::ArrayRef<::mlir::Type> getArgumentTypes() { + return getFunctionType().getInputs(); + } + ::mlir::ArrayRef<::mlir::Type> getResultTypes() { + return getFunctionType().getResults(); + } + ::mlir::Region *getCallableRegion() { return &getBody(); } + bool isDeclaration() { return false; } + }]; + let hasCustomAssemblyFormat = 1; + let hasVerifier = 1; +} + +class ACIR_ModuleDeclarationOp + : ACIR_Op { + let arguments = (ins SymbolNameAttr:$sym_name, + TypeAttrOf:$function_type, + OptionalAttr:$sym_visibility, + DictionaryAttr:$static_params, + DictionaryAttr:$binding); + let hasVerifier = 1; +} + +def ACIR_ModuleExternOp : ACIR_ModuleDeclarationOp<"module.extern"> { + let arguments = (ins SymbolNameAttr:$sym_name, + TypeAttrOf:$function_type, + OptionalAttr:$sym_visibility, + DictionaryAttr:$static_params, + DictionaryAttr:$implementation); + let assemblyFormat = [{ + $sym_name `:` $function_type `parameters` $static_params + `implementation` $implementation attr-dict + }]; +} + +def ACIR_ModuleGeneratedOp : ACIR_ModuleDeclarationOp<"module.generated"> { + let arguments = (ins SymbolNameAttr:$sym_name, + TypeAttrOf:$function_type, + OptionalAttr:$sym_visibility, + DictionaryAttr:$static_params, + DictionaryAttr:$generator); + let assemblyFormat = [{ + $sym_name `:` $function_type `parameters` $static_params + `generator` $generator attr-dict + }]; +} + +def ACIR_InstanceOp : ACIR_Op<"instance", [Pure]> { + let arguments = (ins Variadic:$inputs, + FlatSymbolRefAttr:$definition, SymbolNameAttr:$sym_name, + StrAttr:$stable_id, StrAttr:$path, + DictionaryAttr:$static_args); + let results = (outs Variadic:$outputs); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `of` $definition `(` $inputs `)` `static` $static_args + `id` $stable_id `path` $path attr-dict `:` functional-type($inputs, $outputs) + }]; +} + +def ACIR_ArrayOp : ACIR_Op<"array", [Pure]> { + let arguments = (ins Variadic:$inputs, + FlatSymbolRefAttr:$definition, SymbolNameAttr:$sym_name, + StrAttr:$stable_id, StrAttr:$path, + DenseI64ArrayAttr:$shape, ArrayAttr:$static_args); + let results = (outs Variadic:$outputs); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `of` $definition `shape` $shape `(` $inputs `)` + `static` $static_args `id` $stable_id `path` $path attr-dict + `:` functional-type($inputs, $outputs) + }]; +} + +def ACIR_InstancesOp : ACIR_Op<"instances", [Pure]> { + let arguments = (ins Variadic:$inputs, + SymbolNameAttr:$sym_name, StrAttr:$stable_id, + StrAttr:$path, ArrayAttr:$definitions, + StrArrayAttr:$names, StrArrayAttr:$stable_ids, + StrArrayAttr:$paths, + TypeAttrOf:$interface, + ArrayAttr:$static_args); + let results = (outs Variadic:$outputs); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `definitions` $definitions `names` $names `ids` $stable_ids + `paths` $paths `interface` $interface `(` $inputs `)` + `static` $static_args `id` $stable_id `path` $path attr-dict + `:` functional-type($inputs, $outputs) + }]; +} + +def ACIR_ViewOp : ACIR_Op<"view", [Pure]> { + let arguments = (ins Variadic:$inputs, + SymbolNameAttr:$sym_name, StrAttr:$kind, + FlatSymbolRefArrayAttr:$source_producers, + ArrayAttr:$source_shapes, + OptionalAttr:$axis, + DenseI64ArrayAttr:$indices, + DenseI64ArrayAttr:$shape); + let results = (outs Variadic:$outputs); + let extraClassDeclaration = [{ + ::mlir::LogicalResult verifyWithProducerIndex( + const ::llvm::StringMap<::mlir::Operation *> &producerIndex); + }]; + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name $kind `(` $inputs `)` `producers` $source_producers + `sources` $source_shapes (`axis` $axis^)? + `indices` $indices `shape` $shape attr-dict + `:` functional-type($inputs, $outputs) + }]; +} + +class ACIR_OwnedStateOp traits = []> + : ACIR_Op + ], traits)>; + +def ACIR_QueueOp : ACIR_OwnedStateOp<"queue"> { + let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$stable_id, + StrAttr:$path, TypeAttr:$payload, + I64Attr:$entry_capacity, + OptionalAttr:$byte_capacity, + StrAttr:$ordering, FlatSymbolRefAttr:$protocol, + StrAttr:$ownership, + OptionalAttr:$watermarks, + DefaultValuedAttr:$delay_ticks); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `payload` $payload `entries` $entry_capacity + (`bytes` $byte_capacity^)? `ordering` $ordering `protocol` $protocol + `ownership` $ownership `id` $stable_id `path` $path + (`watermarks` $watermarks^)? attr-dict + }]; +} + +def ACIR_EventQueueOp : ACIR_OwnedStateOp<"event_queue"> { + let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$stable_id, + StrAttr:$path, TypeAttr:$payload, I64Attr:$capacity, + StrAttr:$ordering, FlatSymbolRefAttr:$time_domain, + DefaultValuedAttr:$delay_ticks); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `payload` $payload `capacity` $capacity `ordering` $ordering + `domain` $time_domain `id` $stable_id `path` $path attr-dict + }]; +} + +def ACIR_ResourceOp : ACIR_OwnedStateOp<"resource"> { + let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$stable_id, + StrAttr:$path, I64Attr:$capacity, + I64Attr:$issue_width, I64Attr:$initiation_interval, + DictionaryAttr:$latency_model, + DictionaryAttr:$lifecycle, StrAttr:$ownership, + OptionalAttr:$arbitration_owner, + ArrayAttr:$transaction_classes, + DefaultValuedAttr:$delay_ticks); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `capacity` $capacity `issue_width` $issue_width + `ii` $initiation_interval `latency` $latency_model + `lifecycle` $lifecycle `ownership` $ownership + (`arbiter` $arbitration_owner^)? `classes` $transaction_classes + `id` $stable_id `path` $path attr-dict + }]; +} + +def ACIR_AddressSpaceOp : ACIR_Op<"address_space"> { + let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$stable_id, + StrAttr:$path, I64Attr:$address_width, + StrAttr:$address_unit, + OptionalAttr:$data_layout, + OptionalAttr:$parent, + OptionalAttr:$translation); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `width` $address_width `unit` $address_unit + `id` $stable_id `path` $path (`layout` $data_layout^)? + (`parent` $parent^ `translate` $translation)? attr-dict + }]; +} + +def ACIR_AddressMapOp : ACIR_Op<"address_map"> { + let arguments = (ins SymbolNameAttr:$sym_name, + FlatSymbolRefAttr:$source, ArrayAttr:$entries, + DictionaryAttr:$default_behavior); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `source` $source `entries` $entries + `default` $default_behavior attr-dict + }]; +} + +def ACIR_TimeDomainOp : ACIR_Op<"time_domain"> { + let arguments = (ins SymbolNameAttr:$sym_name, I64Attr:$period, + I64Attr:$phase, I64Attr:$tick_scale, + OptionalAttr:$parent, + OptionalAttr:$bridge); + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `period` $period `phase` $phase `scale` $tick_scale + (`parent` $parent^ `bridge` $bridge)? attr-dict + }]; +} + +class ACIR_RuntimeEffectOp traits = []> + : ACIR_Op + ], traits)>; + +def ACIR_ProcessOp : ACIR_Op<"process", [ + IsolatedFromAbove, SingleBlock, RecursiveMemoryEffects, ACIR_ProcessRegion + ]> { + let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$kind, + Variadic:$captures); + let regions = (region AnyRegion:$body); + let builders = [OpBuilder<(ins "::llvm::StringRef":$name, + "::llvm::StringRef":$kind, + "::mlir::ValueRange":$captures), [{ + $_state.addAttribute(::mlir::SymbolTable::getSymbolAttrName(), + $_builder.getStringAttr(name)); + $_state.addAttribute("kind", $_builder.getStringAttr(kind)); + $_state.addOperands(captures); + $_state.addRegion(); + }]>]; + let hasVerifier = 1; + let assemblyFormat = [{ + $sym_name `kind` $kind (`captures` `(` $captures^ `:` type($captures) `)`)? + $body attr-dict + }]; +} + +def ACIR_TrySendOp : ACIR_RuntimeEffectOp<"try_send"> { + let arguments = (ins AnyType:$value, FlatSymbolRefAttr:$queue); + let results = (outs I1:$accepted); + let hasVerifier = 1; + let assemblyFormat = "$queue $value attr-dict `:` type($value)"; +} + +def ACIR_TryRecvOp : ACIR_RuntimeEffectOp<"try_recv"> { + let arguments = (ins FlatSymbolRefAttr:$queue); + let results = (outs AnyType:$value, I1:$received); + let hasVerifier = 1; + let assemblyFormat = "$queue attr-dict `:` type($value)"; +} + +def ACIR_ScheduleOp : ACIR_RuntimeEffectOp<"schedule"> { + let arguments = (ins AnyType:$value, I64:$delay, + FlatSymbolRefAttr:$target); + let hasVerifier = 1; + let assemblyFormat = "$target $value `after` $delay attr-dict `:` type($value)"; +} + +def ACIR_WaitUntilOp : ACIR_RuntimeEffectOp<"wait_until"> { + let arguments = (ins I1:$condition); + let hasVerifier = 1; + let assemblyFormat = "$condition attr-dict"; +} + +def ACIR_WaitForOp : ACIR_RuntimeEffectOp<"wait_for"> { + let arguments = (ins FlatSymbolRefAttr:$resource); + let hasVerifier = 1; + let assemblyFormat = "$resource attr-dict"; +} + +def ACIR_AwaitEventOp : ACIR_RuntimeEffectOp<"await_event"> { + let arguments = (ins FlatSymbolRefAttr:$event_queue); + let hasVerifier = 1; + let assemblyFormat = "$event_queue attr-dict"; +} + +def ACIR_YieldSimOp : ACIR_RuntimeEffectOp<"yield_sim", [Terminator]> { + let hasVerifier = 1; + let assemblyFormat = "attr-dict"; +} + +def ACIR_TraceOpenOp : ACIR_RuntimeEffectOp<"trace.open"> { + let arguments = (ins StrAttr:$source); + let results = (outs Index:$cursor); + let hasVerifier = 1; + let assemblyFormat = "`source` $source attr-dict"; +} + +def ACIR_TraceNextOp : ACIR_RuntimeEffectOp<"trace.next"> { + let arguments = (ins Index:$input_cursor, StrAttr:$source); + let results = (outs Index:$cursor, AnyType:$entry, I1:$advanced); + let hasVerifier = 1; + let assemblyFormat = "$input_cursor `from` `source` $source attr-dict `:` type($entry)"; +} + +def ACIR_TraceDecodeOp : ACIR_Op<"trace.decode", [Pure]> { + let arguments = (ins AnyType:$entry); + let results = (outs AnyType:$result); + let hasVerifier = 1; + let assemblyFormat = "$entry attr-dict `:` type($entry) `to` type($result)"; +} + +def ACIR_TraceEofOp : ACIR_RuntimeEffectOp<"trace.eof"> { + let arguments = (ins Index:$input_cursor, StrAttr:$source); + let results = (outs I1:$eof); + let hasVerifier = 1; + let assemblyFormat = "$input_cursor `from` `source` $source attr-dict"; +} + +def ACIR_TracePositionOp : ACIR_RuntimeEffectOp<"trace.position"> { + let arguments = (ins Index:$input_cursor, StrAttr:$source); + let results = (outs Index:$position); + let hasVerifier = 1; + let assemblyFormat = "$input_cursor `from` `source` $source attr-dict"; +} + +class ACIR_ContractOp + : ACIR_RuntimeEffectOp { + let arguments = (ins I1:$condition, StrAttr:$message); + let hasVerifier = 1; + let assemblyFormat = "$condition `,` $message attr-dict"; +} + +def ACIR_RequireOp : ACIR_ContractOp<"require">; +def ACIR_EnsureOp : ACIR_ContractOp<"ensure">; +def ACIR_AssertOp : ACIR_ContractOp<"assert">; + +def ACIR_ProbeOp : ACIR_RuntimeEffectOp<"probe", [ACIR_Observation]> { + let arguments = (ins FlatSymbolRefAttr:$target, StrAttr:$kind); + let results = (outs AnyType:$value); + let hasVerifier = 1; + let assemblyFormat = "$target `kind` $kind attr-dict `:` type($value)"; +} + +def ACIR_StatOp : ACIR_RuntimeEffectOp<"stat", [ACIR_Observation]> { + let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$kind); + let hasVerifier = 1; + let assemblyFormat = "$sym_name `kind` $kind attr-dict"; +} + +def ACIR_StatAddOp : ACIR_RuntimeEffectOp<"stat.add", [ACIR_Observation]> { + let arguments = (ins AnyType:$value, FlatSymbolRefAttr:$stat); + let hasVerifier = 1; + let assemblyFormat = "$stat $value attr-dict `:` type($value)"; +} + +def ACIR_InstrumentationOp : ACIR_Op<"instrumentation", [ + SingleBlock, NoTerminator, RecursiveMemoryEffects, + ACIR_Observation + ]> { + let arguments = (ins SymbolNameAttr:$sym_name); + let regions = (region AnyRegion:$body); + let builders = [OpBuilder<(ins "::llvm::StringRef":$name), [{ + $_state.addAttribute(::mlir::SymbolTable::getSymbolAttrName(), + $_builder.getStringAttr(name)); + $_state.addRegion(); + }]>]; + let hasVerifier = 1; + let assemblyFormat = "$sym_name $body attr-dict"; +} + +def ACIR_ReturnOp : ACIR_Op<"return", [Pure, Terminator, ReturnLike, + HasParent<"ModuleOp">]> { + let arguments = (ins Variadic:$operands); + let assemblyFormat = "attr-dict ($operands^ `:` type($operands))?"; + let hasVerifier = 1; +} + +#endif diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRResources.h b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRResources.h new file mode 100644 index 00000000..9e2d2f49 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRResources.h @@ -0,0 +1,115 @@ +#ifndef ACIR_DIALECT_ACIR_ACIRRESOURCES_H +#define ACIR_DIALECT_ACIR_ACIRRESOURCES_H + +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" + +#include + +namespace mlir { +class Operation; +} + +namespace acir::ac { + +inline constexpr uint64_t kMaxTickScale = uint64_t{1} << 32; +/// ACIR capability bound for exact general mixed-interleave relations. +inline constexpr uint64_t kMaxGeneralSelectorIntersectionQueries = 256; + +/// Wide half-open address endpoint type. A 64-bit address space needs the +/// representable endpoint 2^64 even though its largest address is 2^64-1. +using WideAddress = unsigned __int128; + +struct AddressInterval { + WideAddress begin; + WideAddress end; +}; + +struct AddressMapOrderKey { + uint64_t base; + uint64_t size; + bool hasPriority; + uint64_t priority; +}; + +bool checkedAdd(uint64_t left, uint64_t right, uint64_t &result); +bool checkedMultiply(uint64_t left, uint64_t right, uint64_t &result); +bool intervalsOverlap(AddressInterval left, AddressInterval right); +int compareAddressMapOrder(AddressMapOrderKey left, AddressMapOrderKey right); + +/// Computes the normative global tick phase + cycle * period. The result is +/// bounded by the signed i64 tick domain used by ACIR attributes. +bool checkedDomainTick(uint64_t phase, uint64_t period, uint64_t cycle, + uint64_t &tick); + +/// Converts an exact rational duration to an integral number of global ticks. +/// The duration is numerator/denominator and the global quantum is +/// quantumNumerator/quantumDenominator. The reduced conversion scale (the +/// dimensionless multiplier between duration and quantum) is bounded by +/// kMaxTickScale. Returns false for invalid, inexact, or overflowing +/// conversions; no floating-point rounding is permitted. +bool normalizeRationalToTicks(uint64_t numerator, uint64_t denominator, + uint64_t quantumNumerator, + uint64_t quantumDenominator, uint64_t &ticks); + +struct QueueStateResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.queue.state"; } +}; + +struct EventQueueStateResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.event_queue.state"; } +}; + +struct ReservationStateResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.resource.reservation"; } +}; + +struct ModuleStateResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.module.state"; } +}; + +struct StorageStateResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.storage.state"; } +}; + +struct ProtocolStateResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.protocol.state"; } +}; + +struct TracePositionResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.trace.position"; } +}; + +struct ExternalIOResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.external_io"; } +}; + +struct StatisticsResource + : public mlir::SideEffects::Resource::Base { + llvm::StringRef getName() final { return "ac.statistics"; } +}; + +/// Resolves all Task7 cross-operation references from the ac.module producer +/// index and verifies address/time parent graphs exactly once per module. +mlir::LogicalResult verifyModuleResourceReferences( + mlir::Operation *module, + const llvm::StringMap &producerIndex); + +/// Rewrites address-map set fields and entries into the unique ACIR +/// total order. This is the deterministic normalization phase used by the +/// mandatory public pipeline; operation verifiers remain mutation-free. +void normalizeAddressMaps(mlir::Operation *topLevel); + +} // namespace acir::ac + +#endif diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRTypes.h b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRTypes.h new file mode 100644 index 00000000..6b8a47f6 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRTypes.h @@ -0,0 +1,34 @@ +#ifndef ACIR_DIALECT_ACIR_ACIRTYPES_H +#define ACIR_DIALECT_ACIR_ACIRTYPES_H + +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Types.h" +#include "mlir/Interfaces/DataLayoutInterfaces.h" + +#include "acir/Dialect/ACIR/ACIREnums.h.inc" + +#define GET_TYPEDEF_CLASSES +#include "acir/Dialect/ACIR/ACIRTypes.h.inc" + +namespace acir::ac { + +/// Returns true when `type` is, or recursively contains, an interface-only +/// channel type. +bool containsChannelType(mlir::Type type); + +/// Returns true when `type` is, or recursively contains, a Queue or Var +/// runtime-wrapper type. Immutable payload types cannot contain either wrapper. +bool containsQueueOrVarType(mlir::Type type); + +/// Returns true for a closed, immutable payload value that may be carried by a +/// Queue or represented by a Var. +bool isImmutablePayloadType(mlir::Type type); + +/// Returns true for the broader v0.1 transaction-level payload inventory. This +/// includes dynamically sized lists and remains separate during the hard-break +/// migration. +bool isNormativePayloadType(mlir::Type type); + +} // namespace acir::ac + +#endif // ACIR_DIALECT_ACIR_ACIRTYPES_H diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRTypes.td b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRTypes.td new file mode 100644 index 00000000..99cf791f --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/ACIRTypes.td @@ -0,0 +1,133 @@ +#ifndef ACIR_DIALECT_ACIR_ACIRTYPES_TD +#define ACIR_DIALECT_ACIR_ACIRTYPES_TD + +include "acir/Dialect/ACIR/ACIRAttributes.td" +include "mlir/IR/AttrTypeBase.td" +include "mlir/Interfaces/DataLayoutInterfaces.td" + +class ACIR_Type traits = []> + : TypeDef { + let mnemonic = typeMnemonic; +} + +class ACIR_NamedType traits = []> + : ACIR_Type { + let parameters = (ins "mlir::SymbolRefAttr":$name); + let assemblyFormat = "`<` $name `>`"; +} + +class ACIR_DataNamedType traits = []> + : ACIR_NamedType { + let genVerifyDecl = 1; +} + +class ACIR_LayoutNamedType + : ACIR_DataNamedType + ]>; + +def ACIR_StructType : ACIR_LayoutNamedType<"Struct", "struct">; +def ACIR_PacketType : ACIR_LayoutNamedType<"Packet", "packet">; +def ACIR_TransactionType : ACIR_DataNamedType<"Transaction", "transaction">; +def ACIR_EnumType : ACIR_LayoutNamedType<"Enum", "enum">; +def ACIR_UnionType : ACIR_LayoutNamedType<"Union", "union">; +def ACIR_AddressType : ACIR_NamedType<"Address", "address">; +def ACIR_ResourceTokenType + : ACIR_NamedType<"ResourceToken", "resource_token">; + +def ACIR_OptionalType : ACIR_Type<"Optional", "optional"> { + let parameters = (ins "mlir::Type":$elementType); + let assemblyFormat = "`<` $elementType `>`"; + let genVerifyDecl = 1; +} + +def ACIR_ListType : ACIR_Type<"List", "list"> { + let parameters = (ins "mlir::Type":$elementType); + let assemblyFormat = "`<` $elementType `>`"; + let genVerifyDecl = 1; +} + +def ACIR_VectorType : ACIR_Type<"Vector", "vector"> { + let parameters = (ins "int64_t":$length, "mlir::Type":$elementType); + let assemblyFormat = "`<` $length `x` $elementType `>`"; + let genVerifyDecl = 1; +} + +def ACIR_VarType : ACIR_Type<"Var", "var"> { + let parameters = (ins "mlir::Type":$elementType); + let assemblyFormat = "`<` $elementType `>`"; + let genVerifyDecl = 1; +} + +def ACIR_QueueType : ACIR_Type<"Queue", "queue"> { + let parameters = (ins "mlir::Type":$elementType); + let assemblyFormat = "`<` $elementType `>`"; + let genVerifyDecl = 1; +} + +def ACIR_ArrayType : ACIR_Type<"Array", "array"> { + let parameters = (ins "int64_t":$length, "mlir::Type":$elementType); + let assemblyFormat = "`<` $length `x` $elementType `>`"; + let genVerifyDecl = 1; +} + +def ACIR_MapType : ACIR_Type<"Map", "map"> { + let parameters = (ins "mlir::ArrayAttr":$keys, + "mlir::Type":$elementType); + let assemblyFormat = "`<` $keys `,` $elementType `>`"; + let genVerifyDecl = 1; +} + +def ACIR_SetType : ACIR_Type<"Set", "set"> { + let parameters = (ins "int64_t":$length, "mlir::Type":$elementType); + let assemblyFormat = "`<` $length `x` $elementType `>`"; + let genVerifyDecl = 1; +} + +def ACIR_FlowType : ACIR_Type<"Flow", "flow"> { + let parameters = (ins "mlir::Type":$elementType, + "mlir::FlatSymbolRefAttr":$protocol); + let assemblyFormat = "`<` $elementType `,` $protocol `>`"; + let genVerifyDecl = 1; +} + +def ACIR_EndpointType : ACIR_Type<"Endpoint", "endpoint"> { + let parameters = (ins "mlir::FlatSymbolRefAttr":$interface, + "mlir::FlatSymbolRefAttr":$role); + let assemblyFormat = "`<` $interface `,` $role `>`"; +} + +def ACIR_ResourceRefType : ACIR_Type<"ResourceRef", "resource_ref"> { + let parameters = (ins "mlir::FlatSymbolRefAttr":$resourceType, + "mlir::FlatSymbolRefAttr":$role); + let assemblyFormat = "`<` $resourceType `,` $role `>`"; +} + +def ACIR_ChannelType : ACIR_Type<"Channel", "channel"> { + let parameters = (ins "mlir::Type":$elementType, + "mlir::FlatSymbolRefAttr":$protocol); + let assemblyFormat = "`<` $elementType `,` $protocol `>`"; + let genVerifyDecl = 1; +} + +def ACIR_DurationType : ACIR_Type<"Duration", "duration"> { + let parameters = (ins EnumParameter:$unit); + let assemblyFormat = "`<` $unit `>`"; + let genVerifyDecl = 1; +} + +def ACIR_RateType : ACIR_Type<"Rate", "rate"> { + let parameters = (ins EnumParameter:$numerator, + EnumParameter:$denominator); + let assemblyFormat = "`<` $numerator `,` $denominator `>`"; + let genVerifyDecl = 1; +} + +def ACIR_EventType : ACIR_Type<"Event", "event"> { + let parameters = (ins "mlir::Type":$elementType); + let assemblyFormat = "`<` $elementType `>`"; + let genVerifyDecl = 1; +} + +#endif // ACIR_DIALECT_ACIR_ACIRTYPES_TD diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/CMakeLists.txt b/components/agentic-circuit/include/acir/Dialect/ACIR/CMakeLists.txt new file mode 100644 index 00000000..7d5cf5b0 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/CMakeLists.txt @@ -0,0 +1,24 @@ +set(LLVM_TARGET_DEFINITIONS ACIRDialect.td) +mlir_tablegen(ACIRDialect.h.inc -gen-dialect-decls -dialect=ac) +mlir_tablegen(ACIRDialect.cpp.inc -gen-dialect-defs -dialect=ac) +add_public_tablegen_target(ACIRDialectIncGen) + +set(LLVM_TARGET_DEFINITIONS ACIRAttributes.td) +mlir_tablegen(ACIREnums.h.inc -gen-enum-decls) +mlir_tablegen(ACIREnums.cpp.inc -gen-enum-defs) +add_public_tablegen_target(ACIRAttributesIncGen) + +set(LLVM_TARGET_DEFINITIONS ACIRTypes.td) +mlir_tablegen(ACIRTypes.h.inc -gen-typedef-decls -typedefs-dialect=ac) +mlir_tablegen(ACIRTypes.cpp.inc -gen-typedef-defs -typedefs-dialect=ac) +add_public_tablegen_target(ACIRTypesIncGen) + +set(LLVM_TARGET_DEFINITIONS ACIROps.td) +mlir_tablegen(ACIROps.h.inc -gen-op-decls) +mlir_tablegen(ACIROps.cpp.inc -gen-op-defs) +add_public_tablegen_target(ACIROpsIncGen) + +set(LLVM_TARGET_DEFINITIONS ACIRInterfaces.td) +mlir_tablegen(ACIROpInterfaces.h.inc -gen-op-interface-decls) +mlir_tablegen(ACIROpInterfaces.cpp.inc -gen-op-interface-defs) +add_public_tablegen_target(ACIROpInterfacesIncGen) diff --git a/components/agentic-circuit/include/acir/Dialect/ACIR/GraphRegion.h b/components/agentic-circuit/include/acir/Dialect/ACIR/GraphRegion.h new file mode 100644 index 00000000..d73f7e11 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACIR/GraphRegion.h @@ -0,0 +1,99 @@ +#ifndef ACIR_DIALECT_ACIR_GRAPHREGION_H +#define ACIR_DIALECT_ACIR_GRAPHREGION_H + +#include "mlir/IR/Attributes.h" +#include "mlir/IR/DialectInterface.h" +#include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSet.h" + +#include + +namespace mlir { +class Operation; +} + +namespace acir::ac { + +class ACIRDialect; + +/// Absolute identity assigned to a Task 7/8 state owner by static hierarchy +/// elaboration. This is intentionally distinct from the pre-freeze, +/// definition-qualified identity exposed by the declaration's effect. +struct ElaboratedStateOwner { + mlir::Operation *declaration; + std::string path; + std::string stableId; + /// Logical harness-bound trace sources owned by this process instance. + llvm::SmallVector traceSources; +}; + +/// Every owning structural object expanded in the selected hierarchy. Unlike +/// ElaboratedStateOwner this also includes instances, arrays, ordered +/// collections, and their elements, so topology freeze can persist one stable +/// machine-checkable ownership manifest. +struct ElaboratedTopologyOwner { + mlir::Operation *declaration; + std::string path; + std::string stableId; +}; + +/// Context-owned registry of exact build-time structural providers. The +/// registry is populated by dialect-registry extensions before parsing and is +/// never model/runtime configuration. +class StructuralProviderRegistry { +public: + void registerExternal(llvm::StringRef name); + void registerGenerator(llvm::StringRef name); + bool hasExternal(llvm::StringRef name) const; + bool hasGenerator(llvm::StringRef name) const; + +private: + llvm::StringSet<> externalProviders; + llvm::StringSet<> generatorProviders; +}; + +class StructuralProviderDialectInterface + : public mlir::DialectInterface::Base { +public: + explicit StructuralProviderDialectInterface(mlir::Dialect *dialect) + : Base(dialect) {} + StructuralProviderRegistry &getRegistry() { return registry; } + +private: + StructuralProviderRegistry registry; +}; + +StructuralProviderRegistry & +getStructuralProviderRegistry(mlir::MLIRContext *context); + +/// Verifies whole-file hierarchy selection, stable ownership identities and +/// the statically-resolved subset of topology freeze implemented by ACIR. +mlir::LogicalResult verifyGraphStructure(mlir::Operation *topLevel); + +/// Verifies the graph and returns every elaborated queue/event/resource, +/// address-space, process, and statistics owner in deterministic hierarchy +/// order. +mlir::LogicalResult collectElaboratedStateOwners( + mlir::Operation *topLevel, + llvm::SmallVectorImpl &owners); + +/// Verifies the graph and returns every elaborated structural owner in +/// deterministic hierarchy order. +mlir::LogicalResult collectElaboratedTopologyOwners( + mlir::Operation *topLevel, + llvm::SmallVectorImpl &owners); + +/// Returns true for concrete builtin static parameter values admitted by the +/// public v0.1 graph contract. +bool isConcreteStaticValue(mlir::Attribute value); + +/// Builds the canonical lexicographic element path for a static N-D array. +std::string buildArrayElementPath(llvm::StringRef base, + llvm::ArrayRef indices); + +} // namespace acir::ac + +#endif diff --git a/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimDialect.h b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimDialect.h new file mode 100644 index 00000000..54564c3b --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimDialect.h @@ -0,0 +1,8 @@ +#ifndef ACIR_DIALECT_ACSIM_ACSIMDIALECT_H +#define ACIR_DIALECT_ACSIM_ACSIMDIALECT_H + +#include "mlir/IR/Dialect.h" + +#include "acir/Dialect/ACSim/ACSimDialect.h.inc" + +#endif // ACIR_DIALECT_ACSIM_ACSIMDIALECT_H diff --git a/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimDialect.td b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimDialect.td new file mode 100644 index 00000000..a18f528d --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimDialect.td @@ -0,0 +1,8 @@ +include "mlir/IR/DialectBase.td" + +def ACSim_Dialect : Dialect { + let name = "acsim"; + let cppNamespace = "::acir::acsim"; + let summary = "Agentic Circuit canonical construction dialect"; + let useDefaultTypePrinterParser = 1; +} diff --git a/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimOps.h b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimOps.h new file mode 100644 index 00000000..49ba064a --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimOps.h @@ -0,0 +1,36 @@ +#ifndef ACIR_DIALECT_ACSIM_ACSIMOPS_H +#define ACIR_DIALECT_ACSIM_ACSIMOPS_H + +#include "acir/Dialect/ACSim/ACSimDialect.h" +#include "acir/Dialect/ACSim/ACSimTypes.h" + +#include "mlir/Bytecode/BytecodeOpInterface.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" + +#include + +#define GET_OP_CLASSES +#include "acir/Dialect/ACSim/ACSimOps.h.inc" + +namespace acir::acsim { + +inline constexpr uint64_t kMaxModelNodes = 1ULL << 20; +inline constexpr uint64_t kMaxModelEdges = 1ULL << 22; +inline constexpr uint64_t kMaxModelRegionDepth = 512; +inline constexpr uint64_t kMaxExpandedObjects = 1ULL << 20; +inline constexpr uint64_t kMaxAttributeElements = 1ULL << 20; +inline constexpr uint64_t kMaxAttributeStringBytes = 1ULL << 24; + +/// Whole-file gate used by the canonical optimizer entrypoint. Files without +/// ACSim operations are ignored; files containing ACSim require exactly one +/// closed model. +mlir::LogicalResult verifyCanonicalACSimFile(mlir::ModuleOp module); + +} // namespace acir::acsim + +#endif // ACIR_DIALECT_ACSIM_ACSIMOPS_H diff --git a/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimOps.td b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimOps.td new file mode 100644 index 00000000..652d46f8 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimOps.td @@ -0,0 +1,271 @@ +#ifndef ACIR_DIALECT_ACSIM_ACSIMOPS_TD +#define ACIR_DIALECT_ACSIM_ACSIMOPS_TD + +include "acir/Dialect/ACSim/ACSimTypes.td" +include "mlir/IR/SymbolInterfaces.td" +include "mlir/Interfaces/SideEffectInterfaces.td" + +class ACSim_Op traits = []> + : Op; + +def ACSim_ModelOp : ACSim_Op<"model", [ + Symbol, SymbolTable, IsolatedFromAbove, SingleBlock, NoTerminator, + RecursiveMemoryEffects + ]> { + let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$contract_epoch, + FlatSymbolRefAttr:$root, + ArrayAttr:$construction_order, + ArrayAttr:$destruction_order, + DictionaryAttr:$fingerprints); + let regions = (region AnyRegion:$body); + let assemblyFormat = [{ + $sym_name `epoch` $contract_epoch `root` $root + `construction` $construction_order `destruction` $destruction_order + `fingerprints` $fingerprints attr-dict-with-keyword $body + }]; + let hasVerifier = 1; +} + +def ACSim_TypeOp : ACSim_Op<"type", [Symbol, Pure]> { + let arguments = (ins SymbolNameAttr:$sym_name, StrAttr:$cpp_name, + StrAttr:$kind, StrAttr:$fingerprint, + OptionalAttr:$period, + OptionalAttr:$phase, + OptionalAttr:$tick_scale, + OptionalAttr:$parent, + OptionalAttr:$bridge); + let assemblyFormat = [{ + $sym_name `cpp` $cpp_name `kind` $kind `fingerprint` $fingerprint attr-dict + }]; + let hasVerifier = 1; +} + +def ACSim_BindingOp : ACSim_Op<"binding", [Symbol, Pure]> { + let arguments = (ins SymbolNameAttr:$sym_name, DictionaryAttr:$record); + let assemblyFormat = "$sym_name `record` $record attr-dict"; + let extraClassDeclaration = [{ + ::mlir::StringAttr getBindingSchemaAttr() { + return getRecord().getAs<::mlir::StringAttr>("binding_schema"); + } + ::llvm::StringRef getBindingSchema() { + auto value = getBindingSchemaAttr(); + return value ? value.getValue() : ::llvm::StringRef(); + } + ::mlir::FlatSymbolRefAttr getCppTypeAttr() { + return getRecord().getAs<::mlir::FlatSymbolRefAttr>("cpp_type"); + } + ::mlir::FlatSymbolRefAttr getSchemaAttr() { + return getRecord().getAs<::mlir::FlatSymbolRefAttr>("component_schema"); + } + ::mlir::FlatSymbolRefAttr getProviderAttr() { + return getRecord().getAs<::mlir::FlatSymbolRefAttr>("provider"); + } + ::mlir::FlatSymbolRefAttr getImplementationAttr() { + return getRecord().getAs<::mlir::FlatSymbolRefAttr>("implementation"); + } + ::mlir::StringAttr getEffectAttr() { + return getRecord().getAs<::mlir::StringAttr>("effect"); + } + ::llvm::StringRef getEffect() { + auto value = getEffectAttr(); + return value ? value.getValue() : ::llvm::StringRef(); + } + ::mlir::StringAttr getFingerprintAttr() { + return getRecord().getAs<::mlir::StringAttr>("fingerprint"); + } + ::mlir::DictionaryAttr getCppRecord() { + return getRecord().getAs<::mlir::DictionaryAttr>("cpp"); + } + }]; + let hasVerifier = 1; +} + +def ACSim_ModuleOp : ACSim_Op<"module", [ + Symbol, SingleBlock, RecursiveMemoryEffects + ]> { + let arguments = (ins SymbolNameAttr:$sym_name, DictionaryAttr:$interface, + ArrayAttr:$static_params, + StrAttr:$specialization_fingerprint, + ArrayAttr:$exports); + let regions = (region SizedRegion<1>:$body); + let assemblyFormat = [{ + $sym_name `interface` $interface `static` $static_params + `specialization` $specialization_fingerprint `exports` $exports + attr-dict-with-keyword $body + }]; + let hasVerifier = 1; +} + +def ACSim_InstanceOp : ACSim_Op<"instance"> { + let arguments = (ins SymbolNameAttr:$sym_name, SymbolRefAttr:$target, + ArrayAttr:$static_args, + StrAttr:$specialization_fingerprint); + let results = (outs AnyType:$result); + let assemblyFormat = [{ + $sym_name `target` $target `args` $static_args + `specialization` $specialization_fingerprint attr-dict `:` type($result) + }]; + let hasVerifier = 1; +} + +def ACSim_ArrayOp : ACSim_Op<"array"> { + let arguments = (ins SymbolNameAttr:$sym_name, SymbolRefAttr:$target, + ArrayAttr:$static_args, + StrAttr:$specialization_fingerprint, + DenseI64ArrayAttr:$shape); + let results = (outs AnyType:$result); + let assemblyFormat = [{ + $sym_name `target` $target `args` $static_args + `specialization` $specialization_fingerprint + `shape` $shape attr-dict `:` type($result) + }]; + let hasVerifier = 1; +} + +def ACSim_ElementOp : ACSim_Op<"element", [Pure]> { + let arguments = (ins AnyType:$array, DenseI64ArrayAttr:$indices); + let results = (outs AnyType:$result); + let assemblyFormat = [{ + $array `indices` $indices attr-dict `:` type($array) `->` type($result) + }]; + let hasVerifier = 1; +} + +def ACSim_PortOp : ACSim_Op<"port", [Pure]> { + let arguments = (ins AnyType:$base, FlatSymbolRefAttr:$accessor); + let results = (outs AnyType:$result); + let assemblyFormat = [{ + $base `accessor` $accessor attr-dict `:` type($base) `->` type($result) + }]; + let hasVerifier = 1; +} + +def ACSim_ResourceOp : ACSim_Op<"resource", [Pure]> { + let arguments = (ins AnyType:$base, FlatSymbolRefAttr:$accessor); + let results = (outs AnyType:$result); + let assemblyFormat = [{ + $base `accessor` $accessor attr-dict `:` type($base) `->` type($result) + }]; + let hasVerifier = 1; +} + +def ACSim_BindOp : ACSim_Op<"bind"> { + let arguments = (ins AnyType:$source, AnyType:$target, StrAttr:$kind); + let assemblyFormat = [{ + $source `to` $target `kind` $kind attr-dict `:` type($source) `to` + type($target) + }]; + let hasVerifier = 1; +} + +def ACSim_InlineOp : ACSim_Op<"inline", [Pure]> { + let arguments = (ins Variadic:$args, FlatSymbolRefAttr:$callee); + let results = (outs AnyType:$result); + let assemblyFormat = [{ + $callee `(` $args `)` attr-dict `:` functional-type($args, $result) + }]; + let hasVerifier = 1; +} + +def ACSim_ProcessOp : ACSim_Op<"process", [ + IsolatedFromAbove, RecursiveMemoryEffects + ]> { + let arguments = (ins Variadic:$captures, + SymbolNameAttr:$sym_name, + ArrayAttr:$capture_names, + FlatSymbolRefAttr:$entry_pc, ArrayAttr:$pcs, + ArrayAttr:$live_slots, I64Attr:$fairness_cap, + StrAttr:$specialization_fingerprint); + let regions = (region VariadicRegion:$states); + let hasCustomAssemblyFormat = 1; + let hasVerifier = 1; +} + +def ACSim_LiveLoadOp : ACSim_Op<"live.load"> { + let arguments = (ins FlatSymbolRefAttr:$process, StrAttr:$slot); + let results = (outs AnyType:$result); + let assemblyFormat = [{ + $process `slot` $slot attr-dict `:` type($result) + }]; + let hasVerifier = 1; +} + +def ACSim_LiveStoreOp : ACSim_Op<"live.store"> { + let arguments = (ins AnyType:$value, FlatSymbolRefAttr:$process, + StrAttr:$slot); + let assemblyFormat = [{ + $value `in` $process `slot` $slot attr-dict `:` type($value) + }]; + let hasVerifier = 1; +} + +def ACSim_InvokeOp : ACSim_Op<"invoke"> { + let arguments = (ins Variadic:$args, FlatSymbolRefAttr:$callee); + let results = (outs Variadic:$results); + let assemblyFormat = [{ + $callee `(` $args `)` attr-dict `:` functional-type($args, $results) + }]; + let hasVerifier = 1; +} + +def ACSim_ContinueOp : ACSim_Op<"continue", [Terminator]> { + let arguments = (ins FlatSymbolRefAttr:$target_pc); + let assemblyFormat = "$target_pc attr-dict"; + let hasVerifier = 1; +} + +def ACSim_SuspendOp : ACSim_Op<"suspend", [Terminator]> { + let arguments = (ins AnyType:$wake, FlatSymbolRefAttr:$target_pc); + let assemblyFormat = "$target_pc `on` $wake attr-dict `:` type($wake)"; + let hasVerifier = 1; +} + +def ACSim_TerminateOp : ACSim_Op<"terminate", [Terminator]> { + let arguments = (ins StrAttr:$status); + let assemblyFormat = "$status attr-dict"; + let hasVerifier = 1; +} + +def ACSim_ExportOp : ACSim_Op<"export", [Pure]> { + let arguments = (ins AnyType:$value, SymbolNameAttr:$sym_name, + FlatSymbolRefAttr:$role); + let results = (outs AnyType:$result); + let assemblyFormat = [{ + $sym_name $value `role` $role attr-dict `:` type($value) `->` type($result) + }]; + let hasVerifier = 1; +} + +def ACSim_DispatchOp : ACSim_Op<"dispatch"> { + let arguments = (ins SymbolRefAttr:$target, StrAttr:$path, + DenseI64ArrayAttr:$indices, + I64Attr:$object_id, I64Attr:$activation_id, + StrAttr:$work, StrAttr:$xfer, StrAttr:$reset, + StrAttr:$validate); + let results = (outs ACSim_ObjectIdType:$object, + ACSim_ActivationIdType:$activation); + let assemblyFormat = [{ + $target `path` $path `indices` $indices + `object` $object_id `activation` $activation_id + `work` $work `xfer` $xfer `reset` $reset `validate` $validate attr-dict + `:` type($object) `,` type($activation) + }]; + let hasVerifier = 1; +} + +def ACSim_ActivateOp : ACSim_Op<"activate"> { + let arguments = (ins ACSim_ActivationIdType:$source, + ACSim_ObjectIdType:$target); + let assemblyFormat = [{ + $source `to` $target attr-dict `:` type($source) `to` type($target) + }]; + let hasVerifier = 1; +} + +def ACSim_ReturnOp : ACSim_Op<"return", [Terminator]> { + let arguments = (ins Variadic:$values); + let assemblyFormat = "($values^ `:` type($values))? attr-dict"; + let hasVerifier = 1; +} + +#endif // ACIR_DIALECT_ACSIM_ACSIMOPS_TD diff --git a/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimTypes.h b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimTypes.h new file mode 100644 index 00000000..4320edb8 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimTypes.h @@ -0,0 +1,20 @@ +#ifndef ACIR_DIALECT_ACSIM_ACSIMTYPES_H +#define ACIR_DIALECT_ACSIM_ACSIMTYPES_H + +#include "acir/Dialect/ACSim/ACSimDialect.h" + +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Types.h" + +#include + +#define GET_TYPEDEF_CLASSES +#include "acir/Dialect/ACSim/ACSimTypes.h.inc" + +namespace acir::acsim { + +inline constexpr uint64_t kMaxArrayVolume = 1ULL << 20; + +} // namespace acir::acsim + +#endif // ACIR_DIALECT_ACSIM_ACSIMTYPES_H diff --git a/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimTypes.td b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimTypes.td new file mode 100644 index 00000000..49965da7 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACSim/ACSimTypes.td @@ -0,0 +1,58 @@ +#ifndef ACIR_DIALECT_ACSIM_ACSIMTYPES_TD +#define ACIR_DIALECT_ACSIM_ACSIMTYPES_TD + +include "acir/Dialect/ACSim/ACSimDialect.td" +include "mlir/IR/AttrTypeBase.td" + +class ACSim_Type traits = []> + : TypeDef { + let mnemonic = typeMnemonic; +} + +class ACSim_SymbolType + : ACSim_Type { + let parameters = (ins "mlir::SymbolRefAttr":$symbol); + let assemblyFormat = "`<` $symbol `>`"; +} + +def ACSim_ValueType : ACSim_SymbolType<"Value", "value">; +def ACSim_ExprType : ACSim_SymbolType<"Expr", "expr">; + +def ACSim_OwnerType : ACSim_Type<"Owner", "owner"> { + let parameters = (ins "mlir::SymbolRefAttr":$realization); + let assemblyFormat = "`<` $realization `>`"; +} + +def ACSim_RefType : ACSim_Type<"Ref", "ref"> { + let parameters = (ins "mlir::SymbolRefAttr":$realization); + let assemblyFormat = "`<` $realization `>`"; +} + +def ACSim_PortType : ACSim_Type<"Port", "port"> { + let parameters = (ins "mlir::SymbolRefAttr":$interface, + "mlir::SymbolRefAttr":$role, + "mlir::SymbolRefAttr":$payload, + "mlir::SymbolRefAttr":$protocol); + let assemblyFormat = + "`<` $interface `,` $role `,` $payload `,` $protocol `>`"; +} + +def ACSim_ResourceType : ACSim_Type<"Resource", "resource"> { + let parameters = (ins "mlir::SymbolRefAttr":$resource, + "mlir::SymbolRefAttr":$role); + let assemblyFormat = "`<` $resource `,` $role `>`"; +} + +def ACSim_ArrayType : ACSim_Type<"Array", "array"> { + let parameters = (ins "mlir::DenseI64ArrayAttr":$shape, + "mlir::Type":$elementType); + let hasCustomAssemblyFormat = 1; + let genVerifyDecl = 1; +} + +def ACSim_ObjectIdType : ACSim_Type<"ObjectId", "object_id">; +def ACSim_ActivationIdType : ACSim_Type<"ActivationId", "activation_id">; +def ACSim_PcType : ACSim_SymbolType<"Pc", "pc">; +def ACSim_WakeType : ACSim_SymbolType<"Wake", "wake">; + +#endif // ACIR_DIALECT_ACSIM_ACSIMTYPES_TD diff --git a/components/agentic-circuit/include/acir/Dialect/ACSim/CMakeLists.txt b/components/agentic-circuit/include/acir/Dialect/ACSim/CMakeLists.txt new file mode 100644 index 00000000..189cf359 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/ACSim/CMakeLists.txt @@ -0,0 +1,14 @@ +set(LLVM_TARGET_DEFINITIONS ACSimDialect.td) +mlir_tablegen(ACSimDialect.h.inc -gen-dialect-decls -dialect=acsim) +mlir_tablegen(ACSimDialect.cpp.inc -gen-dialect-defs -dialect=acsim) +add_public_tablegen_target(ACSimDialectIncGen) + +set(LLVM_TARGET_DEFINITIONS ACSimTypes.td) +mlir_tablegen(ACSimTypes.h.inc -gen-typedef-decls -typedefs-dialect=acsim) +mlir_tablegen(ACSimTypes.cpp.inc -gen-typedef-defs -typedefs-dialect=acsim) +add_public_tablegen_target(ACSimTypesIncGen) + +set(LLVM_TARGET_DEFINITIONS ACSimOps.td) +mlir_tablegen(ACSimOps.h.inc -gen-op-decls) +mlir_tablegen(ACSimOps.cpp.inc -gen-op-defs) +add_public_tablegen_target(ACSimOpsIncGen) diff --git a/components/agentic-circuit/include/acir/Dialect/CMakeLists.txt b/components/agentic-circuit/include/acir/Dialect/CMakeLists.txt new file mode 100644 index 00000000..a0fe9b40 --- /dev/null +++ b/components/agentic-circuit/include/acir/Dialect/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(ACIR) +add_subdirectory(ACSim) diff --git a/components/agentic-circuit/include/acir/InitAllDialects.h b/components/agentic-circuit/include/acir/InitAllDialects.h new file mode 100644 index 00000000..7bffaba4 --- /dev/null +++ b/components/agentic-circuit/include/acir/InitAllDialects.h @@ -0,0 +1,26 @@ +#ifndef ACIR_INITALLDIALECTS_H +#define ACIR_INITALLDIALECTS_H + +#include "acir/Dialect/ACIR/ACIRDialect.h" +#include "acir/Dialect/ACSim/ACSimDialect.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlow.h" +#include "mlir/Dialect/DLTI/DLTI.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Index/IR/IndexDialect.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinDialect.h" +#include "mlir/IR/DialectRegistry.h" + +namespace acir { + +inline void registerAllDialects(mlir::DialectRegistry ®istry) { + registry.insert(); +} + +} // namespace acir + +#endif // ACIR_INITALLDIALECTS_H diff --git a/components/agentic-circuit/include/acir/InitAllPasses.h b/components/agentic-circuit/include/acir/InitAllPasses.h new file mode 100644 index 00000000..25c04c87 --- /dev/null +++ b/components/agentic-circuit/include/acir/InitAllPasses.h @@ -0,0 +1,25 @@ +#ifndef ACIR_INITALLPASSES_H +#define ACIR_INITALLPASSES_H + +#include "acir/Transforms/Passes.h" +#include "mlir/Pass/PassRegistry.h" +#include "mlir/Transforms/Passes.h" + +#include + +namespace acir { + +inline void registerAllPasses() { + mlir::registerTransformsPasses(); + registerACIRTransformsPasses(); + mlir::registerPass([]() -> std::unique_ptr { + return createNormalizeACIRFilePass(); + }); + mlir::registerPass([]() -> std::unique_ptr { + return createVerifyACIRFilePass(); + }); +} + +} // namespace acir + +#endif // ACIR_INITALLPASSES_H diff --git a/components/agentic-circuit/include/acir/Transforms/CMakeLists.txt b/components/agentic-circuit/include/acir/Transforms/CMakeLists.txt new file mode 100644 index 00000000..c109cf76 --- /dev/null +++ b/components/agentic-circuit/include/acir/Transforms/CMakeLists.txt @@ -0,0 +1,3 @@ +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls -name ACIRTransforms) +add_public_tablegen_target(ACIRTransformsIncGen) diff --git a/components/agentic-circuit/include/acir/Transforms/Passes.h b/components/agentic-circuit/include/acir/Transforms/Passes.h new file mode 100644 index 00000000..331d7511 --- /dev/null +++ b/components/agentic-circuit/include/acir/Transforms/Passes.h @@ -0,0 +1,31 @@ +#ifndef ACIR_TRANSFORMS_PASSES_H +#define ACIR_TRANSFORMS_PASSES_H + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Support/LogicalResult.h" + +#include + +namespace acir { + +std::unique_ptr createNormalizeACIRFilePass(); +std::unique_ptr createVerifyACIRFilePass(); +std::unique_ptr createLowerProcessStatePass(); + +#define GEN_PASS_DECL_VERIFYMODELPASS +#define GEN_PASS_DECL_LOWERPROCESSSTATEPASS +#define GEN_PASS_DECL_CANONICALIZEMODELPASS +#define GEN_PASS_DECL_FREEZETOPOLOGYPASS +#include "acir/Transforms/Passes.h.inc" + +/// Shared implementation used by ac-canonicalize-model and the atomic freeze +/// pass. It is idempotent and never depends on host pointer order. +mlir::LogicalResult canonicalizeModel(mlir::ModuleOp model); + +#define GEN_PASS_REGISTRATION +#include "acir/Transforms/Passes.h.inc" + +} // namespace acir + +#endif // ACIR_TRANSFORMS_PASSES_H diff --git a/components/agentic-circuit/include/acir/Transforms/Passes.td b/components/agentic-circuit/include/acir/Transforms/Passes.td new file mode 100644 index 00000000..1e286e76 --- /dev/null +++ b/components/agentic-circuit/include/acir/Transforms/Passes.td @@ -0,0 +1,41 @@ +#ifndef ACIR_TRANSFORMS_PASSES +#define ACIR_TRANSFORMS_PASSES + +include "mlir/Pass/PassBase.td" + +def VerifyModelPass : Pass<"ac-verify-model", "::mlir::ModuleOp"> { + let summary = "Verify complete Agentic Circuit model semantics"; + let description = [{ + Runs local ACIR verification together with deterministic whole-model + hierarchy, ownership, purity, zero-delay dependency, and frozen-integrity + checks. + }]; +} + +def CanonicalizeModelPass + : Pass<"ac-canonicalize-model", "::mlir::ModuleOp"> { + let summary = "Canonicalize Agentic Circuit model topology"; + let description = [{ + Sorts symbol and Graph declarations by stable semantic keys and removes + source-location variance before deterministic topology freeze. + }]; +} + +def FreezeTopologyPass : Pass<"ac-freeze-topology", "::mlir::ModuleOp"> { + let summary = "Freeze deterministic Agentic Circuit topology"; + let description = [{ + Verifies the selected static architecture, proves freeze-phase contracts, + binds elaborated owner paths and stable IDs, and persists an integrity + digest that makes later topology mutation a hard error. + }]; +} + +def LowerProcessStatePass : Pass<"ac-lower-process-state", "::mlir::ModuleOp"> { + let summary = "Plan ACSim process state from frozen ACIR"; + let description = [{ + Analyzes a selected topology-frozen model without mutation and produces + an immutable ProcessStatePlanSet. + }]; +} + +#endif // ACIR_TRANSFORMS_PASSES diff --git a/components/agentic-circuit/include/acir/Transforms/ResolveBindings.h b/components/agentic-circuit/include/acir/Transforms/ResolveBindings.h new file mode 100644 index 00000000..578a2f9f --- /dev/null +++ b/components/agentic-circuit/include/acir/Transforms/ResolveBindings.h @@ -0,0 +1,35 @@ +#ifndef ACIR_TRANSFORMS_RESOLVEBINDINGS_H +#define ACIR_TRANSFORMS_RESOLVEBINDINGS_H + +#include "acir/Bindings/Registry.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Pass/Pass.h" +#include "llvm/Support/Error.h" + +#include +#include +#include + +namespace acir { + +struct ResolveBindingsPassOptions { + std::vector candidates; + std::vector requests; + std::string profile; + std::string target; + std::string lockOutputPath; +}; + +/// Resolves a frozen module without mutating its topology or creating ACSim +/// operations. The returned immutable result is the Task 12 lowering input. +llvm::Expected +resolveModuleBindings(mlir::ModuleOp module, + const ResolveBindingsPassOptions &options); + +std::unique_ptr +createResolveBindingsPass(ResolveBindingsPassOptions options); + +} // namespace acir + +#endif // ACIR_TRANSFORMS_RESOLVEBINDINGS_H diff --git a/components/agentic-circuit/include/gfsim/components.h b/components/agentic-circuit/include/gfsim/components.h new file mode 100644 index 00000000..4039a886 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/components.h @@ -0,0 +1,941 @@ +#ifndef GFSIM_COMPONENTS_H +#define GFSIM_COMPONENTS_H + +#include "gfsim/core.h" +#include "gfsim/object.h" +#include "gfsim/queue.h" +#include "gfsim/resource.h" +#include "gfsim/trace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +// ── Baseline component concepts ─────────────────────────────────────── + +/// A component that can be registered in the SimSystem. +template +concept Component = std::derived_from && requires(T &t, Epoch e) { + { T::contractName } -> std::convertible_to; + { T::componentKind } -> std::convertible_to; + { t.doWork(e) } -> std::same_as; + { t.doXfer(e) } -> std::same_as; + { std::as_const(t).hasPendingCommit() } -> std::same_as; +}; + +// ── Compute ─────────────────────────────────────────────────────────── + +/// A stateless compute component that transforms inputs to outputs. +/// Pure, zero-delay, effect-free. +template struct IdentityComputePolicy { + T operator()(const T &input) const { return input; } +}; + +template > + requires std::invocable && + std::convertible_to< + std::invoke_result_t, + Output> +class Compute : public SimObject { +public: + static constexpr std::string_view contractName = "ac.Compute"; + static constexpr ObjectKind componentKind = ObjectKind::Compute; + + Compute(std::string name, ObjectId id, SimObject *parent, + FunctionalPolicy policy = {}, ObservationSink *observations = nullptr) + : SimObject(ObjectKind::Compute, std::move(name), id, parent, + observations), + policy_(std::move(policy)) {} + + void setInput(Input value) { + inputProposal_ = std::move(value); + hasInput_ = true; + } + + void doWork(Epoch) override { + if (hasInput_) { + outputProposal_ = std::invoke(std::as_const(policy_), inputProposal_); + hasOutput_ = true; + emitObservation({.category = "transaction", + .name = "completed", + .phase = TraceEventPhase::Complete, + .duration = 0}); + } + } + + void doXfer(Epoch epoch) override { + if (hasOutput_) { + output_ = outputProposal_; + hasOutput_ = false; + ++totalComputations_; + lastUpdate_ = epoch; + } + hasInput_ = false; + } + + bool hasPendingCommit() const override { return hasOutput_; } + + const Output &output() const { return output_; } + bool isRunnable(Epoch) const override { return hasInput_; } + + void collectStatistics(std::vector &out) const override { + out.push_back({.name = "completed_transactions", + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = totalComputations_, + .lastUpdate = lastUpdate_}); + } + + void reset() override { + output_ = {}; + outputProposal_ = {}; + inputProposal_ = {}; + hasInput_ = false; + hasOutput_ = false; + totalComputations_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + [[no_unique_address]] FunctionalPolicy policy_; + Output output_{}; + Output outputProposal_{}; + Input inputProposal_{}; + bool hasInput_ = false; + bool hasOutput_ = false; + uint64_t totalComputations_ = 0; + Epoch lastUpdate_; +}; + +// ── Sink ────────────────────────────────────────────────────────────── + +/// A terminal component that consumes data and produces statistics. +template class Sink : public SimObject { +public: + static constexpr std::string_view contractName = "ac.Sink"; + static constexpr ObjectKind componentKind = ObjectKind::Sink; + + Sink(std::string name, ObjectId id, SimObject *parent, + ObservationSink *observations = nullptr) + : SimObject(ObjectKind::Sink, std::move(name), id, parent, observations) { + } + + void receive(T value) { receivedProposals_.push_back(std::move(value)); } + + void doArbitrate(Epoch) override { + for (size_t index = 0; index < receivedProposals_.size(); ++index) + emitObservation({.category = "transaction", + .name = "accepted", + .phase = TraceEventPhase::Instant}); + } + + void doXfer(Epoch epoch) override { + bool changed = !receivedProposals_.empty(); + for (auto v : receivedProposals_) { + received_.push_back(v); + ++totalReceived_; + } + receivedProposals_.clear(); + if (changed) + lastUpdate_ = epoch; + } + + bool hasPendingCommit() const override { return !receivedProposals_.empty(); } + + const std::vector &received() const { return received_; } + uint64_t totalReceived() const { return totalReceived_; } + + bool isRunnable(Epoch) const override { return false; } + + void collectStatistics(std::vector &out) const override { + out.push_back({.name = "accepted_transactions", + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = totalReceived_, + .lastUpdate = lastUpdate_}); + } + + void reset() override { + received_.clear(); + receivedProposals_.clear(); + totalReceived_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + std::vector received_; + std::vector receivedProposals_; + uint64_t totalReceived_ = 0; + Epoch lastUpdate_; +}; + +// ── Link ────────────────────────────────────────────────────────────── + +/// A connector that forwards data between components. +template class Link : public SimObject { +public: + static constexpr std::string_view contractName = "ac.Link"; + static constexpr ObjectKind componentKind = ObjectKind::Link; + + Link(std::string name, ObjectId id, SimObject *parent, + ObservationSink *observations = nullptr) + : SimObject(ObjectKind::Link, std::move(name), id, parent, observations) { + } + + void forward(T value) { + forwardedProposal_ = std::move(value); + hasProposal_ = true; + } + + void doArbitrate(Epoch) override { + if (hasProposal_) + emitObservation({.category = "transaction", + .name = "completed", + .phase = TraceEventPhase::Instant}); + } + + void doXfer(Epoch epoch) override { + if (hasProposal_) { + forwarded_ = forwardedProposal_; + hasForwarded_ = true; + hasProposal_ = false; + ++totalTransfers_; + lastUpdate_ = epoch; + } + } + + bool hasPendingCommit() const override { return hasProposal_; } + + const T &value() const { return forwarded_; } + bool hasValue() const { return hasForwarded_; } + + bool isRunnable(Epoch) const override { return hasProposal_; } + + void collectStatistics(std::vector &out) const override { + out.push_back({.name = "completed_transactions", + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = totalTransfers_, + .lastUpdate = lastUpdate_}); + } + + void reset() override { + forwarded_ = {}; + forwardedProposal_ = {}; + hasProposal_ = false; + hasForwarded_ = false; + totalTransfers_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + T forwarded_{}; + T forwardedProposal_{}; + bool hasProposal_ = false; + bool hasForwarded_ = false; + uint64_t totalTransfers_ = 0; + Epoch lastUpdate_; +}; + +// ── Memory ──────────────────────────────────────────────────────────── + +/// A storage component with read/write proposals and capacity. +template class Memory : public SimObject { +public: + static constexpr std::string_view contractName = "ac.Memory"; + static constexpr ObjectKind componentKind = ObjectKind::Memory; + + Memory(std::string name, ObjectId id, SimObject *parent, size_t capacity, + ObservationSink *observations = nullptr) + : SimObject(ObjectKind::Memory, std::move(name), id, parent, + observations), + storage_(capacity) {} + + size_t capacity() const { return storage_.size(); } + + bool proposeWrite(size_t addr, T value) { + if (addr >= storage_.size()) + return false; + writeProposals_[addr] = std::move(value); + return true; + } + + T read(size_t addr) const { + if (addr >= storage_.size()) + return {}; + return storage_[addr]; + } + + void doArbitrate(Epoch) override { + for (const auto &proposal : writeProposals_) + emitObservation( + {.category = "memory", + .name = "write", + .phase = TraceEventPhase::Instant, + .arguments = {{"address", static_cast(proposal.first)}}}); + } + + void doXfer(Epoch epoch) override { + size_t committedWrites = writeProposals_.size(); + for (auto &[addr, value] : writeProposals_) + storage_[addr] = value; + writeProposals_.clear(); + totalWrites_ += committedWrites; + if (committedWrites != 0) + lastUpdate_ = epoch; + } + + bool hasPendingCommit() const override { return !writeProposals_.empty(); } + + void collectStatistics(std::vector &out) const override { + out.push_back({.name = "accepted_transactions", + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = totalWrites_, + .lastUpdate = lastUpdate_}); + } + + void reset() override { + std::fill(storage_.begin(), storage_.end(), T{}); + writeProposals_.clear(); + totalWrites_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + std::vector storage_; + std::map writeProposals_; + uint64_t totalWrites_ = 0; + Epoch lastUpdate_; +}; + +// ── Scheduler ──────────────────────────────────────────────────────── + +/// A finite deterministic scheduler. Proposal admission checks identity; +/// arbitration selects by stable keys and never by insertion order. +template class Scheduler final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.Scheduler"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + Scheduler(std::string name, ObjectId id, SimObject *parent, size_t capacity, + ObservationSink *observations = nullptr) + : SimObject(ObjectKind::Scheduler, std::move(name), id, parent, + observations), + capacity_(capacity) {} + + bool proposeSchedule(T value, uint32_t priority, uint32_t portIndex, + uint32_t instanceIndex, ObjectId ownerId, + uint64_t transactionId) { + if (ownerId == kInvalidObjectId || containsIdentity(ownerId, transactionId)) + return false; + proposals_.push_back({std::move(value), + priority, + {}, + portIndex, + instanceIndex, + ownerId, + transactionId}); + return true; + } + + std::optional proposePop() { + if (popProposalCount_ >= committed_.size()) + return std::nullopt; + return committed_[popProposalCount_++].value; + } + + const T *peek() const { + return committed_.empty() ? nullptr : &committed_.front().value; + } + + void doArbitrate(Epoch epoch) override { + for (Entry &entry : proposals_) + entry.issueEpoch = epoch; + std::sort(proposals_.begin(), proposals_.end(), stableLess); + size_t occupied = committed_.size() + accepted_.size(); + size_t available = capacity_ > occupied ? capacity_ - occupied : 0; + size_t acceptedCount = std::min(available, proposals_.size()); + accepted_.insert( + accepted_.end(), std::make_move_iterator(proposals_.begin()), + std::make_move_iterator(proposals_.begin() + acceptedCount)); + rejected_.insert( + rejected_.end(), + std::make_move_iterator(proposals_.begin() + acceptedCount), + std::make_move_iterator(proposals_.end())); + proposals_.clear(); + auto observe = [&](const Entry &entry, std::string category, + std::string name) { + emitObservation({.category = std::move(category), + .name = std::move(name), + .phase = TraceEventPhase::Instant, + .rootSequenceId = entry.transactionId, + .arguments = {{"child_owner_id", + static_cast(entry.ownerId)}, + {"transaction_id", entry.transactionId}}}); + }; + for (const Entry &entry : accepted_) + observe(entry, "transaction", "accepted"); + for (const Entry &entry : rejected_) + observe(entry, "stall", "capacity"); + if (!accepted_.empty() || !rejected_.empty() || popProposalCount_ != 0) { + const uint64_t occupancy = + committed_.size() - std::min(popProposalCount_, committed_.size()) + + accepted_.size(); + emitObservation({.category = "queue", + .name = "occupancy", + .phase = TraceEventPhase::Counter, + .arguments = {{"occupancy", occupancy}}}); + } + } + + void doXfer(Epoch epoch) override { + bool changed = hasPendingCommit(); + size_t popped = std::min(popProposalCount_, committed_.size()); + committed_.erase(committed_.begin(), committed_.begin() + popped); + totalPops_ += popped; + popProposalCount_ = 0; + + totalScheduled_ += accepted_.size(); + committed_.insert(committed_.end(), + std::make_move_iterator(accepted_.begin()), + std::make_move_iterator(accepted_.end())); + accepted_.clear(); + std::sort(committed_.begin(), committed_.end(), stableLess); + + for (const Entry &entry : rejected_) + rejectedTransactions_.push_back(entry.transactionId); + rejected_.clear(); + highWatermark_ = std::max(highWatermark_, committed_.size()); + if (changed) + lastUpdate_ = epoch; + } + + bool hasPendingCommit() const override { + return !proposals_.empty() || !accepted_.empty() || !rejected_.empty() || + popProposalCount_ != 0; + } + + RuntimeObjectState runtimeState(Epoch epoch) const override { + RuntimeObjectState state = SimObject::runtimeState(epoch); + state.queueOccupancy = committed_.size(); + state.pendingOffers = + proposals_.size() + accepted_.size() + rejected_.size(); + state.quiescent = committed_.empty() && !hasPendingCommit(); + if (!state.quiescent) + state.reason = hasPendingCommit() ? "scheduler_pending_proposal" + : "scheduler_not_empty"; + return state; + } + + void collectStatistics(std::vector &out) const override { + auto append = [&](std::string name, uint64_t value, StatisticKind kind) { + out.push_back({.name = std::move(name), + .objectPath = std::string(path()), + .kind = kind, + .value = value, + .lastUpdate = lastUpdate_}); + }; + append("queue_occupancy", committed_.size(), StatisticKind::Gauge); + append("queue_occupancy_peak", highWatermark_, StatisticKind::Gauge); + append("accepted_transactions", totalScheduled_, StatisticKind::Counter); + append("completed_transactions", totalPops_, StatisticKind::Counter); + } + + bool isRunnable(Epoch) const override { return !proposals_.empty(); } + size_t capacity() const { return capacity_; } + size_t size() const { return committed_.size(); } + bool empty() const { return committed_.empty(); } + size_t highWatermark() const { return highWatermark_; } + uint64_t totalScheduled() const { return totalScheduled_; } + uint64_t totalPops() const { return totalPops_; } + const std::vector &rejectedTransactions() const { + return rejectedTransactions_; + } + + bool validate() const { + if (committed_.size() > capacity_) + return false; + for (size_t left = 0; left < committed_.size(); ++left) + for (size_t right = left + 1; right < committed_.size(); ++right) + if (sameIdentity(committed_[left], committed_[right])) + return false; + return true; + } + + void reset() override { + proposals_.clear(); + accepted_.clear(); + rejected_.clear(); + committed_.clear(); + rejectedTransactions_.clear(); + popProposalCount_ = 0; + highWatermark_ = 0; + totalScheduled_ = 0; + totalPops_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + struct Entry { + T value; + uint32_t priority; + Epoch issueEpoch; + uint32_t portIndex; + uint32_t instanceIndex; + ObjectId ownerId; + uint64_t transactionId; + }; + + static auto stableKey(const Entry &entry) { + return std::tie(entry.priority, entry.issueEpoch, entry.portIndex, + entry.instanceIndex, entry.ownerId, entry.transactionId); + } + static bool stableLess(const Entry &left, const Entry &right) { + return stableKey(left) < stableKey(right); + } + static bool sameIdentity(const Entry &left, const Entry &right) { + return left.ownerId == right.ownerId && + left.transactionId == right.transactionId; + } + bool containsIdentity(ObjectId ownerId, uint64_t transactionId) const { + auto matches = [&](const Entry &entry) { + return entry.ownerId == ownerId && entry.transactionId == transactionId; + }; + return std::any_of(proposals_.begin(), proposals_.end(), matches) || + std::any_of(accepted_.begin(), accepted_.end(), matches) || + std::any_of(committed_.begin(), committed_.end(), matches); + } + + size_t capacity_; + std::vector proposals_; + std::vector accepted_; + std::vector rejected_; + std::vector committed_; + std::vector rejectedTransactions_; + size_t popProposalCount_ = 0; + size_t highWatermark_ = 0; + uint64_t totalScheduled_ = 0; + uint64_t totalPops_ = 0; + Epoch lastUpdate_; +}; + +// ── ReadyValid ──────────────────────────────────────────────────────── + +template class ReadyValid : public SimObject { +public: + static constexpr std::string_view contractName = "ac.ready_valid"; + static constexpr ObjectKind componentKind = ObjectKind::Link; + + ReadyValid(std::string name, ObjectId id, SimObject *parent, + ObservationSink *observations = nullptr) + : SimObject(ObjectKind::Link, std::move(name), id, parent, observations) { + } + + bool proposeOffer(T data) { + if (offer_ || offerProposal_) + return false; + offerProposal_ = std::move(data); + return true; + } + + void proposeReady(bool ready) { readyProposal_ = ready; } + bool isReady() const { return ready_; } + bool hasOffer() const { return offer_.has_value(); } + const T *peekOffer() const { return offer_ ? &*offer_ : nullptr; } + + void doArbitrate(Epoch) override { + const bool nextReady = readyProposal_.value_or(ready_); + const bool nextOffer = offer_.has_value() || offerProposal_.has_value(); + if (nextOffer && nextReady) { + emitObservation({.category = "transaction", + .name = "completed", + .phase = TraceEventPhase::Instant}); + } else if (offerProposal_ && !nextReady) { + emitObservation({.category = "stall", + .name = "backpressure", + .phase = TraceEventPhase::Instant, + .arguments = {{"pending_offers", uint64_t{1}}}}); + } + } + + void doXfer(Epoch epoch) override { + bool changed = hasPendingCommit() || (offer_ && ready_); + if (readyProposal_) + ready_ = *readyProposal_; + if (offerProposal_) + offer_ = std::move(offerProposal_); + readyProposal_.reset(); + offerProposal_.reset(); + + if (offer_ && ready_) { + lastTransferred_ = *offer_; + offer_.reset(); + ++transferCount_; + } + if (changed) + lastUpdate_ = epoch; + } + + bool hasPendingCommit() const override { + return offerProposal_.has_value() || readyProposal_.has_value(); + } + + RuntimeObjectState runtimeState(Epoch epoch) const override { + RuntimeObjectState state = SimObject::runtimeState(epoch); + state.pendingOffers = offer_.has_value() + offerProposal_.has_value(); + state.protocolState = ready_ ? "ready" : "backpressure"; + state.quiescent = !offer_ && !hasPendingCommit(); + if (!state.quiescent) + state.reason = + offer_ ? "ready_valid_offer_blocked" : "ready_valid_pending_proposal"; + return state; + } + + void collectStatistics(std::vector &out) const override { + out.push_back({.name = "completed_transactions", + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = transferCount_, + .lastUpdate = lastUpdate_}); + } + + const T &lastTransferred() const { return lastTransferred_; } + uint64_t transferCount() const { return transferCount_; } + bool isRunnable(Epoch) const override { return hasPendingCommit(); } + bool validate() const { return !(offer_ && ready_); } + + void reset() override { + ready_ = false; + offer_.reset(); + offerProposal_.reset(); + readyProposal_.reset(); + lastTransferred_ = {}; + transferCount_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + bool ready_ = false; + std::optional offer_; + std::optional offerProposal_; + std::optional readyProposal_; + T lastTransferred_{}; + uint64_t transferCount_ = 0; + Epoch lastUpdate_; +}; + +// ── RequestResponse ────────────────────────────────────────────────── + +template +class RequestResponse : public SimObject { +public: + static constexpr std::string_view contractName = "ac.request_response"; + static constexpr ObjectKind componentKind = ObjectKind::Link; + + RequestResponse(std::string name, ObjectId id, SimObject *parent, + size_t maxInFlight = 16, + ObservationSink *observations = nullptr) + : SimObject(ObjectKind::Link, std::move(name), id, parent, observations), + maxInFlight_(maxInFlight) {} + + struct RequestEnvelope { + Req payload; + uint64_t correlationId; + }; + struct ResponseEnvelope { + Resp payload; + uint64_t correlationId; + }; + + bool proposeRequest(Req request, uint64_t correlationId) { + if (active_.size() + requestProposals_.size() >= maxInFlight_ || + containsCorrelation(correlationId)) + return false; + requestProposals_.push_back({std::move(request), correlationId}); + return true; + } + + const RequestEnvelope *peekRequest() const { + return committedRequests_.empty() ? nullptr : &committedRequests_.front(); + } + + std::optional proposePopRequest() { + if (requestPopCount_ >= committedRequests_.size()) + return std::nullopt; + return committedRequests_[requestPopCount_++]; + } + + bool proposeResponse(Resp response, uint64_t correlationId) { + if (!active_.contains(correlationId) || + !deliveredRequests_.contains(correlationId) || + std::any_of(responseProposals_.begin(), responseProposals_.end(), + [&](const ResponseEnvelope &entry) { + return entry.correlationId == correlationId; + })) + return false; + responseProposals_.push_back({std::move(response), correlationId}); + return true; + } + + bool hasResponse() const { return !committedResponses_.empty(); } + const ResponseEnvelope *peekResponse() const { + return committedResponses_.empty() ? nullptr : &committedResponses_.front(); + } + std::optional proposePopResponse() { + if (responsePopCount_ >= committedResponses_.size()) + return std::nullopt; + return committedResponses_[responsePopCount_++]; + } + + void doArbitrate(Epoch) override { + auto observe = [&](uint64_t correlationId, std::string name) { + emitObservation({.category = "transaction", + .name = std::move(name), + .phase = TraceEventPhase::Instant, + .rootSequenceId = correlationId, + .arguments = {{"correlation_id", correlationId}}}); + }; + for (const RequestEnvelope &request : requestProposals_) + observe(request.correlationId, "accepted"); + for (size_t index = 0; + index < std::min(requestPopCount_, committedRequests_.size()); ++index) + observe(committedRequests_[index].correlationId, "request"); + for (const ResponseEnvelope &response : responseProposals_) + observe(response.correlationId, "completed"); + for (size_t index = 0; + index < std::min(responsePopCount_, committedResponses_.size()); + ++index) + observe(committedResponses_[index].correlationId, "response"); + } + + void doXfer(Epoch epoch) override { + bool changed = hasPendingCommit(); + size_t requestPops = std::min(requestPopCount_, committedRequests_.size()); + for (size_t index = 0; index < requestPops; ++index) + deliveredRequests_.insert(committedRequests_[index].correlationId); + committedRequests_.erase(committedRequests_.begin(), + committedRequests_.begin() + requestPops); + requestPopCount_ = 0; + + size_t responsePops = + std::min(responsePopCount_, committedResponses_.size()); + committedResponses_.erase(committedResponses_.begin(), + committedResponses_.begin() + responsePops); + responsePopCount_ = 0; + + for (auto &request : requestProposals_) { + active_.insert(request.correlationId); + committedRequests_.push_back(std::move(request)); + } + requestProposals_.clear(); + + for (auto &response : responseProposals_) { + active_.erase(response.correlationId); + deliveredRequests_.erase(response.correlationId); + committedResponses_.push_back(std::move(response)); + ++totalCompleted_; + } + responseProposals_.clear(); + if (changed) + lastUpdate_ = epoch; + } + + bool hasPendingCommit() const override { + return !requestProposals_.empty() || !responseProposals_.empty() || + requestPopCount_ != 0 || responsePopCount_ != 0; + } + + RuntimeObjectState runtimeState(Epoch epoch) const override { + RuntimeObjectState state = SimObject::runtimeState(epoch); + state.pendingOffers = requestProposals_.size() + responseProposals_.size() + + committedRequests_.size() + + committedResponses_.size(); + state.correlationChain.assign(active_.begin(), active_.end()); + state.protocolState = active_.empty() ? "idle" : "in_flight"; + state.quiescent = + active_.empty() && state.pendingOffers == 0 && !hasPendingCommit(); + if (!state.quiescent) + state.reason = "request_response_blocked"; + return state; + } + + void collectStatistics(std::vector &out) const override { + out.push_back({.name = "completed_transactions", + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = totalCompleted_, + .lastUpdate = lastUpdate_}); + out.push_back({.name = "active_correlations", + .objectPath = std::string(path()), + .kind = StatisticKind::Gauge, + .value = active_.size(), + .lastUpdate = lastUpdate_}); + } + + size_t inFlight() const { return active_.size(); } + size_t maxInFlight() const { return maxInFlight_; } + uint64_t totalCompleted() const { return totalCompleted_; } + + bool validate() const { + if (active_.size() > maxInFlight_ || + active_.size() + requestProposals_.size() > maxInFlight_) + return false; + for (uint64_t correlationId : deliveredRequests_) + if (!active_.contains(correlationId)) + return false; + for (const RequestEnvelope &request : committedRequests_) + if (!active_.contains(request.correlationId)) + return false; + for (const ResponseEnvelope &response : responseProposals_) + if (!active_.contains(response.correlationId) || + !deliveredRequests_.contains(response.correlationId)) + return false; + return true; + } + + void reset() override { + requestProposals_.clear(); + responseProposals_.clear(); + committedRequests_.clear(); + committedResponses_.clear(); + active_.clear(); + deliveredRequests_.clear(); + requestPopCount_ = 0; + responsePopCount_ = 0; + totalCompleted_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + bool containsCorrelation(uint64_t correlationId) const { + auto requestMatches = [&](const RequestEnvelope &entry) { + return entry.correlationId == correlationId; + }; + auto responseMatches = [&](const ResponseEnvelope &entry) { + return entry.correlationId == correlationId; + }; + return active_.contains(correlationId) || + std::any_of(requestProposals_.begin(), requestProposals_.end(), + requestMatches) || + std::any_of(committedResponses_.begin(), committedResponses_.end(), + responseMatches); + } + + size_t maxInFlight_; + uint64_t totalCompleted_ = 0; + std::vector requestProposals_; + std::vector committedRequests_; + std::vector responseProposals_; + std::vector committedResponses_; + std::set active_; + std::set deliveredRequests_; + size_t requestPopCount_ = 0; + size_t responsePopCount_ = 0; + Epoch lastUpdate_; +}; + +// ── Protocol state ──────────────────────────────────────────────────── + +enum class ProtocolPhase : uint8_t { + Idle, + Request, + Response, + Transfer, + Backpressure, +}; + +class ProtocolState { +public: + explicit ProtocolState(size_t maxCredits = 1) + : maxCredits_(maxCredits), credits_(maxCredits) {} + ProtocolPhase phase() const { return phase_; } + size_t credits() const { return credits_; } + size_t inFlight() const { return inFlight_; } + bool canSend() const { + return phase_ != ProtocolPhase::Backpressure && credits_ > 0 && + inFlight_ < maxCredits_; + } + bool canReceive() const { return inFlight_ > 0; } + bool startRequest() { + if (!canSend()) + return false; + --credits_; + ++inFlight_; + phase_ = ProtocolPhase::Request; + return true; + } + bool beginResponse() { + if (phase_ == ProtocolPhase::Backpressure || inFlight_ == 0) + return false; + phase_ = ProtocolPhase::Response; + return true; + } + bool completeResponse() { + if (phase_ != ProtocolPhase::Response || inFlight_ == 0) + return false; + --inFlight_; + ++credits_; + phase_ = inFlight_ > 0 ? ProtocolPhase::Transfer : ProtocolPhase::Idle; + return true; + } + bool setBackpressure(bool enabled) { + if (enabled) { + if (phase_ == ProtocolPhase::Backpressure) + return false; + resumePhase_ = phase_; + phase_ = ProtocolPhase::Backpressure; + return true; + } + if (phase_ != ProtocolPhase::Backpressure) + return false; + phase_ = resumePhase_; + return true; + } + bool validate() const { + if (credits_ > maxCredits_ || inFlight_ > maxCredits_ || + credits_ + inFlight_ != maxCredits_) + return false; + if (phase_ == ProtocolPhase::Idle) + return inFlight_ == 0; + if (phase_ == ProtocolPhase::Backpressure) + return resumePhase_ == ProtocolPhase::Idle || inFlight_ > 0; + return inFlight_ > 0; + } + void reset() { + phase_ = ProtocolPhase::Idle; + resumePhase_ = ProtocolPhase::Idle; + credits_ = maxCredits_; + inFlight_ = 0; + } + +private: + ProtocolPhase phase_ = ProtocolPhase::Idle; + ProtocolPhase resumePhase_ = ProtocolPhase::Idle; + size_t maxCredits_, credits_; + size_t inFlight_ = 0; +}; + +// ── No-progress diagnostics ────────────────────────────────────────── + +} // namespace gfsim + +#endif // GFSIM_COMPONENTS_H diff --git a/components/agentic-circuit/include/gfsim/core.h b/components/agentic-circuit/include/gfsim/core.h new file mode 100644 index 00000000..43077284 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/core.h @@ -0,0 +1,216 @@ +#ifndef GFSIM_CORE_H +#define GFSIM_CORE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +// ── Exact time and epoch ────────────────────────────────────────────── + +/// Simulation time is a non-negative, unbounded-in-semantics integer tick. +using Tick = uint64_t; + +/// Causal-delta index within a single tick. +using CausalDelta = uint32_t; + +/// The global epoch is the exact pair (time, delta). Equality, ordering, +/// scheduling, tracing, and wake decisions use this exact pair. +struct Epoch { + Tick time = 0; + CausalDelta delta = 0; + + auto operator<=>(const Epoch &) const = default; + + Epoch nextDelta() const { return {time, delta + 1}; } + bool sameTime(const Epoch &other) const { return time == other.time; } +}; + +/// Maximum causal deltas per tick (must be finite). +inline constexpr CausalDelta kMaxDeltasPerTick = 1024; + +// ── Object identity ─────────────────────────────────────────────────── + +/// Compile-time-assigned stable object ID. +using ObjectId = uint32_t; + +inline constexpr ObjectId kInvalidObjectId = + std::numeric_limits::max(); +inline constexpr ObjectId kSystemObjectId = kInvalidObjectId - 1; +inline constexpr ObjectId kRootObjectId = kInvalidObjectId - 2; + +/// Statically known runtime kind for every SimObject. +enum class ObjectKind : uint8_t { + System, + Module, + Queue, + EventQueue, + Resource, + Process, + TraceSource, + Compute, + Link, + Memory, + Sink, + Probe, + Statistic, + Scheduler, +}; + +// ── Termination ─────────────────────────────────────────────────────── + +enum class TerminationClass : uint8_t { + Completed, // All work finished, no pending events, trace exhausted + Incomplete, // Reached a declared cap without contract violation + Failed, // Contract violation, assertion, or runtime error +}; + +struct TimeDomainRuntime { + std::string name; + uint64_t period = 1; + uint64_t phase = 0; + uint64_t tickScale = 1; +}; + +struct RuntimeLimits { + std::optional deadlockWindow; + std::optional maxTicks; + std::map maxDomainCycles; +}; + +struct TerminationResult { + TerminationClass classification = TerminationClass::Incomplete; + Epoch finalEpoch; + uint64_t committedEventCount = 0; + uint64_t tracePosition = 0; + std::optional traceLastCommittedSequenceId; + std::optional terminationCap; + std::map domainCycles; + std::string diagnosticCode; + std::optional message; +}; + +// ── Build profiles ──────────────────────────────────────────────────── + +enum class BuildProfile : uint8_t { + Fast, // Required representation, safety, capacity, and contract checks + Validated, // Fast + post-pass verification, transaction-lifetime checks, + // arbitration audits, event provenance, quiescence checks + Custom, // Explicit pass pipeline retaining all mandatory Fast checks +}; + +// ── Static preflight ────────────────────────────────────────────────── + +struct PreflightResult { + bool passed = false; + std::vector errors; + std::vector warnings; +}; + +// ── Work and proposal ───────────────────────────────────────────────── + +/// A proposal is a private buffer associated with a stable object ID. +/// Each Work execution writes only to its own proposal. +struct Proposal { + ObjectId ownerId = kInvalidObjectId; + bool hasCommit = false; + // Component-specific proposal data is stored in derived types +}; + +// ── Event ───────────────────────────────────────────────────────────── + +/// An event scheduled for a future epoch. +struct Event { + Epoch readyTime; + ObjectId targetId = kInvalidObjectId; + uint32_t eventKind = 0; + uint64_t payload = 0; + + // Stable ordering: epoch first, then target, event kind, and payload. + auto operator<=>(const Event &other) const { + if (auto cmp = readyTime <=> other.readyTime; cmp != 0) + return cmp; + if (auto cmp = targetId <=> other.targetId; cmp != 0) + return cmp; + if (auto cmp = eventKind <=> other.eventKind; cmp != 0) + return cmp; + return payload <=> other.payload; + } +}; + +// ── Statistics ──────────────────────────────────────────────────────── + +enum class StatisticKind : uint8_t { Counter, Gauge, Histogram }; + +struct HistogramBucket { + uint64_t upperBound = 0; + uint64_t count = 0; + bool operator==(const HistogramBucket &) const = default; +}; + +struct StatSnapshot { + std::string name; + std::string objectPath; + StatisticKind kind = StatisticKind::Counter; + uint64_t value = 0; + uint64_t count = 0; + uint64_t sum = 0; + uint64_t minimum = 0; + uint64_t maximum = 0; + std::vector buckets; + Epoch lastUpdate; +}; + +struct RuntimeObjectState { + bool quiescent = true; + bool runnable = false; + bool pendingCommit = false; + std::string reason; + std::vector subscriptions; + std::vector dependencyChain; + std::vector correlationChain; + size_t queueOccupancy = 0; + size_t pendingOffers = 0; + size_t activeReservations = 0; + std::string protocolState; + bool traceOwner = false; + uint64_t tracePosition = 0; + std::optional traceLastCommittedSequenceId; + bool traceEof = false; +}; + +struct BlockedObject { + ObjectId id = kInvalidObjectId; + std::string path; + std::string reason; + std::vector subscriptions; + std::vector dependencyChain; + std::vector correlationChain; + size_t queueOccupancy = 0; + size_t pendingOffers = 0; + size_t activeReservations = 0; + std::string protocolState; +}; + +struct NoProgressReport { + std::vector blockedObjects; + size_t queueOccupancy = 0; + size_t pendingOffers = 0; + size_t activeReservations = 0; + std::optional nextEvent; + uint64_t tracePosition = 0; + std::optional lastCommittedSequenceId; + std::string summary; + bool empty() const { return blockedObjects.empty() && !nextEvent; } +}; + +} // namespace gfsim + +#endif // GFSIM_CORE_H diff --git a/components/agentic-circuit/include/gfsim/dispatch.h b/components/agentic-circuit/include/gfsim/dispatch.h new file mode 100644 index 00000000..b6dbf688 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/dispatch.h @@ -0,0 +1,165 @@ +#ifndef GFSIM_DISPATCH_H +#define GFSIM_DISPATCH_H + +#include "gfsim/core.h" + +#include +#include +#include +#include + +namespace gfsim { + +class SimObject; + +/// The two invocations of the generated Xfer thunk preserve the global +/// arbitration/Xfer barrier without adding a second dispatch entry point. +enum class XferPhase : uint8_t { Arbitrate, Probe, Commit }; + +using WorkThunk = void (*)(void *, Epoch); +using XferThunk = bool (*)(void *, Epoch, XferPhase); +using ResetThunk = void (*)(void *); +using ValidateThunk = bool (*)(const void *, ObjectId, ObjectKind); + +/// One generated row per runtime object. Rows are indexed by their dense, +/// stable ObjectId; the object pointer is recovered only by typed thunks. +struct DispatchRow { + ObjectId id = kInvalidObjectId; + ObjectKind kind = ObjectKind::Module; + void *object = nullptr; + WorkThunk work = nullptr; + XferThunk xfer = nullptr; + ResetThunk reset = nullptr; + ValidateThunk validate = nullptr; +}; + +template +concept DispatchObject = + std::derived_from && + requires(T &object, const T &constObject, Epoch epoch) { + { constObject.id() } -> std::convertible_to; + { constObject.kind() } -> std::same_as; + { constObject.hasPendingCommit() } -> std::same_as; + object.doWork(epoch); + object.doArbitrate(epoch); + object.doXfer(epoch); + object.reset(); + }; + +template DispatchRow makeDispatchRow(T *object) { + return DispatchRow{ + .id = object ? object->id() : kInvalidObjectId, + .kind = object ? object->kind() : ObjectKind::Module, + .object = static_cast(object), + .work = + [](void *storage, Epoch epoch) { + static_cast(static_cast(storage)) + ->T::doWork(epoch); + }, + .xfer = + [](void *storage, Epoch epoch, XferPhase phase) { + T *typed = static_cast(static_cast(storage)); + if (phase == XferPhase::Arbitrate) { + typed->T::doArbitrate(epoch); + return false; + } + bool committed = typed->T::hasPendingCommit(); + if (phase == XferPhase::Probe) + return committed; + typed->T::doXfer(epoch); + return committed; + }, + .reset = + [](void *storage) { + static_cast(static_cast(storage))->T::reset(); + }, + .validate = + [](const void *storage, ObjectId id, ObjectKind kind) { + const T *typed = + static_cast(static_cast(storage)); + if (typed->id() != id || typed->kind() != kind) + return false; + if constexpr (requires(const T &typed) { + { typed.validate() } -> std::convertible_to; + }) + return typed->T::validate(); + return true; + }, + }; +} + +/// Non-owning view of the generated static table. Generated storage has static +/// lifetime; tests may provide an array whose lifetime encloses the system. +class DispatchTable { +public: + DispatchTable() = default; + explicit DispatchTable(std::span rows) : rows_(rows) {} + + size_t size() const { return rows_.size(); } + bool empty() const { return rows_.empty(); } + + const DispatchRow *lookup(ObjectId id) const { + if (id >= rows_.size()) + return nullptr; + return &rows_[id]; + } + + bool validate() const { + for (size_t index = 0; index < rows_.size(); ++index) { + const DispatchRow &row = rows_[index]; + if (row.id != index || !row.object || !row.work || !row.xfer || + !row.reset || !row.validate || + !row.validate(row.object, row.id, row.kind)) + return false; + } + return true; + } + +private: + std::span rows_; +}; + +/// Canonical compressed adjacency indexed by activation-source/object ID. +class ActivationPlan { +public: + ActivationPlan() = default; + ActivationPlan(std::span offsets, + std::span targets) + : offsets_(offsets), targets_(targets) {} + + bool empty() const { return offsets_.empty(); } + + bool validate(size_t objectCount) const { + if (offsets_.size() != objectCount + 1 || offsets_.empty() || + targets_.size() > std::numeric_limits::max() || + offsets_.front() != 0 || offsets_.back() != targets_.size()) + return false; + for (size_t source = 0; source < objectCount; ++source) { + uint32_t begin = offsets_[source]; + uint32_t end = offsets_[source + 1]; + if (begin > end || end > targets_.size()) + return false; + for (uint32_t index = begin; index < end; ++index) { + if (targets_[index] >= objectCount || + (index > begin && targets_[index - 1] >= targets_[index])) + return false; + } + } + return true; + } + + std::span targetsFor(ObjectId source) const { + if (offsets_.empty() || source >= offsets_.size() - 1) + return {}; + return targets_.subspan(offsets_[source], + offsets_[source + 1] - offsets_[source]); + } + +private: + std::span offsets_; + std::span targets_; +}; + +} // namespace gfsim + +#endif // GFSIM_DISPATCH_H diff --git a/components/agentic-circuit/include/gfsim/harness.h b/components/agentic-circuit/include/gfsim/harness.h new file mode 100644 index 00000000..059d17e6 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/harness.h @@ -0,0 +1,131 @@ +#ifndef GFSIM_HARNESS_H +#define GFSIM_HARNESS_H + +#include "gfsim/core.h" +#include "gfsim/trace.h" + +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/Error.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +struct HarnessFileHash { + std::string path; + std::string sha256; +}; + +struct TraceIdentity { + std::string path; + std::string schema; + std::string version; + std::string sha256; +}; + +struct TerminationExpectation { + std::string kind; + std::optional reason; +}; + +struct RunManifest { + HarnessFileHash buildManifest; + TraceIdentity trace; + uint64_t seed = 0; + std::string outputDirectory; + RuntimeLimits limits; + std::string statsFormat; + std::string eventLog; + TerminationExpectation expectation; + + // Cold-path resolution identity, not part of the public JSON document. + std::string rootDirectory; + std::string manifestSha256; +}; + +enum class RunStatus { Completed, Incomplete, Failed }; + +struct ValidationResult { + std::string status; + std::optional reportSha256; +}; + +struct RunResultDocument { + HarnessFileHash runManifest; + RunStatus status = RunStatus::Failed; + std::string terminationReason; + uint64_t simulatedTicks = 0; + std::map domainCycles; + uint64_t eventCount = 0; + TracePosition tracePosition; + std::vector outputs; + ValidationResult validation; +}; + +llvm::Expected loadRunManifest(llvm::StringRef bytes, + llvm::StringRef rootDirectory); + +llvm::Expected +preflightRunManifest(const RunManifest &manifest, + llvm::StringRef buildFingerprint, + std::span timeDomains, + llvm::StringRef resultStage); + +RunResultDocument makeRunResult(const RunManifest &manifest, + const TerminationResult &termination); + +llvm::Error publishRunResult(const RunManifest &manifest, + RunResultDocument &result, + std::span statistics, + std::span events, + llvm::StringRef resultStage); + +template +llvm::Expected runGeneratedModel(Model &model, + const RunManifest &manifest, + llvm::StringRef resultStage) + requires requires(Model &value, const RuntimeLimits &limits) { + { value.loadTrace(PtoTraceDocument{}) } -> std::same_as; + value.configure(limits); + { value.run() } -> std::same_as; + { value.buildFingerprint() } -> std::convertible_to; + { + value.timeDomains() + } -> std::convertible_to>; + { value.statistics() } -> std::same_as>; + { + value.observations() + } -> std::convertible_to>; + } +{ + auto trace = preflightRunManifest( + manifest, std::string_view(model.buildFingerprint()), + std::span(model.timeDomains()), resultStage); + if (!trace) + return trace.takeError(); + if (!model.loadTrace(std::move(*trace))) + return llvm::createStringError(llvm::errc::invalid_argument, + "ACRUN-PREFLIGHT-001: generated model " + "rejected the validated trace document"); + model.configure(manifest.limits); + RunResultDocument result = makeRunResult(manifest, model.run()); + std::vector statistics = model.statistics(); + std::span events = model.observations(); + if (llvm::Error error = + publishRunResult(manifest, result, statistics, events, resultStage)) + return std::move(error); + return result; +} + +} // namespace gfsim + +#endif // GFSIM_HARNESS_H diff --git a/components/agentic-circuit/include/gfsim/npu.h b/components/agentic-circuit/include/gfsim/npu.h new file mode 100644 index 00000000..5bcc2804 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/npu.h @@ -0,0 +1,318 @@ +#ifndef GFSIM_NPU_H +#define GFSIM_NPU_H + +#include "gfsim/components.h" +#include "gfsim/observation.h" +#include "gfsim/trace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +enum class NpuEngineClass : uint8_t { + Scalar, + Vector, + Cube, + Tma, +}; + +struct NpuTimestamps { + std::optional decoded; + std::optional dispatched; + std::optional issued; + std::optional completed; + std::optional retired; + + bool operator==(const NpuTimestamps &) const = default; +}; + +struct NpuScalarImmediate { + std::string type; + PtoValue value; + + bool operator==(const NpuScalarImmediate &) const = default; +}; + +struct NpuTileDescriptor { + std::string identity; + std::string address; + std::string type; + std::string layout; + PtoValue::Array shape; + + bool operator==(const NpuTileDescriptor &) const = default; +}; + +struct NpuInstruction { + uint64_t sequenceId = 0; + uint64_t blockId = 0; + std::string opcode; + std::vector dependencies; + std::vector operands; + std::vector inputTiles; + std::vector outputTiles; + std::vector inputTileDescriptors; + std::vector outputTileDescriptors; + std::vector scalarInputs; + NpuEngineClass engine = NpuEngineClass::Scalar; + NpuTimestamps timestamps; + + bool operator==(const NpuInstruction &) const = default; +}; + +struct NpuDecodeDiagnostic { + std::string code; + std::string message; + + bool operator==(const NpuDecodeDiagnostic &) const = default; +}; + +struct NpuDecodeResult { + std::optional instruction; + std::vector diagnostics; + std::vector observations; + + bool succeeded() const { + return instruction.has_value() && diagnostics.empty(); + } + std::string_view primaryDiagnostic() const { + return diagnostics.empty() ? std::string_view{} : diagnostics.front().code; + } +}; + +class NpuDecoder { +public: + NpuDecodeResult decode(const PtoTraceRecord &record) const; + std::optional operator()(const PtoTraceRecord &record) const { + return decode(record).instruction; + } +}; + +std::string_view toString(NpuEngineClass engine); + +struct NpuIssueQueueCapacities { + size_t scalar = 0; + size_t vector = 0; + size_t cube = 0; + size_t tma = 0; +}; + +struct NpuDependency { + uint64_t producerSequenceId = 0; + std::string tileIdentity; + uint64_t flowId = 0; + + bool operator==(const NpuDependency &) const = default; +}; + +struct NpuIssueEntry { + NpuInstruction instruction; + ObjectId stableObjectId = kInvalidObjectId; + std::vector derivedDependencies; + + bool operator==(const NpuIssueEntry &) const = default; +}; + +/// Block-local tile rename state and four finite deterministic issue queues. +class NpuDependencyTracker final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.npu.DependencyTracker"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + NpuDependencyTracker(std::string name, ObjectId id, SimObject *parent, + NpuIssueQueueCapacities capacities, + ObservationSink *observations = nullptr); + + bool proposeDispatch(const NpuInstruction &instruction, + ObjectId stableObjectId); + bool proposeIssue(NpuEngineClass engine); + bool proposeComplete(uint64_t sequenceId); + + void doArbitrate(Epoch epoch) override; + void doXfer(Epoch epoch) override; + bool hasPendingCommit() const override; + bool isRunnable(Epoch epoch) const override; + RuntimeObjectState runtimeState(Epoch epoch) const override; + void collectStatistics(std::vector &out) const override; + void reset() override; + + const NpuIssueEntry *proposedIssue(NpuEngineClass engine) const; + const std::vector &issued() const { return issued_; } + std::vector queued(NpuEngineClass engine) const; + std::vector dependencies(uint64_t sequenceId) const; + bool isReady(uint64_t sequenceId) const; + bool dispatchAccepted(uint64_t sequenceId) const; + size_t queueSize(NpuEngineClass engine) const; + const std::vector &rejectedDispatches() const { + return rejectedDispatches_; + } + +private: + using TileKey = std::pair; + + struct DispatchProposal { + NpuInstruction instruction; + ObjectId stableObjectId = kInvalidObjectId; + }; + + static size_t engineIndex(NpuEngineClass engine); + size_t capacity(NpuEngineClass engine) const; + bool knownSequence(uint64_t sequenceId) const; + bool ready(const NpuIssueEntry &entry) const; + + NpuIssueQueueCapacities capacities_; + std::array, 4> queues_; + std::vector dispatchProposals_; + std::vector acceptedDispatches_; + std::vector proposedRejectedDispatches_; + std::vector rejectedDispatches_; + std::set acceptedDispatchSequences_; + std::array, 4> issueProposals_; + std::vector issued_; + std::vector completionProposals_; + std::set outstandingSequences_; + std::set completedSequences_; + std::map producers_; + std::set usedFlowIds_; + std::optional lastDispatchedSequence_; + std::array highWatermarks_{}; + uint64_t totalDispatches_ = 0; + uint64_t totalDispatchStalls_ = 0; + uint64_t totalIssues_ = 0; + uint64_t totalDependencyWakeups_ = 0; + Epoch lastUpdate_; +}; + +struct NpuExecutionConfig { + size_t scalarUnits = 0; + size_t vectorUnits = 0; + size_t cubeUnits = 0; + size_t tmaUnits = 0; + size_t memoryRequests = 0; + size_t scratchpadTiles = 0; +}; + +struct NpuMemoryRequest { + uint64_t sequenceId = 0; + uint64_t correlationId = 0; + uint64_t address = 0; + bool write = false; + std::string tileIdentity; + + bool operator==(const NpuMemoryRequest &) const = default; +}; + +struct NpuMemoryResponse { + uint64_t correlationId = 0; + uint64_t value = 0; + + bool operator==(const NpuMemoryResponse &) const = default; +}; + +struct NpuArchitecturalResult { + uint64_t retiredInstructions = 0; + uint64_t digest = 14695981039346656037ULL; + std::vector retiredSequenceIds; + + bool operator==(const NpuArchitecturalResult &) const = default; +}; + +/// Finite event-driven execution, memory, completion, and retirement state. +class NpuExecutionPipeline final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.npu.ExecutionPipeline"; + static constexpr ObjectKind componentKind = ObjectKind::Compute; + + NpuExecutionPipeline(std::string name, ObjectId id, SimObject *parent, + NpuExecutionConfig config, SimSystem *system = nullptr, + ObservationSink *observations = nullptr); + ~NpuExecutionPipeline() override; + + bool proposeAdmit(const NpuInstruction &instruction); + bool proposeExecute(const NpuIssueEntry &entry, Epoch issueEpoch); + bool proposeTraceExhausted(); + + void doWork(Epoch epoch) override; + void doArbitrate(Epoch epoch) override; + void doXfer(Epoch epoch) override; + bool hasPendingCommit() const override; + bool isRunnable(Epoch epoch) const override; + RuntimeObjectState runtimeState(Epoch epoch) const override; + void collectStatistics(std::vector &out) const override; + void bindSystem(SimSystem *system) override; + void reset() override; + + static uint64_t executionLatency(const NpuInstruction &instruction); + Epoch completionEpoch(uint64_t sequenceId) const; + bool canAccept(NpuEngineClass engine) const; + size_t activeExecutions(NpuEngineClass engine) const; + const std::vector &completed() const; + const std::vector &retired() const; + const std::vector &memoryRequests() const; + const std::vector &memoryResponses() const; + bool scratchpadContains(std::string_view tileIdentity) const; + std::optional globalMemoryValue(uint64_t address) const; + const NpuArchitecturalResult &architecturalResult() const; + bool complete() const; + +private: + struct Impl; + std::unique_ptr impl_; +}; + +/// Trace-owning provider component for the checked-in hierarchical NPU model. +class NpuTraceSource final : public SimObject { +public: + static constexpr std::string_view contractName = "workspace.Npu"; + static constexpr ObjectKind componentKind = ObjectKind::TraceSource; + + NpuTraceSource(std::string name, ObjectId id, SimObject *parent, + ObservationSink *observations = nullptr); + + bool loadDocument(PtoTraceDocument document); + void doWork(Epoch epoch) override; + void doXfer(Epoch epoch) override; + bool hasPendingCommit() const override; + RuntimeObjectState runtimeState(Epoch epoch) const override; + void collectStatistics(std::vector &out) const override; + void reset() override; + bool validate() const; + +private: + PtoTraceDocument document_; + NpuArchitecturalResult result_; + uint64_t eventCount_ = 0; + bool loaded_ = false; + bool pending_ = false; + bool committed_ = false; + Epoch lastUpdate_; +}; + +/// Quiescent structural marker used to preserve the generated NPU hierarchy. +class NpuNode final : public SimObject { +public: + static constexpr std::string_view contractName = "workspace.NpuNode"; + static constexpr ObjectKind componentKind = ObjectKind::Compute; + + NpuNode(std::string name, ObjectId id, SimObject *parent) + : SimObject(ObjectKind::Compute, std::move(name), id, parent) {} + + RuntimeObjectState runtimeState(Epoch) const override { + return {.quiescent = true}; + } + bool validate() const { return true; } +}; + +} // namespace gfsim + +#endif // GFSIM_NPU_H diff --git a/components/agentic-circuit/include/gfsim/object.h b/components/agentic-circuit/include/gfsim/object.h new file mode 100644 index 00000000..436cd630 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/object.h @@ -0,0 +1,308 @@ +#ifndef GFSIM_OBJECT_H +#define GFSIM_OBJECT_H + +#include "gfsim/core.h" +#include "gfsim/dispatch.h" +#include "gfsim/observation.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +class Module; +class SimSystem; + +// ── SimObject ───────────────────────────────────────────────────────── + +/// Every runtime object has one owning parent (except the root system), +/// a compile-time-assigned stable object ID, a stable local name, +/// and a canonical hierarchy path. +class SimObject { +public: + SimObject(ObjectKind kind, std::string name, ObjectId id, + SimObject *parent = nullptr, + ObservationSink *observationSink = nullptr) + : kind_(kind), name_(std::move(name)), id_(id), parent_(parent), + observationSink_(observationSink) {} + + virtual ~SimObject() = default; + + ObjectKind kind() const { return kind_; } + ObjectId id() const { return id_; } + std::string_view name() const { return name_; } + std::string_view path() const { return path_; } + SimObject *parent() const { return parent_; } + std::string_view runtimeFailureCode() const { return runtimeFailureCode_; } + + /// Set the canonical hierarchy path (called during construction). + virtual void setPath(std::string path) { path_ = std::move(path); } + + /// Set the parent pointer (called during hierarchy construction). + void setParent(SimObject *p) { parent_ = p; } + + /// Children owned by this object (for modules). + virtual std::vector children() const { return {}; } + + // ── Work interface ────────────────────────────────────────────────── + // + // Each component implements Work generation (produce proposals for the + // current epoch) and Xfer commit (accept/reject proposals atomically). + + /// Generate work proposals for the current epoch. + /// Called only when the object is scheduled to run. + virtual void doWork(Epoch epoch) {} + + /// Commit accepted proposals at the Xfer barrier. + virtual void doXfer(Epoch epoch) {} + + /// Arbitration: select the winning proposal among candidates. + virtual void doArbitrate(Epoch epoch) {} + + /// True when the next Xfer call will publish a committed state change. + virtual bool hasPendingCommit() const { return false; } + + // ── Wake conditions ───────────────────────────────────────────────── + + /// Returns true if this object has work to do at the given epoch. + virtual bool isRunnable(Epoch epoch) const { return false; } + + /// Describe committed liveness state for diagnostics outside the hot path. + virtual RuntimeObjectState runtimeState(Epoch epoch) const { + const bool pending = hasPendingCommit(); + const bool runnable = isRunnable(epoch); + return {.quiescent = !pending && !runnable, + .runnable = runnable, + .pendingCommit = pending, + .reason = + pending ? "pending_commit" : (runnable ? "runnable" : "")}; + } + + /// Append deterministic snapshots owned by this object. + virtual void collectStatistics(std::vector &) const {} + + /// Bind generated objects to their owning runtime after construction. + virtual void bindSystem(SimSystem *) {} + void setObservationSink(ObservationSink *sink) { observationSink_ = sink; } + + /// Request deterministic shutdown at a voluntary trace-end yield point. + virtual bool requestTraceEnd() { return false; } + + // ── Reset ─────────────────────────────────────────────────────────── + + virtual void reset() {} + + /// Return this object as a hierarchy module without requiring RTTI. + virtual Module *asModule() { return nullptr; } + virtual const Module *asModule() const { return nullptr; } + +protected: + bool emitObservation(EventProposal proposal) { + if (!observationSink_) + return true; + proposal.ownerId = id_; + if (observationSink_->proposeObservation(std::move(proposal))) + return true; + setRuntimeFailureCode("observation_proposal_failed"); + return false; + } + void setRuntimeFailureCode(std::string_view code) { + runtimeFailureCode_ = code; + } + void clearRuntimeFailureCode() { runtimeFailureCode_ = {}; } + + ObjectKind kind_; + std::string name_; + ObjectId id_ = kInvalidObjectId; + std::string path_; + SimObject *parent_ = nullptr; + ObservationSink *observationSink_ = nullptr; + std::string_view runtimeFailureCode_; +}; + +// ── Module ──────────────────────────────────────────────────────────── + +/// A module owns child modules and local runtime objects. +/// Generated C++ creates one class per specialized ACIR module definition. +class Module : public SimObject { +public: + Module(std::string name, ObjectId id, SimObject *parent = nullptr) + : SimObject(ObjectKind::Module, std::move(name), id, parent) {} + + /// Attach a non-owned child to the same deterministic hierarchy index used + /// by owned children. An object can be attached to exactly one module. + bool attachChild(SimObject &child) { + if (&child == this || + (child.parent() != nullptr && child.parent() != this) || + std::find(children_.begin(), children_.end(), &child) != + children_.end()) + return false; + + children_.push_back(&child); + child.setParent(this); + child.setPath(std::string(path()) + "/" + std::string(child.name())); + return true; + } + + /// Add a child object owned by this module. + void addChild(std::unique_ptr child) { + if (!child || !attachChild(*child)) + throw std::invalid_argument("child is null or already attached"); + owned_.push_back(std::move(child)); + } + + std::vector children() const override { return children_; } + + Module *asModule() override { return this; } + const Module *asModule() const override { return this; } + + void setPath(std::string path) override { + SimObject::setPath(std::move(path)); + for (SimObject *child : children_) + child->setPath(std::string(this->path()) + "/" + + std::string(child->name())); + } + + /// Find a child by name (linear scan, acceptable for small fan-out). + SimObject *findChild(std::string_view name) const { + for (auto *c : children_) + if (c->name() == name) + return c; + return nullptr; + } + + /// Walk all descendants recursively. + template void walk(F &&fn) { + fn(*this); + for (auto *c : children_) { + if (Module *module = c->asModule()) + module->walk(fn); + else + // NOLINTNEXTLINE(clang-analyzer-core.NonNullParamChecker) + fn(*c); // the children_ invariant guarantees non-null entries + } + } + + /// Walk all descendants (const version). + template void walk(F &&fn) const { + fn(*this); + for (auto *c : children_) { + if (const Module *module = c->asModule()) + module->walk(std::forward(fn)); + else + fn(*c); + } + } + + void reset() override { + for (auto *c : children_) + c->reset(); + } + +private: + std::vector children_; + std::vector> owned_; +}; + +// ── SimSystem ───────────────────────────────────────────────────────── + +class SimQueueBase; +class EventQueue; +class Resource; + +/// The system owns the root module, exact global epoch, event scheduling, +/// phase barriers, termination state, and deterministic sequencing. +class SimSystem : public SimObject, public ObservationSink { +public: + explicit SimSystem(std::string name = "system"); + ~SimSystem() override; + + Module &root() { return *root_; } + const Module &root() const { return *root_; } + + Epoch currentEpoch() const { return epoch_; } + + // ── Scheduling ────────────────────────────────────────────────────── + + /// Schedule an object for Work at the given epoch. + bool scheduleWork(ObjectId id, Epoch epoch); + + /// Schedule an event for a future epoch. + bool scheduleEvent(Event event); + + /// Install the generated dense static dispatch table. + bool setDispatchTable(std::span rows); + + /// Install canonical compressed activation adjacency. + bool setActivationPlan(std::span offsets, + std::span targets); + + /// Get the next pending event (earliest ready time). + std::optional nextEvent() const; + + // ── Simulation loop ───────────────────────────────────────────────── + + /// Run the simulation until termination or cap reached. + TerminationResult run(); + + /// Advance one (time, delta) step. Returns false if no more work. + bool step(); + + // ── Termination ───────────────────────────────────────────────────── + + bool isTerminated() const { return terminated_; } + TerminationResult terminationResult() const { return result_; } + NoProgressReport noProgressReport() const; + std::vector statistics() const; + std::span observations() const; + bool proposeObservation(EventProposal proposal) override; + + // ── Object registry ───────────────────────────────────────────────── + + /// Register an object for ID-based lookup. + void registerObject(SimObject *obj); + + /// Look up an object by its stable ID. + SimObject *lookup(ObjectId id) const; + + void reset() override; + + // ── Caps ──────────────────────────────────────────────────────────── + + void setMaxTicks(Tick max) { maxTicks_ = max; } + void setMaxEvents(uint64_t max) { maxEvents_ = max; } + void setMaxTraceRecords(uint64_t max) { maxTraceRecords_ = max; } + bool setDeadlockWindow(std::optional window); + bool setMaxDomainCycles(const std::map &limits); + bool setRuntimeLimits(const RuntimeLimits &limits); + bool setTimeDomains(std::span domains); + +private: + std::unique_ptr root_; + Epoch epoch_; + bool terminated_ = false; + TerminationResult result_; + + Tick maxTicks_ = UINT64_MAX; + uint64_t maxEvents_ = UINT64_MAX; + uint64_t maxTraceRecords_ = UINT64_MAX; + + struct Impl; + std::unique_ptr impl_; + + bool fail(std::string code, std::string message); + std::vector runtimeObjects() const; + void refreshRuntimeSummary(); + bool stopAtTraceCap(); + bool validateRuntimeIdentities(); +}; + +} // namespace gfsim + +#endif // GFSIM_OBJECT_H diff --git a/components/agentic-circuit/include/gfsim/observation.h b/components/agentic-circuit/include/gfsim/observation.h new file mode 100644 index 00000000..cfd94515 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/observation.h @@ -0,0 +1,92 @@ +#ifndef GFSIM_OBSERVATION_H +#define GFSIM_OBSERVATION_H + +#include "gfsim/core.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +enum class TraceEventPhase : uint8_t { + Instant, + Complete, + Counter, + FlowStart, + FlowEnd, +}; + +using ObservationValue = std::variant; + +struct ObservationArgument { + std::string name; + ObservationValue value; + bool operator==(const ObservationArgument &) const = default; +}; + +struct EventProposal { + ObjectId ownerId = kInvalidObjectId; + std::string category; + std::string name; + TraceEventPhase phase = TraceEventPhase::Instant; + std::optional rootSequenceId; + std::optional duration; + std::optional flowId; + std::vector arguments; + bool operator==(const EventProposal &) const = default; +}; + +/// Optional non-owning destination for cold-path observation proposals. +class ObservationSink { +public: + virtual ~ObservationSink() = default; + virtual bool proposeObservation(EventProposal proposal) = 0; +}; + +struct CommittedEvent { + Epoch epoch; + ObjectId ownerId = kInvalidObjectId; + uint64_t localCommittedIndex = 0; + std::string category; + std::string name; + TraceEventPhase phase = TraceEventPhase::Instant; + std::optional rootSequenceId; + std::optional duration; + std::optional flowId; + std::vector arguments; + bool operator==(const CommittedEvent &) const = default; +}; + +/// Buffers private pre-commit proposals and publishes them only for an owner +/// whose functional state commits at the Xfer barrier. +class ObservationRecorder { +public: + bool propose(EventProposal proposal); + bool commitOwner(ObjectId owner, Epoch epoch); + void rejectOwner(ObjectId owner); + + std::span events() const { return committed_; } + size_t pendingCount(ObjectId owner) const; + std::string_view lastError() const { return lastError_; } + void reset(); + +private: + std::map> pending_; + std::map nextIndices_; + std::map activeFlows_; + std::vector committed_; + std::optional lastCommitEpoch_; + std::string lastError_; + + bool reject(std::string message); +}; + +} // namespace gfsim + +#endif // GFSIM_OBSERVATION_H diff --git a/components/agentic-circuit/include/gfsim/packet.h b/components/agentic-circuit/include/gfsim/packet.h new file mode 100644 index 00000000..046678e1 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/packet.h @@ -0,0 +1,124 @@ +#ifndef GFSIM_PACKET_H +#define GFSIM_PACKET_H + +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +enum class PacketEndianness : uint8_t { Little, Big }; + +struct PacketField { + std::string_view name; + size_t offset = 0; + size_t size = 0; + + auto operator<=>(const PacketField &) const = default; +}; + +/// Static layout information for runtime containers. Public packets specialize +/// this template and set isPacket=true only after providing the complete +/// serialization and reflection contract below. +template struct PacketTraits { + static constexpr bool isPacket = false; + static constexpr std::string_view schema = {}; + static constexpr size_t serializedSize = sizeof(T); + static constexpr size_t maximumSerializedSize = serializedSize; + static constexpr size_t alignment = alignof(T); + static constexpr PacketEndianness endianness = PacketEndianness::Little; + static constexpr std::array fields{}; + static constexpr std::optional routingField = std::nullopt; + static constexpr std::optional correlationField = + std::nullopt; +}; + +template consteval bool hasValidPacketLayout() { + if constexpr (!requires { + PacketTraits::isPacket; + PacketTraits::schema; + PacketTraits::serializedSize; + PacketTraits::maximumSerializedSize; + PacketTraits::alignment; + PacketTraits::endianness; + PacketTraits::fields; + PacketTraits::routingField; + PacketTraits::correlationField; + }) { + return false; + } else { + if (!PacketTraits::isPacket || + std::string_view(PacketTraits::schema).empty() || + PacketTraits::serializedSize == 0 || + PacketTraits::maximumSerializedSize < + PacketTraits::serializedSize || + PacketTraits::alignment == 0) + return false; + + size_t previousEnd = 0; + for (const PacketField &field : PacketTraits::fields) { + if (field.name.empty() || field.size == 0 || field.offset < previousEnd || + field.offset > PacketTraits::serializedSize || + field.size > PacketTraits::serializedSize - field.offset) + return false; + previousEnd = field.offset + field.size; + } + auto hasField = [](std::string_view name) { + for (const PacketField &field : PacketTraits::fields) + if (field.name == name) + return true; + return false; + }; + if ((PacketTraits::routingField && + !hasField(*PacketTraits::routingField)) || + (PacketTraits::correlationField && + !hasField(*PacketTraits::correlationField))) + return false; + return true; + } +} + +template +concept Packet = hasValidPacketLayout() && requires( + const T &packet, + std::span + bytes) { + typename PacketTraits::Serialized; + requires std::same_as::Serialized, + std::array::serializedSize>>; + { PacketTraits::schema } -> std::convertible_to; + { PacketTraits::maximumSerializedSize } -> std::convertible_to; + { PacketTraits::alignment } -> std::convertible_to; + { PacketTraits::endianness } -> std::same_as; + { PacketTraits::fields }; + { + PacketTraits::routingField + } -> std::same_as &>; + { + PacketTraits::correlationField + } -> std::same_as &>; + { + PacketTraits::serialize(packet) + } -> std::same_as::Serialized>; + { PacketTraits::deserialize(bytes) } -> std::same_as>; +}; + +template +typename PacketTraits::Serialized serializePacket(const T &packet) { + return PacketTraits::serialize(packet); +} + +template +std::optional deserializePacket(std::span bytes) { + if (bytes.size() != PacketTraits::serializedSize) + return std::nullopt; + return PacketTraits::deserialize(bytes); +} + +} // namespace gfsim + +#endif // GFSIM_PACKET_H diff --git a/components/agentic-circuit/include/gfsim/process.h b/components/agentic-circuit/include/gfsim/process.h new file mode 100644 index 00000000..00bea4cb --- /dev/null +++ b/components/agentic-circuit/include/gfsim/process.h @@ -0,0 +1,270 @@ +#ifndef GFSIM_PROCESS_H +#define GFSIM_PROCESS_H + +#include "gfsim/object.h" + +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +enum class ProcessStatus : uint8_t { + Runnable, + Suspended, + Terminated, + Failed, +}; + +enum class ProcessWakeKind : uint8_t { + Condition, + Resource, + EventQueue, + NextDelta, +}; + +struct ProcessWake { + ProcessWakeKind kind = ProcessWakeKind::Condition; + uint32_t id = std::numeric_limits::max(); + + auto operator<=>(const ProcessWake &) const = default; +}; + +enum class ProcessStepKind : uint8_t { + Continue, + Suspend, + Terminate, + Fail, +}; + +struct ProcessStep { + ProcessStepKind kind = ProcessStepKind::Fail; + uint32_t nextPc = 0; + std::optional wake; + uint64_t continuationId = 0; + std::string_view diagnostic = "invalid_process_step"; + + static ProcessStep continueAt(uint32_t nextPc) { + return {.kind = ProcessStepKind::Continue, .nextPc = nextPc}; + } + + static ProcessStep suspendAt(uint32_t nextPc, ProcessWake wake, + uint64_t continuationId) { + return {.kind = ProcessStepKind::Suspend, + .nextPc = nextPc, + .wake = wake, + .continuationId = continuationId, + .diagnostic = {}}; + } + + static ProcessStep terminate() { + return {.kind = ProcessStepKind::Terminate, .diagnostic = {}}; + } + + static ProcessStep fail(std::string_view diagnostic) { + return {.kind = ProcessStepKind::Fail, .diagnostic = diagnostic}; + } +}; + +/// CRTP runtime for compiler-generated enum-PC processes. The generated +/// Derived::executeProcessStep call is statically bound on the hot path. +template class ProcessRuntime : public SimObject { +public: + ProcessRuntime(std::string name, ObjectId id, SimObject *parent, + uint32_t entryPc, uint64_t fairnessWork) + : SimObject(ObjectKind::Process, std::move(name), id, parent), + entryPc_(entryPc), committedPc_(entryPc), proposedPc_(entryPc), + fairnessWork_(fairnessWork) {} + + void doWork(Epoch epoch) override { + if (committedStatus_ != ProcessStatus::Runnable || pendingCommit_) + return; + + if (traceEndRequested_) { + proposedPc_ = committedPc_; + proposedStatus_ = ProcessStatus::Terminated; + proposedWake_.reset(); + proposedContinuationId_ = 0; + proposedDiagnostic_ = {}; + pendingCommit_ = true; + return; + } + + uint32_t localPc = committedPc_; + for (uint64_t work = 0; work < fairnessWork_; ++work) { + ProcessStep step = derived().executeProcessStep(localPc, epoch); + switch (step.kind) { + case ProcessStepKind::Continue: + localPc = step.nextPc; + break; + case ProcessStepKind::Suspend: + if (!step.wake || step.continuationId == 0) { + proposeFailure("invalid_process_continuation"); + return; + } + proposedPc_ = step.nextPc; + proposedStatus_ = ProcessStatus::Suspended; + proposedWake_ = step.wake; + proposedContinuationId_ = step.continuationId; + proposedDiagnostic_ = {}; + pendingCommit_ = true; + return; + case ProcessStepKind::Terminate: + proposedPc_ = localPc; + proposedStatus_ = ProcessStatus::Terminated; + proposedWake_.reset(); + proposedContinuationId_ = 0; + proposedDiagnostic_ = {}; + pendingCommit_ = true; + return; + case ProcessStepKind::Fail: + proposeFailure(step.diagnostic.empty() ? "process_failed" + : step.diagnostic); + return; + } + } + proposedPc_ = localPc; + proposeFailure("process_fairness_exceeded"); + } + + void doXfer(Epoch) override { + if (!pendingCommit_) + return; + committedPc_ = proposedPc_; + committedStatus_ = proposedStatus_; + committedWake_ = proposedWake_; + committedContinuationId_ = proposedContinuationId_; + committedDiagnostic_ = proposedDiagnostic_; + pendingCommit_ = false; + if (committedStatus_ == ProcessStatus::Terminated) + traceEndRequested_ = false; + if (committedStatus_ == ProcessStatus::Failed) + setRuntimeFailureCode(committedDiagnostic_); + } + + bool hasPendingCommit() const override { return pendingCommit_; } + bool isRunnable(Epoch) const override { + return committedStatus_ == ProcessStatus::Runnable && !pendingCommit_; + } + + RuntimeObjectState runtimeState(Epoch epoch) const override { + RuntimeObjectState state = SimObject::runtimeState(epoch); + state.quiescent = + !pendingCommit_ && (committedStatus_ == ProcessStatus::Terminated || + committedStatus_ == ProcessStatus::Failed); + if (state.quiescent) { + state.reason.clear(); + return state; + } + if (pendingCommit_) + state.reason = "pending_commit"; + else if (committedStatus_ == ProcessStatus::Runnable) + state.reason = "process_runnable_unscheduled"; + else { + state.reason = "process_suspended"; + if (committedWake_) { + std::string kind; + switch (committedWake_->kind) { + case ProcessWakeKind::Condition: + kind = "condition"; + break; + case ProcessWakeKind::Resource: + kind = "resource"; + break; + case ProcessWakeKind::EventQueue: + kind = "event_queue"; + break; + case ProcessWakeKind::NextDelta: + kind = "next_delta"; + break; + } + state.subscriptions.push_back(kind + ":" + + std::to_string(committedWake_->id)); + } + } + return state; + } + + bool wake(ProcessWake wake, uint64_t continuationId) { + if (committedStatus_ != ProcessStatus::Suspended || !committedWake_ || + *committedWake_ != wake || committedContinuationId_ != continuationId) + return false; + committedStatus_ = ProcessStatus::Runnable; + committedWake_.reset(); + return true; + } + + bool requestTraceEnd() override { + if (pendingCommit_ || committedStatus_ != ProcessStatus::Suspended || + !committedWake_ || committedWake_->kind != ProcessWakeKind::NextDelta) + return false; + traceEndRequested_ = true; + committedStatus_ = ProcessStatus::Runnable; + committedWake_.reset(); + return true; + } + + uint32_t pc() const { return committedPc_; } + ProcessStatus status() const { return committedStatus_; } + uint64_t continuationId() const { return committedContinuationId_; } + std::optional subscription() const { return committedWake_; } + uint64_t fairnessWork() const { return fairnessWork_; } + std::string_view diagnosticCode() const { return committedDiagnostic_; } + + bool validate() const { + if (fairnessWork_ == 0) + return false; + if (committedStatus_ == ProcessStatus::Suspended) + return committedWake_.has_value() && committedContinuationId_ != 0; + return !committedWake_.has_value(); + } + + void reset() override { + committedPc_ = entryPc_; + proposedPc_ = entryPc_; + committedStatus_ = ProcessStatus::Runnable; + proposedStatus_ = ProcessStatus::Runnable; + committedWake_.reset(); + proposedWake_.reset(); + committedContinuationId_ = 0; + proposedContinuationId_ = 0; + committedDiagnostic_ = {}; + proposedDiagnostic_ = {}; + pendingCommit_ = false; + traceEndRequested_ = false; + clearRuntimeFailureCode(); + } + +private: + Derived &derived() { return static_cast(*this); } + + void proposeFailure(std::string_view diagnostic) { + proposedStatus_ = ProcessStatus::Failed; + proposedWake_.reset(); + proposedContinuationId_ = 0; + proposedDiagnostic_ = diagnostic; + pendingCommit_ = true; + } + + uint32_t entryPc_ = 0; + uint32_t committedPc_ = 0; + uint32_t proposedPc_ = 0; + uint64_t fairnessWork_ = 0; + ProcessStatus committedStatus_ = ProcessStatus::Runnable; + ProcessStatus proposedStatus_ = ProcessStatus::Runnable; + std::optional committedWake_; + std::optional proposedWake_; + uint64_t committedContinuationId_ = 0; + uint64_t proposedContinuationId_ = 0; + std::string_view committedDiagnostic_; + std::string_view proposedDiagnostic_; + bool pendingCommit_ = false; + bool traceEndRequested_ = false; +}; + +} // namespace gfsim + +#endif // GFSIM_PROCESS_H diff --git a/components/agentic-circuit/include/gfsim/queue.h b/components/agentic-circuit/include/gfsim/queue.h new file mode 100644 index 00000000..9e9a32b7 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/queue.h @@ -0,0 +1,321 @@ +#ifndef GFSIM_QUEUE_H +#define GFSIM_QUEUE_H + +#include "gfsim/core.h" +#include "gfsim/object.h" +#include "gfsim/packet.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +// ── SimQueue ─────────────────────────────────────────────────────── + +/// FIFO data queue with entry capacity, optional byte capacity, +/// ordered read/write proposals, deterministic arbitration, +/// and occupancy/watermark statistics. +template class SimQueue : public SimObject { +public: + SimQueue(std::string name, ObjectId id, SimObject *parent, + size_t entryCapacity, size_t byteCapacity = SIZE_MAX, + ObservationSink *observations = nullptr, size_t latency = 1, + size_t rate = SIZE_MAX) + : SimObject(ObjectKind::Queue, std::move(name), id, parent, observations), + entryCapacity_(entryCapacity), byteCapacity_(byteCapacity), + latency_(latency), rate_(rate == SIZE_MAX ? entryCapacity : rate) { + if (entryCapacity_ == 0 || latency_ == 0 || rate_ == 0 || + rate_ > entryCapacity_) + throw std::invalid_argument( + "SimQueue capacity, latency, and rate are inconsistent"); + } + + // ── Capacity ──────────────────────────────────────────────────────── + + size_t entryCapacity() const { return entryCapacity_; } + size_t byteCapacity() const { return byteCapacity_; } + size_t latency() const { return latency_; } + size_t rate() const { return rate_; } + + size_t committedSize() const { return committed_.size(); } + const std::vector &committedValues() const { return committed_; } + size_t committedBytes() const { + if constexpr (PacketTraits::serializedSize == 0) + return 0; + return committed_.size() * PacketTraits::serializedSize; + } + bool isFull() const { return !canProposePush(); } + bool isEmpty() const { return committed_.empty(); } + bool canProposePush(size_t count = 1) const { + return canProposePushWithAdditionalPops(count, 0); + } + bool canProposePushAfterPop(size_t count = 1) const { + return canProposePop() && canProposePushWithAdditionalPops(count, 1); + } + bool canProposePop() const { + return popProposalCount_ < rate_ && popProposalCount_ < committed_.size(); + } + + const T *peekProposable() const { + return canProposePop() ? &committed_[popProposalCount_] : nullptr; + } + +private: + bool canProposePushWithAdditionalPops(size_t count, + size_t additionalPops) const { + if (count > rate_ - std::min(rate_, pushProposals_.size())) + return false; + const size_t pops = + std::min(committed_.size(), popProposalCount_ + additionalPops); + const size_t occupied = + committed_.size() - pops + delayed_.size() + pushProposals_.size(); + return count <= entryCapacity_ && occupied <= entryCapacity_ - count && + !exceedsByteCapacity(occupied + count); + } + +public: + // ── Proposal interface ────────────────────────────────────────────── + + /// Propose to enqueue an element. Returns false if capacity exceeded. + bool proposePush(T element) { + if (!canProposePush()) + return false; + pushProposals_.push_back(std::move(element)); + return true; + } + + /// Propose to dequeue the next element in FIFO order. + std::optional proposePop() { + if (popProposalCount_ >= committed_.size()) + return std::nullopt; + size_t index = popProposalCount_; + ++popProposalCount_; + return std::optional(committed_[index]); + } + + /// Peek at the front without proposing a pop. + const T *peek() const { + return committed_.empty() ? nullptr : &committed_.front(); + } + + // ── Arbitration ───────────────────────────────────────────────────── + + void doArbitrate(Epoch) override { + // Deterministic local arbitration: FIFO order. + // Push proposals are appended in order. + // Pop proposals are served from the front. + // Arbitration is simple FIFO. + for (size_t index = 0; index < pushProposals_.size(); ++index) + emitObservation({.category = "transaction", + .name = "accepted", + .phase = TraceEventPhase::Instant}); + for (size_t index = 0; index < popProposalCount_; ++index) + emitObservation({.category = "transaction", + .name = "completed", + .phase = TraceEventPhase::Instant}); + if (!pushProposals_.empty() || popProposalCount_ != 0) { + const uint64_t occupancy = + committed_.size() + pushProposals_.size() - popProposalCount_; + emitObservation({.category = "queue", + .name = "occupancy", + .phase = TraceEventPhase::Counter, + .arguments = {{"occupancy", occupancy}}}); + } + } + + // ── Xfer ──────────────────────────────────────────────────────────── + + void doXfer(Epoch epoch) override { + bool changed = hasPendingCommit(); + for (auto iterator = delayed_.begin(); iterator != delayed_.end();) { + if (iterator->first > epoch.time) { + ++iterator; + continue; + } + committed_.push_back(std::move(iterator->second)); + iterator = delayed_.erase(iterator); + ++totalPushes_; + } + for (auto &elem : pushProposals_) { + if (latency_ == 1) { + committed_.push_back(std::move(elem)); + ++totalPushes_; + } else { + delayed_.emplace_back(epoch.time + latency_ - 1, std::move(elem)); + } + } + pushProposals_.clear(); + + // Commit pop proposals + for (size_t i = 0; i < popProposalCount_ && !committed_.empty(); ++i) { + committed_.erase(committed_.begin()); + ++totalPops_; + } + popProposalCount_ = 0; + + // Update statistics + if (committedSize() > highWatermark_) + highWatermark_ = committedSize(); + if (changed) + lastUpdate_ = epoch; + } + + bool hasPendingCommit() const override { + return !pushProposals_.empty() || !delayed_.empty() || + popProposalCount_ != 0; + } + + RuntimeObjectState runtimeState(Epoch epoch) const override { + RuntimeObjectState state = SimObject::runtimeState(epoch); + state.queueOccupancy = committedSize(); + state.pendingOffers = pushProposals_.size() + delayed_.size(); + state.quiescent = committed_.empty() && !hasPendingCommit(); + if (!state.quiescent) + state.reason = hasPendingCommit() ? "pending_commit" : "queue_not_empty"; + return state; + } + + void collectStatistics(std::vector &out) const override { + auto append = [&](std::string suffix, uint64_t value, StatisticKind kind) { + out.push_back({.name = std::move(suffix), + .objectPath = std::string(path()), + .kind = kind, + .value = value, + .lastUpdate = lastUpdate_}); + }; + append("queue_occupancy", committedSize(), StatisticKind::Gauge); + append("queue_occupancy_peak", highWatermark_, StatisticKind::Gauge); + append("accepted_transactions", totalPushes_, StatisticKind::Counter); + append("completed_transactions", totalPops_, StatisticKind::Counter); + } + + // ── Statistics ────────────────────────────────────────────────────── + + size_t highWatermark() const { return highWatermark_; } + uint64_t totalPushes() const { return totalPushes_; } + uint64_t totalPops() const { return totalPops_; } + + void reset() override { + committed_.clear(); + delayed_.clear(); + pushProposals_.clear(); + popProposalCount_ = 0; + highWatermark_ = 0; + totalPushes_ = 0; + totalPops_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + bool exceedsByteCapacity(size_t elementCount) const { + if constexpr (PacketTraits::serializedSize == 0) + return false; + return elementCount > byteCapacity_ / PacketTraits::serializedSize; + } + + size_t entryCapacity_; + size_t byteCapacity_; + size_t latency_; + size_t rate_; + std::vector committed_; + std::vector> delayed_; + std::vector pushProposals_; + size_t popProposalCount_ = 0; + size_t highWatermark_ = 0; + uint64_t totalPushes_ = 0; + uint64_t totalPops_ = 0; + Epoch lastUpdate_; +}; + +/// Standard-library finite FIFO component. The distinct name is the public +/// component contract; SimQueue remains the underlying runtime primitive. +template class Queue final : public SimQueue { +public: + static constexpr std::string_view contractName = "ac.Queue"; + static constexpr ObjectKind componentKind = ObjectKind::Queue; + using SimQueue::SimQueue; +}; + +// ── EventQueue ──────────────────────────────────────────────────────── + +/// Time-ordered event queue. Events are ordered by exact epoch, target object +/// ID, event kind, and payload. +class EventQueue : public SimObject { +public: + EventQueue(std::string name, ObjectId id, SimObject *parent, + size_t capacity = 1024) + : SimObject(ObjectKind::EventQueue, std::move(name), id, parent), + capacity_(capacity) {} + + // ── Capacity ──────────────────────────────────────────────────────── + + size_t capacity() const { return capacity_; } + size_t size() const { return committed_.size(); } + bool isFull() const { + return committed_.size() + pushProposals_.size() >= capacity_; + } + + // ── Proposal ──────────────────────────────────────────────────────── + + bool proposeSchedule(Event event) { + if (committed_.size() + pushProposals_.size() >= capacity_) + return false; + pushProposals_.insert(event); + return true; + } + + // ── Xfer ──────────────────────────────────────────────────────────── + + void doXfer(Epoch epoch) override { + for (auto &event : pushProposals_) + committed_.insert(event); + pushProposals_.clear(); + } + + bool hasPendingCommit() const override { return !pushProposals_.empty(); } + + // ── Query ─────────────────────────────────────────────────────────── + + std::optional nextEvent() const { + if (committed_.empty()) + return std::nullopt; + return *committed_.begin(); + } + + /// Pop the earliest event and return it. + std::optional popNext() { + if (committed_.empty()) + return std::nullopt; + auto it = committed_.begin(); + Event e = *it; + committed_.erase(it); + return e; + } + + bool hasEventAt(Epoch epoch) const { + for (const auto &e : committed_) + if (e.readyTime == epoch) + return true; + return false; + } + + void reset() override { + committed_.clear(); + pushProposals_.clear(); + } + +private: + size_t capacity_; + std::multiset committed_; + std::multiset pushProposals_; +}; + +} // namespace gfsim + +#endif // GFSIM_QUEUE_H diff --git a/components/agentic-circuit/include/gfsim/queue_blocks.h b/components/agentic-circuit/include/gfsim/queue_blocks.h new file mode 100644 index 00000000..99fab420 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/queue_blocks.h @@ -0,0 +1,1613 @@ +#ifndef GFSIM_QUEUE_BLOCKS_H +#define GFSIM_QUEUE_BLOCKS_H + +#include "gfsim/object.h" +#include "gfsim/queue.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +template + requires std::invocable && (Rate > 0) && + std::convertible_to< + std::invoke_result_t, Output> +class QueueTransform : public SimObject { +public: + static constexpr std::string_view contractName = "ac.transform"; + static constexpr ObjectKind componentKind = ObjectKind::Compute; + + QueueTransform(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, SimQueue &output, + Policy policy = {}, ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), output_(output), policy_(std::move(policy)) {} + + void doWork(Epoch) override { + while (fired_ < Rate && input_.canProposePop() && + output_.canProposePush()) { + const Input *head = input_.peekProposable(); + if (head == nullptr) + return; + Output result = std::invoke(std::as_const(policy_), *head); + if (!output_.proposePush(std::move(result)) || !input_.proposePop()) + return; + ++fired_; + } + } + + void doXfer(Epoch) override { fired_ = 0; } + bool hasPendingCommit() const override { return fired_ != 0; } + bool isRunnable(Epoch) const override { + return fired_ < Rate && input_.canProposePop() && output_.canProposePush(); + } + void reset() override { + fired_ = 0; + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + SimQueue &output_; + [[no_unique_address]] Policy policy_; + size_t fired_ = 0; +}; + +template + requires std::invocable && (Rate > 0) && + std::convertible_to< + std::invoke_result_t, Output> +class Compute final : public QueueTransform { +public: + static constexpr std::string_view contractName = "ac.compute"; + static constexpr ObjectKind componentKind = ObjectKind::Compute; + + Compute(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, SimQueue &output, Policy policy = {}, + ObservationSink *observations = nullptr) + : QueueTransform( + std::move(name), id, parent, input, output, std::move(policy), + observations) {} +}; + +template struct Identity { + T operator()(const T &value) const { return value; } +}; + +template + requires(Stages > 0) && (Rate > 0) +class Pipeline final : public QueueTransform, Rate> { +public: + static constexpr std::string_view contractName = "ac.pipeline"; + static constexpr ObjectKind componentKind = ObjectKind::Compute; + + Pipeline(std::string name, ObjectId id, SimObject *parent, SimQueue &input, + SimQueue &output, ObservationSink *observations = nullptr) + : QueueTransform, Rate>( + std::move(name), id, parent, input, output, {}, observations) { + if (output.latency() != Stages) + throw std::invalid_argument( + "Pipeline stages must match output SimQueue latency"); + } +}; + +template +class QueueAtomicTransform; + +template + requires std::invocable && + std::same_as, + std::tuple> +class QueueAtomicTransform, + std::tuple> + final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.transform.atomic"; + static constexpr ObjectKind componentKind = ObjectKind::Compute; + + QueueAtomicTransform(std::string name, ObjectId id, SimObject *parent, + std::tuple *...> inputs, + std::tuple *...> outputs, + Policy policy = {}, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + inputs_(inputs), outputs_(outputs), policy_(std::move(policy)) {} + + void doWork(Epoch) override { + if (fired_ || !allInputsReady() || !allOutputsReady()) + return; + auto values = inputValues(std::index_sequence_for{}); + auto results = std::apply(std::as_const(policy_), values); + if (!pushAll(results, std::index_sequence_for{}) || + !popAll(std::index_sequence_for{})) + return; + fired_ = true; + } + void doXfer(Epoch) override { fired_ = false; } + bool hasPendingCommit() const override { return fired_; } + bool isRunnable(Epoch) const override { + return !fired_ && allInputsReady() && allOutputsReady(); + } + void reset() override { + fired_ = false; + clearRuntimeFailureCode(); + } + +private: + bool allInputsReady() const { + return std::apply( + [](const auto *...queues) { + return ((queues != nullptr && queues->canProposePop()) && ...); + }, + inputs_); + } + bool allOutputsReady() const { + return std::apply( + [](const auto *...queues) { + return ((queues != nullptr && queues->canProposePush()) && ...); + }, + outputs_); + } + template + std::tuple inputValues(std::index_sequence) const { + return std::tuple{*std::get(inputs_)->peek()...}; + } + template + bool pushAll(const std::tuple &values, + std::index_sequence) { + return ( + std::get(outputs_)->proposePush(std::get(values)) && + ...); + } + template bool popAll(std::index_sequence) { + return (std::get(inputs_)->proposePop().has_value() && ...); + } + + std::tuple *...> inputs_; + std::tuple *...> outputs_; + [[no_unique_address]] Policy policy_; + bool fired_ = false; +}; + +template class QueueBarrier; + +template +class QueueBarrier> final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.barrier"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + QueueBarrier(std::string name, ObjectId id, SimObject *parent, + std::tuple *...> inputs, + std::tuple *...> outputs, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + inputs_(inputs), outputs_(outputs) {} + + void doWork(Epoch) override { + if (fired_ || !allInputsReady() || !allOutputsReady()) + return; + auto values = inputValues(std::index_sequence_for{}); + if (!pushAll(values, std::index_sequence_for{}) || + !popAll(std::index_sequence_for{})) + return; + fired_ = true; + } + void doXfer(Epoch) override { fired_ = false; } + bool hasPendingCommit() const override { return fired_; } + bool isRunnable(Epoch) const override { + return !fired_ && allInputsReady() && allOutputsReady(); + } + void reset() override { + fired_ = false; + clearRuntimeFailureCode(); + } + +private: + bool allInputsReady() const { + return std::apply( + [](const auto *...queues) { + return ((queues != nullptr && queues->canProposePop()) && ...); + }, + inputs_); + } + bool allOutputsReady() const { + return std::apply( + [](const auto *...queues) { + return ((queues != nullptr && queues->canProposePush()) && ...); + }, + outputs_); + } + template + std::tuple inputValues(std::index_sequence) const { + return std::tuple{*std::get(inputs_)->peek()...}; + } + template + bool pushAll(const std::tuple &values, + std::index_sequence) { + return ( + std::get(outputs_)->proposePush(std::get(values)) && + ...); + } + template bool popAll(std::index_sequence) { + return (std::get(inputs_)->proposePop().has_value() && ...); + } + + std::tuple *...> inputs_; + std::tuple *...> outputs_; + bool fired_ = false; +}; + +template + requires std::invocable && + std::integral> +class QueueReorder : public SimObject { +public: + static constexpr std::string_view contractName = "ac.reorder"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + QueueReorder(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, SimQueue &output, size_t capacity, + uint64_t start = 0, Key key = {}, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), output_(output), capacity_(capacity), start_(start), + nextKey_(start), key_(std::move(key)) {} + + void doWork(Epoch) override { + if (!pendingInput_ && entries_.size() < capacity_ && + input_.canProposePop()) { + const T *head = input_.peek(); + if (head != nullptr) { + using KeyResult = std::invoke_result_t; + const KeyResult rawKey = std::invoke(std::as_const(key_), *head); + if constexpr (std::signed_integral) + if (rawKey < 0) { + setRuntimeFailureCode("reorder_negative_key"); + return; + } + const uint64_t key = static_cast(rawKey); + if (key < nextKey_) { + setRuntimeFailureCode("reorder_stale_key"); + return; + } + if (entries_.contains(key)) { + setRuntimeFailureCode("reorder_duplicate_key"); + return; + } + if (input_.proposePop()) + pendingInput_ = std::pair{key, *head}; + } + } + if (!pendingOutputKey_) { + auto next = entries_.find(nextKey_); + if (next != entries_.end() && output_.canProposePush() && + output_.proposePush(next->second)) + pendingOutputKey_ = nextKey_; + } + } + + void doXfer(Epoch) override { + if (pendingOutputKey_) { + entries_.erase(*pendingOutputKey_); + ++nextKey_; + pendingOutputKey_.reset(); + } + if (pendingInput_) { + entries_.emplace(pendingInput_->first, std::move(pendingInput_->second)); + pendingInput_.reset(); + } + } + bool hasPendingCommit() const override { + return pendingInput_.has_value() || pendingOutputKey_.has_value(); + } + size_t active() const { return entries_.size(); } + bool isRunnable(Epoch) const override { + const bool canRetire = !pendingOutputKey_ && entries_.contains(nextKey_) && + output_.canProposePush(); + const bool canAdmit = + !pendingInput_ && entries_.size() < capacity_ && input_.canProposePop(); + return canRetire || canAdmit; + } + size_t buffered() const { return entries_.size(); } + uint64_t nextKey() const { return nextKey_; } + void reset() override { + entries_.clear(); + pendingInput_.reset(); + pendingOutputKey_.reset(); + nextKey_ = start_; + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + SimQueue &output_; + size_t capacity_; + uint64_t start_; + uint64_t nextKey_; + [[no_unique_address]] Key key_; + std::map entries_; + std::optional> pendingInput_; + std::optional pendingOutputKey_; +}; + +template + requires(Entries > 0) && std::invocable && + std::integral> +class Reorder final : public QueueReorder { +public: + static constexpr std::string_view contractName = "ac.reorder"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + Reorder(std::string name, ObjectId id, SimObject *parent, SimQueue &input, + SimQueue &output, Key key = {}, + ObservationSink *observations = nullptr) + : QueueReorder(std::move(name), id, parent, input, output, + Entries, Start, std::move(key), observations) {} +}; + +template + requires std::invocable && + std::integral> && + std::invocable && + std::integral> && + std::invocable && + std::integral> && + std::invocable && + std::integral> +class QueueDependency : public SimObject { +public: + static constexpr std::string_view contractName = "ac.dependency"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + QueueDependency(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, SimQueue &output, size_t capacity, + size_t resources, uint64_t noDependency, Key key = {}, + Dependency dependency = {}, Resource resource = {}, + Cost cost = {}, ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), output_(output), capacity_(capacity), + resources_(resources), noDependency_(noDependency), + key_(std::move(key)), dependency_(std::move(dependency)), + resource_(std::move(resource)), cost_(std::move(cost)) {} + + void doWork(Epoch epoch) override { + if (proposed_) + return; + if (entries_.size() < capacity_ && input_.canProposePop()) { + const T *head = input_.peek(); + if (head != nullptr && !proposeInput(*head)) + return; + } + + const Entry *completed = nullptr; + for (const auto &[key, entry] : entries_) + if (entry.state == State::Done && + (completed == nullptr || entry.ready < completed->ready || + (entry.ready == completed->ready && key < completed->key))) + completed = &entry; + if (completed != nullptr && output_.canProposePush() && + output_.proposePush(completed->value)) + pendingOutputKey_ = completed->key; + + for (const auto &[key, entry] : entries_) + if (entry.state == State::Executing && entry.ready <= epoch) + pendingCompletions_.push_back(key); + for (size_t resource = 0; resource < resources_; ++resource) { + if (!resourceFree(resource, epoch)) + continue; + for (const auto &[key, entry] : entries_) + if (entry.state == State::Waiting && entry.resource == resource && + dependencyReady(entry)) { + pendingIssues_.push_back(key); + break; + } + } + proposed_ = pendingInput_.has_value() || pendingOutputKey_.has_value() || + !pendingIssues_.empty() || !pendingCompletions_.empty(); + } + + void doXfer(Epoch epoch) override { + if (!proposed_) + return; + if (pendingOutputKey_) + entries_.erase(*pendingOutputKey_); + for (uint64_t key : pendingCompletions_) + if (auto found = entries_.find(key); found != entries_.end()) + found->second.state = State::Done; + for (uint64_t key : pendingIssues_) + if (auto found = entries_.find(key); found != entries_.end()) { + if (epoch.time > + std::numeric_limits::max() - found->second.cost) { + setRuntimeFailureCode("dependency_time_overflow"); + continue; + } + found->second.state = State::Executing; + found->second.ready = {epoch.time + found->second.cost, 0}; + } + if (pendingInput_) { + Entry entry; + entry.key = pendingInput_->key; + entry.dependency = pendingInput_->dependency; + entry.resource = pendingInput_->resource; + entry.cost = pendingInput_->cost; + entry.value = std::move(pendingInput_->value); + entries_.emplace(entry.key, std::move(entry)); + } + pendingInput_.reset(); + pendingOutputKey_.reset(); + pendingIssues_.clear(); + pendingCompletions_.clear(); + proposed_ = false; + } + bool hasPendingCommit() const override { return proposed_; } + bool isRunnable(Epoch epoch) const override { + if (proposed_) + return false; + if (entries_.size() < capacity_ && input_.canProposePop()) + return true; + for (const auto &[key, entry] : entries_) { + (void)key; + if ((entry.state == State::Done && output_.canProposePush()) || + (entry.state == State::Executing && entry.ready <= epoch) || + (entry.state == State::Waiting && dependencyReady(entry) && + resourceFree(entry.resource, epoch))) + return true; + } + return false; + } + size_t active() const { return entries_.size(); } + size_t resourceActive(size_t resource) const { + return static_cast( + std::count_if(entries_.begin(), entries_.end(), [&](const auto &entry) { + return entry.second.resource == resource && + entry.second.state == State::Executing; + })); + } + void reset() override { + entries_.clear(); + pendingInput_.reset(); + pendingOutputKey_.reset(); + pendingIssues_.clear(); + pendingCompletions_.clear(); + proposed_ = false; + clearRuntimeFailureCode(); + } + +private: + enum class State : uint8_t { Waiting, Executing, Done }; + struct Entry { + uint64_t key = 0; + uint64_t dependency = 0; + uint64_t resource = 0; + uint64_t cost = 0; + T value{}; + State state = State::Waiting; + Epoch ready{}; + }; + struct PendingInput { + uint64_t key = 0; + uint64_t dependency = 0; + uint64_t resource = 0; + uint64_t cost = 0; + T value; + }; + + bool dependencyReady(const Entry &entry) const { + if (entry.dependency == noDependency_) + return true; + auto found = entries_.find(entry.dependency); + return found != entries_.end() && found->second.state == State::Done; + } + bool resourceFree(uint64_t resource, Epoch epoch) const { + if (resource >= resources_) + return false; + for (const auto &[key, entry] : entries_) { + (void)key; + if (entry.resource == resource && entry.state == State::Executing && + entry.ready > epoch) + return false; + } + return true; + } + bool proposeInput(const T &value) { + using KeyResult = std::invoke_result_t; + using DependencyResult = + std::invoke_result_t; + using ResourceResult = std::invoke_result_t; + using CostResult = std::invoke_result_t; + const KeyResult rawKey = std::invoke(std::as_const(key_), value); + const DependencyResult rawDependency = + std::invoke(std::as_const(dependency_), value); + const ResourceResult rawResource = + std::invoke(std::as_const(resource_), value); + const CostResult rawCost = std::invoke(std::as_const(cost_), value); + if constexpr (std::signed_integral) + if (rawKey < 0) { + setRuntimeFailureCode("dependency_negative_key"); + return false; + } + if constexpr (std::signed_integral) + if (rawDependency < 0) { + setRuntimeFailureCode("dependency_negative_predecessor"); + return false; + } + if constexpr (std::signed_integral) + if (rawResource < 0) { + setRuntimeFailureCode("dependency_negative_resource"); + return false; + } + if constexpr (std::signed_integral) + if (rawCost <= 0) { + setRuntimeFailureCode("dependency_nonpositive_cost"); + return false; + } + if constexpr (std::unsigned_integral) + if (rawCost == 0) { + setRuntimeFailureCode("dependency_nonpositive_cost"); + return false; + } + const uint64_t key = static_cast(rawKey); + const uint64_t predecessor = static_cast(rawDependency); + const uint64_t resource = static_cast(rawResource); + const uint64_t cost = static_cast(rawCost); + if (entries_.contains(key)) { + setRuntimeFailureCode("dependency_duplicate_key"); + return false; + } + if (resource >= resources_) { + setRuntimeFailureCode("dependency_resource_out_of_range"); + return false; + } + if (!input_.proposePop()) + return true; + pendingInput_ = PendingInput{key, predecessor, resource, cost, value}; + return true; + } + + SimQueue &input_; + SimQueue &output_; + size_t capacity_; + size_t resources_; + uint64_t noDependency_; + [[no_unique_address]] Key key_; + [[no_unique_address]] Dependency dependency_; + [[no_unique_address]] Resource resource_; + [[no_unique_address]] Cost cost_; + std::map entries_; + std::optional pendingInput_; + std::optional pendingOutputKey_; + std::vector pendingIssues_; + std::vector pendingCompletions_; + bool proposed_ = false; +}; + +template + requires(Entries > 0) && (Resources > 0) && + std::invocable && + std::integral> && + std::invocable && + std::integral> && + std::invocable && + std::integral> && + std::invocable && + std::integral> +class Schedule final + : public QueueDependency { +public: + static constexpr std::string_view contractName = "ac.schedule"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + Schedule(std::string name, ObjectId id, SimObject *parent, SimQueue &input, + SimQueue &output, Key key = {}, Dependency dependency = {}, + Resource resource = {}, Cost cost = {}, + ObservationSink *observations = nullptr) + : QueueDependency( + std::move(name), id, parent, input, output, Entries, Resources, + NoDependency, std::move(key), std::move(dependency), + std::move(resource), std::move(cost), observations) {} +}; + +template + requires std::invocable && + std::integral> +class QueueCredit : public SimObject { +public: + static constexpr std::string_view contractName = "ac.credit"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + QueueCredit(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, SimQueue &output, size_t credits, + Cost cost = {}, ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), output_(output), slots_(credits), + cost_(std::move(cost)) {} + + void doWork(Epoch) override { + if (proposed_) + return; + + for (size_t index = 0; index < slots_.size(); ++index) + if (slots_[index] && slots_[index]->remaining == 0 && + output_.canProposePush() && + output_.proposePush(slots_[index]->value)) { + pendingOutput_ = index; + break; + } + + for (size_t index = 0; index < slots_.size(); ++index) + if (slots_[index] && slots_[index]->remaining > 0) + pendingCountdowns_.push_back(index); + + if (input_.canProposePop()) { + auto free = std::find_if(slots_.begin(), slots_.end(), + [](const auto &slot) { return !slot; }); + const T *head = input_.peek(); + if (free != slots_.end() && head != nullptr) + proposeInput(static_cast(free - slots_.begin()), *head); + } + + proposed_ = pendingInput_.has_value() || pendingOutput_.has_value() || + !pendingCountdowns_.empty(); + } + + void doXfer(Epoch) override { + if (!proposed_) + return; + if (pendingOutput_) + slots_[*pendingOutput_].reset(); + for (size_t index : pendingCountdowns_) + if (slots_[index] && slots_[index]->remaining > 0) + --slots_[index]->remaining; + if (pendingInput_) + slots_[pendingInput_->slot] = + Entry{std::move(pendingInput_->value), pendingInput_->cost}; + pendingInput_.reset(); + pendingOutput_.reset(); + pendingCountdowns_.clear(); + proposed_ = false; + } + + bool hasPendingCommit() const override { return proposed_; } + bool isRunnable(Epoch) const override { + if (proposed_) + return false; + if (input_.canProposePop() && + std::any_of(slots_.begin(), slots_.end(), + [](const auto &slot) { return !slot; })) + return true; + for (const auto &slot : slots_) + if (slot && (slot->remaining > 0 || output_.canProposePush())) + return true; + return false; + } + + size_t active() const { + return static_cast( + std::count_if(slots_.begin(), slots_.end(), + [](const auto &slot) { return slot.has_value(); })); + } + + void reset() override { + for (auto &slot : slots_) + slot.reset(); + pendingInput_.reset(); + pendingOutput_.reset(); + pendingCountdowns_.clear(); + proposed_ = false; + clearRuntimeFailureCode(); + } + +private: + struct Entry { + T value; + uint64_t remaining = 0; + }; + struct PendingInput { + size_t slot = 0; + T value; + uint64_t cost = 0; + }; + + void proposeInput(size_t slot, const T &value) { + using CostResult = std::invoke_result_t; + const CostResult rawCost = std::invoke(std::as_const(cost_), value); + if constexpr (std::signed_integral) + if (rawCost <= 0) { + setRuntimeFailureCode("credit_nonpositive_cost"); + return; + } + if constexpr (std::unsigned_integral) + if (rawCost == 0) { + setRuntimeFailureCode("credit_nonpositive_cost"); + return; + } + if (!input_.proposePop()) + return; + pendingInput_ = PendingInput{slot, value, static_cast(rawCost)}; + } + + SimQueue &input_; + SimQueue &output_; + std::vector> slots_; + [[no_unique_address]] Cost cost_; + std::optional pendingInput_; + std::optional pendingOutput_; + std::vector pendingCountdowns_; + bool proposed_ = false; +}; + +template + requires(Lanes > 0) && std::invocable && + std::integral> +class Engine final : public QueueCredit { +public: + static constexpr std::string_view contractName = "ac.engine"; + static constexpr ObjectKind componentKind = ObjectKind::Scheduler; + + Engine(std::string name, ObjectId id, SimObject *parent, SimQueue &input, + SimQueue &output, Cost cost = {}, + ObservationSink *observations = nullptr) + : QueueCredit(std::move(name), id, parent, input, output, Lanes, + std::move(cost), observations) {} +}; + +template + requires std::invocable && + std::integral> && + std::invocable && + std::convertible_to, + bool> && + std::invocable && + std::convertible_to< + std::invoke_result_t, Data> && + std::invocable && + std::convertible_to< + std::invoke_result_t, + T> +class QueueMemory : public SimObject { +public: + static constexpr std::string_view contractName = "ac.memory"; + static constexpr ObjectKind componentKind = ObjectKind::Memory; + + QueueMemory(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, SimQueue &output, size_t entries, + Data init = {}, Address address = {}, Write write = {}, + WriteData writeData = {}, Response response = {}, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), output_(output), init_(init), storage_(entries, init), + address_(std::move(address)), write_(std::move(write)), + writeData_(std::move(writeData)), response_(std::move(response)) {} + + void doWork(Epoch) override { + if (fired_ || !input_.canProposePop() || !output_.canProposePush()) + return; + const T *head = input_.peek(); + if (head == nullptr) + return; + using AddressResult = std::invoke_result_t; + const AddressResult rawAddress = + std::invoke(std::as_const(address_), *head); + if constexpr (std::signed_integral) + if (rawAddress < 0) { + setRuntimeFailureCode("memory_address_out_of_range"); + return; + } + const uint64_t address = static_cast(rawAddress); + if (address >= storage_.size()) { + setRuntimeFailureCode("memory_address_out_of_range"); + return; + } + const Data oldData = storage_[address]; + T response = std::invoke(std::as_const(response_), *head, oldData); + if (!output_.proposePush(std::move(response)) || !input_.proposePop()) + return; + if (static_cast(std::invoke(std::as_const(write_), *head))) + pendingWrite_ = std::pair{ + static_cast(address), + static_cast(std::invoke(std::as_const(writeData_), *head))}; + fired_ = true; + } + void doXfer(Epoch) override { + if (pendingWrite_) { + storage_[pendingWrite_->first] = std::move(pendingWrite_->second); + pendingWrite_.reset(); + } + fired_ = false; + } + bool hasPendingCommit() const override { return fired_; } + bool isRunnable(Epoch) const override { + return !fired_ && input_.canProposePop() && output_.canProposePush(); + } + const Data &at(size_t address) const { return storage_.at(address); } + void reset() override { + std::fill(storage_.begin(), storage_.end(), init_); + pendingWrite_.reset(); + fired_ = false; + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + SimQueue &output_; + Data init_; + std::vector storage_; + [[no_unique_address]] Address address_; + [[no_unique_address]] Write write_; + [[no_unique_address]] WriteData writeData_; + [[no_unique_address]] Response response_; + std::optional> pendingWrite_; + bool fired_ = false; +}; + +template + requires(Entries > 0) && std::invocable && + std::integral> && + std::invocable && + std::convertible_to, + bool> && + std::invocable && + std::convertible_to< + std::invoke_result_t, Data> && + std::invocable && + std::convertible_to< + std::invoke_result_t, + T> +class Table final + : public QueueMemory { +public: + static constexpr std::string_view contractName = "ac.table"; + static constexpr ObjectKind componentKind = ObjectKind::Memory; + + Table(std::string name, ObjectId id, SimObject *parent, SimQueue &input, + SimQueue &output, Address address = {}, Write write = {}, + WriteData writeData = {}, Response response = {}, + ObservationSink *observations = nullptr) + : QueueMemory( + std::move(name), id, parent, input, output, Entries, Init, + std::move(address), std::move(write), std::move(writeData), + std::move(response), observations) {} +}; + +/// One physical, single-outstanding memory shared by a statically ordered set +/// of logical request/response endpoints. Requests are accepted in endpoint +/// index order only while idle. The selected response is retained until its +/// response Queue accepts it; no other request is admitted while busy. +template + requires std::invocable && + std::integral< + std::invoke_result_t> && + std::invocable && + std::convertible_to< + std::invoke_result_t, bool> && + std::invocable && + std::convertible_to< + std::invoke_result_t, + Data> && + std::invocable && + std::convertible_to, + T> +class QueueMemoryArbiter final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.memory.instance"; + static constexpr ObjectKind componentKind = ObjectKind::Memory; + + QueueMemoryArbiter(std::string name, ObjectId id, SimObject *parent, + std::array *, N> inputs, + std::array *, N> outputs, size_t entries, + Data init = {}, size_t latency = 1, Address address = {}, + Write write = {}, WriteData writeData = {}, + Response response = {}, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + inputs_(inputs), outputs_(outputs), init_(init), + storage_(entries, init), latency_(latency), + address_(std::move(address)), write_(std::move(write)), + writeData_(std::move(writeData)), response_(std::move(response)) { + if (latency_ == 0) + throw std::invalid_argument("memory latency must be positive"); + } + + void doWork(Epoch epoch) override { + if (fired_) + return; + if (busy_) { + if (!responseReady_ || epoch < *responseReady_) { + ticking_ = true; + fired_ = true; + workEpoch_ = epoch; + return; + } + if (!outputs_[selected_]->canProposePush() || !pendingResponse_) + return; + if (!outputs_[selected_]->proposePush(*pendingResponse_)) + return; + completing_ = true; + fired_ = true; + workEpoch_ = epoch; + return; + } + if (completedEpoch_ && *completedEpoch_ == epoch) + return; + for (size_t endpoint = 0; endpoint < N; ++endpoint) { + if (!inputs_[endpoint]->canProposePop()) + continue; + const T *head = inputs_[endpoint]->peek(); + if (!head) + continue; + using AddressResult = + std::invoke_result_t; + const AddressResult rawAddress = + std::invoke(std::as_const(address_), endpoint, *head); + if constexpr (std::signed_integral) + if (rawAddress < 0) { + setRuntimeFailureCode("memory_address_out_of_range"); + return; + } + const uint64_t address = static_cast(rawAddress); + if (address >= storage_.size()) { + setRuntimeFailureCode("memory_address_out_of_range"); + return; + } + const Data oldData = storage_[address]; + if (epoch.time > std::numeric_limits::max() - latency_) { + setRuntimeFailureCode("memory_latency_overflow"); + return; + } + pendingResponse_ = + std::invoke(std::as_const(response_), endpoint, *head, oldData); + responseReady_ = Epoch{epoch.time + latency_, 0}; + if (!inputs_[endpoint]->proposePop()) { + pendingResponse_.reset(); + responseReady_.reset(); + return; + } + if (static_cast( + std::invoke(std::as_const(write_), endpoint, *head))) + pendingWrite_ = std::pair{ + static_cast(address), + static_cast( + std::invoke(std::as_const(writeData_), endpoint, *head))}; + selected_ = endpoint; + accepting_ = true; + fired_ = true; + workEpoch_ = epoch; + return; + } + } + + void doXfer(Epoch) override { + if (accepting_) { + if (pendingWrite_) { + storage_[pendingWrite_->first] = std::move(pendingWrite_->second); + pendingWrite_.reset(); + } + busy_ = true; + } + if (completing_) { + busy_ = false; + pendingResponse_.reset(); + responseReady_.reset(); + completedEpoch_ = workEpoch_; + } + accepting_ = false; + completing_ = false; + ticking_ = false; + fired_ = false; + } + + bool hasPendingCommit() const override { return fired_; } + bool isRunnable(Epoch epoch) const override { + if (fired_) + return false; + if (busy_) { + if (!responseReady_ || epoch < *responseReady_) + return true; + return pendingResponse_.has_value() && + outputs_[selected_]->canProposePush(); + } + if (completedEpoch_ && *completedEpoch_ == epoch) + return false; + return std::any_of( + inputs_.begin(), inputs_.end(), + [](const SimQueue *queue) { return queue->canProposePop(); }); + } + bool busy() const { return busy_; } + size_t latency() const { return latency_; } + size_t selectedEndpoint() const { return selected_; } + const Data &at(size_t address) const { return storage_.at(address); } + void reset() override { + std::fill(storage_.begin(), storage_.end(), init_); + pendingResponse_.reset(); + responseReady_.reset(); + pendingWrite_.reset(); + completedEpoch_.reset(); + busy_ = accepting_ = completing_ = ticking_ = fired_ = false; + selected_ = 0; + clearRuntimeFailureCode(); + } + +private: + std::array *, N> inputs_; + std::array *, N> outputs_; + Data init_; + std::vector storage_; + size_t latency_ = 1; + [[no_unique_address]] Address address_; + [[no_unique_address]] Write write_; + [[no_unique_address]] WriteData writeData_; + [[no_unique_address]] Response response_; + std::optional pendingResponse_; + std::optional responseReady_; + std::optional> pendingWrite_; + std::optional completedEpoch_; + Epoch workEpoch_{}; + size_t selected_ = 0; + bool busy_ = false; + bool accepting_ = false; + bool completing_ = false; + bool ticking_ = false; + bool fired_ = false; +}; + +template class QueueSink final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.sink"; + static constexpr ObjectKind componentKind = ObjectKind::Sink; + + QueueSink(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input) {} + + void doWork(Epoch) override { + if (pending_ || !input_.canProposePop()) + return; + pending_ = input_.proposePop(); + } + void doXfer(Epoch) override { + if (pending_) + received_.push_back(std::move(*pending_)); + pending_.reset(); + } + bool hasPendingCommit() const override { return pending_.has_value(); } + bool isRunnable(Epoch) const override { + return !pending_ && input_.canProposePop(); + } + const std::vector &received() const { return received_; } + void reset() override { + pending_.reset(); + received_.clear(); + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + std::optional pending_; + std::vector received_; +}; + +template + requires std::equality_comparable +class QueueObserve final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.observe"; + static constexpr ObjectKind componentKind = ObjectKind::Probe; + + QueueObserve(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input) {} + + void doWork(Epoch) override { + const T *head = input_.peek(); + if (pending_ || head == nullptr) + return; + if (last_ && input_.totalPops() == lastPopCount_ && *last_ == *head) + return; + pending_ = *head; + pendingPopCount_ = input_.totalPops(); + } + void doXfer(Epoch) override { + if (!pending_) + return; + observed_.push_back(*pending_); + last_ = std::move(pending_); + pending_.reset(); + lastPopCount_ = pendingPopCount_; + } + bool hasPendingCommit() const override { return pending_.has_value(); } + bool isRunnable(Epoch) const override { + const T *head = input_.peek(); + return !pending_ && head != nullptr && + (!last_ || input_.totalPops() != lastPopCount_ || *last_ != *head); + } + const std::vector &observed() const { return observed_; } + void reset() override { + pending_.reset(); + last_.reset(); + observed_.clear(); + lastPopCount_ = 0; + pendingPopCount_ = 0; + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + std::optional pending_; + std::optional last_; + std::vector observed_; + uint64_t lastPopCount_ = 0; + uint64_t pendingPopCount_ = 0; +}; + +template + requires std::equality_comparable && + std::predicate +class QueueExpect final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.expect"; + static constexpr ObjectKind componentKind = ObjectKind::Probe; + + QueueExpect(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, std::string message, Predicate predicate = {}, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), message_(std::move(message)), + predicate_(std::move(predicate)) {} + + void doWork(Epoch) override { + const T *head = input_.peek(); + if (pending_ || head == nullptr) + return; + if (last_ && input_.totalPops() == lastPopCount_ && *last_ == *head) + return; + if (!std::invoke(std::as_const(predicate_), *head)) { + setRuntimeFailureCode("expectation_failed"); + return; + } + pending_ = *head; + pendingPopCount_ = input_.totalPops(); + } + void doXfer(Epoch) override { + if (!pending_) + return; + last_ = std::move(pending_); + pending_.reset(); + lastPopCount_ = pendingPopCount_; + } + bool hasPendingCommit() const override { return pending_.has_value(); } + bool isRunnable(Epoch) const override { + const T *head = input_.peek(); + return !pending_ && head != nullptr && + (!last_ || input_.totalPops() != lastPopCount_ || *last_ != *head); + } + std::string_view message() const { return message_; } + void reset() override { + pending_.reset(); + last_.reset(); + lastPopCount_ = 0; + pendingPopCount_ = 0; + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + std::string message_; + [[no_unique_address]] Predicate predicate_; + std::optional pending_; + std::optional last_; + uint64_t lastPopCount_ = 0; + uint64_t pendingPopCount_ = 0; +}; + +template +class QueueBroadcast final : public SimObject { +public: + static_assert(Outputs >= 2); + static constexpr std::string_view contractName = "ac.broadcast"; + static constexpr ObjectKind componentKind = ObjectKind::Link; + + QueueBroadcast(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, std::array *, Outputs> outputs, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), outputs_(outputs) {} + + void doWork(Epoch) override { + if (fired_ || !input_.canProposePop() || + std::any_of(outputs_.begin(), outputs_.end(), [](const auto *output) { + return output == nullptr || !output->canProposePush(); + })) + return; + const T *head = input_.peek(); + if (head == nullptr) + return; + for (SimQueue *output : outputs_) + if (!output->proposePush(*head)) + return; + if (!input_.proposePop()) + return; + fired_ = true; + } + void doXfer(Epoch) override { fired_ = false; } + bool hasPendingCommit() const override { return fired_; } + bool isRunnable(Epoch) const override { + return !fired_ && input_.canProposePop() && + std::all_of(outputs_.begin(), outputs_.end(), + [](const auto *output) { + return output != nullptr && output->canProposePush(); + }); + } + void reset() override { + fired_ = false; + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + std::array *, Outputs> outputs_; + bool fired_ = false; +}; + +template class QueueFork final : public SimObject { +public: + static_assert(Outputs >= 2); + static constexpr std::string_view contractName = "ac.fork"; + static constexpr ObjectKind componentKind = ObjectKind::Link; + + QueueFork(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, std::array *, Outputs> outputs, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), outputs_(outputs) {} + + void doWork(Epoch) override { + if (proposal_) + return; + const T *token = pending_ ? &*pending_ : input_.peek(); + if (token == nullptr) + return; + std::array next = delivered_; + bool changed = !pending_.has_value(); + for (size_t index = 0; index < Outputs; ++index) { + SimQueue *output = outputs_[index]; + if (next[index] || output == nullptr || !output->canProposePush()) + continue; + if (!output->proposePush(*token)) + continue; + next[index] = true; + changed = true; + } + const bool deliveredAll = + std::all_of(next.begin(), next.end(), [](bool value) { return value; }); + bool complete = false; + if (deliveredAll && input_.canProposePop()) { + complete = input_.proposePop().has_value(); + changed = changed || complete; + } + if (!changed) + return; + proposedToken_ = *token; + proposedDelivered_ = next; + proposedComplete_ = complete; + proposal_ = true; + } + void doXfer(Epoch) override { + if (!proposal_) + return; + if (proposedComplete_) { + pending_.reset(); + delivered_.fill(false); + } else { + pending_ = std::move(proposedToken_); + delivered_ = proposedDelivered_; + } + proposedToken_.reset(); + proposedDelivered_.fill(false); + proposedComplete_ = false; + proposal_ = false; + } + bool hasPendingCommit() const override { return proposal_; } + bool isRunnable(Epoch) const override { + if (proposal_ || (!pending_ && input_.peek() == nullptr)) + return false; + for (size_t index = 0; index < Outputs; ++index) + if (!delivered_[index] && outputs_[index] != nullptr && + outputs_[index]->canProposePush()) + return true; + return false; + } + void reset() override { + pending_.reset(); + proposedToken_.reset(); + delivered_.fill(false); + proposedDelivered_.fill(false); + proposedComplete_ = false; + proposal_ = false; + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + std::array *, Outputs> outputs_; + std::optional pending_; + std::optional proposedToken_; + std::array delivered_{}; + std::array proposedDelivered_{}; + bool proposedComplete_ = false; + bool proposal_ = false; +}; + +template + requires std::invocable && + std::integral> +class QueueRoute final : public SimObject { +public: + static_assert(Outputs >= 2); + static constexpr std::string_view contractName = "ac.route"; + static constexpr ObjectKind componentKind = ObjectKind::Link; + + QueueRoute(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, std::array *, Outputs> outputs, + Selector selector = {}, ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), outputs_(outputs), selector_(std::move(selector)) {} + + void doWork(Epoch) override { + if (fired_ || !input_.canProposePop()) + return; + const T *head = input_.peek(); + if (head == nullptr) + return; + auto selected = std::invoke(std::as_const(selector_), *head); + if constexpr (std::signed_integral) + if (selected < 0) { + setRuntimeFailureCode("route_selector_out_of_range"); + return; + } + const size_t index = static_cast(selected); + if (index >= Outputs || outputs_[index] == nullptr) { + setRuntimeFailureCode("route_selector_out_of_range"); + return; + } + if (!outputs_[index]->canProposePush()) + return; + if (!outputs_[index]->proposePush(*head) || !input_.proposePop()) + return; + fired_ = true; + } + void doXfer(Epoch) override { fired_ = false; } + bool hasPendingCommit() const override { return fired_; } + void reset() override { + fired_ = false; + clearRuntimeFailureCode(); + } + +private: + SimQueue &input_; + std::array *, Outputs> outputs_; + [[no_unique_address]] Selector selector_; + bool fired_ = false; +}; + +template + requires std::invocable && + std::integral< + std::invoke_result_t> +class QueueSelect final : public SimObject { +public: + static_assert(Inputs >= 2); + static constexpr std::string_view contractName = "ac.select"; + static constexpr ObjectKind componentKind = ObjectKind::Link; + + QueueSelect(std::string name, ObjectId id, SimObject *parent, + SimQueue &control, + std::array *, Inputs> inputs, SimQueue &output, + Selector selector = {}, ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + control_(control), inputs_(inputs), output_(output), + selector_(std::move(selector)) {} + + void doWork(Epoch) override { + if (fired_ || !control_.canProposePop() || !output_.canProposePush()) + return; + const Control *control = control_.peek(); + if (control == nullptr) + return; + auto selected = std::invoke(std::as_const(selector_), *control); + if constexpr (std::signed_integral) + if (selected < 0) { + setRuntimeFailureCode("select_selector_out_of_range"); + return; + } + const size_t index = static_cast(selected); + if (index >= Inputs || inputs_[index] == nullptr) { + setRuntimeFailureCode("select_selector_out_of_range"); + return; + } + SimQueue &input = *inputs_[index]; + const T *head = input.peek(); + if (!input.canProposePop() || head == nullptr) + return; + if (!output_.proposePush(*head) || !input.proposePop() || + !control_.proposePop()) + return; + fired_ = true; + } + void doXfer(Epoch) override { fired_ = false; } + bool hasPendingCommit() const override { return fired_; } + bool isRunnable(Epoch) const override { + return !fired_ && control_.canProposePop() && output_.canProposePush(); + } + void reset() override { + fired_ = false; + clearRuntimeFailureCode(); + } + +private: + SimQueue &control_; + std::array *, Inputs> inputs_; + SimQueue &output_; + [[no_unique_address]] Selector selector_; + bool fired_ = false; +}; + +enum class QueueMergePolicy { RoundRobin, Priority }; + +template class QueueMerge final : public SimObject { +public: + static_assert(Inputs >= 2); + static constexpr std::string_view contractName = "ac.merge"; + static constexpr ObjectKind componentKind = ObjectKind::Link; + + QueueMerge(std::string name, ObjectId id, SimObject *parent, + std::array *, Inputs> inputs, SimQueue &output, + QueueMergePolicy policy = QueueMergePolicy::RoundRobin, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + inputs_(inputs), output_(output), policy_(policy) {} + + void doWork(Epoch) override { + if (selected_ || !output_.canProposePush()) + return; + const size_t start = + policy_ == QueueMergePolicy::RoundRobin ? cursor_ : size_t{0}; + for (size_t offset = 0; offset < Inputs; ++offset) { + const size_t index = (start + offset) % Inputs; + SimQueue *input = inputs_[index]; + if (input == nullptr || !input->canProposePop()) + continue; + const T *head = input->peek(); + if (head == nullptr || !output_.proposePush(*head) || + !input->proposePop()) + return; + selected_ = index; + return; + } + } + void doXfer(Epoch) override { + if (selected_ && policy_ == QueueMergePolicy::RoundRobin) + cursor_ = (*selected_ + 1) % Inputs; + selected_.reset(); + } + bool hasPendingCommit() const override { return selected_.has_value(); } + void reset() override { + cursor_ = 0; + selected_.reset(); + clearRuntimeFailureCode(); + } + +private: + std::array *, Inputs> inputs_; + SimQueue &output_; + QueueMergePolicy policy_; + size_t cursor_ = 0; + std::optional selected_; +}; + +template struct FeedbackToken { + T value; + size_t iteration = 0; +}; + +template + requires std::invocable && + std::convertible_to, + T> && + std::predicate +class QueueFeedback final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.feedback"; + static constexpr ObjectKind componentKind = ObjectKind::Compute; + + QueueFeedback(std::string name, ObjectId id, SimObject *parent, + SimQueue &input, SimQueue> &feedback, + SimQueue &output, size_t maxIterations, Update update = {}, + Condition condition = {}, + ObservationSink *observations = nullptr) + : SimObject(componentKind, std::move(name), id, parent, observations), + input_(input), feedback_(feedback), output_(output), + maxIterations_(maxIterations), update_(std::move(update)), + condition_(std::move(condition)) {} + + void doWork(Epoch) override { + if (fired_) + return; + if (feedback_.canProposePop()) { + const FeedbackToken *head = feedback_.peek(); + if (head != nullptr) + fire(*head, feedback_); + return; + } + if (!input_.canProposePop()) + return; + const T *head = input_.peek(); + if (head != nullptr) + fire(FeedbackToken{*head, 0}, input_); + } + void doXfer(Epoch) override { fired_ = false; } + bool hasPendingCommit() const override { return fired_; } + void reset() override { + fired_ = false; + clearRuntimeFailureCode(); + } + +private: + template + void fire(const FeedbackToken &token, InputQueue &source) { + if (!std::invoke(std::as_const(condition_), token.value)) { + if (!output_.canProposePush() || !output_.proposePush(token.value) || + !source.proposePop()) + return; + fired_ = true; + return; + } + if (token.iteration >= maxIterations_) { + setRuntimeFailureCode("feedback_iteration_limit"); + return; + } + const bool replacesFeedback = + std::same_as>>; + const bool canPush = replacesFeedback ? feedback_.canProposePushAfterPop() + : feedback_.canProposePush(); + if (!canPush) + return; + FeedbackToken next{std::invoke(std::as_const(update_), token.value), + token.iteration + 1}; + if (!source.proposePop() || !feedback_.proposePush(std::move(next))) + return; + fired_ = true; + } + + SimQueue &input_; + SimQueue> &feedback_; + SimQueue &output_; + size_t maxIterations_; + [[no_unique_address]] Update update_; + [[no_unique_address]] Condition condition_; + bool fired_ = false; +}; + +} // namespace gfsim + +#endif // GFSIM_QUEUE_BLOCKS_H diff --git a/components/agentic-circuit/include/gfsim/resource.h b/components/agentic-circuit/include/gfsim/resource.h new file mode 100644 index 00000000..ff53f666 --- /dev/null +++ b/components/agentic-circuit/include/gfsim/resource.h @@ -0,0 +1,352 @@ +#ifndef GFSIM_RESOURCE_H +#define GFSIM_RESOURCE_H + +#include "gfsim/core.h" +#include "gfsim/object.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +class Resource : public SimObject { +public: + struct Reservation { + ObjectId ownerId = kInvalidObjectId; + uint32_t amount = 0; + Epoch issueTime; + Epoch readyTime; + uint64_t transactionId = 0; + uint64_t rootTransactionId = 0; + uint32_t priority = 0; + uint32_t portIndex = 0; + uint32_t instanceIndex = 0; + }; + + Resource(std::string name, ObjectId id, SimObject *parent, + uint32_t totalCapacity, ObservationSink *observations = nullptr) + : SimObject(ObjectKind::Resource, std::move(name), id, parent, + observations), + totalCapacity_(totalCapacity) {} + + uint32_t totalCapacity() const { return totalCapacity_; } + uint32_t availableCapacity() const { + return totalCapacity_ - activeCapacity_; + } + uint32_t activeReservations() const { return activeCapacity_; } + bool canReserve(uint32_t amount = 1) const { + return amount != 0 && availableCapacity() >= amount; + } + + bool proposeReserve(ObjectId owner, uint32_t amount, Epoch issueTime, + uint64_t transactionId) { + return proposeReserve(owner, amount, issueTime, transactionId, issueTime, + transactionId); + } + + bool proposeReserve(ObjectId owner, uint32_t amount, Epoch issueTime, + uint64_t transactionId, Epoch readyTime, + uint64_t rootTransactionId) { + return proposeReserve({owner, amount, issueTime, readyTime, transactionId, + rootTransactionId}); + } + + bool proposeReserve(Reservation request) { + if (request.ownerId == kInvalidObjectId || request.amount == 0 || + request.amount > totalCapacity_ || + request.readyTime < request.issueTime || + hasReservation(request.transactionId) || + std::ranges::any_of( + proposals_, + [&](const Reservation &proposal) { + return proposal.transactionId == request.transactionId; + })) + return false; + proposals_.push_back(request); + return true; + } + + bool proposeRelease(ObjectId owner, uint32_t amount) { + if (amount == 0) + return false; + uint32_t releasable = 0; + for (const auto &[transactionId, reservation] : reservations_) { + if (reservation.ownerId == owner && !isCancellationPending(transactionId)) + releasable += reservation.amount; + } + for (const ReleaseProposal &release : releaseProposals_) + if (release.ownerId == owner) + releasable = + release.amount > releasable ? 0 : releasable - release.amount; + if (amount > releasable) + return false; + releaseProposals_.push_back({owner, amount}); + return true; + } + + bool proposeCancel(ObjectId owner, uint64_t transactionId) { + const Reservation *reservation = findReservation(transactionId); + if (!reservation || reservation->ownerId != owner || + isCancellationPending(transactionId)) + return false; + cancellationProposals_.push_back(transactionId); + return true; + } + + void doArbitrate(Epoch) override { + acceptedProposals_.clear(); + proposedRejectedTransactions_.clear(); + std::sort(proposals_.begin(), proposals_.end(), reservationLess); + uint32_t remaining = availableCapacity(); + for (const Reservation &proposal : proposals_) { + if (proposal.amount <= remaining) { + acceptedProposals_.push_back(proposal); + remaining -= proposal.amount; + } else { + proposedRejectedTransactions_.push_back(proposal.transactionId); + } + } + for (const Reservation &reservation : acceptedProposals_) { + auto start = presentationTime(reservation.issueTime); + auto end = presentationTime(reservation.readyTime); + if (!start || !end) { + setRuntimeFailureCode("observation_time_overflow"); + continue; + } + emitObservation( + {.category = "resource", + .name = "reservation", + .phase = TraceEventPhase::Complete, + .rootSequenceId = reservation.rootTransactionId, + .duration = *end - *start, + .arguments = { + {"amount", static_cast(reservation.amount)}, + {"child_owner_id", static_cast(reservation.ownerId)}, + {"transaction_id", reservation.transactionId}}}); + } + for (const Reservation &reservation : proposals_) { + if (std::ranges::find(proposedRejectedTransactions_, + reservation.transactionId) == + proposedRejectedTransactions_.end()) + continue; + emitObservation( + {.category = "stall", + .name = "capacity", + .phase = TraceEventPhase::Instant, + .rootSequenceId = reservation.rootTransactionId, + .arguments = { + {"amount", static_cast(reservation.amount)}, + {"child_owner_id", static_cast(reservation.ownerId)}, + {"transaction_id", reservation.transactionId}}}); + } + } + + void doXfer(Epoch epoch) override { + bool changed = !acceptedProposals_.empty() || !releaseProposals_.empty() || + !cancellationProposals_.empty() || + !proposedRejectedTransactions_.empty(); + for (const Reservation &reservation : acceptedProposals_) { + reservations_.emplace(reservation.transactionId, reservation); + activeCapacity_ += reservation.amount; + totalReservations_ += reservation.amount; + } + + for (uint64_t transactionId : cancellationProposals_) { + auto reservation = reservations_.find(transactionId); + if (reservation == reservations_.end()) + continue; + activeCapacity_ -= reservation->second.amount; + totalCancellations_ += reservation->second.amount; + reservations_.erase(reservation); + } + + for (const ReleaseProposal &release : releaseProposals_) + applyRelease(release); + + rejectedTransactions_ = proposedRejectedTransactions_; + + proposals_.clear(); + acceptedProposals_.clear(); + releaseProposals_.clear(); + cancellationProposals_.clear(); + proposedRejectedTransactions_.clear(); + if (activeCapacity_ > highWatermark_) + highWatermark_ = activeCapacity_; + if (changed) + lastUpdate_ = epoch; + } + + bool hasPendingCommit() const override { + return !acceptedProposals_.empty() || !releaseProposals_.empty() || + !cancellationProposals_.empty() || + !proposedRejectedTransactions_.empty(); + } + + RuntimeObjectState runtimeState(Epoch epoch) const override { + RuntimeObjectState state = SimObject::runtimeState(epoch); + state.activeReservations = activeCapacity_; + state.pendingOffers = proposals_.size() + acceptedProposals_.size() + + releaseProposals_.size() + + cancellationProposals_.size() + + proposedRejectedTransactions_.size(); + state.quiescent = reservations_.empty() && state.pendingOffers == 0; + if (!state.quiescent) + state.reason = state.pendingOffers ? "resource_pending_proposal" + : "resource_reservation_live"; + return state; + } + + void collectStatistics(std::vector &out) const override { + auto append = [&](std::string name, uint64_t value, StatisticKind kind) { + out.push_back({.name = std::move(name), + .objectPath = std::string(path()), + .kind = kind, + .value = value, + .lastUpdate = lastUpdate_}); + }; + append("active_reservations", activeCapacity_, StatisticKind::Gauge); + append("reservation_peak", highWatermark_, StatisticKind::Gauge); + append("total_reservations", totalReservations_, StatisticKind::Counter); + append("total_releases", totalReleases_, StatisticKind::Counter); + append("total_cancellations", totalCancellations_, StatisticKind::Counter); + } + + bool hasReservation(uint64_t transactionId) const { + return reservations_.contains(transactionId); + } + + const Reservation *findReservation(uint64_t transactionId) const { + auto reservation = reservations_.find(transactionId); + return reservation == reservations_.end() ? nullptr : &reservation->second; + } + + std::vector readyReservations(Epoch epoch) const { + std::vector ready; + for (const auto &[transactionId, reservation] : reservations_) + if (reservation.readyTime == epoch) + ready.push_back(&reservation); + std::sort(ready.begin(), ready.end(), + [](const Reservation *left, const Reservation *right) { + return reservationLess(*left, *right); + }); + return ready; + } + + const std::vector &rejectedTransactions() const { + return rejectedTransactions_; + } + + bool validate() const { + uint64_t capacity = 0; + for (const auto &[transactionId, reservation] : reservations_) { + if (transactionId != reservation.transactionId || + reservation.ownerId == kInvalidObjectId || reservation.amount == 0 || + reservation.readyTime < reservation.issueTime) + return false; + capacity += reservation.amount; + } + return capacity == activeCapacity_ && capacity <= totalCapacity_; + } + + uint32_t highWatermark() const { return highWatermark_; } + uint64_t totalReservations() const { return totalReservations_; } + uint64_t totalReleases() const { return totalReleases_; } + uint64_t totalCancellations() const { return totalCancellations_; } + + void reset() override { + activeCapacity_ = 0; + proposals_.clear(); + acceptedProposals_.clear(); + rejectedTransactions_.clear(); + proposedRejectedTransactions_.clear(); + releaseProposals_.clear(); + cancellationProposals_.clear(); + reservations_.clear(); + highWatermark_ = 0; + totalReservations_ = 0; + totalReleases_ = 0; + totalCancellations_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); + } + +private: + struct ReleaseProposal { + ObjectId ownerId; + uint32_t amount; + }; + + static bool reservationLess(const Reservation &left, + const Reservation &right) { + return std::tie(left.priority, left.portIndex, left.instanceIndex, + left.ownerId, left.rootTransactionId, left.transactionId) < + std::tie(right.priority, right.portIndex, right.instanceIndex, + right.ownerId, right.rootTransactionId, + right.transactionId); + } + + static std::optional presentationTime(Epoch epoch) { + if (epoch.time > (std::numeric_limits::max() - epoch.delta) / + kMaxDeltasPerTick) + return std::nullopt; + return epoch.time * kMaxDeltasPerTick + epoch.delta; + } + + bool isCancellationPending(uint64_t transactionId) const { + return std::ranges::find(cancellationProposals_, transactionId) != + cancellationProposals_.end(); + } + + void applyRelease(const ReleaseProposal &release) { + std::vector owned; + for (auto &[transactionId, reservation] : reservations_) + if (reservation.ownerId == release.ownerId) + owned.push_back(&reservation); + std::sort(owned.begin(), owned.end(), + [](const Reservation *left, const Reservation *right) { + return reservationLess(*left, *right); + }); + + uint32_t remaining = release.amount; + std::vector emptyReservations; + for (Reservation *reservation : owned) { + uint32_t released = std::min(remaining, reservation->amount); + reservation->amount -= released; + activeCapacity_ -= released; + totalReleases_ += released; + remaining -= released; + if (reservation->amount == 0) + emptyReservations.push_back(reservation->transactionId); + if (remaining == 0) + break; + } + for (uint64_t transactionId : emptyReservations) + reservations_.erase(transactionId); + } + + uint32_t totalCapacity_ = 0; + uint32_t activeCapacity_ = 0; + uint32_t highWatermark_ = 0; + uint64_t totalReservations_ = 0; + uint64_t totalReleases_ = 0; + uint64_t totalCancellations_ = 0; + std::vector proposals_; + std::vector acceptedProposals_; + std::vector rejectedTransactions_; + std::vector proposedRejectedTransactions_; + std::vector releaseProposals_; + std::vector cancellationProposals_; + std::map reservations_; + Epoch lastUpdate_; +}; + +} // namespace gfsim + +#endif // GFSIM_RESOURCE_H diff --git a/components/agentic-circuit/include/gfsim/showcase.h b/components/agentic-circuit/include/gfsim/showcase.h new file mode 100644 index 00000000..8f886d3c --- /dev/null +++ b/components/agentic-circuit/include/gfsim/showcase.h @@ -0,0 +1,123 @@ +#ifndef GFSIM_SHOWCASE_H +#define GFSIM_SHOWCASE_H + +#include "gfsim/components.h" + +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +enum class ShowcaseWorkOrder : uint8_t { Ascending, Descending, Seeded }; + +struct ProducerQueueConsumerPolicy { + std::vector values = {3, 5, 8}; + size_t queueCapacity = 2; +}; + +struct BackpressuredPipelinePolicy { + std::vector values = {21, 34}; + std::vector readyTicks = {2, 4}; +}; + +struct MemoryWorkItem { + uint64_t correlationId = 0; + size_t address = 0; + uint64_t value = 0; +}; + +struct RequestResponseMemoryPolicy { + std::vector requests = { + {.correlationId = 10, .address = 1, .value = 17}, + {.correlationId = 11, .address = 3, .value = 29}}; + size_t memoryCapacity = 4; +}; + +struct NestedArraysPolicy { + std::vector> laneValues = { + {1, 2}, {10, 20}, {100, 200}}; + size_t queueCapacity = 2; +}; + +struct MultiTimeDomainBridgePolicy { + std::vector values = {4, 6, 9}; + uint64_t sourcePeriod = 2; + uint64_t targetPeriod = 3; +}; + +struct SuspendedProcessPolicy { + uint64_t initialValue = 40; + uint64_t incrementAfterWake = 2; + Tick wakeTick = 3; +}; + +using ShowcasePolicy = + std::variant; + +struct ShowcaseHierarchyEntry { + ObjectId id = kInvalidObjectId; + std::string path; + ObjectKind kind = ObjectKind::Module; + bool operator==(const ShowcaseHierarchyEntry &) const = default; +}; + +struct ShowcaseResult { + TerminationResult termination; + std::map architecturalValues; + std::vector hierarchy; + uint64_t tracePosition = 0; + std::vector statistics; + std::vector events; +}; + +ShowcaseResult runShowcase(const ShowcasePolicy &policy, + ShowcaseWorkOrder order, + uint64_t permutationSeed = 0); + +template +ShowcaseResult runShowcase(const Policy &policy, ShowcaseWorkOrder order, + uint64_t permutationSeed = 0) { + return runShowcase(ShowcasePolicy{policy}, order, permutationSeed); +} + +/// Stable byte representation used by conformance tests and golden fixtures. +std::string canonicalShowcaseResult(const ShowcaseResult &result); + +/// Private provider component used by checked-in example workspaces. It is +/// intentionally absent from the public standard-library catalog. +class ShowcaseTraceSource final : public SimObject { +public: + static constexpr std::string_view contractName = "workspace.Showcase"; + static constexpr ObjectKind componentKind = ObjectKind::TraceSource; + + ShowcaseTraceSource(std::string name, ObjectId id, SimObject *parent, + uint64_t scenario); + + bool loadDocument(PtoTraceDocument document); + void doWork(Epoch epoch) override; + void doXfer(Epoch epoch) override; + bool hasPendingCommit() const override; + RuntimeObjectState runtimeState(Epoch epoch) const override; + void collectStatistics(std::vector &out) const override; + void reset() override; + bool validate() const; + +private: + uint64_t scenario_ = 0; + PtoTraceDocument document_; + ShowcaseResult result_; + bool loaded_ = false; + bool pending_ = false; + bool committed_ = false; + Epoch lastUpdate_; +}; + +} // namespace gfsim + +#endif // GFSIM_SHOWCASE_H diff --git a/components/agentic-circuit/include/gfsim/statistics.h b/components/agentic-circuit/include/gfsim/statistics.h new file mode 100644 index 00000000..8d7d8f5f --- /dev/null +++ b/components/agentic-circuit/include/gfsim/statistics.h @@ -0,0 +1,175 @@ +#ifndef GFSIM_STATISTICS_H +#define GFSIM_STATISTICS_H + +#include "gfsim/object.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +/// Deterministic counter, gauge, or histogram. Mutations are private proposals +/// and become visible only at the Xfer barrier. +class Statistic final : public SimObject { +public: + Statistic(std::string name, ObjectId id, SimObject *parent, + StatisticKind kind) + : SimObject(ObjectKind::Statistic, std::move(name), id, parent), + kind_(kind), valid_(kind != StatisticKind::Histogram) {} + + Statistic(std::string name, ObjectId id, SimObject *parent, + std::initializer_list bounds) + : Statistic(std::move(name), id, parent, std::vector(bounds)) {} + + Statistic(std::string name, ObjectId id, SimObject *parent, + std::vector bounds) + : SimObject(ObjectKind::Statistic, std::move(name), id, parent), + kind_(StatisticKind::Histogram), bounds_(std::move(bounds)), + bucketCounts_(bounds_.size() + 1), + proposedBucketCounts_(bounds_.size() + 1), + valid_(std::adjacent_find(bounds_.begin(), bounds_.end(), + std::greater_equal<>()) == bounds_.end()) {} + + StatisticKind statisticKind() const { return kind_; } + + bool proposeAdd(uint64_t amount = 1) { + if (!valid_ || kind_ != StatisticKind::Counter || + amount > maxValue() - proposedValue_ || + proposedValue_ + amount > maxValue() - value_) + return false; + proposedValue_ += amount; + pending_ = true; + return true; + } + + bool proposeSet(uint64_t value) { + if (!valid_ || kind_ != StatisticKind::Gauge || pending_) + return false; + proposedValue_ = value; + pending_ = true; + return true; + } + + bool proposeObserve(uint64_t value) { + if (!valid_ || kind_ != StatisticKind::Histogram || + proposedCount_ >= maxValue() - count_ || + value > maxValue() - proposedSum_ || + proposedSum_ + value > maxValue() - sum_) + return false; + size_t bucket = static_cast( + std::lower_bound(bounds_.begin(), bounds_.end(), value) - + bounds_.begin()); + if (proposedBucketCounts_[bucket] >= maxValue() - bucketCounts_[bucket]) + return false; + ++proposedBucketCounts_[bucket]; + ++proposedCount_; + proposedSum_ += value; + if (!proposedMinimum_ || value < *proposedMinimum_) + proposedMinimum_ = value; + if (!proposedMaximum_ || value > *proposedMaximum_) + proposedMaximum_ = value; + pending_ = true; + return true; + } + + void doXfer(Epoch epoch) override { + if (!pending_) + return; + if (kind_ == StatisticKind::Counter) + value_ += proposedValue_; + else if (kind_ == StatisticKind::Gauge) + value_ = proposedValue_; + else { + count_ += proposedCount_; + sum_ += proposedSum_; + if (proposedMinimum_ && (!minimum_ || *proposedMinimum_ < *minimum_)) + minimum_ = proposedMinimum_; + if (proposedMaximum_ && (!maximum_ || *proposedMaximum_ > *maximum_)) + maximum_ = proposedMaximum_; + for (size_t index = 0; index < bucketCounts_.size(); ++index) + bucketCounts_[index] += proposedBucketCounts_[index]; + } + lastUpdate_ = epoch; + clearProposals(); + } + + bool hasPendingCommit() const override { return pending_; } + bool validate() const { return valid_; } + + StatSnapshot snapshot() const { + StatSnapshot result{.name = std::string(name()), + .objectPath = std::string(path()), + .kind = kind_, + .value = + kind_ == StatisticKind::Histogram ? count_ : value_, + .count = count_, + .sum = sum_, + .minimum = minimum_.value_or(0), + .maximum = maximum_.value_or(0), + .lastUpdate = lastUpdate_}; + if (kind_ == StatisticKind::Histogram) { + result.buckets.reserve(bucketCounts_.size()); + for (size_t index = 0; index < bounds_.size(); ++index) + result.buckets.push_back({bounds_[index], bucketCounts_[index]}); + result.buckets.push_back({maxValue(), bucketCounts_.back()}); + } + return result; + } + + void collectStatistics(std::vector &out) const override { + out.push_back(snapshot()); + } + + void reset() override { + value_ = 0; + count_ = 0; + sum_ = 0; + minimum_.reset(); + maximum_.reset(); + std::fill(bucketCounts_.begin(), bucketCounts_.end(), 0); + lastUpdate_ = {}; + clearProposals(); + } + +private: + static constexpr uint64_t maxValue() { + return std::numeric_limits::max(); + } + + void clearProposals() { + proposedValue_ = 0; + proposedCount_ = 0; + proposedSum_ = 0; + proposedMinimum_.reset(); + proposedMaximum_.reset(); + std::fill(proposedBucketCounts_.begin(), proposedBucketCounts_.end(), 0); + pending_ = false; + } + + StatisticKind kind_; + uint64_t value_ = 0; + uint64_t count_ = 0; + uint64_t sum_ = 0; + std::optional minimum_; + std::optional maximum_; + std::vector bounds_; + std::vector bucketCounts_; + Epoch lastUpdate_; + bool pending_ = false; + uint64_t proposedValue_ = 0; + uint64_t proposedCount_ = 0; + uint64_t proposedSum_ = 0; + std::optional proposedMinimum_; + std::optional proposedMaximum_; + std::vector proposedBucketCounts_; + bool valid_ = true; +}; + +} // namespace gfsim + +#endif // GFSIM_STATISTICS_H diff --git a/components/agentic-circuit/include/gfsim/trace.h b/components/agentic-circuit/include/gfsim/trace.h new file mode 100644 index 00000000..d478b9bc --- /dev/null +++ b/components/agentic-circuit/include/gfsim/trace.h @@ -0,0 +1,415 @@ +#ifndef GFSIM_TRACE_H +#define GFSIM_TRACE_H + +#include "gfsim/object.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +struct PtoValue { + using Array = std::vector; + using Object = std::map; + using Storage = std::variant; + Storage value; + + bool operator==(const PtoValue &) const = default; +}; + +enum class PtoOperandKind : uint8_t { + Immediate, + Buffer, + Tile, + Address, + Symbol, + RecordResult, +}; + +struct PtoTraceOperand { + PtoOperandKind kind = PtoOperandKind::Immediate; + std::string type; + std::optional immediate; + std::string id; + std::string addressSpace; + uint64_t address = 0; + uint64_t sequenceId = 0; + uint64_t resultIndex = 0; + + bool operator==(const PtoTraceOperand &) const = default; +}; + +struct PtoSourceLocation { + std::string file; + uint64_t line = 0; + uint64_t column = 0; + + bool operator==(const PtoSourceLocation &) const = default; +}; + +struct PtoTraceRecord { + uint64_t sequenceId = 0; + std::string opcode; + std::vector operands; + std::vector dependencies; + PtoValue::Object attributes; + std::optional issueTime; + std::optional source; + + bool operator==(const PtoTraceRecord &) const = default; +}; + +struct PtoTraceMetadata { + std::string producer; + std::string ptoIdentity; + std::string sourceProgram; + std::vector addressSpaces; + std::string dataLayout; + std::optional recordCount; + std::string contentHash; + + bool operator==(const PtoTraceMetadata &) const = default; +}; + +struct PtoTraceDocument { + PtoTraceMetadata metadata; + std::vector records; + + PtoTraceDocument() = default; + PtoTraceDocument(const PtoTraceDocument &) = delete; + PtoTraceDocument &operator=(const PtoTraceDocument &) = delete; + PtoTraceDocument(PtoTraceDocument &&) = default; + PtoTraceDocument &operator=(PtoTraceDocument &&) = default; + bool operator==(const PtoTraceDocument &) const = default; +}; + +struct TraceValidationLimits { + size_t maxDocumentBytes = 1U << 20; + size_t maxNestingDepth = 64; + size_t maxStringBytes = 1U << 18; + size_t maxRecordCount = 65536; + size_t maxOperandsPerRecord = 1024; + size_t maxDependenciesPerRecord = 1024; + size_t maxAttributeMembers = 4096; + size_t maxAggregateDecodedBytes = 1U << 24; + size_t maxDiagnostics = 16; +}; + +struct TraceDiagnostic { + std::string code; + std::string jsonPointer; + std::optional sequenceId; + std::string message; + + bool operator==(const TraceDiagnostic &) const = default; +}; + +struct TraceLoadResult { + std::optional document; + std::vector diagnostics; + + bool succeeded() const { return document.has_value() && diagnostics.empty(); } + std::string primaryDiagnostic() const; +}; + +TraceLoadResult +parsePtoTrace(std::string_view input, + const TraceValidationLimits &limits = TraceValidationLimits()); + +class PtoTraceStream { +public: + explicit PtoTraceStream( + TraceValidationLimits limits = TraceValidationLimits()) + : limits_(limits) {} + + bool append(std::string_view bytes); + TraceLoadResult finish(); + +private: + TraceValidationLimits limits_; + std::string buffer_; + bool finished_ = false; + bool exceededByteLimit_ = false; +}; + +struct TracePosition { + size_t nextRecordIndex = 0; + std::optional lastCommittedSequenceId; + bool endOfTrace = false; + + bool operator==(const TracePosition &) const = default; +}; + +template struct IdentityTraceDecoder { + std::optional operator()(const PtoTraceRecord &record) const + requires std::constructible_from + { + return Transaction(record); + } +}; + +template +concept TraceDecoder = + requires(const Decoder &decoder, const PtoTraceRecord &record) { + { + std::invoke(decoder, record) + } -> std::same_as>; + }; + +/// The unique cursor owner for one validated PTO trace document. +template > + requires TraceDecoder +class TraceSource final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.TraceSource"; + static constexpr ObjectKind componentKind = ObjectKind::TraceSource; + + TraceSource(std::string name, ObjectId id, SimObject *parent, + Decoder decoder = {}, SimSystem *system = nullptr, + ObservationSink *observations = nullptr) + : SimObject(ObjectKind::TraceSource, std::move(name), id, parent, + observations), + decoder_(std::move(decoder)), system_(system) { + position_.endOfTrace = true; + } + + TraceSource(std::string name, ObjectId id, SimObject *parent, + PtoTraceDocument document, Decoder decoder = {}, + SimSystem *system = nullptr, + ObservationSink *observations = nullptr) + : SimObject(ObjectKind::TraceSource, std::move(name), id, parent, + observations), + document_(std::move(document)), decoder_(std::move(decoder)), + documentLoaded_(true), system_(system) { + position_.endOfTrace = document_.records.empty(); + } + + bool loadDocument(PtoTraceDocument document) { + if (documentLoaded_ || committedOffer_ || offerProposal_ || + acceptProposal_ || position_.nextRecordIndex != 0 || + !issuedSequences_.empty() || !completedSequences_.empty()) + return false; + document_ = std::move(document); + documentLoaded_ = true; + position_.endOfTrace = document_.records.empty(); + return true; + } + + const Transaction *peekOffer() const { + return committedOffer_ ? &*committedOffer_ : nullptr; + } + bool hasOffer() const { return committedOffer_.has_value(); } + bool eof() const { return position_.endOfTrace; } + TracePosition position() const { return position_; } + + bool proposeAccept() { + if (!committedOffer_ || acceptProposal_) + return false; + acceptProposal_ = true; + return true; + } + + bool markDependencyComplete(uint64_t sequenceId) { + if (!issuedSequences_.contains(sequenceId)) + return false; + completedSequences_.insert(sequenceId); + return true; + } + + void doWork(Epoch epoch) override { + if (position_.endOfTrace || committedOffer_ || offerProposal_ || + acceptProposal_ || + position_.nextRecordIndex >= document_.records.size()) + return; + const PtoTraceRecord &record = document_.records[position_.nextRecordIndex]; + if (record.issueTime && epoch.time < *record.issueTime) { + if (system_ && !issueWakeScheduled_) { + if (!system_->scheduleEvent({{*record.issueTime, 0}, + id(), + kIssueEventKind, + record.sequenceId})) { + setRuntimeFailureCode("trace_issue_wake_failed"); + return; + } + issueWakeScheduled_ = true; + } + return; + } + issueWakeScheduled_ = false; + for (uint64_t dependency : record.dependencies) + if (!completedSequences_.contains(dependency)) + return; + + std::optional decoded = std::invoke(decoder_, record); + if (!decoded) { + setRuntimeFailureCode("trace_decode_failed"); + return; + } + offerProposal_ = std::move(decoded); + proposedSequenceId_ = record.sequenceId; + } + + void bindSystem(SimSystem *system) override { system_ = system; } + + void doArbitrate(Epoch) override { + if (acceptProposal_ && committedSequenceId_) + emitObservation({.category = "transaction", + .name = "accepted", + .phase = TraceEventPhase::Instant, + .rootSequenceId = committedSequenceId_, + .arguments = {{"trace_position", + static_cast( + position_.nextRecordIndex + 1)}}}); + if (offerProposal_ && proposedSequenceId_) + emitObservation( + {.category = "transaction", + .name = "offered", + .phase = TraceEventPhase::Instant, + .rootSequenceId = proposedSequenceId_, + .arguments = {{"trace_position", + static_cast(position_.nextRecordIndex)}}}); + } + + void doXfer(Epoch epoch) override { + if (acceptProposal_) { + issuedSequences_.insert(*committedSequenceId_); + position_.lastCommittedSequenceId = committedSequenceId_; + ++position_.nextRecordIndex; + position_.endOfTrace = + position_.nextRecordIndex == document_.records.size(); + committedOffer_.reset(); + committedSequenceId_.reset(); + acceptProposal_ = false; + lastUpdate_ = epoch; + } + if (offerProposal_) { + committedOffer_ = std::move(offerProposal_); + committedSequenceId_ = proposedSequenceId_; + offerProposal_.reset(); + proposedSequenceId_.reset(); + } + } + + bool hasPendingCommit() const override { + return offerProposal_.has_value() || acceptProposal_; + } + + bool isRunnable(Epoch epoch) const override { + if (position_.endOfTrace || committedOffer_ || offerProposal_ || + acceptProposal_ || + position_.nextRecordIndex >= document_.records.size()) + return false; + const PtoTraceRecord &record = document_.records[position_.nextRecordIndex]; + if (record.issueTime && epoch.time < *record.issueTime) + return false; + for (uint64_t dependency : record.dependencies) + if (!completedSequences_.contains(dependency)) + return false; + return true; + } + + RuntimeObjectState runtimeState(Epoch epoch) const override { + RuntimeObjectState state = SimObject::runtimeState(epoch); + state.traceOwner = true; + state.tracePosition = position_.nextRecordIndex; + state.traceLastCommittedSequenceId = position_.lastCommittedSequenceId; + state.traceEof = position_.endOfTrace; + state.pendingOffers = + committedOffer_.has_value() || offerProposal_.has_value(); + state.quiescent = position_.endOfTrace && !hasPendingCommit() && + !committedOffer_.has_value(); + if (state.quiescent) + state.reason.clear(); + else if (committedOffer_) + state.reason = "trace_offer_blocked"; + else if (hasPendingCommit()) + state.reason = "pending_commit"; + else if (position_.nextRecordIndex < document_.records.size()) { + const PtoTraceRecord &record = + document_.records[position_.nextRecordIndex]; + for (uint64_t dependency : record.dependencies) + if (!completedSequences_.contains(dependency)) + state.dependencyChain.push_back(dependency); + if (!state.dependencyChain.empty()) + state.reason = "trace_dependency_blocked"; + else if (record.issueTime && epoch.time < *record.issueTime) + state.reason = "trace_issue_time_pending"; + else + state.reason = "trace_runnable_unscheduled"; + } + return state; + } + + void collectStatistics(std::vector &out) const override { + out.push_back({.name = "accepted_transactions", + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = position_.nextRecordIndex, + .lastUpdate = lastUpdate_}); + out.push_back({.name = "trace_position", + .objectPath = std::string(path()), + .kind = StatisticKind::Gauge, + .value = position_.nextRecordIndex, + .lastUpdate = lastUpdate_}); + } + + bool validate() const { + return !(committedOffer_ && offerProposal_) && + (committedOffer_.has_value() == committedSequenceId_.has_value()) && + (offerProposal_.has_value() == proposedSequenceId_.has_value()) && + (!acceptProposal_ || committedOffer_.has_value()) && + position_.nextRecordIndex <= document_.records.size() && + position_.endOfTrace == + (position_.nextRecordIndex == document_.records.size()) && + issuedSequences_.size() == position_.nextRecordIndex && + position_.lastCommittedSequenceId.has_value() == + (position_.nextRecordIndex != 0); + } + + void reset() override { + position_ = TracePosition{.endOfTrace = document_.records.empty()}; + committedOffer_.reset(); + offerProposal_.reset(); + committedSequenceId_.reset(); + proposedSequenceId_.reset(); + acceptProposal_ = false; + issueWakeScheduled_ = false; + issuedSequences_.clear(); + completedSequences_.clear(); + clearRuntimeFailureCode(); + lastUpdate_ = {}; + } + +private: + PtoTraceDocument document_; + [[no_unique_address]] Decoder decoder_; + TracePosition position_; + std::optional committedOffer_; + std::optional offerProposal_; + std::optional committedSequenceId_; + std::optional proposedSequenceId_; + bool acceptProposal_ = false; + bool issueWakeScheduled_ = false; + bool documentLoaded_ = false; + std::set issuedSequences_; + std::set completedSequences_; + SimSystem *system_ = nullptr; + Epoch lastUpdate_; + static constexpr uint32_t kIssueEventKind = 0x54524345; +}; + +} // namespace gfsim + +#endif // GFSIM_TRACE_H diff --git a/components/agentic-circuit/lib/Analysis/CMakeLists.txt b/components/agentic-circuit/lib/Analysis/CMakeLists.txt new file mode 100644 index 00000000..5c8c6af6 --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/CMakeLists.txt @@ -0,0 +1,27 @@ +add_acir_library(ACIRAnalysis SOURCES + ModelAnalysis.cpp + ProcessStateIdentity.cpp + ProcessStateExpansion.cpp + ProcessStateContinuation.cpp + ProcessStateWake.cpp + ProcessStateLiveness.cpp + ProcessStateCost.cpp + ProcessStatePlan.cpp + ProcessStateReport.cpp +) +target_link_libraries(ACIRAnalysis PUBLIC + ACIRDialect + ACIRBindings + MLIRFuncDialect + MLIRSideEffectInterfaces + LLVMSupport +) +target_include_directories(ACIRAnalysis PUBLIC + "$" + "$" + "$" +) +target_include_directories(ACIRAnalysis PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +add_library(AgenticCircuit::ACIRAnalysis ALIAS ACIRAnalysis) diff --git a/components/agentic-circuit/lib/Analysis/ModelAnalysis.cpp b/components/agentic-circuit/lib/Analysis/ModelAnalysis.cpp new file mode 100644 index 00000000..c5e8ddec --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ModelAnalysis.cpp @@ -0,0 +1,1072 @@ +#include "acir/Analysis/ModelAnalysis.h" + +#include "Dialect/ACIR/ProcessLowerability.h" +#include "ModelAnalysisInternal.h" +#include "ModelAnalysisTestHooks.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/GraphRegion.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/SHA256.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include + +using namespace mlir; + +namespace acir { +namespace { + +bool isTopologyDigestAttribute(StringRef name) { + // Every other freeze attribute participates in the integrity digest. Only + // the digest itself must be omitted to avoid self-reference. + return name == "ac.topology_digest"; +} + +std::string attributeToken(Attribute attribute) { + std::string storage; + llvm::raw_string_ostream stream(storage); + stream << attribute; + return storage; +} + +StringRef symbolName(Operation *operation) { + if (auto name = operation->getAttrOfType( + SymbolTable::getSymbolAttrName())) + return name.getValue(); + return {}; +} + +std::string ownerJoinKey(Operation *declaration, StringRef path, + StringRef stableId) { + std::string key; + llvm::raw_string_ostream stream(key); + if (auto definition = declaration->getParentOfType()) + stream << definition.getSymName(); + stream << "::" << declaration->getName().getStringRef() + << "::" << symbolName(declaration) << '\0' << path << '\0' << stableId; + return key; +} + +std::string operationKey(Operation *operation) { + std::string key = operation->getName().getStringRef().str(); + key.push_back('|'); + key.append(symbolName(operation)); + key.push_back('|'); + SmallVector attributes(operation->getAttrs().begin(), + operation->getAttrs().end()); + llvm::sort(attributes, [](NamedAttribute left, NamedAttribute right) { + return left.getName().getValue() < right.getName().getValue(); + }); + for (NamedAttribute attribute : attributes) { + if (isTopologyDigestAttribute(attribute.getName().getValue())) + continue; + key.append(attribute.getName().getValue()); + key.push_back('='); + key.append(attributeToken(attribute.getValue())); + key.push_back(';'); + } + return key; +} + +class StructuredTopologySerializer { +public: + explicit StructuredTopologySerializer(llvm::raw_ostream &stream) + : stream(stream) {} + + void run(ModuleOp model) { + SmallVector topLevel; + for (Operation &operation : model.getBody()->getOperations()) + if (operation.getName().getStringRef().starts_with("ac.")) + topLevel.push_back(&operation); + + indexResults(model.getOperation(), "root"); + for (auto [ordinal, operation] : llvm::enumerate(topLevel)) + indexOperation(operation, ("root/r0/b0/o" + llvm::Twine(ordinal)).str()); + + serializeHeader(model.getOperation(), "root"); + stream << "region root/r0\nblock root/r0/b0\n"; + for (auto [ordinal, operation] : llvm::enumerate(topLevel)) + serializeOperation(operation, + ("root/r0/b0/o" + llvm::Twine(ordinal)).str()); + } + +private: + bool skipsRegions(Operation *operation) const { + return isa(operation); + } + + void indexResults(Operation *operation, StringRef path) { + for (auto [ordinal, result] : llvm::enumerate(operation->getResults())) + valueIds[result] = (path + "/v" + llvm::Twine(ordinal)).str(); + } + + void indexOperation(Operation *operation, StringRef path) { + indexResults(operation, path); + if (skipsRegions(operation)) + return; + for (auto [regionIndex, region] : + llvm::enumerate(operation->getRegions())) { + for (auto [blockIndex, block] : llvm::enumerate(region)) { + std::string blockPath = (path + "/r" + llvm::Twine(regionIndex) + "/b" + + llvm::Twine(blockIndex)) + .str(); + for (auto [argumentIndex, argument] : + llvm::enumerate(block.getArguments())) + valueIds[argument] = + (blockPath + "/a" + llvm::Twine(argumentIndex)).str(); + for (auto [operationIndex, child] : llvm::enumerate(block)) + indexOperation( + &child, (blockPath + "/o" + llvm::Twine(operationIndex)).str()); + } + } + } + + void serializeHeader(Operation *operation, StringRef path) { + stream << "op " << path << ' ' << operation->getName().getStringRef() + << '{'; + SmallVector attributes(operation->getAttrs().begin(), + operation->getAttrs().end()); + llvm::sort(attributes, [](NamedAttribute left, NamedAttribute right) { + return left.getName().getValue() < right.getName().getValue(); + }); + for (NamedAttribute attribute : attributes) { + if (isTopologyDigestAttribute(attribute.getName().getValue())) + continue; + stream << attribute.getName().getValue() << '=' << attribute.getValue() + << ';'; + } + stream << "}props=" << operation->getPropertiesAsAttribute() + << " operands="; + for (Value operand : operation->getOperands()) { + auto found = valueIds.find(operand); + if (found == valueIds.end()) + stream << "external"; + else + stream << found->second; + stream << ':' << operand.getType() << ','; + } + stream << " results="; + for (auto [ordinal, type] : llvm::enumerate(operation->getResultTypes())) + stream << valueIds.lookup(operation->getResult(ordinal)) << ':' << type + << ','; + stream << '\n'; + } + + void serializeOperation(Operation *operation, StringRef path) { + serializeHeader(operation, path); + if (skipsRegions(operation)) + return; + for (auto [regionIndex, region] : + llvm::enumerate(operation->getRegions())) { + std::string regionPath = (path + "/r" + llvm::Twine(regionIndex)).str(); + stream << "region " << regionPath << '\n'; + for (auto [blockIndex, block] : llvm::enumerate(region)) { + std::string blockPath = + (regionPath + "/b" + llvm::Twine(blockIndex)).str(); + stream << "block " << blockPath << " args="; + for (auto [argumentIndex, argument] : + llvm::enumerate(block.getArguments())) + stream << valueIds.lookup(argument) << ':' << argument.getType() + << ','; + stream << '\n'; + for (auto [operationIndex, child] : llvm::enumerate(block)) + serializeOperation( + &child, (blockPath + "/o" + llvm::Twine(operationIndex)).str()); + } + } + } + + llvm::raw_ostream &stream; + DenseMap valueIds; +}; + +bool isProcessSkeletonOperation(Operation *operation) { + StringRef name = operation->getName().getStringRef(); + return name.starts_with("ac.") || name == "func.call"; +} + +class ProcessSkeletonSerializer { +public: + explicit ProcessSkeletonSerializer(ac::ProcessOp process) + : process(process), builder(process.getContext()) {} + + FailureOr run() { + if (failed(enqueueSeeds())) + return failure(); + + for (size_t cursor = 0; cursor < worklist.size(); ++cursor) { + Operation *operation = worklist[cursor]; + for (Value operand : operation->getOperands()) + if (Operation *producer = operand.getDefiningOp(); + producer && failed(enqueue(producer, operation, + /*dependency=*/true))) + return failure(); + if (failed(enqueue(operation->getParentOp(), operation, + /*dependency=*/true))) + return failure(); + // Region results are defined by terminator operands. Commit those + // dependencies whenever a region-bearing producer/control ancestor is + // part of the semantic closure. + for (Region ®ion : operation->getRegions()) + for (Block &block : region) + if (!block.empty() && failed(enqueue(&block.back(), operation, + /*dependency=*/true))) + return failure(); + } + + indexRegions(); + serializeRegions(); + return builder.getArrayAttr(entries); + } + +private: + LogicalResult enqueueSeeds() { + struct TraversalTask { + Operation *operation; + bool expanded; + }; + + SmallVector roots; + for (Block &block : process.getBody()) + for (Operation &operation : block) + roots.push_back(&operation); + SmallVector pending; + for (Operation *operation : llvm::reverse(roots)) + pending.push_back({operation, false}); + + while (!pending.empty()) { + TraversalTask task = pending.pop_back_val(); + if (task.expanded) { + if (isProcessSkeletonOperation(task.operation) && + failed(enqueue(task.operation, task.operation, + /*dependency=*/false))) + return failure(); + continue; + } + + pending.push_back({task.operation, true}); + SmallVector children; + for (Region ®ion : task.operation->getRegions()) + for (Block &block : region) + for (Operation &child : block) + children.push_back(&child); + for (Operation *child : llvm::reverse(children)) + pending.push_back({child, false}); + } + return success(); + } + + bool isInsideProcess(Operation *operation) { + return operation && operation != process.getOperation() && + operation->getParentOfType() == process; + } + + LogicalResult enqueue(Operation *candidate, Operation *origin, + bool dependency) { + if (!isInsideProcess(candidate)) + return success(); + + if (dependency) { + uint64_t limit = detail::processSkeletonEdgeLimit(); + if (dependencyEdges >= limit) + return origin->emitOpError() + << "process skeleton dependency edge count exceeds bound " + << limit; + ++dependencyEdges; + } + + if (included.contains(candidate)) + return success(); + uint64_t limit = detail::processSkeletonNodeLimit(); + if (included.size() >= limit) + return origin->emitOpError() + << "process skeleton dependency node count exceeds bound " + << limit; + included.insert(candidate); + worklist.push_back(candidate); + return success(); + } + + void indexRegions() { + struct RegionTask { + Region *region; + std::string path; + }; + + SmallVector pending{{&process.getBody(), "process/r0"}}; + while (!pending.empty()) { + RegionTask task = pending.pop_back_val(); + SmallVector nestedRegions; + for (auto [blockIndex, block] : llvm::enumerate(*task.region)) { + std::string blockPath = + (task.path + "/b" + llvm::Twine(blockIndex)).str(); + for (auto [argumentIndex, argument] : + llvm::enumerate(block.getArguments())) + valueIds[argument] = + (blockPath + "/a" + llvm::Twine(argumentIndex)).str(); + unsigned operationIndex = 0; + for (Operation &operation : block) { + if (!included.contains(&operation)) + continue; + std::string path = + (blockPath + "/o" + llvm::Twine(operationIndex++)).str(); + operationPaths[&operation] = path; + for (auto [resultIndex, result] : + llvm::enumerate(operation.getResults())) + valueIds[result] = (path + "/v" + llvm::Twine(resultIndex)).str(); + for (auto [regionIndex, nested] : + llvm::enumerate(operation.getRegions())) + nestedRegions.push_back( + {&nested, (path + "/r" + llvm::Twine(regionIndex)).str()}); + } + } + for (RegionTask &nested : llvm::reverse(nestedRegions)) + pending.push_back(std::move(nested)); + } + } + + void serializeRegions() { + struct SerializationTask { + Region *region = nullptr; + Operation *operation = nullptr; + }; + + SmallVector pending{{&process.getBody(), nullptr}}; + while (!pending.empty()) { + SerializationTask task = pending.pop_back_val(); + if (task.operation) { + serializeOperation(task.operation, + operationPaths.lookup(task.operation)); + for (Region &nested : llvm::reverse(task.operation->getRegions())) + pending.push_back({&nested, nullptr}); + continue; + } + + SmallVector operations; + for (Block &block : *task.region) + for (Operation &operation : block) + if (included.contains(&operation)) + operations.push_back(&operation); + for (Operation *operation : llvm::reverse(operations)) + pending.push_back({nullptr, operation}); + } + } + + void serializeOperation(Operation *operation, StringRef path) { + std::string storage; + llvm::raw_string_ostream stream(storage); + stream << path << ' ' << operation->getName().getStringRef() << '{'; + SmallVector attributes(operation->getAttrs().begin(), + operation->getAttrs().end()); + llvm::sort(attributes, [](NamedAttribute left, NamedAttribute right) { + return left.getName().getValue() < right.getName().getValue(); + }); + for (NamedAttribute attribute : attributes) + stream << attribute.getName().getValue() << '=' << attribute.getValue() + << ';'; + stream << "}props=" << operation->getPropertiesAsAttribute() + << " operands="; + for (Value operand : operation->getOperands()) { + auto found = valueIds.find(operand); + stream << (found == valueIds.end() ? "external" : found->second) << ':' + << operand.getType() << ','; + } + stream << " results="; + for (Value result : operation->getResults()) + stream << valueIds.lookup(result) << ':' << result.getType() << ','; + stream << " regions="; + for (Region ®ion : operation->getRegions()) { + stream << '['; + for (Block &block : region) { + stream << '('; + for (BlockArgument argument : block.getArguments()) + stream << argument.getType() << ','; + stream << ')'; + } + stream << ']'; + } + entries.push_back(builder.getStringAttr(storage)); + } + + ac::ProcessOp process; + Builder builder; + DenseSet included; + SmallVector worklist; + uint64_t dependencyEdges = 0; + DenseMap operationPaths; + DenseMap valueIds; + SmallVector entries; +}; + +void serializeTopology(ModuleOp model, llvm::raw_ostream &stream) { + StructuredTopologySerializer(stream).run(model); +} + +Operation *lookupDefinition(ModuleOp model, FlatSymbolRefAttr reference) { + return reference ? SymbolTable::lookupSymbolIn(model, reference) : nullptr; +} + +bool isDirectStateOwner(Operation *operation) { + return isa(operation); +} + +std::optional constantBoolean(Value value) { + Attribute constant; + if (!matchPattern(value, m_Constant(&constant))) + return std::nullopt; + if (auto boolean = dyn_cast(constant)) + return boolean.getValue(); + if (auto integer = dyn_cast(constant); + integer && integer.getType().isInteger(1)) + return integer.getValue().getBoolValue(); + return std::nullopt; +} + +FailureOr> +verifyFunctionEffectsIterative(func::FuncOp function) { + struct EffectTask { + Operation *operation; + bool expanded; + }; + SmallVector pending{{function, false}}; + DenseMap subtreeEffectFree; + std::vector calls; + while (!pending.empty()) { + EffectTask task = pending.pop_back_val(); + if (!task.expanded) { + pending.push_back({task.operation, true}); + SmallVector children; + for (Region ®ion : task.operation->getRegions()) + for (Block &block : region) + for (Operation &child : block) + children.push_back(&child); + for (Operation *child : children) + pending.push_back({child, false}); + continue; + } + + bool localEffectFree = true; + if (auto call = dyn_cast(task.operation)) { + calls.push_back(call); + } else if (task.operation != function.getOperation() && + !isa(task.operation)) { + if (task.operation->hasTrait()) { + // Region effects are aggregated explicitly from the postorder summary + // below. Do not invoke a recursive interface implementation here. + localEffectFree = true; + } else if (auto effects = + dyn_cast(task.operation)) { + SmallVector localEffects; + effects.getEffects(localEffects); + localEffectFree = localEffects.empty(); + } else { + localEffectFree = false; + } + if (!localEffectFree) { + task.operation->emitOpError( + "function reachable from ac.process is not effect-free"); + return failure(); + } + } + + bool childrenEffectFree = true; + for (Region ®ion : task.operation->getRegions()) + for (Block &block : region) + for (Operation &child : block) + childrenEffectFree &= subtreeEffectFree.lookup(&child); + subtreeEffectFree[task.operation] = localEffectFree && childrenEffectFree; + } + llvm::sort(calls, [](func::CallOp left, func::CallOp right) { + return left.getCallee() < right.getCallee(); + }); + return calls; +} + +} // namespace + +LogicalResult detail::preflightModelStructure(ModuleOp model) { + return ac::preflightRawModelStructure(model); +} + +const detail::ValidatedPureFunction * +detail::ValidatedPureCallGraph::lookup(StringRef name, uint64_t *probes) const { + size_t begin = 0; + size_t end = functions.size(); + while (begin < end) { + if (probes) + ++*probes; + size_t middle = begin + (end - begin) / 2; + func::FuncOp function = functions[middle].function; + StringRef candidate = function.getSymName(); + if (candidate < name) + begin = middle + 1; + else if (name < candidate) + end = middle; + else + return &functions[middle]; + } + return nullptr; +} + +FailureOr +detail::validatePureProcessCallGraph(ModuleOp model, + const ac::RawModelStructureLimits &limits, + const PureCallGraphLimits &callLimits) { + std::map symbols; + for (func::FuncOp function : model.getOps()) { + auto [position, inserted] = + symbols.emplace(function.getSymName().str(), function); + if (!inserted) { + function.emitOpError() + << "duplicate pure func.call symbol '@" << position->first << "'"; + return failure(); + } + } + if (symbols.size() > callLimits.maxFunctions) { + model.emitError() << "pure func.call analysis exceeds ACIR function " + "limit " + << callLimits.maxFunctions; + return failure(); + } + + SmallVector> roots; + if (failed(ac::walkStructuredOperationsIterative( + model, + [&](Operation *operation) -> LogicalResult { + auto call = dyn_cast(operation); + if (!call || !operation->getParentOfType()) + return success(); + ac::ProcessOp process = operation->getParentOfType(); + ac::ModuleOp owner = process->getParentOfType(); + roots.push_back({(owner.getSymName() + "::" + process.getSymName() + + "::" + call.getCallee()) + .str(), + call}); + return success(); + }, + limits))) + return failure(); + llvm::sort(roots, [](const auto &left, const auto &right) { + return left.first < right.first; + }); + + enum class State : uint8_t { Unvisited, Active, Pure }; + std::map states; + std::map> indexedCalls; + ValidatedPureCallGraph graph; + uint64_t edges = 0; + struct Frame { + func::FuncOp function; + Operation *origin; + size_t nextCall = 0; + bool entered = false; + }; + + for (auto &[key, root] : roots) { + (void)key; + auto rootFunction = symbols.find(root.getCallee().str()); + if (rootFunction == symbols.end()) { + root.emitOpError() << "process func.call callee '@" << root.getCallee() + << "' is unresolved"; + return failure(); + } + if (states[rootFunction->first] == State::Pure) + continue; + SmallVector stack{{rootFunction->second, root, 0, false}}; + while (!stack.empty()) { + Frame &frame = stack.back(); + std::string name = frame.function.getSymName().str(); + if (!frame.entered) { + if (frame.function.isExternal()) { + frame.origin->emitOpError() + << "process func.call callee '@" << name + << "' has no body and cannot be proven effect-free"; + return failure(); + } + if (stack.size() > callLimits.maxDepth) { + frame.origin->emitOpError() + << "pure func.call analysis exceeds ACIR depth limit " + << callLimits.maxDepth; + return failure(); + } + if (failed(ac::verifyProcessLowerability(frame.function, limits))) + return failure(); + FailureOr> calls = + verifyFunctionEffectsIterative(frame.function); + if (failed(calls)) + return failure(); + indexedCalls[name] = std::move(*calls); + states[name] = State::Active; + frame.entered = true; + } + + auto &calls = indexedCalls[name]; + if (frame.nextCall == calls.size()) { + states[name] = State::Pure; + graph.functions.push_back({frame.function, calls}); + stack.pop_back(); + continue; + } + func::CallOp call = calls[frame.nextCall++]; + if (edges == callLimits.maxEdges) { + call.emitOpError() << "pure func.call analysis exceeds ACIR edge limit " + << callLimits.maxEdges; + return failure(); + } + ++edges; + auto target = symbols.find(call.getCallee().str()); + if (target == symbols.end()) { + call.emitOpError() << "process func.call callee '@" << call.getCallee() + << "' is unresolved"; + return failure(); + } + State targetState = states[target->first]; + if (targetState == State::Pure) + continue; + if (targetState == State::Active) { + InFlightDiagnostic diagnostic = + call.emitOpError("recursive func.call purity cycle: "); + auto begin = llvm::find_if(stack, [&](const Frame &active) { + return cast( + active.function->getAttr(SymbolTable::getSymbolAttrName())) + .getValue() == target->first; + }); + for (auto current = begin; current != stack.end(); ++current) + diagnostic << '@' << current->function.getSymName() << " -> "; + diagnostic << '@' << target->first; + return failure(); + } + stack.push_back({target->second, call, 0, false}); + } + } + llvm::sort(graph.functions, [](const ValidatedPureFunction &left, + const ValidatedPureFunction &right) { + auto name = [](func::FuncOp function) { + return cast( + function->getAttr(SymbolTable::getSymbolAttrName())) + .getValue(); + }; + return name(left.function) < name(right.function); + }); + return graph; +} + +FailureOr detail::buildFrozenProcessSkeleton(ac::ProcessOp process) { + return ProcessSkeletonSerializer(process).run(); +} + +bool detail::hasTopologyFreezeEvidence(ModuleOp model) { + bool evidence = false; + model.walk([&](Operation *operation) { + for (NamedAttribute attribute : operation->getAttrs()) { + StringRef name = attribute.getName().getValue(); + if (name == "ac.freeze_epoch" || name == "ac.freeze_proven" || + name.starts_with("ac.frozen_") || name.starts_with("ac.topology_")) { + evidence = true; + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + return evidence; +} + +bool isTopologyFrozen(ModuleOp model) { + auto marker = model->getAttrOfType("ac.topology_frozen"); + return marker && marker.getValue(); +} + +LogicalResult ModelAnalysis::verifyPureProcessCalls() { + return succeeded(detail::validatePureProcessCallGraph(model)) ? success() + : failure(); +} + +LogicalResult ModelAnalysis::verifyZeroDelayDependencies() { + std::map modules; + for (ac::ModuleOp module : model.getOps()) + modules.emplace(module.getSymName().str(), module); + + DenseMap moduleStateful; + std::function hasState = [&](ac::ModuleOp module) { + auto found = moduleStateful.find(module); + if (found != moduleStateful.end()) + return found->second; + // The finite-instantiation verifier has already rejected recursion. Mark + // first to keep this query total even for malformed IR under + // --verify-each=false. + moduleStateful[module] = true; + bool stateful = false; + if (!module.getBody().empty()) + for (Operation &child : module.getBody().front()) { + if (isDirectStateOwner(&child)) { + stateful = true; + break; + } + auto targetIsStateful = [&](FlatSymbolRefAttr reference) { + Operation *target = lookupDefinition(model, reference); + if (auto nested = dyn_cast_or_null(target)) + return hasState(nested); + return target != nullptr; + }; + if (auto instance = dyn_cast(child)) + stateful = targetIsStateful(instance.getDefinitionAttr()); + else if (auto array = dyn_cast(child)) + stateful = targetIsStateful(array.getDefinitionAttr()); + else if (auto instances = dyn_cast(child)) + for (Attribute reference : instances.getDefinitions()) + stateful |= targetIsStateful(cast(reference)); + if (stateful) + break; + } + moduleStateful[module] = stateful; + return stateful; + }; + for (const auto &[name, module] : modules) { + (void)name; + (void)hasState(module); + } + + StringRef selectedRoot; + StringRef selectedRootName; + for (ac::SystemOp system : model.getOps()) + if (system.getSelected()) { + selectedRoot = system.getRoot(); + selectedRootName = system.getRootName(); + break; + } + + for (auto &[moduleName, module] : modules) { + if (module.getBody().empty()) + continue; + struct Node { + Operation *operation; + std::string label; + }; + SmallVector nodes; + for (Operation &operation : module.getBody().front()) { + if (operation.getNumResults() == 0) + continue; + bool stateful = + isDirectStateOwner(&operation) || + (!isMemoryEffectFree(&operation) && !isa(operation)); + auto targetStateful = [&](FlatSymbolRefAttr reference) { + if (auto target = dyn_cast_or_null( + lookupDefinition(model, reference))) + return moduleStateful.lookup(target); + return lookupDefinition(model, reference) != nullptr; + }; + if (auto instance = dyn_cast(operation)) + stateful |= targetStateful(instance.getDefinitionAttr()); + else if (auto array = dyn_cast(operation)) + stateful |= targetStateful(array.getDefinitionAttr()); + else if (auto instances = dyn_cast(operation)) + for (Attribute reference : instances.getDefinitions()) + stateful |= targetStateful(cast(reference)); + if (stateful) + continue; + StringRef local = symbolName(&operation); + std::string prefix = moduleName == selectedRoot ? selectedRootName.str() + : "@" + moduleName; + std::string label = local.empty() + ? (prefix + "." + operationKey(&operation)) + : (prefix + "." + local).str(); + nodes.push_back({&operation, std::move(label)}); + } + if (nodes.size() > kMaxModelAnalysisNodes) + return module.emitOpError() + << "zero-delay analysis exceeds ACIR node limit " + << kMaxModelAnalysisNodes; + llvm::sort(nodes, [](const Node &left, const Node &right) { + return left.label < right.label; + }); + DenseMap index; + for (auto [ordinal, node] : llvm::enumerate(nodes)) + index[node.operation] = ordinal; + SmallVector> edges(nodes.size()); + uint64_t edgeCount = 0; + for (auto [ordinal, node] : llvm::enumerate(nodes)) { + for (Value operand : node.operation->getOperands()) { + Operation *producer = operand.getDefiningOp(); + auto found = producer ? index.find(producer) : index.end(); + if (found == index.end()) + continue; + edges[found->second].push_back(ordinal); + if (++edgeCount > kMaxModelAnalysisEdges) + return module.emitOpError() + << "zero-delay analysis exceeds ACIR edge limit " + << kMaxModelAnalysisEdges; + } + } + for (SmallVector &successors : edges) { + llvm::sort(successors); + successors.erase(std::unique(successors.begin(), successors.end()), + successors.end()); + } + + SmallVector color(nodes.size()); + SmallVector activePosition(nodes.size(), -1); + struct Frame { + unsigned node; + size_t next = 0; + }; + SmallVector stack; + for (unsigned start = 0; start < nodes.size(); ++start) { + if (color[start] != 0) + continue; + color[start] = 1; + activePosition[start] = 0; + stack.push_back({start, 0}); + while (!stack.empty()) { + Frame &frame = stack.back(); + if (frame.next == edges[frame.node].size()) { + color[frame.node] = 2; + activePosition[frame.node] = -1; + stack.pop_back(); + continue; + } + unsigned successor = edges[frame.node][frame.next++]; + if (color[successor] == 0) { + color[successor] = 1; + activePosition[successor] = stack.size(); + stack.push_back({successor, 0}); + continue; + } + if (color[successor] != 1) + continue; + InFlightDiagnostic diagnostic = nodes[successor].operation->emitOpError( + "forbidden zero-delay cycle: "); + for (size_t position = activePosition[successor]; + position < stack.size(); ++position) + diagnostic << nodes[stack[position].node].label << " -> "; + diagnostic << nodes[successor].label; + return failure(); + } + } + } + return success(); +} + +LogicalResult ModelAnalysis::verifyFreezeContracts() { + SmallVector contracts; + for (ac::ModuleOp module : model.getOps()) { + if (module.getBody().empty()) + continue; + for (Operation &operation : module.getBody().front()) + if (isa(operation)) + contracts.push_back(&operation); + } + llvm::sort(contracts, [](Operation *left, Operation *right) { + auto leftModule = left->getParentOfType(); + auto rightModule = right->getParentOfType(); + return std::tuple(leftModule.getSymName(), left->getName().getStringRef(), + left->getAttrOfType("message").getValue()) < + std::tuple(rightModule.getSymName(), right->getName().getStringRef(), + right->getAttrOfType("message").getValue()); + }); + for (Operation *operation : contracts) { + Value condition = isa(operation) + ? cast(operation).getCondition() + : cast(operation).getCondition(); + StringRef message = + operation->getAttrOfType("message").getValue(); + std::optional value = constantBoolean(condition); + if (!value) + return operation->emitOpError() + << "topology-freeze contract is not statically provable: " + << message; + if (!*value) + return operation->emitOpError() + << "topology-freeze contract failed: " << message; + } + return success(); +} + +FailureOr detail::buildFrozenOwnerManifest(ModuleOp model) { + SmallVector topologyOwners; + SmallVector stateOwners; + if (failed(ac::collectElaboratedTopologyOwners(model, topologyOwners)) || + failed(ac::collectElaboratedStateOwners(model, stateOwners))) + return failure(); + + struct Record { + std::string path; + std::string stableId; + std::string kind; + SymbolRefAttr owner; + SmallVector traceSources; + }; + llvm::StringMap> stateOwnerIndex; + for (const ac::ElaboratedStateOwner &owner : stateOwners) { + if (activeFreezeWork) + ++activeFreezeWork->stateIndexInsertions; + std::string key = + ownerJoinKey(owner.declaration, owner.path, owner.stableId); + bool inserted = stateOwnerIndex.try_emplace(key, owner.traceSources).second; + if (!inserted) + return owner.declaration->emitOpError() + << "duplicate elaborated state-owner identity for path '" + << owner.path << "' and stable ID '" << owner.stableId << "'"; + } + + SmallVector records; + for (ac::SystemOp system : model.getOps()) + if (system.getSelected()) + records.push_back({system.getRootName().str(), + system.getRootName().str(), + "ac.system_root", + system.getRootAttr(), + {}}); + + for (const ac::ElaboratedTopologyOwner &owner : topologyOwners) { + Operation *declaration = owner.declaration; + auto definition = declaration->getParentOfType(); + StringRef local = symbolName(declaration); + SymbolRefAttr reference; + if (definition && !local.empty()) + reference = SymbolRefAttr::get( + model.getContext(), definition.getSymName(), + {FlatSymbolRefAttr::get(model.getContext(), local)}); + else if (definition) + reference = + FlatSymbolRefAttr::get(model.getContext(), definition.getSymName()); + if (activeFreezeWork) + ++activeFreezeWork->topologyIndexLookups; + SmallVector traces; + auto state = stateOwnerIndex.find( + ownerJoinKey(declaration, owner.path, owner.stableId)); + if (state != stateOwnerIndex.end()) + traces = state->second; + llvm::sort(traces); + records.push_back({owner.path, owner.stableId, + declaration->getName().getStringRef().str(), reference, + std::move(traces)}); + } + llvm::sort(records, [](const Record &left, const Record &right) { + return std::tie(left.path, left.stableId, left.kind) < + std::tie(right.path, right.stableId, right.kind); + }); + + Builder builder(model.getContext()); + SmallVector manifest; + manifest.reserve(records.size()); + for (const Record &record : records) { + SmallVector fields = { + builder.getNamedAttr("kind", builder.getStringAttr(record.kind)), + builder.getNamedAttr("owner", record.owner), + builder.getNamedAttr("path", builder.getStringAttr(record.path)), + builder.getNamedAttr("stable_id", + builder.getStringAttr(record.stableId)), + }; + if (!record.traceSources.empty()) { + SmallVector sources; + for (const std::string &source : record.traceSources) + sources.push_back(builder.getStringAttr(source)); + fields.push_back( + builder.getNamedAttr("trace_sources", builder.getArrayAttr(sources))); + } + manifest.push_back(builder.getDictionaryAttr(fields)); + } + return builder.getArrayAttr(manifest); +} + +std::string detail::computeTopologyDigest(ModuleOp model) { + std::string serialized; + llvm::raw_string_ostream stream(serialized); + serializeTopology(model, stream); + stream.flush(); + llvm::SHA256 sha; + sha.update(serialized); + return llvm::toHex(sha.final(), /*LowerCase=*/true); +} + +LogicalResult ModelAnalysis::verifyFrozenIntegrity() { + if (!detail::hasTopologyFreezeEvidence(model)) + return success(); + auto marker = model->getAttrOfType("ac.topology_frozen"); + auto epoch = model->getAttrOfType("ac.freeze_epoch"); + auto digest = model->getAttrOfType("ac.topology_digest"); + auto owners = model->getAttrOfType("ac.frozen_owners"); + if (!marker || !marker.getValue() || !epoch || epoch.getValue() != "0.3" || + !digest || digest.getValue().size() != 64 || !owners) + return model.emitError( + "malformed topology freeze marker; expected epoch 0.3, owner manifest, " + "and SHA-256 digest"); + LogicalResult skeletonResult = success(); + model.walk([&](ac::ProcessOp process) { + if (failed(skeletonResult)) + return; + ArrayAttr frozen = + process->getAttrOfType("ac.frozen_process_skeleton"); + FailureOr expected = detail::buildFrozenProcessSkeleton(process); + if (failed(expected)) { + skeletonResult = failure(); + return; + } + if (!frozen || frozen != *expected) { + process.emitOpError( + "frozen process skeleton mismatch; effect semantics were mutated " + "after ac-freeze-topology"); + skeletonResult = failure(); + } + }); + if (failed(skeletonResult)) + return failure(); + std::string actual = detail::computeTopologyDigest(model); + if (digest.getValue() != actual) + return model.emitError( + "frozen topology digest mismatch; topology was mutated after " + "ac-freeze-topology"); + FailureOr expectedOwners = detail::buildFrozenOwnerManifest(model); + if (failed(expectedOwners)) + return failure(); + if (owners != *expectedOwners) + return model.emitError( + "frozen owner manifest mismatch; topology ownership was mutated after " + "ac-freeze-topology"); + return success(); +} + +LogicalResult ModelAnalysis::verify() { + if (failed(detail::preflightModelStructure(model))) + return failure(); + + auto epoch = model->getAttrOfType("ac.contract_epoch"); + if (!epoch || epoch.getValue() != "0.3") + return model.emitError( + "expected top-level 'ac.contract_epoch' string attribute equal to " + "\"0.3\""); + + if (failed(verifyPureProcessCalls())) + return failure(); + if (failed(mlir::verify(model))) + return failure(); + WalkResult types = model.walk([&](Operation *operation) { + return failed(ac::verifyTopologyTypeUses(operation)) + ? WalkResult::interrupt() + : WalkResult::advance(); + }); + if (types.wasInterrupted()) + return failure(); + if (failed(verifyZeroDelayDependencies())) + return failure(); + return verifyFrozenIntegrity(); +} + +LogicalResult verifyModel(ModuleOp model) { + return ModelAnalysis(model).verify(); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Analysis/ModelAnalysisInternal.h b/components/agentic-circuit/lib/Analysis/ModelAnalysisInternal.h new file mode 100644 index 00000000..17384dd4 --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ModelAnalysisInternal.h @@ -0,0 +1,57 @@ +#ifndef ACIR_ANALYSIS_MODELANALYSISINTERNAL_H +#define ACIR_ANALYSIS_MODELANALYSISINTERNAL_H + +#include "Dialect/ACIR/ProcessLowerability.h" +#include "acir/Analysis/ModelAnalysis.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Support/LogicalResult.h" + +#include +#include + +namespace acir::detail { + +struct PureCallGraphLimits { + uint64_t maxFunctions = kMaxPureCallFunctions; + uint64_t maxEdges = kMaxPureCallEdges; + uint64_t maxDepth = kMaxPureCallDepth; +}; + +struct ValidatedPureFunction { + mlir::func::FuncOp function; + std::vector calls; +}; + +struct ValidatedPureCallGraph { + std::vector functions; + + const ValidatedPureFunction *lookup(llvm::StringRef name, + uint64_t *probes = nullptr) const; +}; + +/// The single lowerability and purity authority for process-reachable calls. +mlir::FailureOr validatePureProcessCallGraph( + mlir::ModuleOp model, + const ac::RawModelStructureLimits &structureLimits = + ac::RawModelStructureLimits(), + const PureCallGraphLimits &callLimits = PureCallGraphLimits()); + +/// Checks whole-file structural budgets without recursive or typed IR access. +mlir::LogicalResult preflightModelStructure(mlir::ModuleOp model); + +/// Returns whether any top-level or nested reserved freeze attribute exists. +bool hasTopologyFreezeEvidence(mlir::ModuleOp model); + +/// Internal seal construction primitives. These declarations are deliberately +/// unavailable from the installed public include tree; only the trusted +/// first-freeze writer and frozen-integrity verifier consume them. +mlir::FailureOr buildFrozenOwnerManifest(mlir::ModuleOp model); +mlir::FailureOr +buildFrozenProcessSkeleton(ac::ProcessOp process); +std::string computeTopologyDigest(mlir::ModuleOp model); + +} // namespace acir::detail + +#endif // ACIR_ANALYSIS_MODELANALYSISINTERNAL_H diff --git a/components/agentic-circuit/lib/Analysis/ModelAnalysisTestHooks.h b/components/agentic-circuit/lib/Analysis/ModelAnalysisTestHooks.h new file mode 100644 index 00000000..6a8dadac --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ModelAnalysisTestHooks.h @@ -0,0 +1,86 @@ +#ifndef ACIR_ANALYSIS_MODELANALYSISTESTHOOKS_H +#define ACIR_ANALYSIS_MODELANALYSISTESTHOOKS_H + +#include "acir/Analysis/ModelAnalysis.h" + +#include + +namespace acir::detail { + +/// Test-only accounting for logical indexed work performed by the complete +/// topology-freeze path. Production execution leaves the thread-local pointer +/// null and pays only the guarded null checks at accounting sites. +struct FreezeWork { + uint64_t stateIndexInsertions = 0; + uint64_t topologyIndexLookups = 0; + uint64_t manifestIndexInsertions = 0; + uint64_t manifestOwnerLookups = 0; + uint64_t declarationIndexInsertions = 0; + uint64_t declarationLookups = 0; + + uint64_t total() const { + return stateIndexInsertions + topologyIndexLookups + + manifestIndexInsertions + manifestOwnerLookups + + declarationIndexInsertions + declarationLookups; + } +}; + +inline thread_local FreezeWork *activeFreezeWork = nullptr; + +class ScopedFreezeWorkRecorder { +public: + explicit ScopedFreezeWorkRecorder(FreezeWork &work) + : previous(activeFreezeWork) { + work = {}; + activeFreezeWork = &work; + } + + ~ScopedFreezeWorkRecorder() { activeFreezeWork = previous; } + + ScopedFreezeWorkRecorder(const ScopedFreezeWorkRecorder &) = delete; + ScopedFreezeWorkRecorder & + operator=(const ScopedFreezeWorkRecorder &) = delete; + +private: + FreezeWork *previous; +}; + +struct ProcessSkeletonLimits { + uint64_t nodes; + uint64_t edges; +}; + +inline thread_local const ProcessSkeletonLimits *activeProcessSkeletonLimits = + nullptr; + +class ScopedProcessSkeletonLimits { +public: + ScopedProcessSkeletonLimits(uint64_t nodes, uint64_t edges) + : limits{nodes, edges}, previous(activeProcessSkeletonLimits) { + activeProcessSkeletonLimits = &limits; + } + + ~ScopedProcessSkeletonLimits() { activeProcessSkeletonLimits = previous; } + + ScopedProcessSkeletonLimits(const ScopedProcessSkeletonLimits &) = delete; + ScopedProcessSkeletonLimits & + operator=(const ScopedProcessSkeletonLimits &) = delete; + +private: + ProcessSkeletonLimits limits; + const ProcessSkeletonLimits *previous; +}; + +inline uint64_t processSkeletonNodeLimit() { + return activeProcessSkeletonLimits ? activeProcessSkeletonLimits->nodes + : kMaxModelAnalysisNodes; +} + +inline uint64_t processSkeletonEdgeLimit() { + return activeProcessSkeletonLimits ? activeProcessSkeletonLimits->edges + : kMaxModelAnalysisEdges; +} + +} // namespace acir::detail + +#endif // ACIR_ANALYSIS_MODELANALYSISTESTHOOKS_H diff --git a/components/agentic-circuit/lib/Analysis/ProcessStateContinuation.cpp b/components/agentic-circuit/lib/Analysis/ProcessStateContinuation.cpp new file mode 100644 index 00000000..0e60446e --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStateContinuation.cpp @@ -0,0 +1,410 @@ +#include "ProcessStatePlanInternal.h" + +#include "acir/Dialect/ACIR/ACIROps.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Diagnostics.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/ErrorHandling.h" + +#include +#include +#include + +using namespace mlir; + +namespace acir::detail { +namespace { + +static bool isSuspensionOp(Operation *op) { + return isa(op) || isa(op) || + isa(op) || isa(op); +} + +static bool isYieldSim(Operation *op) { return isa(op); } + +static ProcessWakeKind wakeKindForOp(Operation *op) { + if (isa(op)) + return ProcessWakeKind::Condition; + if (isa(op)) + return ProcessWakeKind::Resource; + if (isa(op)) + return ProcessWakeKind::EventQueue; + if (isa(op)) + return ProcessWakeKind::NextDelta; + llvm_unreachable("unknown suspension op"); +} + +static std::string wakeTypeKeyForOp(Operation *op) { + if (isa(op)) + return "@acir_wake_condition"; + if (isa(op)) + return "@acir_wake_resource"; + if (isa(op)) + return "@acir_wake_event_queue"; + if (isa(op)) + return "@acir_wake_next_delta"; + llvm_unreachable("unknown suspension op"); +} + +static std::string pcName(uint32_t index) { + if (index == 0) + return "entry"; + std::ostringstream s; + s << "pc" << std::setfill('0') << std::setw(8) << index; + return s.str(); +} + +static std::string blockPath(const std::string &defKey, + const std::string &pcNameStr, uint32_t blockIdx) { + std::ostringstream s; + s << defKey << "/plan/pc/" << pcNameStr << "/b" << std::setfill('0') + << std::setw(8) << blockIdx; + return s.str(); +} + +} // namespace + +FailureOr> +PlanSetBuilder::planProcessContinuation(const ExpandedProcess &expanded, + const ProcessStateLimits &limits) { + auto plan = std::make_unique(); + + if (expanded.actions.empty()) + return mlir::failure(); + + uint32_t nextPcId = 0; + uint32_t nextBlockId = 0; + uint32_t nextWakeId = 0; + uint32_t nextTransitionId = 0; + + // Entry PC + auto entryPc = std::make_shared(); + entryPc->id = ProcessPcId(nextPcId++); + entryPc->name = pcName(0); + plan->pcs.push_back(entryPc); + + struct Susp { + size_t idx; + ProcessWakeKind kind; + std::string typeKey; + Operation *op; + }; + SmallVector suspensions; + for (auto [i, a] : llvm::enumerate(expanded.actions)) { + if (isSuspensionOp(a.operation)) + suspensions.push_back({i, wakeKindForOp(a.operation), + wakeTypeKeyForOp(a.operation), a.operation}); + } + + // Resume PCs + uint32_t resumeIdx = 1; + SmallVector pcMap(expanded.actions.size(), 0); + for (const auto &s : suspensions) { + if (!isYieldSim(s.op)) { + auto rpc = std::make_shared(); + rpc->id = ProcessPcId(nextPcId); + rpc->name = pcName(resumeIdx); + plan->pcs.push_back(rpc); + pcMap[s.idx] = nextPcId; + ++nextPcId; + ++resumeIdx; + } else { + pcMap[s.idx] = 0; // yield_sim resumes at entry + } + } + + // Segment starts + SmallVector starts; + starts.push_back(0); + for (const auto &s : suspensions) + starts.push_back(s.idx + 1); + + for (size_t seg = 0; seg < suspensions.size(); ++seg) { + size_t start = starts[seg]; + size_t end = suspensions[seg].idx; + uint32_t pcId = (seg == 0) ? 0 : pcMap[suspensions[seg - 1].idx]; + const auto &susp = suspensions[seg]; + + auto block = std::make_shared(); + block->id = ProcessBlockId(nextBlockId); + block->pc = ProcessPcId(pcId); + block->originBlock = expanded.actions[start].operation->getBlock(); + block->originRegion = block->originBlock->getParent(); + block->path = + blockPath(expanded.definitionKey, plan->pcs[pcId]->name, nextBlockId); + plan->pcs[pcId]->blocks.push_back(ProcessBlockId(nextBlockId)); + if (plan->pcs[pcId]->entryPath.empty()) + plan->pcs[pcId]->entryPath = block->path; + + // Actions in segment + for (size_t i = start; i <= end; ++i) { + auto act = std::make_shared(); + act->id = static_cast(i - start); + act->kind = expanded.actions[i].kind; + act->emission = ProcessEmissionClass::ForwardOnly; + if (act->kind == ProcessActionKind::ForCondition || + act->kind == ProcessActionKind::ForIncrement) + act->emission = ProcessEmissionClass::CopyScalar; + act->occurrence = expanded.actions[i].occurrence; + act->sourceOperation = act->kind == ProcessActionKind::Constant + ? nullptr + : expanded.actions[i].operation; + act->iterationVector = expanded.actions[i].iterationVector; + act->operands = expanded.actions[i].operands; + act->results = expanded.actions[i].results; + act->cost = act->emission == ProcessEmissionClass::ForwardOnly ? 0 : 1; + for (const ProcessPlannedValue &result : act->results) + act->resultTypes.push_back(result.type()); + act->scalarOp = expanded.actions[i].scalarOperation; + if (act->kind == ProcessActionKind::Constant) { + act->emission = ProcessEmissionClass::CopyScalar; + act->cost = 1; + auto scalar = std::make_shared(); + scalar->name = "index.constant"; + scalar->properties = "{}"; + act->scalarOp = ProcessScalarOperationPlan(std::move(scalar)); + } + if (act->kind == ProcessActionKind::Original && act->sourceOperation) { + llvm::StringRef dialect = + act->sourceOperation->getName().getDialectNamespace(); + if ((dialect == "arith" || dialect == "index" || + dialect == "builtin") && + act->sourceOperation->getNumRegions() == 0) { + act->emission = ProcessEmissionClass::CopyScalar; + act->cost = 1; + if (!act->scalarOp) { + auto scalar = std::make_shared(); + scalar->name = act->sourceOperation->getName().getStringRef().str(); + scalar->properties = "{}"; + act->scalarOp = ProcessScalarOperationPlan(std::move(scalar)); + } + } + } + block->actions.push_back(ProcessActionPlan(act)); + } + + // Suspension edge + auto edge = std::make_shared(); + edge->kind = ProcessControlEdgeKind::Suspend; + + // Wake + auto wake = std::make_shared(); + wake->id = ProcessWakeId(nextWakeId); + wake->kind = susp.kind; + wake->typeKey = susp.typeKey; + wake->operation = susp.op; + wake->operationPath = expanded.actions[susp.idx].operationPath; + wake->target = ""; + wake->occurrence = expanded.actions[susp.idx].occurrence; + wake->iterationVector = expanded.actions[susp.idx].iterationVector; + + // Subscription sources + for (const auto &opd : expanded.actions[susp.idx].operands) { + auto src = std::make_shared(); + if (opd.kind() == ProcessPlannedValueKind::Original) { + src->kind = ProcessSubscriptionSourceKind::Value; + src->value = opd.original().value(); + src->owner = opd.original().occurrence().original().operation(); + src->path = opd.original().path().str(); + } else if (opd.kind() == ProcessPlannedValueKind::Capture) { + src->kind = ProcessSubscriptionSourceKind::Capture; + src->capture = opd.capture().capture(); + } else { + src->kind = ProcessSubscriptionSourceKind::Value; + } + wake->sources.push_back(ProcessSubscriptionSourcePlan(src)); + } + + // Transition + auto tr = std::make_shared(); + tr->id = ProcessTransitionId(nextTransitionId); + tr->sourcePc = ProcessPcId(pcId); + tr->targetPc = ProcessPcId(pcMap[susp.idx]); + tr->wake = ProcessWakeId(nextWakeId); + + edge->transition = ProcessTransitionId(nextTransitionId); + block->edge = ProcessControlEdgePlan(edge); + + plan->blocks.push_back(block); + plan->wakes.push_back(wake); + plan->transitions.push_back(tr); + + ++nextWakeId; + ++nextTransitionId; + ++nextBlockId; + } + + // A suspension nested directly in an scf.if does not dominate the + // continuation after the if. Materialize the branch in the current PC and + // clone the post-if continuation for the non-suspending arm. The original + // continuation remains the resume PC for the suspending arm. + // + // Expansion deliberately keeps occurrence-qualified leaf actions, so this + // repair operates on the immutable action records and never rewrites the + // frozen source IR. + ac::ProcessOp process = expanded.process; + for (const Susp &susp : suspensions) { + auto ifOp = susp.op->getParentOfType(); + if (!ifOp || ifOp->getParentOp() != process.getOperation()) + continue; + + auto belongsTo = [&](Operation *operation, Region ®ion) { + if (!operation) + return false; + Operation *nested = operation; + while (nested && nested->getParentOp() != ifOp.getOperation()) + nested = nested->getParentOp(); + return nested && nested->getParentRegion() == ®ion; + }; + bool suspendedInThen = belongsTo(susp.op, ifOp.getThenRegion()); + bool suspendedInElse = !ifOp.getElseRegion().empty() && + belongsTo(susp.op, ifOp.getElseRegion()); + if (!suspendedInThen && !suspendedInElse) + continue; + Region *otherRegion = + suspendedInThen ? &ifOp.getElseRegion() : &ifOp.getThenRegion(); + if (llvm::any_of(suspensions, [&](const Susp &candidate) { + return candidate.op != susp.op && !otherRegion->empty() && + belongsTo(candidate.op, *otherRegion); + })) + continue; + + auto branchBlockIt = llvm::find_if(plan->blocks, [&](const auto &block) { + return llvm::any_of(block->actions, [&](const ProcessActionPlan &action) { + return action.sourceOperation() == susp.op; + }); + }); + if (branchBlockIt == plan->blocks.end() || + !(*branchBlockIt)->edge.has_value() || + (*branchBlockIt)->edge->kind() != ProcessControlEdgeKind::Suspend) + continue; + + auto appendRenumbered = [](std::vector &target, + const ProcessActionPlan &source) { + auto action = std::make_shared(*source.impl_); + action->id = static_cast(target.size()); + target.push_back(ProcessActionPlan(action)); + }; + std::vector prefix; + std::vector thenActions; + std::vector elseActions; + for (const ProcessActionPlan &action : (*branchBlockIt)->actions) { + if (belongsTo(action.sourceOperation(), ifOp.getThenRegion())) + appendRenumbered(thenActions, action); + else if (!ifOp.getElseRegion().empty() && + belongsTo(action.sourceOperation(), ifOp.getElseRegion())) + appendRenumbered(elseActions, action); + else + appendRenumbered(prefix, action); + } + std::vector &suspendingActions = + suspendedInThen ? thenActions : elseActions; + std::vector &continuingActions = + suspendedInThen ? elseActions : thenActions; + if (llvm::none_of(suspendingActions, [&](const ProcessActionPlan &action) { + return action.sourceOperation() == susp.op; + })) + continue; + + ProcessTransitionId originalTransition = + (*branchBlockIt)->edge->transition(); + if (originalTransition.value() >= plan->transitions.size()) + return failure(); + ProcessPcId resumePc = + plan->transitions[originalTransition.value()]->targetPc.value(); + auto resumeBlockIt = llvm::find_if(plan->blocks, [&](const auto &block) { + return block->pc == resumePc && + block->path == plan->pcs[resumePc.value()]->entryPath; + }); + if (resumeBlockIt == plan->blocks.end() || + !(*resumeBlockIt)->edge.has_value()) + return failure(); + + ProcessPcId branchPc = (*branchBlockIt)->pc.value(); + auto suspendingBlock = std::make_shared(); + suspendingBlock->id = ProcessBlockId(nextBlockId++); + suspendingBlock->pc = branchPc; + suspendingBlock->originBlock = susp.op->getBlock(); + suspendingBlock->originRegion = susp.op->getParentRegion(); + suspendingBlock->path = + blockPath(expanded.definitionKey, plan->pcs[branchPc.value()]->name, + suspendingBlock->id->value()); + suspendingBlock->actions = std::move(suspendingActions); + suspendingBlock->edge = (*branchBlockIt)->edge; + + auto continuingBlock = std::make_shared(); + continuingBlock->id = ProcessBlockId(nextBlockId++); + continuingBlock->pc = branchPc; + continuingBlock->originBlock = + otherRegion->empty() ? ifOp->getBlock() : &otherRegion->front(); + continuingBlock->originRegion = + otherRegion->empty() ? ifOp->getParentRegion() : otherRegion; + continuingBlock->path = + blockPath(expanded.definitionKey, plan->pcs[branchPc.value()]->name, + continuingBlock->id->value()); + continuingBlock->actions = std::move(continuingActions); + for (const ProcessActionPlan &action : (*resumeBlockIt)->actions) + appendRenumbered(continuingBlock->actions, action); + + const ProcessControlEdgePlan &resumeEdge = *(*resumeBlockIt)->edge; + if (resumeEdge.kind() == ProcessControlEdgeKind::Suspend) { + ProcessTransitionId resumeTransition = resumeEdge.transition(); + if (resumeTransition.value() >= plan->transitions.size()) + return failure(); + auto transition = std::make_shared( + *plan->transitions[resumeTransition.value()]); + transition->id = ProcessTransitionId(nextTransitionId++); + transition->sourcePc = branchPc; + ProcessWakeId resumeWake = transition->wake.value(); + if (resumeWake.value() >= plan->wakes.size()) + return failure(); + auto wake = std::make_shared( + *plan->wakes[resumeWake.value()]); + wake->id = ProcessWakeId(nextWakeId++); + transition->wake = wake->id; + auto edge = std::make_shared(); + edge->kind = ProcessControlEdgeKind::Suspend; + edge->transition = transition->id; + continuingBlock->edge = ProcessControlEdgePlan(edge); + plan->wakes.push_back(std::move(wake)); + plan->transitions.push_back(std::move(transition)); + } else { + continuingBlock->edge = resumeEdge; + } + + std::optional condition; + for (const ExpandedAction &action : expanded.actions) { + for (const ProcessPlannedValue &result : action.results) { + if (result.kind() == ProcessPlannedValueKind::Original && + result.original().value() == ifOp.getCondition()) { + condition = result; + break; + } + } + if (condition) + break; + } + if (!condition) + return failure(); + + auto edge = std::make_shared(); + edge->kind = ProcessControlEdgeKind::Branch; + edge->condition = *condition; + edge->trueBlock = + suspendedInThen ? suspendingBlock->id : continuingBlock->id; + edge->falseBlock = + suspendedInThen ? continuingBlock->id : suspendingBlock->id; + (*branchBlockIt)->actions = std::move(prefix); + (*branchBlockIt)->edge = ProcessControlEdgePlan(edge); + + plan->pcs[branchPc.value()]->blocks.push_back(*suspendingBlock->id); + plan->pcs[branchPc.value()]->blocks.push_back(*continuingBlock->id); + plan->blocks.push_back(std::move(suspendingBlock)); + plan->blocks.push_back(std::move(continuingBlock)); + } + + return plan; +} + +} // namespace acir::detail diff --git a/components/agentic-circuit/lib/Analysis/ProcessStateCost.cpp b/components/agentic-circuit/lib/Analysis/ProcessStateCost.cpp new file mode 100644 index 00000000..c21abb0d --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStateCost.cpp @@ -0,0 +1,150 @@ +#include "ProcessStatePlanInternal.h" + +#include "mlir/IR/Diagnostics.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include +#include + +using namespace mlir; + +namespace acir::detail { + +LogicalResult +PlanSetBuilder::planProcessCost(ControlPlan &control, + const ProcessStateLimits &limits) { + // Compute exact per-block cost from the contract formula: + // block_cost = sum(entry loads, each 1) + // + sum(scalar_unwrap actions, each 1) + // + sum(copy_scalar, inline, invoke, for_condition, + // for_increment leaf actions, each 1) + // + if edge is suspend: + // sum(scalar_wrap actions, each 1) + // + sum(store emissions, each 1) + // + 1 (wake invoke) + // + 1 (acsim.suspend) + // otherwise: 1 (cf.cond_br, cf.br, or terminate) + // + // Fairness = iterative max sum of block costs over every path + // in every PC-local DAG. Reject zero, overflow, cycles, cap excess. + // + // For yield-only: block cost = 2 (wake invoke + acsim.suspend) + + for (auto &block : control.blocks) { + block->cost = 0; + + // Count entry loads. Scalar unwraps are explicit actions below. + block->cost += static_cast(block->loads.size()); + + // Count scalar_unwrap, copy_scalar, inline, invoke actions + for (const auto &action : block->actions) { + if (action.kind() == ProcessActionKind::ScalarWrap || + action.kind() == ProcessActionKind::ScalarUnwrap || + action.emission() == ProcessEmissionClass::CopyScalar || + action.emission() == ProcessEmissionClass::Inline || + action.emission() == ProcessEmissionClass::Invoke || + action.kind() == ProcessActionKind::ForCondition || + action.kind() == ProcessActionKind::ForIncrement) { + block->cost += 1; + } + } + + // Edge cost + if (block->edge.has_value() && + block->edge->kind() == ProcessControlEdgeKind::Suspend) { + ProcessTransitionId transition = block->edge->transition(); + if (transition.value() >= control.transitions.size()) + return failure(); + block->cost += control.transitions[transition.value()]->stores.size(); + // Wake invoke + acsim.suspend + block->cost += 2; + } else if (block->edge.has_value()) { + block->cost += 1; // cf.cond_br, cf.br, or terminate + } + } + + // Compute the longest path in each PC-local DAG with Kahn traversal. Block + // IDs are dense global ordinals, while each PC owns a canonical ordered + // subset of those IDs. + uint64_t fairness = 0; + for (const auto &pc : control.pcs) { + if (pc->blocks.empty()) + return failure(); + SmallVector indegree(control.blocks.size()); + SmallVector> distance(control.blocks.size()); + auto successors = [](const ProcessControlEdgePlan &edge) { + SmallVector result; + if (edge.kind() == ProcessControlEdgeKind::Branch) { + result.push_back(edge.trueBlock()); + result.push_back(edge.falseBlock()); + } else if (edge.kind() == ProcessControlEdgeKind::LocalContinue) { + result.push_back(edge.targetBlock()); + } + return result; + }; + for (ProcessBlockId id : pc->blocks) { + if (id.value() >= control.blocks.size() || + control.blocks[id.value()]->pc != pc->id) + return failure(); + for (ProcessBlockId successor : + successors(*control.blocks[id.value()]->edge)) { + if (successor.value() >= control.blocks.size() || + control.blocks[successor.value()]->pc != pc->id) + return failure(); + ++indegree[successor.value()]; + } + } + + SmallVector ready; + for (ProcessBlockId id : pc->blocks) + if (indegree[id.value()] == 0) + ready.push_back(id); + ProcessBlockId entry = pc->blocks.front(); + distance[entry.value()] = control.blocks[entry.value()]->cost; + size_t processed = 0; + uint64_t pcWork = 0; + for (size_t cursor = 0; cursor < ready.size(); ++cursor) { + ProcessBlockId id = ready[cursor]; + ++processed; + if (distance[id.value()]) + pcWork = std::max(pcWork, *distance[id.value()]); + for (ProcessBlockId successor : + successors(*control.blocks[id.value()]->edge)) { + if (distance[id.value()]) { + uint64_t successorCost = control.blocks[successor.value()]->cost; + if (*distance[id.value()] > + std::numeric_limits::max() - successorCost) + return failure(); + uint64_t candidate = *distance[id.value()] + successorCost; + if (!distance[successor.value()] || + candidate > *distance[successor.value()]) + distance[successor.value()] = candidate; + } + if (--indegree[successor.value()] == 0) + ready.push_back(successor); + } + } + if (processed != pc->blocks.size() || + llvm::any_of(pc->blocks, [&](ProcessBlockId id) { + return !distance[id.value()].has_value(); + })) + return failure(); + fairness = std::max(fairness, pcWork); + } + + if (fairness == 0) + return control.blocks.front()->originBlock + ? control.blocks.front() + ->originBlock->getParentOp() + ->emitOpError("process fairness must be non-zero") + : failure(); + + if (fairness > limits.maxFairnessWork) + return failure(); + control.fairnessWork = fairness; + + return success(); +} + +} // namespace acir::detail diff --git a/components/agentic-circuit/lib/Analysis/ProcessStateExpansion.cpp b/components/agentic-circuit/lib/Analysis/ProcessStateExpansion.cpp new file mode 100644 index 00000000..68f0490c --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStateExpansion.cpp @@ -0,0 +1,525 @@ +#include "ModelAnalysisInternal.h" +#include "ProcessStatePlanInternal.h" + +#include "acir/Analysis/ModelAnalysis.h" + +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" + +#include + +using namespace mlir; + +namespace acir::detail { +namespace { + +struct ExpansionContext { + std::vector callSites; + std::vector iterations; +}; + +struct ExpansionTask { + Operation *operation = nullptr; + ExpansionContext context; +}; + +std::string contextKey(const ExpansionContext &context, size_t iterationCount) { + std::string key; + llvm::raw_string_ostream stream(key); + stream << 'c' << context.callSites.size() << ':'; + for (const ProcessCallSitePlan &site : context.callSites) { + stream << site.operationPath().size() << ':' << site.operationPath() << '['; + for (uint64_t iteration : site.iterationVector()) + stream << iteration << ','; + stream << "]"; + } + stream << "i" << iterationCount << ':'; + for (uint64_t iteration : + llvm::ArrayRef(context.iterations).take_front(iterationCount)) + stream << iteration << ','; + return key; +} + +std::string processDefinitionKey(ac::ProcessOp process) { + ac::ModuleOp owner = process->getParentOfType(); + return ("@" + owner.getSymName() + "::@" + process.getSymName()).str(); +} + +void indexOperationTree(Operation *root, StringRef rootPath, + DenseMap &paths, + DenseMap &blockPaths) { + struct Task { + Operation *operation; + std::string path; + }; + SmallVector pending{{root, rootPath.str()}}; + while (!pending.empty()) { + Task task = std::move(pending.back()); + pending.pop_back(); + paths[task.operation] = task.path; + SmallVector children; + for (auto [regionIndex, region] : + llvm::enumerate(task.operation->getRegions())) + for (auto [blockIndex, block] : llvm::enumerate(region)) { + std::string blockPath = (task.path + "/r" + llvm::Twine(regionIndex) + + "/b" + llvm::Twine(blockIndex)) + .str(); + blockPaths[&block] = blockPath; + for (auto [operationIndex, child] : llvm::enumerate(block)) + children.push_back( + {&child, (blockPath + "/o" + llvm::Twine(operationIndex)).str()}); + } + for (Task &child : llvm::reverse(children)) + pending.push_back(std::move(child)); + } +} + +} // namespace + +FailureOr +PlanSetBuilder::expandProcess(ac::ProcessOp process, + const ac::RawModelStructureLimits &limits) { + if (failed(ac::verifyProcessLowerability(process, limits))) + return failure(); + + ExpandedProcess expanded; + expanded.process = process; + expanded.definitionKey = processDefinitionKey(process); + uint64_t expansionNodes = 0; + uint64_t expansionEdges = 0; + bool budgetFailed = false; + auto reserveNodes = [&](Operation *origin, uint64_t count) { + if (count > limits.maxNodes - expansionNodes) { + origin->emitOpError( + "pure process expansion exceeds ACIR node/edge limits"); + budgetFailed = true; + return false; + } + expansionNodes += count; + return true; + }; + auto reserveEdges = [&](Operation *origin, uint64_t count) { + if (count > limits.maxEdges - expansionEdges) { + origin->emitOpError( + "pure process expansion exceeds ACIR node/edge limits"); + budgetFailed = true; + return false; + } + expansionEdges += count; + return true; + }; + + ModuleOp file = process->getParentOfType(); + FailureOr callGraph = + validatePureProcessCallGraph(file); + if (failed(callGraph)) + return failure(); + + DenseMap operationPaths; + DenseMap blockPaths; + indexOperationTree(process, expanded.definitionKey, operationPaths, + blockPaths); + for (const ValidatedPureFunction &entry : callGraph->functions) { + func::FuncOp function = entry.function; + indexOperationTree( + function, + (expanded.definitionKey + "/func/@" + function.getSymName()).str(), + operationPaths, blockPaths); + } + + auto makeCallSite = [&](Operation *operation, + const ExpansionContext &context) { + auto impl = std::make_shared(); + impl->operation = operation; + impl->operationPath = operationPaths.lookup(operation); + impl->iterationVector = context.iterations; + return ProcessCallSitePlan(impl); + }; + auto makeOriginalOccurrence = [&](Operation *operation, + const ExpansionContext &context) { + auto original = std::make_shared(); + original->operation = operation; + original->operationPath = operationPaths.lookup(operation); + original->callSites = context.callSites; + original->iterationVector = context.iterations; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::Original; + occurrence->original = ProcessOriginalOccurrence(original); + return ProcessOccurrenceId(occurrence); + }; + auto makeSyntheticLoopOccurrence = [&](scf::ForOp loop, + const ExpansionContext &context, + ProcessLoopPhase phase) { + ProcessOccurrenceId anchor = + makeOriginalOccurrence(loop.getOperation(), context); + auto loopImpl = std::make_shared(); + loopImpl->anchor = anchor; + loopImpl->phase = phase; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::SyntheticLoop; + occurrence->syntheticLoop = ProcessSyntheticLoopOccurrence(loopImpl); + return ProcessOccurrenceId(occurrence); + }; + auto makeSyntheticConstantOccurrence = + [&](scf::ForOp loop, const ExpansionContext &context, uint32_t ordinal) { + ProcessOccurrenceId anchor = + makeOriginalOccurrence(loop.getOperation(), context); + auto constant = + std::make_shared(); + constant->anchor = anchor; + constant->constant = ordinal; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::SyntheticConstant; + occurrence->syntheticConstant = + ProcessSyntheticConstantOccurrence(constant); + return ProcessOccurrenceId(occurrence); + }; + auto makeValueAtDefinition = [&](Value value, + const ExpansionContext &context) { + Operation *owner = value.getDefiningOp(); + auto coordinateImpl = std::make_shared(); + std::string path; + if (auto argument = dyn_cast(value)) { + owner = argument.getOwner()->getParentOp(); + coordinateImpl->kind = ProcessValueCoordinateKind::BlockArgument; + coordinateImpl->ownerPath = blockPaths.lookup(argument.getOwner()); + coordinateImpl->index = argument.getArgNumber(); + path = coordinateImpl->ownerPath + "/a" + + std::to_string(argument.getArgNumber()); + } else { + coordinateImpl->kind = ProcessValueCoordinateKind::Result; + coordinateImpl->ownerPath = operationPaths.lookup(owner); + coordinateImpl->index = cast(value).getResultNumber(); + path = coordinateImpl->ownerPath + "/v" + + std::to_string(coordinateImpl->index); + } + ProcessValueCoordinate coordinate(coordinateImpl); + auto originalImpl = std::make_shared(); + originalImpl->value = value; + originalImpl->occurrence = makeOriginalOccurrence(owner, context); + originalImpl->coordinate = coordinate; + originalImpl->path = std::move(path); + auto planned = std::make_shared(); + planned->kind = ProcessPlannedValueKind::Original; + planned->type = value.getType(); + planned->original = ProcessOriginalPlannedValue(originalImpl); + return ProcessPlannedValue(planned); + }; + DenseMap> valueEnvironment; + uint64_t valueLookupProbes = 0; + uint64_t maxValueLookupProbes = 0; + auto defineValue = [&](Value value, const ExpansionContext &context) { + std::string key = contextKey(context, context.iterations.size()); + auto &bindings = valueEnvironment[value]; + if (auto found = bindings.find(key); found != bindings.end()) + return found->second; + ProcessPlannedValue planned = makeValueAtDefinition(value, context); + bindings.emplace(std::move(key), planned); + return planned; + }; + auto resolveValue = [&](Value value, const ExpansionContext &context) { + auto found = valueEnvironment.find(value); + uint64_t probes = 0; + if (found != valueEnvironment.end()) { + size_t iterationCount = context.iterations.size(); + while (true) { + ++probes; + auto binding = found->second.find(contextKey(context, iterationCount)); + if (binding != found->second.end()) { + valueLookupProbes += probes; + maxValueLookupProbes = std::max(maxValueLookupProbes, probes); + return binding->second; + } + if (iterationCount == 0) + break; + --iterationCount; + } + } + valueLookupProbes += probes; + maxValueLookupProbes = std::max(maxValueLookupProbes, probes); + return defineValue(value, context); + }; + auto makeSyntheticValue = [&](scf::ForOp loop, + const ExpansionContext &context, + uint32_t ordinal) { + auto coordinateImpl = std::make_shared(); + coordinateImpl->kind = ProcessValueCoordinateKind::Result; + coordinateImpl->ownerPath = operationPaths.lookup(loop); + coordinateImpl->index = 0; + auto syntheticImpl = std::make_shared(); + syntheticImpl->occurrence = + makeSyntheticConstantOccurrence(loop, context, ordinal); + syntheticImpl->coordinate = ProcessValueCoordinate(coordinateImpl); + auto planned = std::make_shared(); + planned->kind = ProcessPlannedValueKind::Synthetic; + planned->type = loop.getInductionVar().getType(); + planned->synthetic = ProcessSyntheticPlannedValue(syntheticImpl); + return ProcessPlannedValue(planned); + }; + auto makeSyntheticLoopValue = [&](scf::ForOp loop, + const ExpansionContext &context, + ProcessLoopPhase phase, Type type, + uint32_t index) { + auto coordinateImpl = std::make_shared(); + coordinateImpl->kind = ProcessValueCoordinateKind::Result; + coordinateImpl->ownerPath = operationPaths.lookup(loop); + coordinateImpl->index = index; + auto syntheticImpl = std::make_shared(); + syntheticImpl->occurrence = + makeSyntheticLoopOccurrence(loop, context, phase); + syntheticImpl->coordinate = ProcessValueCoordinate(coordinateImpl); + auto planned = std::make_shared(); + planned->kind = ProcessPlannedValueKind::Synthetic; + planned->type = type; + planned->synthetic = ProcessSyntheticPlannedValue(syntheticImpl); + return ProcessPlannedValue(planned); + }; + auto makeScalar = [&](StringRef name, bool predicate) { + auto impl = std::make_shared(); + impl->name = name.str(); + impl->properties = "{}"; + if (predicate) { + auto attribute = std::make_shared(); + attribute->name = "predicate"; + attribute->value = "2 : i64"; + impl->attributes.push_back(ProcessScalarAttribute(attribute)); + } + return ProcessScalarOperationPlan(impl); + }; + auto addForwarding = [&](Value from, const ExpansionContext &fromContext, + Value to, const ExpansionContext &toContext) { + Operation *origin = from.getDefiningOp(); + if (!origin) + origin = cast(from).getOwner()->getParentOp(); + if (!reserveEdges(origin, 1)) + return; + expanded.forwarding.push_back( + {resolveValue(from, fromContext), defineValue(to, toContext)}); + }; + auto addOriginalAction = [&](Operation *operation, + const ExpansionContext &context) { + ExpandedAction action; + action.operation = operation; + action.operationPath = operationPaths.lookup(operation); + action.callSites = context.callSites; + action.iterationVector = context.iterations; + action.occurrence = makeOriginalOccurrence(operation, context); + for (Value operand : operation->getOperands()) + action.operands.push_back(resolveValue(operand, context)); + for (Value result : operation->getResults()) + action.results.push_back(defineValue(result, context)); + expanded.actions.push_back(std::move(action)); + }; + + SmallVector pending; + auto pushBlock = [&](Block &block, const ExpansionContext &context) { + SmallVector operations; + for (Operation &operation : block) + operations.push_back(&operation); + for (Operation *operation : llvm::reverse(operations)) { + if (!reserveNodes(operation, 1) || + !reserveEdges(operation, operation->getNumOperands())) + return; + pending.push_back({operation, context}); + } + }; + for (BlockArgument argument : process.getBody().front().getArguments()) + (void)defineValue(argument, {}); + pushBlock(process.getBody().front(), {}); + if (budgetFailed) + return failure(); + while (!pending.empty()) { + ExpansionTask task = std::move(pending.back()); + pending.pop_back(); + Operation *operation = task.operation; + if (auto call = dyn_cast(operation)) { + const ValidatedPureFunction *callee = callGraph->lookup(call.getCallee()); + assert(callee && "validated graph must contain every reachable callee"); + ExpansionContext calleeContext = task.context; + calleeContext.callSites.push_back(makeCallSite(operation, task.context)); + if (calleeContext.callSites.size() > kMaxPureCallDepth) { + call.emitOpError() << "pure func.call expansion exceeds ACIR " + "depth limit " + << kMaxPureCallDepth; + return failure(); + } + func::FuncOp calleeFunction = callee->function; + Block &entry = calleeFunction.getBody().front(); + for (auto [operand, argument] : + llvm::zip(call.getOperands(), entry.getArguments())) + addForwarding(operand, task.context, argument, calleeContext); + for (Operation &calleeOperation : entry) { + if (auto returnOp = dyn_cast(calleeOperation)) + for (auto [returned, result] : + llvm::zip(returnOp.getOperands(), call.getResults())) + addForwarding(returned, calleeContext, result, task.context); + } + pushBlock(entry, calleeContext); + if (budgetFailed) + return failure(); + continue; + } + if (isa(operation)) + continue; + if (auto forOp = dyn_cast(operation)) { + FailureOr staticTrip = + ac::analyzeStaticFor(forOp); + if (succeeded(staticTrip)) { + auto yield = cast(forOp.getBody()->getTerminator()); + if (staticTrip->tripCount == 0) + for (auto [init, result] : + llvm::zip(forOp.getInitArgs(), forOp.getResults())) + addForwarding(init, task.context, result, task.context); + for (uint64_t iteration = 0; iteration < staticTrip->tripCount; + ++iteration) { + ExpansionContext bodyContext = task.context; + bodyContext.iterations.push_back(iteration); + ProcessPlannedValue induction = makeSyntheticValue( + forOp, bodyContext, static_cast(iteration)); + ExpandedAction constant; + constant.kind = ProcessActionKind::Constant; + constant.operation = operation; + constant.operationPath = operationPaths.lookup(operation); + constant.callSites = task.context.callSites; + constant.iterationVector = bodyContext.iterations; + constant.occurrence = induction.synthetic().occurrence(); + constant.results.push_back(induction); + if (!reserveNodes(operation, 1)) + return failure(); + expanded.actions.push_back(std::move(constant)); + if (!reserveEdges(operation, 1)) + return failure(); + expanded.forwarding.push_back( + {induction, defineValue(forOp.getInductionVar(), bodyContext)}); + if (iteration == 0) + for (auto [init, argument] : + llvm::zip(forOp.getInitArgs(), forOp.getRegionIterArgs())) + addForwarding(init, task.context, argument, bodyContext); + if (iteration + 1 < staticTrip->tripCount) { + ExpansionContext nextContext = task.context; + nextContext.iterations.push_back(iteration + 1); + for (auto [yielded, argument] : + llvm::zip(yield.getOperands(), forOp.getRegionIterArgs())) + addForwarding(yielded, bodyContext, argument, nextContext); + } else { + for (auto [yielded, result] : + llvm::zip(yield.getOperands(), forOp.getResults())) + addForwarding(yielded, bodyContext, result, task.context); + } + } + for (uint64_t iteration = staticTrip->tripCount; iteration > 0; + --iteration) { + ExpansionContext bodyContext = task.context; + bodyContext.iterations.push_back(iteration - 1); + pushBlock(*forOp.getBody(), bodyContext); + if (budgetFailed) + return failure(); + } + continue; + } + + ProcessPlannedValue induction = makeSyntheticLoopValue( + forOp, task.context, ProcessLoopPhase::Initialize, + forOp.getInductionVar().getType(), 0); + ProcessPlannedValue condition = makeSyntheticLoopValue( + forOp, task.context, ProcessLoopPhase::Condition, + IntegerType::get(process.getContext(), 1), 0); + ProcessPlannedValue nextInduction = makeSyntheticLoopValue( + forOp, task.context, ProcessLoopPhase::Increment, + forOp.getInductionVar().getType(), 0); + for (auto [kind, phase] : {std::pair{ProcessActionKind::ForInitialize, + ProcessLoopPhase::Initialize}, + std::pair{ProcessActionKind::ForCondition, + ProcessLoopPhase::Condition}, + std::pair{ProcessActionKind::ForIncrement, + ProcessLoopPhase::Increment}}) { + ExpandedAction action; + action.kind = kind; + action.operation = operation; + action.operationPath = operationPaths.lookup(operation); + action.callSites = task.context.callSites; + action.iterationVector = task.context.iterations; + action.occurrence = + makeSyntheticLoopOccurrence(forOp, task.context, phase); + if (kind == ProcessActionKind::ForInitialize) { + action.operands.push_back( + resolveValue(forOp.getLowerBound(), task.context)); + action.results.push_back(induction); + for (Value init : forOp.getInitArgs()) { + ProcessPlannedValue initial = resolveValue(init, task.context); + action.operands.push_back(initial); + action.results.push_back(initial); + } + } else if (kind == ProcessActionKind::ForCondition) { + action.operands = {induction, + resolveValue(forOp.getUpperBound(), task.context)}; + action.results = {condition}; + action.scalarOperation = makeScalar("arith.cmpi", true); + } else { + action.operands = {induction, + resolveValue(forOp.getStep(), task.context)}; + action.results = {nextInduction}; + action.scalarOperation = makeScalar("arith.addi", false); + } + if (!reserveNodes(operation, 1)) + return failure(); + expanded.actions.push_back(std::move(action)); + } + if (!reserveEdges(operation, 1)) + return failure(); + expanded.forwarding.push_back( + {induction, defineValue(forOp.getInductionVar(), task.context)}); + if (!reserveEdges(operation, 1)) + return failure(); + expanded.forwarding.push_back( + {nextInduction, resolveValue(forOp.getInductionVar(), task.context)}); + auto yield = cast(forOp.getBody()->getTerminator()); + for (auto [init, argument] : + llvm::zip(forOp.getInitArgs(), forOp.getRegionIterArgs())) + addForwarding(init, task.context, argument, task.context); + for (auto [yielded, argument] : + llvm::zip(yield.getOperands(), forOp.getRegionIterArgs())) + addForwarding(yielded, task.context, argument, task.context); + for (auto [yielded, result] : + llvm::zip(yield.getOperands(), forOp.getResults())) + addForwarding(yielded, task.context, result, task.context); + pushBlock(*forOp.getBody(), task.context); + if (budgetFailed) + return failure(); + continue; + } + if (auto ifOp = dyn_cast(operation)) { + pushBlock(ifOp.getThenRegion().front(), task.context); + if (!ifOp.getElseRegion().empty()) + pushBlock(ifOp.getElseRegion().front(), task.context); + if (budgetFailed) + return failure(); + continue; + } + if (auto whileOp = dyn_cast(operation)) { + pushBlock(whileOp.getBefore().front(), task.context); + pushBlock(whileOp.getAfter().front(), task.context); + if (budgetFailed) + return failure(); + continue; + } + addOriginalAction(operation, task.context); + } + + expanded.expandedNodes = expansionNodes; + expanded.expandedEdges = expansionEdges; + expanded.valueLookupProbes = valueLookupProbes; + expanded.maxValueLookupProbes = maxValueLookupProbes; + return budgetFailed ? FailureOr(failure()) + : FailureOr(std::move(expanded)); +} + +FailureOr +expandProcessForPlanning(ac::ProcessOp process, + const ac::RawModelStructureLimits &limits) { + return PlanSetBuilder::expandProcess(process, limits); +} + +} // namespace acir::detail diff --git a/components/agentic-circuit/lib/Analysis/ProcessStateIdentity.cpp b/components/agentic-circuit/lib/Analysis/ProcessStateIdentity.cpp new file mode 100644 index 00000000..dd7934cc --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStateIdentity.cpp @@ -0,0 +1,346 @@ +#include "ProcessStatePlanInternal.h" + +#include + +namespace acir { + +#define FIELD_GET(Class, Type, Method, Field) \ + Type Class::Method() const { return impl_->Field; } +#define REF_GET(Class, Type, Method, Field) \ + const Type &Class::Method() const { \ + assert(impl_->Field); \ + return *impl_->Field; \ + } +#define ID_GET(Class, Type, Method, Field) \ + Type Class::Method() const { \ + assert(impl_->Field); \ + return *impl_->Field; \ + } +#define ARRAY_GET(Class, Type, Method, Field) \ + llvm::ArrayRef Class::Method() const { return impl_->Field; } +#define STR_GET(Class, Method, Field) \ + llvm::StringRef Class::Method() const { return impl_->Field; } + +FIELD_GET(ProcessCallSitePlan, mlir::Operation *, operation, operation) +STR_GET(ProcessCallSitePlan, operationPath, operationPath) +ARRAY_GET(ProcessCallSitePlan, uint64_t, iterationVector, iterationVector) +FIELD_GET(ProcessOriginalOccurrence, mlir::Operation *, operation, operation) +STR_GET(ProcessOriginalOccurrence, operationPath, operationPath) +ARRAY_GET(ProcessOriginalOccurrence, ProcessCallSitePlan, callSites, callSites) +ARRAY_GET(ProcessOriginalOccurrence, uint64_t, iterationVector, iterationVector) +REF_GET(ProcessSyntheticLoopOccurrence, ProcessOccurrenceId, anchor, anchor) +FIELD_GET(ProcessSyntheticLoopOccurrence, ProcessLoopPhase, phase, phase) +REF_GET(ProcessSyntheticWrapperOccurrence, ProcessOccurrenceId, anchor, anchor) +ID_GET(ProcessSyntheticWrapperOccurrence, ProcessTransitionId, transition, + transition) +ID_GET(ProcessSyntheticWrapperOccurrence, ProcessLiveSlotId, slot, slot) +FIELD_GET(ProcessSyntheticWrapperOccurrence, ProcessWrapperDirection, direction, + direction) +REF_GET(ProcessSyntheticConstantOccurrence, ProcessOccurrenceId, anchor, anchor) +FIELD_GET(ProcessSyntheticConstantOccurrence, uint32_t, constant, constant) +FIELD_GET(ProcessOccurrenceId, ProcessOccurrenceKind, kind, kind) +REF_GET(ProcessOccurrenceId, ProcessOriginalOccurrence, original, original) +REF_GET(ProcessOccurrenceId, ProcessSyntheticLoopOccurrence, syntheticLoop, + syntheticLoop) +REF_GET(ProcessOccurrenceId, ProcessSyntheticWrapperOccurrence, + syntheticWrapper, syntheticWrapper) +REF_GET(ProcessOccurrenceId, ProcessSyntheticConstantOccurrence, + syntheticConstant, syntheticConstant) +FIELD_GET(ProcessValueCoordinate, ProcessValueCoordinateKind, kind, kind) +STR_GET(ProcessValueCoordinate, ownerPath, ownerPath) +FIELD_GET(ProcessValueCoordinate, uint32_t, index, index) +FIELD_GET(ProcessOriginalPlannedValue, mlir::Value, value, value) +REF_GET(ProcessOriginalPlannedValue, ProcessOccurrenceId, occurrence, + occurrence) +REF_GET(ProcessOriginalPlannedValue, ProcessValueCoordinate, coordinate, + coordinate) +STR_GET(ProcessOriginalPlannedValue, path, path) +ID_GET(ProcessCapturePlannedValue, ProcessCaptureId, capture, capture) +ID_GET(ProcessLiveSlotPlannedValue, ProcessLiveSlotId, slot, slot) +REF_GET(ProcessSyntheticPlannedValue, ProcessOccurrenceId, occurrence, + occurrence) +REF_GET(ProcessSyntheticPlannedValue, ProcessValueCoordinate, coordinate, + coordinate) +STR_GET(ProcessConstantPlannedValue, value, value) +FIELD_GET(ProcessPlannedValue, ProcessPlannedValueKind, kind, kind) +FIELD_GET(ProcessPlannedValue, mlir::Type, type, type) +REF_GET(ProcessPlannedValue, ProcessOriginalPlannedValue, original, original) +REF_GET(ProcessPlannedValue, ProcessCapturePlannedValue, capture, capture) +REF_GET(ProcessPlannedValue, ProcessLiveSlotPlannedValue, liveSlot, liveSlot) +REF_GET(ProcessPlannedValue, ProcessSyntheticPlannedValue, synthetic, synthetic) +REF_GET(ProcessPlannedValue, ProcessConstantPlannedValue, constant, constant) +STR_GET(ProcessScalarAttribute, name, name) +STR_GET(ProcessScalarAttribute, value, value) +STR_GET(ProcessScalarOperationPlan, name, name) +ARRAY_GET(ProcessScalarOperationPlan, ProcessScalarAttribute, attributes, + attributes) +STR_GET(ProcessScalarOperationPlan, properties, properties) +ID_GET(ProcessCapturePlan, ProcessCaptureId, id, id) +STR_GET(ProcessCapturePlan, name, name) +FIELD_GET(ProcessCapturePlan, mlir::Value, operand, operand) +FIELD_GET(ProcessCapturePlan, mlir::Value, entryArgument, entryArgument) +FIELD_GET(ProcessCapturePlan, mlir::Type, type, type) +STR_GET(ProcessCapturePlan, operandPath, operandPath) +STR_GET(ProcessCapturePlan, argumentPath, argumentPath) +FIELD_GET(ProcessActionPlan, uint32_t, id, id) +FIELD_GET(ProcessActionPlan, ProcessActionKind, kind, kind) +FIELD_GET(ProcessActionPlan, ProcessEmissionClass, emission, emission) +REF_GET(ProcessActionPlan, ProcessOccurrenceId, occurrence, occurrence) +FIELD_GET(ProcessActionPlan, mlir::Operation *, sourceOperation, + sourceOperation) +ARRAY_GET(ProcessActionPlan, uint64_t, iterationVector, iterationVector) +ARRAY_GET(ProcessActionPlan, ProcessPlannedValue, operands, operands) +ARRAY_GET(ProcessActionPlan, ProcessPlannedValue, results, results) +FIELD_GET(ProcessActionPlan, uint32_t, cost, cost) +ARRAY_GET(ProcessActionPlan, mlir::Type, resultTypes, resultTypes) +FIELD_GET(ProcessActionPlan, std::optional, callee, callee) +const ProcessScalarOperationPlan *ProcessActionPlan::scalarOp() const { + return impl_->scalarOp ? &*impl_->scalarOp : nullptr; +} +ID_GET(ProcessLiveSlotPlan, ProcessLiveSlotId, id, id) +STR_GET(ProcessLiveSlotPlan, name, name) +FIELD_GET(ProcessLiveSlotPlan, mlir::Type, type, type) +ID_GET(ProcessLiveSlotPlan, ProcessValueTypeId, storageType, storageType) +ARRAY_GET(ProcessLiveSlotPlan, ProcessPlannedValue, memberValues, memberValues) +FIELD_GET(ProcessLiveSlotPlan, std::optional, wrapCallee, + wrapCallee) +FIELD_GET(ProcessLiveSlotPlan, std::optional, unwrapCallee, + unwrapCallee) +FIELD_GET(ProcessSubscriptionSourcePlan, ProcessSubscriptionSourceKind, kind, + kind) +FIELD_GET(ProcessSubscriptionSourcePlan, mlir::Value, value, value) +FIELD_GET(ProcessSubscriptionSourcePlan, mlir::Operation *, owner, owner) +FIELD_GET(ProcessSubscriptionSourcePlan, mlir::Operation *, declaration, + declaration) +FIELD_GET(ProcessSubscriptionSourcePlan, std::optional, + capture, capture) +STR_GET(ProcessSubscriptionSourcePlan, symbol, symbol) +STR_GET(ProcessSubscriptionSourcePlan, path, path) +STR_GET(ProcessSubscriptionSourcePlan, ownerPath, ownerPath) +ID_GET(ProcessWakePlan, ProcessWakeId, id, id) +FIELD_GET(ProcessWakePlan, ProcessWakeKind, kind, kind) +FIELD_GET(ProcessWakePlan, mlir::Operation *, operation, operation) +FIELD_GET(ProcessWakePlan, mlir::Value, triggeringValue, triggeringValue) +FIELD_GET(ProcessWakePlan, mlir::Operation *, declaration, declaration) +ID_GET(ProcessWakePlan, ProcessCalleeId, callee, callee) +STR_GET(ProcessWakePlan, typeKey, typeKey) +STR_GET(ProcessWakePlan, operationPath, operationPath) +STR_GET(ProcessWakePlan, target, target) +REF_GET(ProcessWakePlan, ProcessOccurrenceId, occurrence, occurrence) +ARRAY_GET(ProcessWakePlan, uint64_t, iterationVector, iterationVector) +ARRAY_GET(ProcessWakePlan, ProcessSubscriptionSourcePlan, sources, sources) +ID_GET(ProcessTransitionStorePlan, ProcessLiveSlotId, slot, slot) +REF_GET(ProcessTransitionStorePlan, ProcessPlannedValue, source, source) +FIELD_GET(ProcessTransitionStorePlan, mlir::Value, sourceValue, sourceValue) +ID_GET(ProcessTransitionLoadPlan, ProcessLiveSlotId, slot, slot) +ARRAY_GET(ProcessTransitionLoadPlan, ProcessPlannedValue, replacements, + replacements) +ID_GET(ProcessTransitionPlan, ProcessTransitionId, id, id) +ID_GET(ProcessTransitionPlan, ProcessPcId, sourcePc, sourcePc) +ID_GET(ProcessTransitionPlan, ProcessPcId, targetPc, targetPc) +ID_GET(ProcessTransitionPlan, ProcessWakeId, wake, wake) +ARRAY_GET(ProcessTransitionPlan, uint64_t, iterationVector, iterationVector) +ARRAY_GET(ProcessTransitionPlan, ProcessTransitionStorePlan, stores, stores) +ARRAY_GET(ProcessTransitionPlan, ProcessTransitionLoadPlan, loads, loads) +REF_GET(ProcessForwardingBindingPlan, ProcessPlannedValue, from, from) +REF_GET(ProcessForwardingBindingPlan, ProcessPlannedValue, to, to) +FIELD_GET(ProcessControlFramePlan, ProcessFrameKind, kind, kind) +FIELD_GET(ProcessControlFramePlan, ProcessFramePhase, phase, phase) +FIELD_GET(ProcessControlFramePlan, mlir::Operation *, operation, operation) +STR_GET(ProcessControlFramePlan, operationPath, operationPath) +ARRAY_GET(ProcessControlFramePlan, ProcessForwardingBindingPlan, bindings, + bindings) +FIELD_GET(ProcessControlEdgePlan, ProcessControlEdgeKind, kind, kind) +REF_GET(ProcessControlEdgePlan, ProcessPlannedValue, condition, condition) +ID_GET(ProcessControlEdgePlan, ProcessBlockId, trueBlock, trueBlock) +ID_GET(ProcessControlEdgePlan, ProcessBlockId, falseBlock, falseBlock) +ARRAY_GET(ProcessControlEdgePlan, ProcessForwardingBindingPlan, trueBindings, + trueBindings) +ARRAY_GET(ProcessControlEdgePlan, ProcessForwardingBindingPlan, falseBindings, + falseBindings) +ID_GET(ProcessControlEdgePlan, ProcessBlockId, targetBlock, targetBlock) +ARRAY_GET(ProcessControlEdgePlan, ProcessForwardingBindingPlan, bindings, + bindings) +ID_GET(ProcessControlEdgePlan, ProcessTransitionId, transition, transition) +FIELD_GET(ProcessControlEdgePlan, ProcessTerminateStatus, status, status) +ID_GET(ProcessBlockPlan, ProcessBlockId, id, id) +ID_GET(ProcessBlockPlan, ProcessPcId, pc, pc) +FIELD_GET(ProcessBlockPlan, mlir::Region *, originRegion, originRegion) +FIELD_GET(ProcessBlockPlan, mlir::Block *, originBlock, originBlock) +STR_GET(ProcessBlockPlan, path, path) +ARRAY_GET(ProcessBlockPlan, ProcessControlFramePlan, frames, frames) +ARRAY_GET(ProcessBlockPlan, ProcessTransitionLoadPlan, loads, loads) +ARRAY_GET(ProcessBlockPlan, ProcessActionPlan, actions, actions) +REF_GET(ProcessBlockPlan, ProcessControlEdgePlan, edge, edge) +FIELD_GET(ProcessBlockPlan, uint64_t, cost, cost) +ID_GET(ProcessPcPlan, ProcessPcId, id, id) +STR_GET(ProcessPcPlan, name, name) +STR_GET(ProcessPcPlan, entryPath, entryPath) +ARRAY_GET(ProcessPcPlan, ProcessBlockId, blocks, blocks) +STR_GET(ProcessStatePlan, definitionKey, definitionKey) +FIELD_GET(ProcessStatePlan, ac::ProcessOp, process, process) +ARRAY_GET(ProcessStatePlan, ProcessCapturePlan, captures, captures) +ID_GET(ProcessStatePlan, ProcessPcId, entryPc, entryPc) +ARRAY_GET(ProcessStatePlan, ProcessPcPlan, pcs, pcs) +ARRAY_GET(ProcessStatePlan, ProcessBlockPlan, blocks, blocks) +ARRAY_GET(ProcessStatePlan, ProcessLiveSlotPlan, liveSlots, liveSlots) +ARRAY_GET(ProcessStatePlan, ProcessWakePlan, wakes, wakes) +ARRAY_GET(ProcessStatePlan, ProcessTransitionPlan, transitions, transitions) +FIELD_GET(ProcessStatePlan, uint32_t, pcBitWidth, pcBitWidth) +FIELD_GET(ProcessStatePlan, uint64_t, fairnessWork, fairnessWork) +STR_GET(ProcessRecordFieldDescriptor, name, name) +STR_GET(ProcessRecordFieldDescriptor, typeKey, typeKey) +ARRAY_GET(ProcessRecordCreatePayload, ProcessRecordFieldDescriptor, fields, + fields) +STR_GET(ProcessRecordCreatePayload, recordType, recordType) +STR_GET(ProcessRecordGetPayload, field, field) +STR_GET(ProcessRecordGetPayload, record, record) +STR_GET(ProcessRecordGetPayload, result, result) +STR_GET(ProcessRecordWithPayload, field, field) +STR_GET(ProcessRecordWithPayload, record, record) +STR_GET(ProcessRecordWithPayload, value, value) +FIELD_GET(ProcessPacketSerializePayload, uint64_t, bytes, bytes) +STR_GET(ProcessPacketSerializePayload, packet, packet) +STR_GET(ProcessPacketSerializePayload, packetType, packetType) +FIELD_GET(ProcessPacketDeserializePayload, uint64_t, bytes, bytes) +STR_GET(ProcessPacketDeserializePayload, packet, packet) +STR_GET(ProcessPacketDeserializePayload, packetType, packetType) +STR_GET(ProcessTraceDecodePayload, entry, entry) +STR_GET(ProcessTraceDecodePayload, result, result) +STR_GET(ProcessTraceDecodePayload, source, source) +STR_GET(ProcessQueueTrySendPayload, element, element) +STR_GET(ProcessQueueTrySendPayload, queue, queue) +STR_GET(ProcessQueueTryRecvPayload, element, element) +STR_GET(ProcessQueueTryRecvPayload, queue, queue) +STR_GET(ProcessEventSchedulePayload, delay, delay) +STR_GET(ProcessEventSchedulePayload, target, target) +STR_GET(ProcessEventSchedulePayload, value, value) +STR_GET(ProcessTraceOpenPayload, source, source) +STR_GET(ProcessTraceNextPayload, entry, entry) +STR_GET(ProcessTraceNextPayload, source, source) +STR_GET(ProcessTraceEofPayload, source, source) +STR_GET(ProcessTracePositionPayload, source, source) +STR_GET(ProcessContractRequirePayload, message, message) +STR_GET(ProcessContractEnsurePayload, message, message) +STR_GET(ProcessContractAssertPayload, message, message) +STR_GET(ProcessProbePayload, kind, kind) +STR_GET(ProcessProbePayload, result, result) +STR_GET(ProcessProbePayload, target, target) +STR_GET(ProcessStatAddPayload, stat, stat) +STR_GET(ProcessStatAddPayload, valueType, valueType) +#define WAKE_GETS(Class) \ + FIELD_GET(Class, ProcessWakeKind, wakeKind, wakeKind) \ + STR_GET(Class, wakeType, wakeType) +WAKE_GETS(ProcessWakeConditionPayload) +WAKE_GETS(ProcessWakeResourcePayload) +WAKE_GETS(ProcessWakeEventQueuePayload) +WAKE_GETS(ProcessWakeNextDeltaPayload) +#undef WAKE_GETS +FIELD_GET(ProcessScalarWrapPayload, ProcessWrapperDirection, direction, + direction) +STR_GET(ProcessScalarWrapPayload, scalar, scalar) +STR_GET(ProcessScalarWrapPayload, valueType, valueType) +FIELD_GET(ProcessScalarUnwrapPayload, ProcessWrapperDirection, direction, + direction) +STR_GET(ProcessScalarUnwrapPayload, scalar, scalar) +STR_GET(ProcessScalarUnwrapPayload, valueType, valueType) +FIELD_GET(ProcessGeneratedCalleePayload, ProcessHelperRole, role, role) +#define PAYLOAD_REF(Type, Method, Field) \ + REF_GET(ProcessGeneratedCalleePayload, Type, Method, Field) +PAYLOAD_REF(ProcessRecordCreatePayload, recordCreate, recordCreate) +PAYLOAD_REF(ProcessRecordGetPayload, recordGet, recordGet) +PAYLOAD_REF(ProcessRecordWithPayload, recordWith, recordWith) +PAYLOAD_REF(ProcessPacketSerializePayload, packetSerialize, packetSerialize) +PAYLOAD_REF(ProcessPacketDeserializePayload, packetDeserialize, + packetDeserialize) +PAYLOAD_REF(ProcessTraceDecodePayload, traceDecode, traceDecode) +PAYLOAD_REF(ProcessQueueTrySendPayload, queueTrySend, queueTrySend) +PAYLOAD_REF(ProcessQueueTryRecvPayload, queueTryRecv, queueTryRecv) +PAYLOAD_REF(ProcessEventSchedulePayload, eventSchedule, eventSchedule) +PAYLOAD_REF(ProcessTraceOpenPayload, traceOpen, traceOpen) +PAYLOAD_REF(ProcessTraceNextPayload, traceNext, traceNext) +PAYLOAD_REF(ProcessTraceEofPayload, traceEof, traceEof) +PAYLOAD_REF(ProcessTracePositionPayload, tracePosition, tracePosition) +PAYLOAD_REF(ProcessContractRequirePayload, contractRequire, contractRequire) +PAYLOAD_REF(ProcessContractEnsurePayload, contractEnsure, contractEnsure) +PAYLOAD_REF(ProcessContractAssertPayload, contractAssert, contractAssert) +PAYLOAD_REF(ProcessProbePayload, probe, probe) +PAYLOAD_REF(ProcessStatAddPayload, statAdd, statAdd) +PAYLOAD_REF(ProcessWakeConditionPayload, wakeCondition, wakeCondition) +PAYLOAD_REF(ProcessWakeResourcePayload, wakeResource, wakeResource) +PAYLOAD_REF(ProcessWakeEventQueuePayload, wakeEventQueue, wakeEventQueue) +PAYLOAD_REF(ProcessWakeNextDeltaPayload, wakeNextDelta, wakeNextDelta) +PAYLOAD_REF(ProcessScalarWrapPayload, scalarWrap, scalarWrap) +PAYLOAD_REF(ProcessScalarUnwrapPayload, scalarUnwrap, scalarUnwrap) +#undef PAYLOAD_REF +FIELD_GET(ProcessValueTypeMemberPlan, ProcessValueTypeMemberKind, kind, kind) +STR_GET(ProcessValueTypeMemberPlan, name, name) +FIELD_GET(ProcessValueTypeMemberPlan, std::optional, index, index) +FIELD_GET(ProcessValueTypeMemberPlan, uint64_t, offsetBits, offsetBits) +FIELD_GET(ProcessValueTypeMemberPlan, uint64_t, widthBits, widthBits) +FIELD_GET(ProcessValueTypeMemberPlan, std::optional, + signedness, signedness) +STR_GET(ProcessValueTypeMemberPlan, encoding, encoding) +STR_GET(ProcessValueTypeMemberPlan, typeKey, typeKey) +ARRAY_GET(ProcessStorageValuePayload, ProcessValueTypeMemberPlan, members, + members) +FIELD_GET(ProcessStorageValuePayload, uint64_t, widthBits, widthBits) +STR_GET(ProcessStorageValuePayload, encoding, encoding) +ARRAY_GET(ProcessStoragePacketPayload, ProcessValueTypeMemberPlan, members, + members) +FIELD_GET(ProcessStoragePacketPayload, uint64_t, widthBits, widthBits) +FIELD_GET(ProcessStoragePacketPayload, uint64_t, bytes, bytes) +STR_GET(ProcessStoragePacketPayload, encoding, encoding) +FIELD_GET(ProcessValueTypePayload, ProcessValueTypeKind, kind, kind) +REF_GET(ProcessValueTypePayload, ProcessStorageValuePayload, value, value) +REF_GET(ProcessValueTypePayload, ProcessStoragePacketPayload, packet, packet) +ID_GET(ProcessGeneratedCalleePlan, ProcessCalleeId, id, id) +STR_GET(ProcessGeneratedCalleePlan, symbol, symbol) +STR_GET(ProcessGeneratedCalleePlan, cpp, cpp) +STR_GET(ProcessGeneratedCalleePlan, kind, kind) +STR_GET(ProcessGeneratedCalleePlan, fingerprint, fingerprint) +FIELD_GET(ProcessGeneratedCalleePlan, ProcessEffectKind, effect, effect) +ARRAY_GET(ProcessGeneratedCalleePlan, llvm::StringRef, inputTypeKeys, + inputTypeKeys) +ARRAY_GET(ProcessGeneratedCalleePlan, llvm::StringRef, resultTypeKeys, + resultTypeKeys) +FIELD_GET(ProcessGeneratedCalleePlan, ProcessHelperRole, role, role) +REF_GET(ProcessGeneratedCalleePlan, ProcessGeneratedCalleePayload, payload, + payload) +ARRAY_GET(ProcessGeneratedCalleePlan, mlir::Operation *, sourceOperations, + sourceOperations) +ARRAY_GET(ProcessGeneratedCalleePlan, mlir::Operation *, declarations, + declarations) +ARRAY_GET(ProcessGeneratedCalleePlan, llvm::StringRef, sourcePaths, sourcePaths) +ID_GET(ProcessValueTypePlan, ProcessValueTypeId, id, id) +STR_GET(ProcessValueTypePlan, symbol, symbol) +STR_GET(ProcessValueTypePlan, cpp, cpp) +FIELD_GET(ProcessValueTypePlan, ProcessValueTypeKind, kind, kind) +STR_GET(ProcessValueTypePlan, fingerprint, fingerprint) +FIELD_GET(ProcessValueTypePlan, mlir::Type, acirType, acirType) +REF_GET(ProcessValueTypePlan, ProcessValueTypePayload, payload, payload) +ARRAY_GET(ProcessStatePlanSet, ProcessStatePlan, processes, processes) +ARRAY_GET(ProcessStatePlanSet, ProcessGeneratedCalleePlan, callees, callees) +ARRAY_GET(ProcessStatePlanSet, ProcessValueTypePlan, valueTypes, valueTypes) + +const ProcessStatePlan *ProcessStatePlanSet::lookupByDefinitionKey( + llvm::StringRef definitionKey) const { + auto processes = this->processes(); + auto iterator = + llvm::lower_bound(processes, definitionKey, + [](const ProcessStatePlan &plan, llvm::StringRef key) { + return plan.definitionKey().compare(key) < 0; + }); + return iterator != processes.end() && + iterator->definitionKey() == definitionKey + ? &*iterator + : nullptr; +} + +#undef FIELD_GET +#undef REF_GET +#undef ID_GET +#undef ARRAY_GET +#undef STR_GET + +} // namespace acir diff --git a/components/agentic-circuit/lib/Analysis/ProcessStateLiveness.cpp b/components/agentic-circuit/lib/Analysis/ProcessStateLiveness.cpp new file mode 100644 index 00000000..7d61eedd --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStateLiveness.cpp @@ -0,0 +1,198 @@ +#include "ProcessStatePlanInternal.h" + +#include "mlir/IR/Diagnostics.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/FormatVariadic.h" + +#include + +using namespace mlir; + +namespace acir::detail { + +namespace { + +bool needsScalarStorageWrapper(mlir::Type type) { + return mlir::isa(type); +} + +} // namespace + +LogicalResult +PlanSetBuilder::planProcessLiveness(ControlPlan &control, + const ProcessStateLimits &limits) { + auto makeWrapperOccurrence = [&](const ProcessPlannedValue &anchor, + ProcessTransitionId transition, + ProcessLiveSlotId slot, + ProcessWrapperDirection direction) { + auto wrapper = std::make_shared(); + wrapper->anchor = anchor.original().occurrence(); + wrapper->transition = transition; + wrapper->slot = slot; + wrapper->direction = direction; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::SyntheticWrapper; + occurrence->syntheticWrapper = ProcessSyntheticWrapperOccurrence(wrapper); + return ProcessOccurrenceId(occurrence); + }; + auto makeSyntheticWrapperValue = [&](mlir::Type type, + const ProcessOccurrenceId &occurrence, + llvm::StringRef ownerPath) { + auto coordinate = std::make_shared(); + coordinate->kind = ProcessValueCoordinateKind::Result; + coordinate->ownerPath = ownerPath.str(); + coordinate->index = 0; + auto synthetic = std::make_shared(); + synthetic->occurrence = occurrence; + synthetic->coordinate = ProcessValueCoordinate(coordinate); + auto value = std::make_shared(); + value->kind = ProcessPlannedValueKind::Synthetic; + value->type = type; + value->synthetic = ProcessSyntheticPlannedValue(synthetic); + return ProcessPlannedValue(value); + }; + auto makeLiveSlotValue = [&](mlir::Type type, ProcessLiveSlotId slot) { + auto live = std::make_shared(); + live->slot = slot; + auto value = std::make_shared(); + value->kind = ProcessPlannedValueKind::LiveSlot; + value->type = type; + value->liveSlot = ProcessLiveSlotPlannedValue(live); + return ProcessPlannedValue(value); + }; + auto renumberActions = [&](ProcessBlockPlan::Impl &block) { + for (auto [index, action] : llvm::enumerate(block.actions)) + std::const_pointer_cast(action.impl_)->id = + index; + }; + std::map slotsByPath; + llvm::DenseMap> + wrapsByBlock; + llvm::DenseMap> + unwrapsByBlock; + for (auto &transition : control.transitions) { + if (transition->sourcePc == transition->targetPc) + continue; + auto sourceBlock = llvm::find_if(control.blocks, [&](const auto &block) { + return block->pc == transition->sourcePc; + }); + auto targetBlock = llvm::find_if(control.blocks, [&](const auto &block) { + return block->pc == transition->targetPc; + }); + if (sourceBlock == control.blocks.end() || + targetBlock == control.blocks.end()) + return failure(); + + std::map definitions; + for (const ProcessActionPlan &action : (*sourceBlock)->actions) + for (const ProcessPlannedValue &result : action.results()) + if (result.kind() == ProcessPlannedValueKind::Original) + definitions.emplace(result.original().path().str(), result); + + for (const ProcessActionPlan &action : (*targetBlock)->actions) { + for (const ProcessPlannedValue &operand : action.operands()) { + if (operand.kind() != ProcessPlannedValueKind::Original) + continue; + auto definition = definitions.find(operand.original().path().str()); + if (definition == definitions.end()) + continue; + auto [slotIt, inserted] = slotsByPath.emplace( + definition->first, + ProcessLiveSlotId(static_cast(control.liveSlots.size()))); + ProcessLiveSlotId slotId = slotIt->second; + if (inserted) { + if (control.liveSlots.size() >= limits.maxLiveSlots) + return failure(); + auto slot = std::make_shared(); + slot->id = slotId; + slot->name = llvm::formatv("live{0:D8}", slotId.value()).str(); + slot->type = definition->second.type(); + slot->memberValues.push_back(definition->second); + control.liveSlots.push_back(std::move(slot)); + } + if (llvm::none_of(transition->stores, [&](const auto &store) { + return store.impl_->slot == slotId; + })) { + ProcessPlannedValue storedSource = definition->second; + if (needsScalarStorageWrapper(definition->second.type())) { + ProcessOccurrenceId occurrence = + makeWrapperOccurrence(definition->second, *transition->id, + slotId, ProcessWrapperDirection::Wrap); + ProcessPlannedValue wrapped = + makeSyntheticWrapperValue(definition->second.type(), occurrence, + definition->second.original().path()); + auto action = std::make_shared(); + action->kind = ProcessActionKind::ScalarWrap; + action->emission = ProcessEmissionClass::Wrap; + action->occurrence = occurrence; + action->operands = {definition->second}; + action->results = {wrapped}; + action->resultTypes = {definition->second.type()}; + action->cost = 1; + wrapsByBlock[sourceBlock->get()].push_back( + ProcessActionPlan(action)); + storedSource = wrapped; + } + auto store = std::make_shared(); + store->slot = slotId; + store->source = storedSource; + if (definition->second.kind() == ProcessPlannedValueKind::Original) + store->sourceValue = definition->second.original().value(); + transition->stores.push_back(ProcessTransitionStorePlan(store)); + } + auto existingLoad = llvm::find_if( + transition->loads, [&](const ProcessTransitionLoadPlan &load) { + return load.impl_->slot == slotId; + }); + if (existingLoad == transition->loads.end()) { + auto load = std::make_shared(); + load->slot = slotId; + if (needsScalarStorageWrapper(operand.type())) { + ProcessPlannedValue loaded = + makeLiveSlotValue(operand.type(), slotId); + load->replacements.push_back(loaded); + ProcessOccurrenceId occurrence = + makeWrapperOccurrence(operand, *transition->id, slotId, + ProcessWrapperDirection::Unwrap); + auto action = std::make_shared(); + action->kind = ProcessActionKind::ScalarUnwrap; + action->emission = ProcessEmissionClass::Unwrap; + action->occurrence = occurrence; + action->operands = {loaded}; + action->results = {operand}; + action->resultTypes = {operand.type()}; + action->cost = 1; + unwrapsByBlock[targetBlock->get()].push_back( + ProcessActionPlan(action)); + } else { + load->replacements.push_back(operand); + } + transition->loads.push_back(ProcessTransitionLoadPlan(load)); + (*targetBlock)->loads.push_back(ProcessTransitionLoadPlan(load)); + } else { + auto load = std::const_pointer_cast( + existingLoad->impl_); + if (!needsScalarStorageWrapper(operand.type())) + load->replacements.push_back(operand); + } + } + } + } + for (auto &block : control.blocks) { + auto unwraps = unwrapsByBlock.find(block.get()); + if (unwraps != unwrapsByBlock.end()) + block->actions.insert(block->actions.begin(), unwraps->second.begin(), + unwraps->second.end()); + auto wraps = wrapsByBlock.find(block.get()); + if (wraps != wrapsByBlock.end()) + block->actions.insert(block->actions.end(), wraps->second.begin(), + wraps->second.end()); + renumberActions(*block); + } + return success(); +} + +} // namespace acir::detail diff --git a/components/agentic-circuit/lib/Analysis/ProcessStatePlan.cpp b/components/agentic-circuit/lib/Analysis/ProcessStatePlan.cpp new file mode 100644 index 00000000..dc5c7be6 --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStatePlan.cpp @@ -0,0 +1,3845 @@ +#include "ProcessStatePlanInternal.h" + +#include "acir/Bindings/Binding.h" + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/SymbolTable.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" + +#include +#include +#include +#include +#include + +namespace acir { +namespace { + +thread_local std::string lastDiagnostic; + +constexpr llvm::StringLiteral kWakeNextDeltaSpecialization = + R"json({"contract_epoch":"0.3","effect":"stateful","inputs":[],"kind":"implementation","payload":{"wake_kind":"next_delta","wake_type":"@acir_wake_next_delta"},"results":["@acir_wake_next_delta"],"role":"wake_next_delta","schema":"acir-generated-implementation-0.1","source_paths":[]})json"; +constexpr llvm::StringLiteral kWakeNextDeltaDigest = + "28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c"; + +mlir::LogicalResult reject(const ProcessStatePlanSet &plans, + llvm::StringRef diagnostic) { + lastDiagnostic = diagnostic.str(); + if (!plans.processes().empty() && plans.processes().front().process()) + plans.processes().front().process().emitError(diagnostic); + return mlir::failure(); +} + +bool validDefinitionKey(llvm::StringRef key) { + size_t separator = key.find("::"); + return separator > 1 && separator + 3 < key.size() && key.front() == '@' && + key[separator + 2] == '@' && key.find("::", separator + 2) == key.npos; +} + +llvm::StringRef helperRoleSpelling(ProcessHelperRole role) { + static constexpr llvm::StringLiteral names[] = {"record_create", + "record_get", + "record_with", + "packet_serialize", + "packet_deserialize", + "trace_decode", + "queue_try_send", + "queue_try_recv", + "event_schedule", + "trace_open", + "trace_next", + "trace_eof", + "trace_position", + "contract_require", + "contract_ensure", + "contract_assert", + "probe", + "stat_add", + "wake_condition", + "wake_resource", + "wake_event_queue", + "wake_next_delta", + "scalar_wrap", + "scalar_unwrap"}; + return names[static_cast(role)]; +} + +llvm::StringRef wakeTypeKey(ProcessWakeKind kind) { + static constexpr llvm::StringLiteral keys[] = { + "@acir_wake_condition", "@acir_wake_resource", "@acir_wake_event_queue", + "@acir_wake_next_delta"}; + return keys[static_cast(kind)]; +} + +bool validTypeKey(llvm::StringRef key) { + auto validDigest = [](llvm::StringRef value) { + return value.size() == 64 && llvm::all_of(value, [](char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); + }); + }; + if (key.starts_with("mlir:")) + return key.size() > 5; + if (key.consume_front("storage:value:") || + key.consume_front("storage:packet:")) + return validDigest(key); + return key == "@acir_wake_condition" || key == "@acir_wake_resource" || + key == "@acir_wake_event_queue" || key == "@acir_wake_next_delta"; +} + +bool validCalleeSemantics(const ProcessGeneratedCalleePlan &callee) { + auto inputs = callee.inputTypeKeys(); + auto results = callee.resultTypeKeys(); + const ProcessGeneratedCalleePayload &payload = callee.payload(); + switch (callee.role()) { + case ProcessHelperRole::RecordCreate: { + auto fields = payload.recordCreate().fields(); + if (inputs.size() != fields.size() || results.size() != 1 || + results[0] != payload.recordCreate().recordType()) + return false; + llvm::SmallVector names; + for (auto [input, field] : llvm::zip_equal(inputs, fields)) { + if (input != field.typeKey() || field.name().empty() || + llvm::is_contained(names, field.name())) + return false; + names.push_back(field.name()); + } + return true; + } + case ProcessHelperRole::RecordGet: + return inputs.size() == 1 && results.size() == 1 && + inputs[0] == payload.recordGet().record() && + results[0] == payload.recordGet().result(); + case ProcessHelperRole::RecordWith: + return inputs.size() == 2 && results.size() == 1 && + inputs[0] == payload.recordWith().record() && + inputs[1] == payload.recordWith().value() && + results[0] == payload.recordWith().record(); + case ProcessHelperRole::PacketSerialize: + return inputs.size() == 1 && results.size() == 1 && + inputs[0] == payload.packetSerialize().packetType(); + case ProcessHelperRole::PacketDeserialize: + return inputs.size() == 1 && results.size() == 1 && + results[0] == payload.packetDeserialize().packetType(); + case ProcessHelperRole::TraceDecode: + return inputs.size() == 1 && results.size() == 1 && + inputs[0] == payload.traceDecode().entry() && + results[0] == payload.traceDecode().result(); + case ProcessHelperRole::QueueTrySend: + return inputs.size() == 1 && results.size() == 1 && + inputs[0] == payload.queueTrySend().element(); + case ProcessHelperRole::QueueTryRecv: + return inputs.empty() && results.size() == 2 && + results[0] == payload.queueTryRecv().element(); + case ProcessHelperRole::EventSchedule: + return inputs.size() == 2 && results.empty() && + inputs[0] == payload.eventSchedule().value() && + inputs[1] == payload.eventSchedule().delay(); + case ProcessHelperRole::TraceOpen: + return inputs.empty() && results.size() == 1; + case ProcessHelperRole::TraceNext: + return inputs.size() == 1 && results.size() == 3 && + inputs[0] == results[0] && results[1] == payload.traceNext().entry(); + case ProcessHelperRole::TraceEof: + case ProcessHelperRole::TracePosition: + return inputs.size() == 1 && results.size() == 1; + case ProcessHelperRole::ContractRequire: + case ProcessHelperRole::ContractEnsure: + case ProcessHelperRole::ContractAssert: + return inputs.size() == 1 && results.empty(); + case ProcessHelperRole::Probe: + return inputs.empty() && results.size() == 1 && + results[0] == payload.probe().result(); + case ProcessHelperRole::StatAdd: + return inputs.size() == 1 && results.empty() && + inputs[0] == payload.statAdd().valueType(); + case ProcessHelperRole::WakeCondition: + return inputs.empty() && results.size() == 1 && + payload.wakeCondition().wakeKind() == ProcessWakeKind::Condition && + payload.wakeCondition().wakeType() == + wakeTypeKey(ProcessWakeKind::Condition) && + results[0] == payload.wakeCondition().wakeType(); + case ProcessHelperRole::WakeResource: + return inputs.empty() && results.size() == 1 && + payload.wakeResource().wakeKind() == ProcessWakeKind::Resource && + payload.wakeResource().wakeType() == + wakeTypeKey(ProcessWakeKind::Resource) && + results[0] == payload.wakeResource().wakeType(); + case ProcessHelperRole::WakeEventQueue: + return inputs.empty() && results.size() == 1 && + payload.wakeEventQueue().wakeKind() == ProcessWakeKind::EventQueue && + payload.wakeEventQueue().wakeType() == + wakeTypeKey(ProcessWakeKind::EventQueue) && + results[0] == payload.wakeEventQueue().wakeType(); + case ProcessHelperRole::WakeNextDelta: + return inputs.empty() && results.size() == 1 && + payload.wakeNextDelta().wakeKind() == ProcessWakeKind::NextDelta && + payload.wakeNextDelta().wakeType() == + wakeTypeKey(ProcessWakeKind::NextDelta) && + results[0] == payload.wakeNextDelta().wakeType(); + case ProcessHelperRole::ScalarWrap: + return inputs.size() == 1 && results.size() == 1 && + payload.scalarWrap().direction() == ProcessWrapperDirection::Wrap && + inputs[0] == payload.scalarWrap().scalar() && + results[0] == payload.scalarWrap().valueType(); + case ProcessHelperRole::ScalarUnwrap: + return inputs.size() == 1 && results.size() == 1 && + payload.scalarUnwrap().direction() == + ProcessWrapperDirection::Unwrap && + inputs[0] == payload.scalarUnwrap().valueType() && + results[0] == payload.scalarUnwrap().scalar(); + } + return false; +} + +} // namespace + +mlir::FailureOr +detail::PlanSetBuilder::buildEmpty(mlir::ModuleOp module) { + bool hasProcess = false; + module.walk([&](ac::ProcessOp) { hasProcess = true; }); + if (hasProcess) { + module.emitError( + "empty process-state fixture requires zero ac.process operations"); + return mlir::failure(); + } + auto epoch = module->getAttrOfType("ac.contract_epoch"); + if (!epoch || epoch.getValue() != "0.3") { + module.emitError("empty process-state fixture requires contract epoch 0.3"); + return mlir::failure(); + } + return ProcessStatePlanSet(std::make_shared()); +} + +mlir::FailureOr +detail::PlanSetBuilder::buildProduction(mlir::ModuleOp module, + const ProcessStateLimits &limits) { + auto frozenEpoch = module->getAttrOfType("ac.freeze_epoch"); + if (!frozenEpoch || frozenEpoch.getValue() != "0.3") { + module.emitError("process-state planning requires a frozen model"); + return mlir::failure(); + } + + llvm::SmallVector processes; + module.walk([&](ac::ProcessOp process) { processes.push_back(process); }); + if (processes.empty()) { + module.emitError("process-state planning requires at least one ac.process"); + return mlir::failure(); + } + if (processes.size() > limits.maxProcesses) { + module.emitError("process-state plan capability maxProcesses exceeded"); + return mlir::failure(); + } + auto definitionKey = [](ac::ProcessOp process) { + ac::ModuleOp owner = process->getParentOfType(); + return ("@" + owner.getSymName() + "::@" + process.getSymName()).str(); + }; + llvm::sort(processes, [&](ac::ProcessOp lhs, ac::ProcessOp rhs) { + return definitionKey(lhs) < definitionKey(rhs); + }); + + struct PendingProcess { + ac::ProcessOp process; + std::string definitionKey; + std::unique_ptr control; + }; + std::vector pending; + ac::RawModelStructureLimits rawLimits; + rawLimits.maxNodes = limits.maxPlannedOperations; + rawLimits.maxEdges = limits.maxTransitions; + rawLimits.maxNestedRegionDepth = limits.maxNestedRegionDepth; + for (ac::ProcessOp process : processes) { + auto expanded = expandProcess(process, rawLimits); + if (mlir::failed(expanded)) + return mlir::failure(); + auto control = planProcessContinuation(*expanded, limits); + if (mlir::failed(control)) + return mlir::failure(); + control = planProcessWakes(std::move(*control), limits); + if (mlir::failed(control) || + mlir::failed(planProcessLiveness(**control, limits)) || + mlir::failed(planProcessCost(**control, limits))) + return mlir::failure(); + pending.push_back({process, expanded->definitionKey, std::move(*control)}); + } + + auto makeWakePayload = [](ProcessWakeKind kind) { + auto payload = std::make_shared(); + payload->role = static_cast( + static_cast(ProcessHelperRole::WakeCondition) + + static_cast(kind)); + switch (kind) { + case ProcessWakeKind::Condition: { + auto arm = std::make_shared(); + arm->wakeKind = kind; + arm->wakeType = wakeTypeKey(kind).str(); + payload->wakeCondition = ProcessWakeConditionPayload(arm); + break; + } + case ProcessWakeKind::Resource: { + auto arm = std::make_shared(); + arm->wakeKind = kind; + arm->wakeType = wakeTypeKey(kind).str(); + payload->wakeResource = ProcessWakeResourcePayload(arm); + break; + } + case ProcessWakeKind::EventQueue: { + auto arm = std::make_shared(); + arm->wakeKind = kind; + arm->wakeType = wakeTypeKey(kind).str(); + payload->wakeEventQueue = ProcessWakeEventQueuePayload(arm); + break; + } + case ProcessWakeKind::NextDelta: { + auto arm = std::make_shared(); + arm->wakeKind = kind; + arm->wakeType = wakeTypeKey(kind).str(); + payload->wakeNextDelta = ProcessWakeNextDeltaPayload(arm); + break; + } + } + return ProcessGeneratedCalleePayload(payload); + }; + + llvm::SmallVector usedWakeKinds(4, false); + for (const PendingProcess &item : pending) + for (const auto &wake : item.control->wakes) + usedWakeKinds[static_cast(wake->kind)] = true; + std::vector> callees; + for (unsigned kindIndex = 0; kindIndex < usedWakeKinds.size(); ++kindIndex) { + if (!usedWakeKinds[kindIndex]) + continue; + ProcessWakeKind kind = static_cast(kindIndex); + auto callee = std::make_shared(); + callee->effect = ProcessEffectKind::Stateful; + callee->role = static_cast( + static_cast(ProcessHelperRole::WakeCondition) + kindIndex); + callee->payload = makeWakePayload(kind); + callee->resultTypeKeyStorage = {wakeTypeKey(kind).str()}; + callee->resultTypeKeys = {callee->resultTypeKeyStorage.front()}; + ProcessGeneratedCalleePlan value(callee); + auto canonical = canonicalGeneratedCalleeSpecialization(value); + if (!canonical) { + llvm::consumeError(canonical.takeError()); + return mlir::failure(); + } + callee->specializationBytes = std::move(*canonical); + callee->fingerprint = + bindings::sha256Fingerprint(callee->specializationBytes); + llvm::StringRef digest = llvm::StringRef(callee->fingerprint).drop_front(7); + callee->symbol = + ("@acir_impl_" + helperRoleSpelling(callee->role) + "_" + digest).str(); + callee->cpp = ("acir::generated::impl_" + helperRoleSpelling(callee->role) + + "_" + digest) + .str(); + callees.push_back(std::move(callee)); + } + auto typeSpelling = [](mlir::Type type) { + std::string storage; + llvm::raw_string_ostream stream(storage); + stream << type; + return storage; + }; + llvm::SmallVector liveTypes; + for (const PendingProcess &item : pending) + for (const auto &slot : item.control->liveSlots) + if (!llvm::is_contained(liveTypes, slot->type)) + liveTypes.push_back(slot->type); + std::vector> valueTypes; + for (mlir::Type type : liveTypes) { + uint64_t width = + type.isIndex() ? 64 : mlir::cast(type).getWidth(); + std::string spelling = typeSpelling(type); + auto storage = std::make_shared(); + storage->widthBits = width; + storage->encoding = spelling; + auto payload = std::make_shared(); + payload->kind = ProcessValueTypeKind::Value; + payload->value = ProcessStorageValuePayload(storage); + auto value = std::make_shared(); + value->id = ProcessValueTypeId(0); + value->kind = ProcessValueTypeKind::Value; + value->acirType = type; + value->payload = ProcessValueTypePayload(payload); + ProcessValueTypePlan plan(value); + auto canonical = canonicalValueTypeSpecialization(plan); + if (!canonical) { + llvm::consumeError(canonical.takeError()); + return mlir::failure(); + } + value->specializationBytes = std::move(*canonical); + value->fingerprint = + bindings::sha256Fingerprint(value->specializationBytes); + llvm::StringRef digest = llvm::StringRef(value->fingerprint).drop_front(7); + value->symbol = ("@acir_value_" + digest).str(); + value->cpp = ("acir::generated::value_" + digest).str(); + valueTypes.push_back(std::move(value)); + } + llvm::sort(valueTypes, [](const auto &lhs, const auto &rhs) { + return lhs->specializationBytes < rhs->specializationBytes; + }); + for (auto [index, type] : llvm::enumerate(valueTypes)) + type->id = ProcessValueTypeId(index); + + auto addScalarCallee = [&](const auto &type, + ProcessHelperRole role) -> llvm::Error { + std::string scalarKey = "mlir:" + typeSpelling(type->acirType); + std::string valueKey = + "storage:value:" + + llvm::StringRef(type->fingerprint).drop_front(7).str(); + auto payload = std::make_shared(); + payload->role = role; + if (role == ProcessHelperRole::ScalarWrap) { + auto arm = std::make_shared(); + arm->direction = ProcessWrapperDirection::Wrap; + arm->scalar = scalarKey; + arm->valueType = valueKey; + payload->scalarWrap = ProcessScalarWrapPayload(arm); + } else { + auto arm = std::make_shared(); + arm->direction = ProcessWrapperDirection::Unwrap; + arm->scalar = scalarKey; + arm->valueType = valueKey; + payload->scalarUnwrap = ProcessScalarUnwrapPayload(arm); + } + auto callee = std::make_shared(); + callee->effect = ProcessEffectKind::Pure; + callee->role = role; + callee->payload = ProcessGeneratedCalleePayload(payload); + if (role == ProcessHelperRole::ScalarWrap) { + callee->inputTypeKeyStorage = {scalarKey}; + callee->resultTypeKeyStorage = {valueKey}; + } else { + callee->inputTypeKeyStorage = {valueKey}; + callee->resultTypeKeyStorage = {scalarKey}; + } + callee->inputTypeKeys = {callee->inputTypeKeyStorage.front()}; + callee->resultTypeKeys = {callee->resultTypeKeyStorage.front()}; + ProcessGeneratedCalleePlan value(callee); + auto canonical = canonicalGeneratedCalleeSpecialization(value); + if (!canonical) + return canonical.takeError(); + callee->specializationBytes = std::move(*canonical); + callee->fingerprint = + bindings::sha256Fingerprint(callee->specializationBytes); + llvm::StringRef digest = llvm::StringRef(callee->fingerprint).drop_front(7); + callee->symbol = + ("@acir_impl_" + helperRoleSpelling(role) + "_" + digest).str(); + callee->cpp = + ("acir::generated::impl_" + helperRoleSpelling(role) + "_" + digest) + .str(); + callees.push_back(std::move(callee)); + return llvm::Error::success(); + }; + for (const auto &type : valueTypes) { + if (llvm::Error error = + addScalarCallee(type, ProcessHelperRole::ScalarWrap)) { + llvm::consumeError(std::move(error)); + return mlir::failure(); + } + if (llvm::Error error = + addScalarCallee(type, ProcessHelperRole::ScalarUnwrap)) { + llvm::consumeError(std::move(error)); + return mlir::failure(); + } + } + + llvm::sort(callees, [](const auto &lhs, const auto &rhs) { + return lhs->specializationBytes < rhs->specializationBytes; + }); + std::array wakeCalleeIds = { + ProcessCalleeId(0), ProcessCalleeId(0), ProcessCalleeId(0), + ProcessCalleeId(0)}; + for (auto [index, callee] : llvm::enumerate(callees)) { + callee->id = ProcessCalleeId(index); + if (callee->role >= ProcessHelperRole::WakeCondition && + callee->role <= ProcessHelperRole::WakeNextDelta) { + unsigned kindIndex = + static_cast(callee->role) - + static_cast(ProcessHelperRole::WakeCondition); + wakeCalleeIds[kindIndex] = ProcessCalleeId(index); + } + } + + auto set = std::make_shared(); + for (auto &callee : callees) + set->callees.push_back(ProcessGeneratedCalleePlan(callee)); + for (auto &type : valueTypes) + set->valueTypes.push_back(ProcessValueTypePlan(type)); + for (PendingProcess &item : pending) { + auto process = std::make_shared(); + process->definitionKey = item.definitionKey; + process->process = item.process; + process->entryPc = ProcessPcId(0); + for (auto &pc : item.control->pcs) + process->pcs.push_back(ProcessPcPlan(pc)); + for (auto &block : item.control->blocks) + process->blocks.push_back(ProcessBlockPlan(block)); + for (auto &wake : item.control->wakes) { + wake->callee = wakeCalleeIds[static_cast(wake->kind)]; + process->wakes.push_back(ProcessWakePlan(wake)); + } + for (auto &transition : item.control->transitions) + process->transitions.push_back(ProcessTransitionPlan(transition)); + for (auto &slot : item.control->liveSlots) { + auto found = llvm::find_if(valueTypes, [&](const auto &type) { + return type->acirType == slot->type; + }); + if (found == valueTypes.end()) + return mlir::failure(); + slot->storageType = (*found)->id; + std::string scalarKey = "mlir:" + typeSpelling(slot->type); + std::string valueKey = + "storage:value:" + + llvm::StringRef((*found)->fingerprint).drop_front(7).str(); + auto wrap = llvm::find_if(callees, [&](const auto &callee) { + return callee->role == ProcessHelperRole::ScalarWrap && + callee->inputTypeKeys.front() == scalarKey && + callee->resultTypeKeys.front() == valueKey; + }); + auto unwrap = llvm::find_if(callees, [&](const auto &callee) { + return callee->role == ProcessHelperRole::ScalarUnwrap && + callee->inputTypeKeys.front() == valueKey && + callee->resultTypeKeys.front() == scalarKey; + }); + if (wrap == callees.end() || unwrap == callees.end()) + return mlir::failure(); + slot->wrapCallee = (*wrap)->id; + slot->unwrapCallee = (*unwrap)->id; + process->liveSlots.push_back(ProcessLiveSlotPlan(slot)); + } + for (auto &block : item.control->blocks) { + for (const ProcessActionPlan &action : block->actions) { + if (action.kind() != ProcessActionKind::ScalarWrap && + action.kind() != ProcessActionKind::ScalarUnwrap) + continue; + auto mutableAction = + std::const_pointer_cast(action.impl_); + ProcessLiveSlotId slot = action.occurrence().syntheticWrapper().slot(); + if (slot.value() >= item.control->liveSlots.size()) + return mlir::failure(); + mutableAction->callee = + action.kind() == ProcessActionKind::ScalarWrap + ? item.control->liveSlots[slot.value()]->wrapCallee + : item.control->liveSlots[slot.value()]->unwrapCallee; + } + } + for (auto [index, operand] : llvm::enumerate(item.process->getOperands())) { + auto capture = std::make_shared(); + capture->id = ProcessCaptureId(index); + capture->name = llvm::formatv("capture{0:D8}", index).str(); + capture->operand = operand; + capture->entryArgument = + item.process.getBody().front().getArgument(index); + capture->type = operand.getType(); + capture->operandPath = + item.definitionKey + "/capture/operand/" + std::to_string(index); + capture->argumentPath = + item.definitionKey + "/capture/argument/" + std::to_string(index); + process->captures.push_back(ProcessCapturePlan(capture)); + } + process->pcBitWidth = 1; + for (uint32_t largest = static_cast(process->pcs.size() - 1); + largest >>= 1;) + ++process->pcBitWidth; + process->fairnessWork = item.control->fairnessWork; + set->processes.push_back(ProcessStatePlan(process)); + } + ProcessStatePlanSet result(set); + if (mlir::failed(verifyProcessStatePlan(result, limits))) + return mlir::failure(); + return result; +} + +mlir::FailureOr +planProcessState(mlir::ModuleOp module, const ProcessStateLimits &limits) { + return detail::PlanSetBuilder::buildProduction(module, limits); +} + +mlir::FailureOr +detail::PlanSetBuilder::buildYieldOnly(mlir::ModuleOp module) { + return buildFrozenFixture(module, /*requireYieldOnly=*/true); +} + +mlir::FailureOr +detail::PlanSetBuilder::buildFrozenFixture(mlir::ModuleOp module, + bool requireYieldOnly) { + auto frozenEpoch = module->getAttrOfType("ac.freeze_epoch"); + if (!frozenEpoch || frozenEpoch.getValue() != "0.3") { + module.emitError(requireYieldOnly + ? "yield-only process-state fixture requires a frozen " + "model" + : "loop-action process-state fixture requires a " + "frozen model"); + return mlir::failure(); + } + + struct ProcessFixture { + std::string definitionKey; + ac::ProcessOp process; + ac::YieldSimOp yield; + }; + llvm::SmallVector fixtures; + module.walk([&](ac::ProcessOp process) { + auto owner = process->getParentOfType(); + auto moduleName = owner ? mlir::SymbolTable::getSymbolName(owner) : nullptr; + auto processName = mlir::SymbolTable::getSymbolName(process); + ac::YieldSimOp yield; + unsigned yieldCount = 0; + process.walk([&](ac::YieldSimOp candidate) { + yield = candidate; + ++yieldCount; + }); + bool validBody = process.getBody().hasOneBlock() && + (!requireYieldOnly || + llvm::hasSingleElement(process.getBody().front())); + if (!owner || !moduleName || !processName || yieldCount != 1 || + !validBody) { + fixtures.push_back({{}, process, {}}); + return; + } + fixtures.push_back( + {("@" + moduleName.str() + "::@" + processName.str()), process, yield}); + }); + if (fixtures.empty()) { + module.emitError(requireYieldOnly + ? "yield-only process-state fixture requires at least " + "one ac.process" + : "loop-action process-state fixture requires exactly " + "one ac.process"); + return mlir::failure(); + } + if (llvm::any_of(fixtures, [](const ProcessFixture &fixture) { + return fixture.definitionKey.empty() || !fixture.yield; + })) { + module.emitError( + requireYieldOnly + ? "yield-only process-state fixture requires exactly " + "one ac.yield_sim per process and no other operations" + : "loop-action process-state fixture requires exactly " + "one ac.yield_sim per process"); + return mlir::failure(); + } + llvm::sort(fixtures, + [](const ProcessFixture &lhs, const ProcessFixture &rhs) { + return lhs.definitionKey < rhs.definitionKey; + }); + for (auto [index, fixture] : llvm::enumerate(fixtures)) + if (index && fixtures[index - 1].definitionKey == fixture.definitionKey) { + module.emitError(requireYieldOnly + ? "yield-only process-state fixture has duplicate " + "definition key" + : "loop-action process-state fixture has duplicate " + "definition key"); + return mlir::failure(); + } + + auto wakePayloadImpl = std::make_shared(); + wakePayloadImpl->wakeKind = ProcessWakeKind::NextDelta; + wakePayloadImpl->wakeType = "@acir_wake_next_delta"; + ProcessWakeNextDeltaPayload wakePayload(wakePayloadImpl); + auto payloadImpl = std::make_shared(); + payloadImpl->role = ProcessHelperRole::WakeNextDelta; + payloadImpl->wakeNextDelta = wakePayload; + ProcessGeneratedCalleePayload payload(payloadImpl); + + auto calleeImpl = std::make_shared(); + calleeImpl->id = ProcessCalleeId(0); + calleeImpl->symbol = + "@acir_impl_wake_next_delta_" + kWakeNextDeltaDigest.str(); + calleeImpl->cpp = + "acir::generated::impl_wake_next_delta_" + kWakeNextDeltaDigest.str(); + calleeImpl->fingerprint = "sha256:" + kWakeNextDeltaDigest.str(); + calleeImpl->effect = ProcessEffectKind::Stateful; + calleeImpl->resultTypeKeyStorage = {"@acir_wake_next_delta"}; + for (const std::string &key : calleeImpl->resultTypeKeyStorage) + calleeImpl->resultTypeKeys.push_back(key); + calleeImpl->role = ProcessHelperRole::WakeNextDelta; + calleeImpl->payload = payload; + calleeImpl->specializationBytes = kWakeNextDeltaSpecialization.str(); + llvm::json::Object descriptor; + descriptor["cpp"] = calleeImpl->cpp; + descriptor["effect"] = "stateful"; + descriptor["fingerprint"] = calleeImpl->fingerprint; + descriptor["inputs"] = llvm::json::Array(); + descriptor["kind"] = "implementation"; + descriptor["ordinal"] = 0; + llvm::json::Object descriptorPayload; + descriptorPayload["wake_kind"] = "next_delta"; + descriptorPayload["wake_type"] = "@acir_wake_next_delta"; + descriptor["payload"] = std::move(descriptorPayload); + llvm::json::Array results; + results.push_back("@acir_wake_next_delta"); + descriptor["results"] = std::move(results); + descriptor["role"] = "wake_next_delta"; + descriptor["source_paths"] = llvm::json::Array(); + descriptor["symbol"] = calleeImpl->symbol; + if (auto canonical = + bindings::canonicalizeJson(llvm::json::Value(std::move(descriptor)))) + calleeImpl->descriptorBytes = std::move(*canonical); + ProcessGeneratedCalleePlan callee(calleeImpl); + + auto setImpl = std::make_shared(); + setImpl->callees.push_back(callee); + for (ProcessFixture &fixture : fixtures) { + uint32_t yieldOrdinal = 0; + for (mlir::Operation &operation : fixture.process.getBody().front()) { + if (&operation == fixture.yield.getOperation()) + break; + ++yieldOrdinal; + } + std::string operationPath = + fixture.definitionKey + "/r0/b0/o" + std::to_string(yieldOrdinal); + auto originalImpl = std::make_shared(); + originalImpl->operation = fixture.yield; + originalImpl->operationPath = operationPath; + ProcessOriginalOccurrence original(originalImpl); + auto occurrenceImpl = std::make_shared(); + occurrenceImpl->kind = ProcessOccurrenceKind::Original; + occurrenceImpl->original = original; + ProcessOccurrenceId occurrence(occurrenceImpl); + + auto wakeImpl = std::make_shared(); + wakeImpl->id = ProcessWakeId(0); + wakeImpl->kind = ProcessWakeKind::NextDelta; + wakeImpl->operation = fixture.yield; + wakeImpl->callee = ProcessCalleeId(0); + wakeImpl->typeKey = "@acir_wake_next_delta"; + wakeImpl->operationPath = operationPath; + wakeImpl->target = ""; + wakeImpl->occurrence = occurrence; + ProcessWakePlan wake(wakeImpl); + + auto transitionImpl = std::make_shared(); + transitionImpl->id = ProcessTransitionId(0); + transitionImpl->sourcePc = ProcessPcId(0); + transitionImpl->targetPc = ProcessPcId(0); + transitionImpl->wake = ProcessWakeId(0); + ProcessTransitionPlan transition(transitionImpl); + + auto edgeImpl = std::make_shared(); + edgeImpl->kind = ProcessControlEdgeKind::Suspend; + edgeImpl->transition = ProcessTransitionId(0); + ProcessControlEdgePlan edge(edgeImpl); + + auto blockImpl = std::make_shared(); + blockImpl->id = ProcessBlockId(0); + blockImpl->pc = ProcessPcId(0); + blockImpl->originRegion = &fixture.process.getBody(); + blockImpl->originBlock = &fixture.process.getBody().front(); + blockImpl->path = fixture.definitionKey + "/plan/pc/entry/b00000000"; + blockImpl->edge = edge; + blockImpl->cost = 2; + ProcessBlockPlan block(blockImpl); + + auto pcImpl = std::make_shared(); + pcImpl->id = ProcessPcId(0); + pcImpl->name = "entry"; + pcImpl->entryPath = blockImpl->path; + pcImpl->blocks.push_back(ProcessBlockId(0)); + ProcessPcPlan pc(pcImpl); + + auto processImpl = std::make_shared(); + processImpl->definitionKey = fixture.definitionKey; + processImpl->process = fixture.process; + processImpl->entryPc = ProcessPcId(0); + processImpl->pcs.push_back(pc); + processImpl->blocks.push_back(block); + processImpl->wakes.push_back(wake); + processImpl->transitions.push_back(transition); + processImpl->pcBitWidth = 1; + processImpl->fairnessWork = 2; + setImpl->processes.push_back(ProcessStatePlan(processImpl)); + } + return ProcessStatePlanSet(setImpl); +} + +mlir::FailureOr +detail::PlanSetBuilder::buildLoopActionFixture(mlir::ModuleOp module) { + auto built = buildFrozenFixture(module, /*requireYieldOnly=*/false); + if (mlir::failed(built)) + return mlir::failure(); + if (built->impl_->processes.size() != 1) { + module.emitError( + "loop-action process-state fixture requires exactly one ac.process"); + return mlir::failure(); + } + + auto process = std::make_shared( + *built->impl_->processes.front().impl_); + llvm::SmallVector loops; + process->process.walk([&](mlir::scf::ForOp loop) { loops.push_back(loop); }); + if (loops.size() != 2) { + module.emitError( + "loop-action process-state fixture requires exactly two scf.for " + "operations"); + return mlir::failure(); + } + + uint32_t loopOrdinal = 0; + for (mlir::Operation &operation : process->process.getBody().front()) { + if (&operation == loops.front().getOperation()) + break; + ++loopOrdinal; + } + auto original = std::make_shared(); + original->operation = loops.front(); + original->operationPath = + process->definitionKey + "/r0/b0/o" + std::to_string(loopOrdinal); + auto anchor = std::make_shared(); + anchor->kind = ProcessOccurrenceKind::Original; + anchor->original = ProcessOriginalOccurrence(original); + ProcessOccurrenceId loopAnchor(anchor); + + auto makeLoopOccurrence = [&](ProcessLoopPhase phase) { + auto loop = std::make_shared(); + loop->anchor = loopAnchor; + loop->phase = phase; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::SyntheticLoop; + occurrence->syntheticLoop = ProcessSyntheticLoopOccurrence(loop); + return ProcessOccurrenceId(occurrence); + }; + auto makeConstant = [](mlir::Type type, llvm::StringRef literal) { + auto constant = std::make_shared(); + constant->value = literal.str(); + auto value = std::make_shared(); + value->kind = ProcessPlannedValueKind::Constant; + value->type = type; + value->constant = ProcessConstantPlannedValue(constant); + return ProcessPlannedValue(value); + }; + + mlir::MLIRContext *context = module.getContext(); + mlir::Type indexType = mlir::IndexType::get(context); + ProcessPlannedValue lower = makeConstant(indexType, "0"); + ProcessPlannedValue upper = makeConstant(indexType, "4"); + ProcessPlannedValue step = makeConstant(indexType, "1"); + ProcessPlannedValue condition = + makeConstant(mlir::IntegerType::get(context, 1), "true"); + + auto predicate = std::make_shared(); + predicate->name = "predicate"; + predicate->value = "2 : i64"; + auto comparison = std::make_shared(); + comparison->name = "arith.cmpi"; + comparison->attributes.push_back(ProcessScalarAttribute(predicate)); + comparison->properties = "{}"; + auto increment = std::make_shared(); + increment->name = "arith.addi"; + increment->properties = "{}"; + + auto makeAction = + [&](uint32_t id, ProcessActionKind kind, ProcessEmissionClass emission, + ProcessLoopPhase phase, std::vector operands, + std::vector results, + std::shared_ptr scalarOperation) { + auto action = std::make_shared(); + action->id = id; + action->kind = kind; + action->emission = emission; + action->occurrence = makeLoopOccurrence(phase); + action->sourceOperation = loops.front(); + action->operands = std::move(operands); + action->results = std::move(results); + action->cost = emission == ProcessEmissionClass::ForwardOnly ? 0 : 1; + for (const ProcessPlannedValue &result : action->results) + action->resultTypes.push_back(result.type()); + if (scalarOperation) + action->scalarOp = + ProcessScalarOperationPlan(std::move(scalarOperation)); + return ProcessActionPlan(action); + }; + + auto block = + std::make_shared(*process->blocks.front().impl_); + block->actions = { + makeAction(0, ProcessActionKind::ForInitialize, + ProcessEmissionClass::ForwardOnly, + ProcessLoopPhase::Initialize, {lower}, {lower}, nullptr), + makeAction(1, ProcessActionKind::ForCondition, + ProcessEmissionClass::CopyScalar, ProcessLoopPhase::Condition, + {lower, upper}, {condition}, comparison), + makeAction(2, ProcessActionKind::ForIncrement, + ProcessEmissionClass::CopyScalar, ProcessLoopPhase::Increment, + {lower, step}, {step}, increment), + }; + block->cost = 4; + process->blocks.front() = ProcessBlockPlan(block); + process->fairnessWork = 4; + + auto impl = std::make_shared(*built->impl_); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithCorruption( + const ProcessStatePlanSet &plans, + ProcessStatePlanCorruptionForTest corruption) { + auto impl = std::make_shared(*plans.impl_); + auto cloneProcess = [&]() { + auto value = std::make_shared( + *impl->processes.front().impl_); + impl->processes.front() = ProcessStatePlan(value); + return value; + }; + auto cloneBlock = [&]() { + auto process = cloneProcess(); + auto value = std::make_shared( + *process->blocks.front().impl_); + process->blocks.front() = ProcessBlockPlan(value); + return value; + }; + auto cloneWake = [&]() { + auto process = cloneProcess(); + auto value = + std::make_shared(*process->wakes.front().impl_); + process->wakes.front() = ProcessWakePlan(value); + return value; + }; + auto cloneTransition = [&]() { + auto process = cloneProcess(); + auto value = std::make_shared( + *process->transitions.front().impl_); + process->transitions.front() = ProcessTransitionPlan(value); + return value; + }; + auto cloneCallee = [&]() { + auto value = std::make_shared( + *impl->callees.front().impl_); + impl->callees.front() = ProcessGeneratedCalleePlan(value); + return value; + }; + switch (corruption) { + case ProcessStatePlanCorruptionForTest::DuplicateOrdinal: + impl->callees.push_back(impl->callees.front()); + break; + case ProcessStatePlanCorruptionForTest::NonDenseOrdinal: + cloneCallee()->id = ProcessCalleeId(1); + break; + case ProcessStatePlanCorruptionForTest::DanglingReference: + cloneTransition()->targetPc = ProcessPcId(99); + break; + case ProcessStatePlanCorruptionForTest::DuplicateIdentity: { + auto duplicate = std::make_shared( + *impl->callees.front().impl_); + duplicate->id = ProcessCalleeId(1); + impl->callees.push_back(ProcessGeneratedCalleePlan(duplicate)); + break; + } + case ProcessStatePlanCorruptionForTest::UnsortedCanonicalOrder: { + assert(impl->processes.size() > 1 && + "permutation fixture must contain multiple processes"); + std::swap(impl->processes[0], impl->processes[1]); + break; + } + case ProcessStatePlanCorruptionForTest::CostMismatch: + ++cloneBlock()->cost; + break; + case ProcessStatePlanCorruptionForTest::DefinitionKeyMismatch: + cloneProcess()->definitionKey = "workload"; + break; + case ProcessStatePlanCorruptionForTest::CalleeSpecializationMismatch: + cloneCallee()->specializationBytes.push_back(' '); + break; + case ProcessStatePlanCorruptionForTest::ValueTypeSpecializationMismatch: { + ProcessStatePlanSet seeded = cloneWithUnpairedLiveSlotCallee(plans); + impl = std::make_shared(*seeded.impl_); + auto process = std::make_shared( + *impl->processes.front().impl_); + process->liveSlots.clear(); + impl->processes.front() = ProcessStatePlan(process); + auto type = std::make_shared( + *impl->valueTypes.front().impl_); + type->fingerprint = + "sha256:" + "0000000000000000000000000000000000000000000000000000000000000000"; + impl->valueTypes.front() = ProcessValueTypePlan(type); + break; + } + case ProcessStatePlanCorruptionForTest::EffectMismatch: + cloneCallee()->effect = ProcessEffectKind::Pure; + break; + case ProcessStatePlanCorruptionForTest::IdKindMismatch: { + auto process = cloneProcess(); + auto pc = + std::make_shared(*process->pcs.front().impl_); + pc->blocks.front() = ProcessBlockId(1); + process->pcs.front() = ProcessPcPlan(pc); + break; + } + case ProcessStatePlanCorruptionForTest::WrongTypeKey: + cloneWake()->typeKey = "mlir:!acsim.wake"; + break; + case ProcessStatePlanCorruptionForTest::InvalidFramePhase: { + auto frameImpl = std::make_shared(); + frameImpl->kind = ProcessFrameKind::ScfIf; + frameImpl->phase = ProcessFramePhase::Entry; + cloneBlock()->frames.push_back(ProcessControlFramePlan(frameImpl)); + break; + } + case ProcessStatePlanCorruptionForTest::InvalidEdgeBinding: { + auto edgeImpl = std::make_shared(); + edgeImpl->kind = ProcessControlEdgeKind::Branch; + edgeImpl->trueBlock = ProcessBlockId(0); + edgeImpl->falseBlock = ProcessBlockId(0); + cloneBlock()->edge = ProcessControlEdgePlan(edgeImpl); + break; + } + case ProcessStatePlanCorruptionForTest::InvalidWakeCallee: + cloneWake()->callee = ProcessCalleeId(99); + break; + } + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithMissingWakeCallee( + const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto wake = + std::make_shared(*process->wakes.front().impl_); + wake->callee.reset(); + process->wakes.front() = ProcessWakePlan(wake); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithDanglingSuspendTransition( + const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto block = + std::make_shared(*process->blocks.front().impl_); + auto edge = + std::make_shared(*block->edge->impl_); + edge->transition = ProcessTransitionId(99); + block->edge = ProcessControlEdgePlan(edge); + process->blocks.front() = ProcessBlockPlan(block); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithUnpairedLiveSlotCallee( + const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + mlir::MLIRContext *context = plans.processes().front().process().getContext(); + mlir::Type i32 = mlir::IntegerType::get(context, 32); + + auto storageImpl = std::make_shared(); + storageImpl->widthBits = 32; + storageImpl->encoding = "i32"; + ProcessStorageValuePayload storage(storageImpl); + auto payloadImpl = std::make_shared(); + payloadImpl->kind = ProcessValueTypeKind::Value; + payloadImpl->value = storage; + ProcessValueTypePayload payload(payloadImpl); + + llvm::json::Object specialization; + specialization["acir_type"] = "i32"; + specialization["contract_epoch"] = "0.3"; + specialization["kind"] = "value"; + llvm::json::Object payloadObject; + payloadObject["encoding"] = "i32"; + payloadObject["members"] = llvm::json::Array(); + payloadObject["width_bits"] = 32; + specialization["payload"] = std::move(payloadObject); + specialization["schema"] = "acir-generated-value-type-0.1"; + auto canonical = + bindings::canonicalizeJson(llvm::json::Value(std::move(specialization))); + assert(canonical && "literal value-type specialization must canonicalize"); + std::string fingerprint = bindings::sha256Fingerprint(*canonical); + llvm::StringRef digest = llvm::StringRef(fingerprint).drop_front(7); + + auto typeImpl = std::make_shared(); + typeImpl->id = ProcessValueTypeId(0); + typeImpl->symbol = ("@acir_value_" + digest).str(); + typeImpl->cpp = ("acir::generated::value_" + digest).str(); + typeImpl->kind = ProcessValueTypeKind::Value; + typeImpl->fingerprint = fingerprint; + typeImpl->acirType = i32; + typeImpl->payload = payload; + typeImpl->specializationBytes = std::move(*canonical); + impl->valueTypes.push_back(ProcessValueTypePlan(typeImpl)); + + auto process = + std::make_shared(*impl->processes.front().impl_); + auto slotImpl = std::make_shared(); + slotImpl->id = ProcessLiveSlotId(0); + slotImpl->name = "live00000000"; + slotImpl->type = i32; + slotImpl->storageType = ProcessValueTypeId(0); + slotImpl->wrapCallee = ProcessCalleeId(0); + process->liveSlots.push_back(ProcessLiveSlotPlan(slotImpl)); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithMissingValueTypePayload( + const ProcessStatePlanSet &plans) { + ProcessStatePlanSet result = cloneWithUnpairedLiveSlotCallee(plans); + auto impl = std::make_shared(*result.impl_); + auto type = std::make_shared( + *impl->valueTypes.front().impl_); + type->payload.reset(); + impl->valueTypes.front() = ProcessValueTypePlan(type); + auto process = + std::make_shared(*impl->processes.front().impl_); + process->liveSlots.clear(); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithNullEdgeStorage( + const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto block = + std::make_shared(*process->blocks.front().impl_); + block->edge = ProcessControlEdgePlan( + std::shared_ptr()); + process->blocks.front() = ProcessBlockPlan(block); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithInactiveEdgeField( + const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto block = + std::make_shared(*process->blocks.front().impl_); + auto edge = + std::make_shared(*block->edge->impl_); + edge->targetBlock = ProcessBlockId(0); + block->edge = ProcessControlEdgePlan(edge); + process->blocks.front() = ProcessBlockPlan(block); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithDoubleValueTypePayload( + const ProcessStatePlanSet &plans) { + ProcessStatePlanSet seeded = cloneWithUnpairedLiveSlotCallee(plans); + auto impl = std::make_shared(*seeded.impl_); + auto type = std::make_shared( + *impl->valueTypes.front().impl_); + auto payload = + std::make_shared(*type->payload->impl_); + auto packet = std::make_shared(); + packet->widthBits = 32; + packet->bytes = 4; + packet->encoding = "array<4xi8>"; + payload->packet = ProcessStoragePacketPayload(packet); + type->payload = ProcessValueTypePayload(payload); + impl->valueTypes.front() = ProcessValueTypePlan(type); + auto process = + std::make_shared(*impl->processes.front().impl_); + process->liveSlots.clear(); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithMissingOriginalActionSource( + const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto block = + std::make_shared(*process->blocks.front().impl_); + auto action = std::make_shared(); + action->id = 0; + action->kind = ProcessActionKind::Original; + action->emission = ProcessEmissionClass::ForwardOnly; + action->occurrence = process->wakes.front().impl_->occurrence; + action->sourceOperation = process->wakes.front().impl_->operation; + action->cost = 0; + action->sourceOperation = nullptr; + block->actions.push_back(ProcessActionPlan(action)); + process->blocks.front() = ProcessBlockPlan(block); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithUnexpectedConstantActionSource( + const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto block = + std::make_shared(*process->blocks.front().impl_); + auto synthetic = std::make_shared(); + synthetic->anchor = *process->wakes.front().impl_->occurrence; + synthetic->constant = 7; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::SyntheticConstant; + occurrence->syntheticConstant = ProcessSyntheticConstantOccurrence(synthetic); + auto action = std::make_shared(); + action->id = 0; + action->kind = ProcessActionKind::Constant; + action->emission = ProcessEmissionClass::ForwardOnly; + action->occurrence = ProcessOccurrenceId(occurrence); + action->cost = 0; + action->sourceOperation = process->wakes.front().impl_->operation; + block->actions.push_back(ProcessActionPlan(action)); + process->blocks.front() = ProcessBlockPlan(block); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithNonLoopForActionSource( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/1, LoopActionMutationForTest::NonLoopSource); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithForInitializeWrongOwningLoopSource( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/0, + LoopActionMutationForTest::WrongOwningLoopSource); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithForConditionWrongOwningLoopSource( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/1, + LoopActionMutationForTest::WrongOwningLoopSource); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithForIncrementWrongOwningLoopSource( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/2, + LoopActionMutationForTest::WrongOwningLoopSource); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithForConditionWrongResultType( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/1, LoopActionMutationForTest::WrongResultType); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithForIncrementWrongResultType( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/2, LoopActionMutationForTest::WrongResultType); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithForConditionInactiveCallee( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/1, LoopActionMutationForTest::InactiveCallee); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithForInitializeInactiveScalar( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/0, LoopActionMutationForTest::InactiveScalar); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithForConditionWrongEmission( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/1, LoopActionMutationForTest::WrongEmission); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithForConditionWrongScalarOp( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/1, LoopActionMutationForTest::WrongScalarName); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithForIncrementWrongEmission( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/2, LoopActionMutationForTest::WrongEmission); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithForIncrementWrongScalarOp( + const ProcessStatePlanSet &plans) { + return cloneLoopActionWithMutationForTest( + plans, /*actionIndex=*/2, LoopActionMutationForTest::WrongScalarName); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneLoopActionWithMutationForTest( + const ProcessStatePlanSet &plans, uint32_t actionIndex, + LoopActionMutationForTest mutation) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto block = + std::make_shared(*process->blocks.front().impl_); + assert(actionIndex < block->actions.size() && + "loop-action corruption requires a valid fixture action"); + auto action = std::make_shared( + *block->actions[actionIndex].impl_); + auto cloneScalarOperation = [&]() { + assert(action->scalarOp && action->scalarOp->impl_ && + "loop-action scalar corruption requires scalar storage"); + return std::make_shared( + *action->scalarOp->impl_); + }; + + switch (mutation) { + case LoopActionMutationForTest::WrongOwningLoopSource: { + llvm::SmallVector loops; + process->process.walk( + [&](mlir::scf::ForOp loop) { loops.push_back(loop); }); + assert(loops.size() == 2 && + "loop-action source corruption requires two frozen loops"); + action->sourceOperation = loops.back(); + break; + } + case LoopActionMutationForTest::NonLoopSource: + action->sourceOperation = process->wakes.front().impl_->operation; + break; + case LoopActionMutationForTest::WrongResultType: { + assert(action->results.size() == 1 && + "loop-action result corruption requires one result"); + auto result = std::make_shared( + *action->results.front().impl_); + result->type = + action->kind == ProcessActionKind::ForCondition + ? mlir::Type(mlir::IndexType::get(process->process.getContext())) + : mlir::Type( + mlir::IntegerType::get(process->process.getContext(), 1)); + action->results.front() = ProcessPlannedValue(result); + break; + } + case LoopActionMutationForTest::InactiveCallee: + action->callee = ProcessCalleeId(0); + break; + case LoopActionMutationForTest::InactiveScalar: + assert(block->actions.size() > 1 && block->actions[1].impl_->scalarOp && + "loop-action scalar corruption requires a condition action"); + action->scalarOp = block->actions[1].impl_->scalarOp; + break; + case LoopActionMutationForTest::WrongEmission: + action->emission = ProcessEmissionClass::Inline; + break; + case LoopActionMutationForTest::WrongScalarName: { + auto scalar = cloneScalarOperation(); + scalar->name = action->kind == ProcessActionKind::ForCondition + ? "arith.addi" + : "arith.cmpi"; + action->scalarOp = ProcessScalarOperationPlan(scalar); + break; + } + case LoopActionMutationForTest::MissingScalar: + action->scalarOp.reset(); + break; + case LoopActionMutationForTest::WrongScalarProperties: { + auto scalar = cloneScalarOperation(); + scalar->properties = "{unexpected = true}"; + action->scalarOp = ProcessScalarOperationPlan(scalar); + break; + } + case LoopActionMutationForTest::WrongScalarAttributeCount: { + auto scalar = cloneScalarOperation(); + if (action->kind == ProcessActionKind::ForCondition) { + scalar->attributes.clear(); + } else { + auto unexpected = std::make_shared(); + unexpected->name = "unexpected"; + unexpected->value = "true"; + scalar->attributes.push_back(ProcessScalarAttribute(unexpected)); + } + action->scalarOp = ProcessScalarOperationPlan(scalar); + break; + } + case LoopActionMutationForTest::WrongScalarAttributeName: + case LoopActionMutationForTest::WrongScalarAttributeValue: { + auto scalar = cloneScalarOperation(); + assert(scalar->attributes.size() == 1 && scalar->attributes.front().impl_ && + "loop-action attribute corruption requires one attribute"); + auto attribute = std::make_shared( + *scalar->attributes.front().impl_); + if (mutation == LoopActionMutationForTest::WrongScalarAttributeName) + attribute->name = "unexpected"; + else + attribute->value = "3 : i64"; + scalar->attributes.front() = ProcessScalarAttribute(attribute); + action->scalarOp = ProcessScalarOperationPlan(scalar); + break; + } + case LoopActionMutationForTest::WrongOperandCount: + assert(!action->operands.empty() && + "loop-action operand corruption requires an operand"); + action->operands.pop_back(); + break; + case LoopActionMutationForTest::WrongOperandType: { + assert(!action->operands.empty() && action->operands.front().impl_ && + "loop-action operand corruption requires an operand"); + auto operand = std::make_shared( + *action->operands.front().impl_); + operand->type = mlir::IntegerType::get(process->process.getContext(), 1); + action->operands.front() = ProcessPlannedValue(operand); + break; + } + case LoopActionMutationForTest::WrongResultCount: + assert(!action->results.empty() && + "loop-action result corruption requires a result"); + action->results.pop_back(); + break; + } + + block->actions[actionIndex] = ProcessActionPlan(action); + process->blocks.front() = ProcessBlockPlan(block); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithLongLocalChain( + const ProcessStatePlanSet &plans, uint32_t blockCount) { + assert(blockCount > 0 && "long-chain fixture requires at least one block"); + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + process->blocks.clear(); + process->wakes.clear(); + process->transitions.clear(); + auto pc = std::make_shared(*process->pcs.front().impl_); + pc->blocks.clear(); + for (uint32_t index = 0; index < blockCount; ++index) { + auto edge = std::make_shared(); + if (index + 1 == blockCount) { + edge->kind = ProcessControlEdgeKind::Terminate; + edge->status = ProcessTerminateStatus::Success; + } else { + edge->kind = ProcessControlEdgeKind::LocalContinue; + edge->targetBlock = ProcessBlockId(index + 1); + } + auto block = std::make_shared(); + block->id = ProcessBlockId(index); + block->pc = ProcessPcId(0); + block->path = process->definitionKey + "/plan/pc/entry/" + + llvm::formatv("b{0:D8}", index).str(); + block->edge = ProcessControlEdgePlan(edge); + block->cost = 1; + process->blocks.push_back(ProcessBlockPlan(block)); + pc->blocks.push_back(ProcessBlockId(index)); + } + pc->entryPath = process->blocks.front().impl_->path; + process->pcs.front() = ProcessPcPlan(pc); + process->fairnessWork = blockCount; + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet +detail::PlanSetBuilder::cloneWithLocalCycle(const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto block = + std::make_shared(*process->blocks.back().impl_); + auto edge = std::make_shared(); + edge->kind = ProcessControlEdgeKind::LocalContinue; + edge->targetBlock = ProcessBlockId(0); + block->edge = ProcessControlEdgePlan(edge); + process->blocks.back() = ProcessBlockPlan(block); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +ProcessStatePlanSet detail::PlanSetBuilder::cloneWithUnreachableBlock( + const ProcessStatePlanSet &plans) { + auto impl = std::make_shared(*plans.impl_); + auto process = + std::make_shared(*impl->processes.front().impl_); + auto block = + std::make_shared(*process->blocks.front().impl_); + block->id = ProcessBlockId(1); + block->path = process->definitionKey + "/plan/pc/entry/b00000001"; + auto edge = std::make_shared(); + edge->kind = ProcessControlEdgeKind::Terminate; + edge->status = ProcessTerminateStatus::Success; + block->edge = ProcessControlEdgePlan(edge); + block->cost = 1; + process->blocks.push_back(ProcessBlockPlan(block)); + auto pc = std::make_shared(*process->pcs.front().impl_); + pc->blocks.push_back(ProcessBlockId(1)); + process->pcs.front() = ProcessPcPlan(pc); + impl->processes.front() = ProcessStatePlan(process); + return ProcessStatePlanSet(impl); +} + +bool detail::PlanSetBuilder::exerciseCompleteApiFixture( + mlir::MLIRContext &context) { + bool valid = true; + auto expect = [&](bool condition) { valid &= condition; }; + mlir::Type i32 = mlir::IntegerType::get(&context, 32); + mlir::Block values; + mlir::Value operand = + values.addArgument(i32, mlir::UnknownLoc::get(&context)); + mlir::Value argument = + values.addArgument(i32, mlir::UnknownLoc::get(&context)); + + auto siteImpl = std::make_shared(); + siteImpl->operationPath = "@Top::@workload/r0/b0/o0"; + siteImpl->iterationVector = {3}; + ProcessCallSitePlan site(siteImpl); + expect(site.operation() == nullptr); + expect(site.operationPath() == "@Top::@workload/r0/b0/o0"); + expect(site.iterationVector() == llvm::ArrayRef({3})); + + auto originalImpl = std::make_shared(); + originalImpl->operationPath = "@Top::@workload/r0/b0/o1"; + originalImpl->callSites = {site}; + originalImpl->iterationVector = {5}; + ProcessOriginalOccurrence original(originalImpl); + auto occurrenceImpl = std::make_shared(); + occurrenceImpl->kind = ProcessOccurrenceKind::Original; + occurrenceImpl->original = original; + ProcessOccurrenceId anchor(occurrenceImpl); + expect(original.operation() == nullptr); + expect(original.operationPath() == "@Top::@workload/r0/b0/o1"); + expect(original.callSites().size() == 1); + expect(original.iterationVector() == llvm::ArrayRef({5})); + expect(anchor.kind() == ProcessOccurrenceKind::Original); + expect(anchor.original().operationPath() == original.operationPath()); + + auto loopImpl = std::make_shared(); + loopImpl->anchor = anchor; + loopImpl->phase = ProcessLoopPhase::Condition; + ProcessSyntheticLoopOccurrence loop(loopImpl); + auto loopOccurrenceImpl = std::make_shared(); + loopOccurrenceImpl->kind = ProcessOccurrenceKind::SyntheticLoop; + loopOccurrenceImpl->syntheticLoop = loop; + ProcessOccurrenceId loopOccurrence(loopOccurrenceImpl); + expect(loop.anchor().kind() == ProcessOccurrenceKind::Original); + expect(loop.phase() == ProcessLoopPhase::Condition); + expect(loopOccurrence.syntheticLoop().phase() == ProcessLoopPhase::Condition); + expect(loopOccurrence.kind() == ProcessOccurrenceKind::SyntheticLoop); + + auto wrapperImpl = + std::make_shared(); + wrapperImpl->anchor = anchor; + wrapperImpl->transition = ProcessTransitionId(2); + wrapperImpl->slot = ProcessLiveSlotId(3); + wrapperImpl->direction = ProcessWrapperDirection::Unwrap; + ProcessSyntheticWrapperOccurrence wrapper(wrapperImpl); + auto wrapperOccurrenceImpl = std::make_shared(); + wrapperOccurrenceImpl->kind = ProcessOccurrenceKind::SyntheticWrapper; + wrapperOccurrenceImpl->syntheticWrapper = wrapper; + ProcessOccurrenceId wrapperOccurrence(wrapperOccurrenceImpl); + expect(wrapper.anchor().kind() == ProcessOccurrenceKind::Original); + expect(wrapper.transition().value() == 2); + expect(wrapper.slot().value() == 3); + expect(wrapper.direction() == ProcessWrapperDirection::Unwrap); + expect(wrapperOccurrence.syntheticWrapper().slot().value() == 3); + expect(wrapperOccurrence.kind() == ProcessOccurrenceKind::SyntheticWrapper); + + auto constantOccurrenceImpl = + std::make_shared(); + constantOccurrenceImpl->anchor = anchor; + constantOccurrenceImpl->constant = 7; + ProcessSyntheticConstantOccurrence constantOccurrencePayload( + constantOccurrenceImpl); + auto constantOccurrenceUnionImpl = + std::make_shared(); + constantOccurrenceUnionImpl->kind = ProcessOccurrenceKind::SyntheticConstant; + constantOccurrenceUnionImpl->syntheticConstant = constantOccurrencePayload; + ProcessOccurrenceId constantOccurrence(constantOccurrenceUnionImpl); + expect(constantOccurrencePayload.anchor().kind() == + ProcessOccurrenceKind::Original); + expect(constantOccurrencePayload.constant() == 7); + expect(constantOccurrence.syntheticConstant().constant() == 7); + expect(constantOccurrence.kind() == ProcessOccurrenceKind::SyntheticConstant); + + auto coordinateImpl = std::make_shared(); + coordinateImpl->kind = ProcessValueCoordinateKind::BlockArgument; + coordinateImpl->ownerPath = "@Top::@workload/r0/b0"; + coordinateImpl->index = 4; + ProcessValueCoordinate coordinate(coordinateImpl); + expect(coordinate.kind() == ProcessValueCoordinateKind::BlockArgument); + expect(coordinate.ownerPath() == "@Top::@workload/r0/b0"); + expect(coordinate.index() == 4); + + auto originalValueImpl = + std::make_shared(); + originalValueImpl->value = operand; + originalValueImpl->occurrence = anchor; + originalValueImpl->coordinate = coordinate; + originalValueImpl->path = "@Top::@workload/r0/b0/a4"; + ProcessOriginalPlannedValue originalValue(originalValueImpl); + auto captureValueImpl = std::make_shared(); + captureValueImpl->capture = ProcessCaptureId(5); + ProcessCapturePlannedValue captureValue(captureValueImpl); + auto slotValueImpl = std::make_shared(); + slotValueImpl->slot = ProcessLiveSlotId(6); + ProcessLiveSlotPlannedValue slotValue(slotValueImpl); + auto syntheticValueImpl = + std::make_shared(); + syntheticValueImpl->occurrence = loopOccurrence; + syntheticValueImpl->coordinate = coordinate; + ProcessSyntheticPlannedValue syntheticValue(syntheticValueImpl); + auto constantValueImpl = + std::make_shared(); + constantValueImpl->value = "42"; + ProcessConstantPlannedValue constantValue(constantValueImpl); + + auto makePlanned = [&](ProcessPlannedValueKind kind) { + auto impl = std::make_shared(); + impl->kind = kind; + impl->type = i32; + switch (kind) { + case ProcessPlannedValueKind::Original: + impl->original = originalValue; + break; + case ProcessPlannedValueKind::Capture: + impl->capture = captureValue; + break; + case ProcessPlannedValueKind::LiveSlot: + impl->liveSlot = slotValue; + break; + case ProcessPlannedValueKind::Synthetic: + impl->synthetic = syntheticValue; + break; + case ProcessPlannedValueKind::Constant: + impl->constant = constantValue; + break; + } + return ProcessPlannedValue(impl); + }; + std::vector planned = { + makePlanned(ProcessPlannedValueKind::Original), + makePlanned(ProcessPlannedValueKind::Capture), + makePlanned(ProcessPlannedValueKind::LiveSlot), + makePlanned(ProcessPlannedValueKind::Synthetic), + makePlanned(ProcessPlannedValueKind::Constant)}; + expect(originalValue.value() == operand); + expect(originalValue.occurrence().kind() == ProcessOccurrenceKind::Original); + expect(originalValue.coordinate().index() == 4); + expect(originalValue.path() == "@Top::@workload/r0/b0/a4"); + expect(captureValue.capture().value() == 5); + expect(slotValue.slot().value() == 6); + expect(syntheticValue.occurrence().kind() == + ProcessOccurrenceKind::SyntheticLoop); + expect(syntheticValue.coordinate().index() == 4); + expect(constantValue.value() == "42"); + expect(planned[0].original().path() == originalValue.path()); + expect(planned[1].capture().capture().value() == 5); + expect(planned[2].liveSlot().slot().value() == 6); + expect(planned[3].synthetic().coordinate().index() == 4); + expect(planned[4].constant().value() == "42"); + for (auto [index, value] : llvm::enumerate(planned)) { + expect(static_cast(value.kind()) == index); + expect(value.type() == i32); + } + + auto scalarAttributeImpl = std::make_shared(); + scalarAttributeImpl->name = "predicate"; + scalarAttributeImpl->value = "slt"; + ProcessScalarAttribute scalarAttribute(scalarAttributeImpl); + auto scalarOpImpl = std::make_shared(); + scalarOpImpl->name = "arith.cmpi"; + scalarOpImpl->attributes = {scalarAttribute}; + scalarOpImpl->properties = "{}"; + ProcessScalarOperationPlan scalarOp(scalarOpImpl); + expect(scalarAttribute.name() == "predicate"); + expect(scalarAttribute.value() == "slt"); + expect(scalarOp.name() == "arith.cmpi"); + expect(scalarOp.attributes().size() == 1); + expect(scalarOp.properties() == "{}"); + + auto captureImpl = std::make_shared(); + captureImpl->id = ProcessCaptureId(0); + captureImpl->name = "capture00000000"; + captureImpl->operand = operand; + captureImpl->entryArgument = argument; + captureImpl->type = i32; + captureImpl->operandPath = "operand"; + captureImpl->argumentPath = "argument"; + ProcessCapturePlan capture(captureImpl); + expect(capture.id().value() == 0); + expect(capture.name() == "capture00000000"); + expect(capture.operand() == operand); + expect(capture.entryArgument() == argument); + expect(capture.type() == i32); + expect(capture.operandPath() == "operand"); + expect(capture.argumentPath() == "argument"); + + auto actionImpl = std::make_shared(); + actionImpl->id = 9; + actionImpl->kind = ProcessActionKind::ForCondition; + actionImpl->emission = ProcessEmissionClass::CopyScalar; + actionImpl->occurrence = loopOccurrence; + actionImpl->iterationVector = {8}; + actionImpl->operands = {planned[0]}; + actionImpl->results = {planned[3]}; + actionImpl->cost = 1; + actionImpl->resultTypes = {i32}; + actionImpl->scalarOp = scalarOp; + ProcessActionPlan action(actionImpl); + expect(action.id() == 9); + expect(action.kind() == ProcessActionKind::ForCondition); + expect(action.emission() == ProcessEmissionClass::CopyScalar); + expect(action.occurrence().kind() == ProcessOccurrenceKind::SyntheticLoop); + expect(action.sourceOperation() == nullptr); + expect(action.iterationVector() == llvm::ArrayRef({8})); + expect(action.operands().size() == 1); + expect(action.results().size() == 1); + expect(action.cost() == 1); + expect(action.resultTypes() == llvm::ArrayRef({i32})); + expect(!action.callee()); + expect(action.scalarOp() && action.scalarOp()->name() == "arith.cmpi"); + + auto liveSlotImpl = std::make_shared(); + liveSlotImpl->id = ProcessLiveSlotId(1); + liveSlotImpl->name = "live00000001"; + liveSlotImpl->type = i32; + liveSlotImpl->storageType = ProcessValueTypeId(2); + liveSlotImpl->memberValues = {planned[0]}; + liveSlotImpl->wrapCallee = ProcessCalleeId(3); + liveSlotImpl->unwrapCallee = ProcessCalleeId(4); + ProcessLiveSlotPlan liveSlot(liveSlotImpl); + expect(liveSlot.id().value() == 1); + expect(liveSlot.name() == "live00000001"); + expect(liveSlot.type() == i32); + expect(liveSlot.storageType().value() == 2); + expect(liveSlot.memberValues().size() == 1); + expect(liveSlot.wrapCallee()->value() == 3); + expect(liveSlot.unwrapCallee()->value() == 4); + + std::vector sources; + for (ProcessSubscriptionSourceKind kind : + {ProcessSubscriptionSourceKind::Capture, + ProcessSubscriptionSourceKind::Value, + ProcessSubscriptionSourceKind::Symbol}) { + auto impl = std::make_shared(); + impl->kind = kind; + impl->path = "source"; + if (kind == ProcessSubscriptionSourceKind::Capture) + impl->capture = ProcessCaptureId(5); + else if (kind == ProcessSubscriptionSourceKind::Value) { + impl->value = operand; + impl->ownerPath = "owner"; + } else { + // NOLINTNEXTLINE(performance-no-int-to-ptr) sentinel test identity + impl->declaration = reinterpret_cast(uintptr_t{1}); + impl->symbol = "@symbol"; + impl->ownerPath = "owner"; + } + sources.push_back(ProcessSubscriptionSourcePlan(impl)); + } + expect(sources[0].kind() == ProcessSubscriptionSourceKind::Capture); + expect(sources[0].capture()->value() == 5); + expect(sources[0].path() == "source"); + expect(sources[1].value() == operand); + expect(sources[1].owner() == nullptr); + expect(sources[1].ownerPath() == "owner"); + expect(sources[2].declaration() != nullptr); + expect(sources[2].symbol() == "@symbol"); + for (auto [index, source] : llvm::enumerate(sources)) + expect(static_cast(source.kind()) == index); + + auto wakeImpl = std::make_shared(); + wakeImpl->id = ProcessWakeId(6); + wakeImpl->kind = ProcessWakeKind::Condition; + wakeImpl->triggeringValue = operand; + wakeImpl->callee = ProcessCalleeId(7); + wakeImpl->typeKey = "@acir_wake_condition"; + wakeImpl->operationPath = "operation"; + wakeImpl->target = "condition"; + wakeImpl->occurrence = anchor; + wakeImpl->iterationVector = {9}; + wakeImpl->sources = sources; + ProcessWakePlan wake(wakeImpl); + expect(wake.id().value() == 6); + expect(wake.kind() == ProcessWakeKind::Condition); + expect(wake.operation() == nullptr); + expect(wake.triggeringValue() == operand); + expect(wake.declaration() == nullptr); + expect(wake.callee().value() == 7); + expect(wake.typeKey() == "@acir_wake_condition"); + expect(wake.operationPath() == "operation"); + expect(wake.target() == "condition"); + expect(wake.occurrence().kind() == ProcessOccurrenceKind::Original); + expect(wake.iterationVector() == llvm::ArrayRef({9})); + expect(wake.sources().size() == 3); + + auto storeImpl = std::make_shared(); + storeImpl->slot = ProcessLiveSlotId(1); + storeImpl->source = planned[0]; + storeImpl->sourceValue = operand; + ProcessTransitionStorePlan store(storeImpl); + auto loadImpl = std::make_shared(); + loadImpl->slot = ProcessLiveSlotId(1); + loadImpl->replacements = {planned[1]}; + ProcessTransitionLoadPlan load(loadImpl); + auto transitionImpl = std::make_shared(); + transitionImpl->id = ProcessTransitionId(2); + transitionImpl->sourcePc = ProcessPcId(3); + transitionImpl->targetPc = ProcessPcId(4); + transitionImpl->wake = ProcessWakeId(5); + transitionImpl->iterationVector = {6}; + transitionImpl->stores = {store}; + transitionImpl->loads = {load}; + ProcessTransitionPlan transition(transitionImpl); + expect(store.slot().value() == 1); + expect(store.source().kind() == ProcessPlannedValueKind::Original); + expect(store.sourceValue() == operand); + expect(load.slot().value() == 1); + expect(load.replacements().size() == 1); + expect(transition.id().value() == 2); + expect(transition.sourcePc().value() == 3); + expect(transition.targetPc().value() == 4); + expect(transition.wake().value() == 5); + expect(transition.iterationVector() == llvm::ArrayRef({6})); + expect(transition.stores().size() == 1); + expect(transition.loads().size() == 1); + + auto bindingImpl = std::make_shared(); + bindingImpl->from = planned[0]; + bindingImpl->to = planned[3]; + ProcessForwardingBindingPlan binding(bindingImpl); + expect(binding.from().kind() == ProcessPlannedValueKind::Original); + expect(binding.to().kind() == ProcessPlannedValueKind::Synthetic); + + std::vector frames; + for (auto [kind, phase] : + {std::pair{ProcessFrameKind::Entry, ProcessFramePhase::Entry}, + std::pair{ProcessFrameKind::ScfIf, ProcessFramePhase::Then}, + std::pair{ProcessFrameKind::ScfIf, ProcessFramePhase::Else}, + std::pair{ProcessFrameKind::ScfIf, ProcessFramePhase::Merge}, + std::pair{ProcessFrameKind::ScfFor, ProcessFramePhase::Header}, + std::pair{ProcessFrameKind::ScfFor, ProcessFramePhase::Body}, + std::pair{ProcessFrameKind::ScfFor, ProcessFramePhase::Exit}, + std::pair{ProcessFrameKind::ScfWhile, ProcessFramePhase::Before}, + std::pair{ProcessFrameKind::ScfWhile, ProcessFramePhase::After}, + std::pair{ProcessFrameKind::ScfWhile, ProcessFramePhase::Exit}}) { + auto impl = std::make_shared(); + impl->kind = kind; + impl->phase = phase; + impl->operationPath = "frame"; + impl->bindings = {binding}; + frames.push_back(ProcessControlFramePlan(impl)); + } + expect(frames.front().kind() == ProcessFrameKind::Entry); + expect(frames.front().phase() == ProcessFramePhase::Entry); + expect(frames.front().operation() == nullptr); + expect(frames.front().operationPath() == "frame"); + expect(frames.front().bindings().size() == 1); + + std::vector edges; + auto branch = std::make_shared(); + branch->kind = ProcessControlEdgeKind::Branch; + branch->condition = planned[4]; + branch->trueBlock = ProcessBlockId(1); + branch->falseBlock = ProcessBlockId(2); + branch->trueBindings = {binding}; + branch->falseBindings = {binding}; + edges.push_back(ProcessControlEdgePlan(branch)); + auto local = std::make_shared(); + local->kind = ProcessControlEdgeKind::LocalContinue; + local->targetBlock = ProcessBlockId(3); + local->bindings = {binding}; + edges.push_back(ProcessControlEdgePlan(local)); + auto suspend = std::make_shared(); + suspend->kind = ProcessControlEdgeKind::Suspend; + suspend->transition = ProcessTransitionId(4); + edges.push_back(ProcessControlEdgePlan(suspend)); + auto terminate = std::make_shared(); + terminate->kind = ProcessControlEdgeKind::Terminate; + terminate->status = ProcessTerminateStatus::Failure; + edges.push_back(ProcessControlEdgePlan(terminate)); + expect(edges[0].kind() == ProcessControlEdgeKind::Branch); + expect(edges[0].condition().kind() == ProcessPlannedValueKind::Constant); + expect(edges[0].trueBlock().value() == 1); + expect(edges[0].falseBlock().value() == 2); + expect(edges[0].trueBindings().size() == 1); + expect(edges[0].falseBindings().size() == 1); + expect(edges[1].targetBlock().value() == 3); + expect(edges[1].bindings().size() == 1); + expect(edges[2].transition().value() == 4); + expect(edges[3].status() == ProcessTerminateStatus::Failure); + for (auto [index, edge] : llvm::enumerate(edges)) + expect(static_cast(edge.kind()) == index); + + auto blockImpl = std::make_shared(); + blockImpl->id = ProcessBlockId(1); + blockImpl->pc = ProcessPcId(2); + blockImpl->path = "block"; + blockImpl->frames = {frames.front()}; + blockImpl->loads = {load}; + blockImpl->actions = {action}; + blockImpl->edge = edges.back(); + blockImpl->cost = 3; + ProcessBlockPlan block(blockImpl); + expect(block.id().value() == 1); + expect(block.pc().value() == 2); + expect(block.originRegion() == nullptr); + expect(block.originBlock() == nullptr); + expect(block.path() == "block"); + expect(block.frames().size() == 1); + expect(block.loads().size() == 1); + expect(block.actions().size() == 1); + expect(block.edge().kind() == ProcessControlEdgeKind::Terminate); + expect(block.cost() == 3); + + auto pcImpl = std::make_shared(); + pcImpl->id = ProcessPcId(2); + pcImpl->name = "pc00000002"; + pcImpl->entryPath = "block"; + pcImpl->blocks = {ProcessBlockId(1)}; + ProcessPcPlan pc(pcImpl); + expect(pc.id().value() == 2); + expect(pc.name() == "pc00000002"); + expect(pc.entryPath() == "block"); + expect(pc.blocks().front().value() == 1); + + auto stateImpl = std::make_shared(); + stateImpl->definitionKey = "@Top::@workload"; + stateImpl->captures = {capture}; + stateImpl->entryPc = ProcessPcId(2); + stateImpl->pcs = {pc}; + stateImpl->blocks = {block}; + stateImpl->liveSlots = {liveSlot}; + stateImpl->wakes = {wake}; + stateImpl->transitions = {transition}; + stateImpl->pcBitWidth = 3; + stateImpl->fairnessWork = 4; + ProcessStatePlan state(stateImpl); + expect(state.definitionKey() == "@Top::@workload"); + expect(!state.process()); + expect(state.captures().size() == 1); + expect(state.entryPc().value() == 2); + expect(state.pcs().size() == 1); + expect(state.blocks().size() == 1); + expect(state.liveSlots().size() == 1); + expect(state.wakes().size() == 1); + expect(state.transitions().size() == 1); + expect(state.pcBitWidth() == 3); + expect(state.fairnessWork() == 4); + + auto fieldImpl = std::make_shared(); + fieldImpl->name = "field"; + fieldImpl->typeKey = "mlir:i32"; + ProcessRecordFieldDescriptor field(fieldImpl); + auto recordCreateImpl = std::make_shared(); + recordCreateImpl->fields = {field}; + recordCreateImpl->recordType = "mlir:!ac.record"; + ProcessRecordCreatePayload recordCreate(recordCreateImpl); + auto recordGetImpl = std::make_shared(); + recordGetImpl->field = "field"; + recordGetImpl->record = "mlir:!ac.record"; + recordGetImpl->result = "mlir:i32"; + ProcessRecordGetPayload recordGet(recordGetImpl); + auto recordWithImpl = std::make_shared(); + recordWithImpl->field = "field"; + recordWithImpl->record = "mlir:!ac.record"; + recordWithImpl->value = "mlir:i32"; + ProcessRecordWithPayload recordWith(recordWithImpl); + expect(field.name() == "field"); + expect(field.typeKey() == "mlir:i32"); + expect(recordCreate.fields().size() == 1); + expect(recordCreate.recordType() == "mlir:!ac.record"); + expect(recordGet.field() == "field"); + expect(recordGet.record() == "mlir:!ac.record"); + expect(recordGet.result() == "mlir:i32"); + expect(recordWith.field() == "field"); + expect(recordWith.record() == "mlir:!ac.record"); + expect(recordWith.value() == "mlir:i32"); + + auto packetSerializeImpl = + std::make_shared(); + packetSerializeImpl->bytes = 4; + packetSerializeImpl->packet = "@packet"; + packetSerializeImpl->packetType = "mlir:!ac.packet"; + ProcessPacketSerializePayload packetSerialize(packetSerializeImpl); + auto packetDeserializeImpl = + std::make_shared( + ProcessPacketDeserializePayload::Impl{4, "@packet", + "mlir:!ac.packet"}); + ProcessPacketDeserializePayload packetDeserialize(packetDeserializeImpl); + expect(packetSerialize.bytes() == 4); + expect(packetSerialize.packet() == "@packet"); + expect(packetSerialize.packetType() == "mlir:!ac.packet"); + expect(packetDeserialize.bytes() == 4); + expect(packetDeserialize.packet() == "@packet"); + expect(packetDeserialize.packetType() == "mlir:!ac.packet"); + + auto traceDecodeImpl = std::make_shared(); + traceDecodeImpl->entry = "mlir:i32"; + traceDecodeImpl->result = "mlir:i64"; + traceDecodeImpl->source = "trace"; + ProcessTraceDecodePayload traceDecode(traceDecodeImpl); + auto queueSendImpl = std::make_shared(); + queueSendImpl->element = "mlir:i32"; + queueSendImpl->queue = "@queue"; + ProcessQueueTrySendPayload queueSend(queueSendImpl); + auto queueRecvImpl = std::make_shared(); + queueRecvImpl->element = "mlir:i32"; + queueRecvImpl->queue = "@queue"; + ProcessQueueTryRecvPayload queueRecv(queueRecvImpl); + auto eventImpl = std::make_shared(); + eventImpl->delay = "mlir:i64"; + eventImpl->target = "@event"; + eventImpl->value = "mlir:i32"; + ProcessEventSchedulePayload event(eventImpl); + expect(traceDecode.entry() == "mlir:i32"); + expect(traceDecode.result() == "mlir:i64"); + expect(traceDecode.source() == "trace"); + expect(queueSend.element() == "mlir:i32"); + expect(queueSend.queue() == "@queue"); + expect(queueRecv.element() == "mlir:i32"); + expect(queueRecv.queue() == "@queue"); + expect(event.delay() == "mlir:i64"); + expect(event.target() == "@event"); + expect(event.value() == "mlir:i32"); + + auto traceOpenImpl = std::make_shared(); + traceOpenImpl->source = "trace"; + ProcessTraceOpenPayload traceOpen(traceOpenImpl); + auto traceNextImpl = std::make_shared(); + traceNextImpl->entry = "mlir:i32"; + traceNextImpl->source = "trace"; + ProcessTraceNextPayload traceNext(traceNextImpl); + auto traceEofImpl = std::make_shared(); + traceEofImpl->source = "trace"; + ProcessTraceEofPayload traceEof(traceEofImpl); + auto tracePositionImpl = + std::make_shared(); + tracePositionImpl->source = "trace"; + ProcessTracePositionPayload tracePosition(tracePositionImpl); + expect(traceOpen.source() == "trace"); + expect(traceNext.entry() == "mlir:i32"); + expect(traceNext.source() == "trace"); + expect(traceEof.source() == "trace"); + expect(tracePosition.source() == "trace"); + + auto requireImpl = std::make_shared(); + requireImpl->message = "require"; + ProcessContractRequirePayload requirePayload(requireImpl); + auto ensureImpl = std::make_shared(); + ensureImpl->message = "ensure"; + ProcessContractEnsurePayload ensurePayload(ensureImpl); + auto assertImpl = std::make_shared(); + assertImpl->message = "assert"; + ProcessContractAssertPayload assertPayload(assertImpl); + auto probeImpl = std::make_shared(); + probeImpl->kind = "counter"; + probeImpl->result = "mlir:i64"; + probeImpl->target = "@probe"; + ProcessProbePayload probe(probeImpl); + auto statImpl = std::make_shared(); + statImpl->stat = "@stat"; + statImpl->valueType = "mlir:i64"; + ProcessStatAddPayload stat(statImpl); + expect(requirePayload.message() == "require"); + expect(ensurePayload.message() == "ensure"); + expect(assertPayload.message() == "assert"); + expect(probe.kind() == "counter"); + expect(probe.result() == "mlir:i64"); + expect(probe.target() == "@probe"); + expect(stat.stat() == "@stat"); + expect(stat.valueType() == "mlir:i64"); + + auto wakeConditionImpl = + std::make_shared(); + wakeConditionImpl->wakeKind = ProcessWakeKind::Condition; + wakeConditionImpl->wakeType = "@acir_wake_condition"; + ProcessWakeConditionPayload wakeCondition(wakeConditionImpl); + auto wakeResourceImpl = std::make_shared(); + wakeResourceImpl->wakeKind = ProcessWakeKind::Resource; + wakeResourceImpl->wakeType = "@acir_wake_resource"; + ProcessWakeResourcePayload wakeResource(wakeResourceImpl); + auto wakeEventImpl = std::make_shared(); + wakeEventImpl->wakeKind = ProcessWakeKind::EventQueue; + wakeEventImpl->wakeType = "@acir_wake_event_queue"; + ProcessWakeEventQueuePayload wakeEvent(wakeEventImpl); + auto wakeNextImpl = std::make_shared(); + wakeNextImpl->wakeKind = ProcessWakeKind::NextDelta; + wakeNextImpl->wakeType = "@acir_wake_next_delta"; + ProcessWakeNextDeltaPayload wakeNext(wakeNextImpl); + expect(wakeCondition.wakeKind() == ProcessWakeKind::Condition); + expect(wakeCondition.wakeType() == "@acir_wake_condition"); + expect(wakeResource.wakeKind() == ProcessWakeKind::Resource); + expect(wakeResource.wakeType() == "@acir_wake_resource"); + expect(wakeEvent.wakeKind() == ProcessWakeKind::EventQueue); + expect(wakeEvent.wakeType() == "@acir_wake_event_queue"); + expect(wakeNext.wakeKind() == ProcessWakeKind::NextDelta); + expect(wakeNext.wakeType() == "@acir_wake_next_delta"); + + auto scalarWrapImpl = std::make_shared(); + scalarWrapImpl->direction = ProcessWrapperDirection::Wrap; + scalarWrapImpl->scalar = "mlir:i32"; + scalarWrapImpl->valueType = "storage:value:digest"; + ProcessScalarWrapPayload scalarWrap(scalarWrapImpl); + auto scalarUnwrapImpl = std::make_shared(); + scalarUnwrapImpl->direction = ProcessWrapperDirection::Unwrap; + scalarUnwrapImpl->scalar = "mlir:i32"; + scalarUnwrapImpl->valueType = "storage:value:digest"; + ProcessScalarUnwrapPayload scalarUnwrap(scalarUnwrapImpl); + expect(scalarWrap.direction() == ProcessWrapperDirection::Wrap); + expect(scalarWrap.scalar() == "mlir:i32"); + expect(scalarWrap.valueType() == "storage:value:digest"); + expect(scalarUnwrap.direction() == ProcessWrapperDirection::Unwrap); + expect(scalarUnwrap.scalar() == "mlir:i32"); + expect(scalarUnwrap.valueType() == "storage:value:digest"); + + std::vector payloads; + auto addPayload = [&](ProcessHelperRole role, auto value, + auto ProcessGeneratedCalleePayload::Impl::*member) { + auto impl = std::make_shared(); + impl->role = role; + impl.get()->*member = value; + payloads.push_back(ProcessGeneratedCalleePayload(impl)); + }; + addPayload(ProcessHelperRole::RecordCreate, recordCreate, + &ProcessGeneratedCalleePayload::Impl::recordCreate); + addPayload(ProcessHelperRole::RecordGet, recordGet, + &ProcessGeneratedCalleePayload::Impl::recordGet); + addPayload(ProcessHelperRole::RecordWith, recordWith, + &ProcessGeneratedCalleePayload::Impl::recordWith); + addPayload(ProcessHelperRole::PacketSerialize, packetSerialize, + &ProcessGeneratedCalleePayload::Impl::packetSerialize); + addPayload(ProcessHelperRole::PacketDeserialize, packetDeserialize, + &ProcessGeneratedCalleePayload::Impl::packetDeserialize); + addPayload(ProcessHelperRole::TraceDecode, traceDecode, + &ProcessGeneratedCalleePayload::Impl::traceDecode); + addPayload(ProcessHelperRole::QueueTrySend, queueSend, + &ProcessGeneratedCalleePayload::Impl::queueTrySend); + addPayload(ProcessHelperRole::QueueTryRecv, queueRecv, + &ProcessGeneratedCalleePayload::Impl::queueTryRecv); + addPayload(ProcessHelperRole::EventSchedule, event, + &ProcessGeneratedCalleePayload::Impl::eventSchedule); + addPayload(ProcessHelperRole::TraceOpen, traceOpen, + &ProcessGeneratedCalleePayload::Impl::traceOpen); + addPayload(ProcessHelperRole::TraceNext, traceNext, + &ProcessGeneratedCalleePayload::Impl::traceNext); + addPayload(ProcessHelperRole::TraceEof, traceEof, + &ProcessGeneratedCalleePayload::Impl::traceEof); + addPayload(ProcessHelperRole::TracePosition, tracePosition, + &ProcessGeneratedCalleePayload::Impl::tracePosition); + addPayload(ProcessHelperRole::ContractRequire, requirePayload, + &ProcessGeneratedCalleePayload::Impl::contractRequire); + addPayload(ProcessHelperRole::ContractEnsure, ensurePayload, + &ProcessGeneratedCalleePayload::Impl::contractEnsure); + addPayload(ProcessHelperRole::ContractAssert, assertPayload, + &ProcessGeneratedCalleePayload::Impl::contractAssert); + addPayload(ProcessHelperRole::Probe, probe, + &ProcessGeneratedCalleePayload::Impl::probe); + addPayload(ProcessHelperRole::StatAdd, stat, + &ProcessGeneratedCalleePayload::Impl::statAdd); + addPayload(ProcessHelperRole::WakeCondition, wakeCondition, + &ProcessGeneratedCalleePayload::Impl::wakeCondition); + addPayload(ProcessHelperRole::WakeResource, wakeResource, + &ProcessGeneratedCalleePayload::Impl::wakeResource); + addPayload(ProcessHelperRole::WakeEventQueue, wakeEvent, + &ProcessGeneratedCalleePayload::Impl::wakeEventQueue); + addPayload(ProcessHelperRole::WakeNextDelta, wakeNext, + &ProcessGeneratedCalleePayload::Impl::wakeNextDelta); + addPayload(ProcessHelperRole::ScalarWrap, scalarWrap, + &ProcessGeneratedCalleePayload::Impl::scalarWrap); + addPayload(ProcessHelperRole::ScalarUnwrap, scalarUnwrap, + &ProcessGeneratedCalleePayload::Impl::scalarUnwrap); + expect(payloads.size() == 24); + for (auto [index, payload] : llvm::enumerate(payloads)) + expect(static_cast(payload.role()) == index); + expect(payloads[0].recordCreate().recordType() == "mlir:!ac.record"); + expect(payloads[1].recordGet().field() == "field"); + expect(payloads[2].recordWith().value() == "mlir:i32"); + expect(payloads[3].packetSerialize().bytes() == 4); + expect(payloads[4].packetDeserialize().bytes() == 4); + expect(payloads[5].traceDecode().source() == "trace"); + expect(payloads[6].queueTrySend().queue() == "@queue"); + expect(payloads[7].queueTryRecv().queue() == "@queue"); + expect(payloads[8].eventSchedule().target() == "@event"); + expect(payloads[9].traceOpen().source() == "trace"); + expect(payloads[10].traceNext().entry() == "mlir:i32"); + expect(payloads[11].traceEof().source() == "trace"); + expect(payloads[12].tracePosition().source() == "trace"); + expect(payloads[13].contractRequire().message() == "require"); + expect(payloads[14].contractEnsure().message() == "ensure"); + expect(payloads[15].contractAssert().message() == "assert"); + expect(payloads[16].probe().target() == "@probe"); + expect(payloads[17].statAdd().stat() == "@stat"); + expect(payloads[18].wakeCondition().wakeType() == "@acir_wake_condition"); + expect(payloads[19].wakeResource().wakeType() == "@acir_wake_resource"); + expect(payloads[20].wakeEventQueue().wakeType() == "@acir_wake_event_queue"); + expect(payloads[21].wakeNextDelta().wakeType() == "@acir_wake_next_delta"); + expect(payloads[22].scalarWrap().direction() == + ProcessWrapperDirection::Wrap); + expect(payloads[23].scalarUnwrap().direction() == + ProcessWrapperDirection::Unwrap); + + auto fieldMemberImpl = std::make_shared(); + fieldMemberImpl->kind = ProcessValueTypeMemberKind::Field; + fieldMemberImpl->name = "member"; + fieldMemberImpl->offsetBits = 0; + fieldMemberImpl->widthBits = 32; + fieldMemberImpl->signedness = ProcessStorageSignedness::Signed; + fieldMemberImpl->encoding = "i32"; + fieldMemberImpl->typeKey = "mlir:i32"; + ProcessValueTypeMemberPlan fieldMember(fieldMemberImpl); + auto elementMemberImpl = std::make_shared(); + elementMemberImpl->kind = ProcessValueTypeMemberKind::Element; + elementMemberImpl->index = 1; + elementMemberImpl->offsetBits = 32; + elementMemberImpl->widthBits = 32; + elementMemberImpl->signedness = ProcessStorageSignedness::Unsigned; + elementMemberImpl->encoding = "i32"; + elementMemberImpl->typeKey = "mlir:i32"; + ProcessValueTypeMemberPlan elementMember(elementMemberImpl); + expect(fieldMember.kind() == ProcessValueTypeMemberKind::Field); + expect(fieldMember.name() == "member"); + expect(!fieldMember.index()); + expect(fieldMember.offsetBits() == 0); + expect(fieldMember.widthBits() == 32); + expect(fieldMember.signedness() == ProcessStorageSignedness::Signed); + expect(fieldMember.encoding() == "i32"); + expect(fieldMember.typeKey() == "mlir:i32"); + expect(elementMember.kind() == ProcessValueTypeMemberKind::Element); + expect(elementMember.name().empty()); + expect(elementMember.index() == 1); + + auto storageValueImpl = std::make_shared(); + storageValueImpl->members = {fieldMember}; + storageValueImpl->widthBits = 32; + storageValueImpl->encoding = "i32"; + ProcessStorageValuePayload storageValue(storageValueImpl); + auto storagePacketImpl = + std::make_shared(); + storagePacketImpl->members = {fieldMember, elementMember}; + storagePacketImpl->widthBits = 64; + storagePacketImpl->bytes = 8; + storagePacketImpl->encoding = "array<8xi8>"; + ProcessStoragePacketPayload storagePacket(storagePacketImpl); + expect(storageValue.members().size() == 1); + expect(storageValue.widthBits() == 32); + expect(storageValue.encoding() == "i32"); + expect(storagePacket.members().size() == 2); + expect(storagePacket.widthBits() == 64); + expect(storagePacket.bytes() == 8); + expect(storagePacket.encoding() == "array<8xi8>"); + auto valuePayloadImpl = std::make_shared(); + valuePayloadImpl->kind = ProcessValueTypeKind::Value; + valuePayloadImpl->value = storageValue; + ProcessValueTypePayload valuePayload(valuePayloadImpl); + auto packetPayloadImpl = std::make_shared(); + packetPayloadImpl->kind = ProcessValueTypeKind::Packet; + packetPayloadImpl->packet = storagePacket; + ProcessValueTypePayload packetPayload(packetPayloadImpl); + expect(valuePayload.kind() == ProcessValueTypeKind::Value); + expect(valuePayload.value().widthBits() == 32); + expect(packetPayload.kind() == ProcessValueTypeKind::Packet); + expect(packetPayload.packet().bytes() == 8); + + auto calleeImpl = std::make_shared(); + calleeImpl->id = ProcessCalleeId(5); + calleeImpl->symbol = "@callee"; + calleeImpl->cpp = "callee"; + calleeImpl->kind = "implementation"; + calleeImpl->fingerprint = "sha256:fingerprint"; + calleeImpl->effect = ProcessEffectKind::Stateful; + calleeImpl->inputTypeKeyStorage = {"mlir:i32"}; + calleeImpl->inputTypeKeys = {calleeImpl->inputTypeKeyStorage[0]}; + calleeImpl->resultTypeKeyStorage = {"mlir:i64"}; + calleeImpl->resultTypeKeys = {calleeImpl->resultTypeKeyStorage[0]}; + calleeImpl->role = ProcessHelperRole::Probe; + calleeImpl->payload = payloads[16]; + // NOLINTBEGIN(performance-no-int-to-ptr) sentinel test identities + calleeImpl->sourceOperations = { + reinterpret_cast(uintptr_t{1})}; + calleeImpl->declarations = { + reinterpret_cast(uintptr_t{2})}; + // NOLINTEND(performance-no-int-to-ptr) + calleeImpl->sourcePathStorage = {"source"}; + calleeImpl->sourcePaths = {calleeImpl->sourcePathStorage[0]}; + ProcessGeneratedCalleePlan callee(calleeImpl); + expect(callee.id().value() == 5); + expect(callee.symbol() == "@callee"); + expect(callee.cpp() == "callee"); + expect(callee.kind() == "implementation"); + expect(callee.fingerprint() == "sha256:fingerprint"); + expect(callee.effect() == ProcessEffectKind::Stateful); + expect(callee.inputTypeKeys() == + llvm::ArrayRef({"mlir:i32"})); + expect(callee.resultTypeKeys() == + llvm::ArrayRef({"mlir:i64"})); + expect(callee.role() == ProcessHelperRole::Probe); + expect(callee.payload().probe().kind() == "counter"); + expect(callee.sourceOperations().size() == 1); + expect(callee.declarations().size() == 1); + expect(callee.sourcePaths() == llvm::ArrayRef({"source"})); + auto typeImpl = std::make_shared(); + typeImpl->id = ProcessValueTypeId(6); + typeImpl->symbol = "@type"; + typeImpl->cpp = "type"; + typeImpl->kind = ProcessValueTypeKind::Value; + typeImpl->fingerprint = "sha256:type"; + typeImpl->acirType = i32; + typeImpl->payload = valuePayload; + ProcessValueTypePlan type(typeImpl); + expect(type.id().value() == 6); + expect(type.symbol() == "@type"); + expect(type.cpp() == "type"); + expect(type.kind() == ProcessValueTypeKind::Value); + expect(type.fingerprint() == "sha256:type"); + expect(type.acirType() == i32); + expect(type.payload().value().encoding() == "i32"); + + auto setImpl = std::make_shared(); + setImpl->processes = {state}; + setImpl->callees = {callee}; + setImpl->valueTypes = {type}; + ProcessStatePlanSet set(setImpl); + expect(set.processes().size() == 1); + expect(set.callees().size() == 1); + expect(set.valueTypes().size() == 1); + expect(set.lookupByDefinitionKey("@Top::@workload") != nullptr); + expect(set.lookupByDefinitionKey("@Top::@missing") == nullptr); + + auto allEnumsSequential = [](auto values) { + for (auto [index, value] : llvm::enumerate(values)) + if (static_cast(value) != index) + return false; + return true; + }; + expect(allEnumsSequential( + std::array{ProcessWakeKind::Condition, ProcessWakeKind::Resource, + ProcessWakeKind::EventQueue, ProcessWakeKind::NextDelta})); + expect(allEnumsSequential(std::array{ProcessSubscriptionSourceKind::Capture, + ProcessSubscriptionSourceKind::Value, + ProcessSubscriptionSourceKind::Symbol})); + expect(allEnumsSequential(std::array{ + ProcessActionKind::Original, ProcessActionKind::Constant, + ProcessActionKind::ForInitialize, ProcessActionKind::ForCondition, + ProcessActionKind::ForIncrement, ProcessActionKind::ScalarWrap, + ProcessActionKind::ScalarUnwrap})); + expect(allEnumsSequential(std::array{ + ProcessEmissionClass::CopyScalar, ProcessEmissionClass::Inline, + ProcessEmissionClass::Invoke, ProcessEmissionClass::Wrap, + ProcessEmissionClass::Unwrap, ProcessEmissionClass::ForwardOnly})); + expect(allEnumsSequential(std::array{ + ProcessOccurrenceKind::Original, ProcessOccurrenceKind::SyntheticLoop, + ProcessOccurrenceKind::SyntheticWrapper, + ProcessOccurrenceKind::SyntheticConstant})); + expect(allEnumsSequential(std::array{ProcessLoopPhase::Initialize, + ProcessLoopPhase::Condition, + ProcessLoopPhase::Increment})); + expect(allEnumsSequential(std::array{ProcessWrapperDirection::Wrap, + ProcessWrapperDirection::Unwrap})); + expect(allEnumsSequential( + std::array{ProcessFrameKind::Entry, ProcessFrameKind::ScfIf, + ProcessFrameKind::ScfFor, ProcessFrameKind::ScfWhile})); + expect(allEnumsSequential( + std::array{ProcessFramePhase::Entry, ProcessFramePhase::Then, + ProcessFramePhase::Else, ProcessFramePhase::Merge, + ProcessFramePhase::Header, ProcessFramePhase::Body, + ProcessFramePhase::Before, ProcessFramePhase::After, + ProcessFramePhase::Exit})); + expect(allEnumsSequential(std::array{ + ProcessPlannedValueKind::Original, ProcessPlannedValueKind::Capture, + ProcessPlannedValueKind::LiveSlot, ProcessPlannedValueKind::Synthetic, + ProcessPlannedValueKind::Constant})); + expect(allEnumsSequential( + std::array{ProcessValueCoordinateKind::Result, + ProcessValueCoordinateKind::BlockArgument})); + expect(allEnumsSequential(std::array{ + ProcessControlEdgeKind::Branch, ProcessControlEdgeKind::LocalContinue, + ProcessControlEdgeKind::Suspend, ProcessControlEdgeKind::Terminate})); + expect(allEnumsSequential(std::array{ProcessTerminateStatus::Success, + ProcessTerminateStatus::Failure})); + expect(allEnumsSequential( + std::array{ProcessEffectKind::Pure, ProcessEffectKind::Stateful})); + expect(allEnumsSequential( + std::array{ProcessValueTypeKind::Value, ProcessValueTypeKind::Packet})); + expect(allEnumsSequential(std::array{ProcessValueTypeMemberKind::Field, + ProcessValueTypeMemberKind::Element})); + expect(allEnumsSequential(std::array{ProcessStorageSignedness::Signless, + ProcessStorageSignedness::Signed, + ProcessStorageSignedness::Unsigned})); + return valid; +} + +bool detail::PlanSetBuilder::exerciseAllActionArmsFixture( + mlir::MLIRContext &context) { + bool valid = true; + auto expect = [&](bool condition) { valid &= condition; }; + mlir::Type i1 = mlir::IntegerType::get(&context, 1); + mlir::Type i32 = mlir::IntegerType::get(&context, 32); + mlir::OwningOpRef module = + mlir::ModuleOp::create(mlir::UnknownLoc::get(&context)); + context.getOrLoadDialect(); + mlir::OpBuilder builder(&context); + builder.setInsertionPointToEnd(module->getBody()); + mlir::OperationState state(module->getLoc(), + mlir::scf::ForOp::getOperationName()); + state.addRegion(); + mlir::Operation *loopOperation = builder.create(state); + + auto makeOriginal = [&]() { + auto original = std::make_shared(); + original->operation = module->getOperation(); + original->operationPath = "@fixture/r0/b0/o0"; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::Original; + occurrence->original = ProcessOriginalOccurrence(original); + return ProcessOccurrenceId(occurrence); + }; + ProcessOccurrenceId original = makeOriginal(); + auto makeLoop = [&](ProcessLoopPhase phase) { + auto loop = std::make_shared(); + loop->anchor = original; + loop->phase = phase; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::SyntheticLoop; + occurrence->syntheticLoop = ProcessSyntheticLoopOccurrence(loop); + return ProcessOccurrenceId(occurrence); + }; + auto makeWrapper = [&](ProcessWrapperDirection direction) { + auto wrapper = std::make_shared(); + wrapper->anchor = original; + wrapper->transition = ProcessTransitionId(0); + wrapper->slot = ProcessLiveSlotId(0); + wrapper->direction = direction; + auto occurrence = std::make_shared(); + occurrence->kind = ProcessOccurrenceKind::SyntheticWrapper; + occurrence->syntheticWrapper = ProcessSyntheticWrapperOccurrence(wrapper); + return ProcessOccurrenceId(occurrence); + }; + + auto makeConstant = [&](mlir::Type type, llvm::StringRef literal) { + auto constant = std::make_shared(); + constant->value = literal.str(); + auto value = std::make_shared(); + value->kind = ProcessPlannedValueKind::Constant; + value->type = type; + value->constant = ProcessConstantPlannedValue(constant); + return ProcessPlannedValue(value); + }; + ProcessPlannedValue lhs = makeConstant(i32, "7"); + ProcessPlannedValue rhs = makeConstant(i32, "9"); + ProcessPlannedValue boolean = makeConstant(i1, "true"); + ProcessPlannedValue lower = makeConstant(mlir::IndexType::get(&context), "0"); + ProcessPlannedValue upper = makeConstant(mlir::IndexType::get(&context), "8"); + ProcessPlannedValue step = makeConstant(mlir::IndexType::get(&context), "1"); + + auto makePayload = [](ProcessHelperRole role) { + auto payload = std::make_shared(); + payload->role = role; + if (role == ProcessHelperRole::TraceDecode) { + auto arm = std::make_shared(); + arm->entry = "mlir:i32"; + arm->result = "mlir:i32"; + arm->source = "fixture"; + payload->traceDecode = ProcessTraceDecodePayload(arm); + } else if (role == ProcessHelperRole::Probe) { + auto arm = std::make_shared(); + arm->kind = "fixture"; + arm->result = "mlir:i32"; + arm->target = "@fixture"; + payload->probe = ProcessProbePayload(arm); + } else if (role == ProcessHelperRole::ScalarWrap) { + auto arm = std::make_shared(); + arm->direction = ProcessWrapperDirection::Wrap; + arm->scalar = "mlir:i32"; + arm->valueType = "storage:value:fixture"; + payload->scalarWrap = ProcessScalarWrapPayload(arm); + } else { + auto arm = std::make_shared(); + arm->direction = ProcessWrapperDirection::Unwrap; + arm->scalar = "mlir:i32"; + arm->valueType = "storage:value:fixture"; + payload->scalarUnwrap = ProcessScalarUnwrapPayload(arm); + } + return ProcessGeneratedCalleePayload(payload); + }; + const ProcessHelperRole calleeRoles[] = { + ProcessHelperRole::TraceDecode, ProcessHelperRole::Probe, + ProcessHelperRole::ScalarWrap, ProcessHelperRole::ScalarUnwrap}; + std::vector callees; + for (auto [index, role] : llvm::enumerate(calleeRoles)) { + auto callee = std::make_shared(); + callee->id = ProcessCalleeId(index); + callee->role = role; + callee->payload = makePayload(role); + callees.push_back(ProcessGeneratedCalleePlan(callee)); + } + + auto cmpiAttribute = std::make_shared(); + cmpiAttribute->name = "predicate"; + cmpiAttribute->value = "2 : i64"; + auto cmpi = std::make_shared(); + cmpi->name = "arith.cmpi"; + cmpi->attributes = {ProcessScalarAttribute(cmpiAttribute)}; + cmpi->properties = "{}"; + auto addi = std::make_shared(); + addi->name = "arith.addi"; + addi->properties = "{}"; + + auto makeAction = + [&](uint32_t id, ProcessActionKind kind, ProcessEmissionClass emission, + ProcessOccurrenceId occurrence, mlir::Operation *source, + std::vector operands, + std::vector results, + std::optional callee, + std::shared_ptr scalar) { + auto action = std::make_shared(); + action->id = id; + action->kind = kind; + action->emission = emission; + action->occurrence = std::move(occurrence); + action->sourceOperation = source; + action->iterationVector = {100 + id}; + action->operands = std::move(operands); + action->results = std::move(results); + action->cost = emission == ProcessEmissionClass::ForwardOnly ? 0 : 1; + for (const ProcessPlannedValue &result : action->results) + action->resultTypes.push_back(result.type()); + action->callee = callee; + if (scalar) + action->scalarOp = ProcessScalarOperationPlan(std::move(scalar)); + return ProcessActionPlan(action); + }; + + std::vector actions; + actions.push_back(makeAction( + 0, ProcessActionKind::ForCondition, ProcessEmissionClass::CopyScalar, + makeLoop(ProcessLoopPhase::Condition), loopOperation, {lower, upper}, + {boolean}, std::nullopt, cmpi)); + actions.push_back(makeAction( + 1, ProcessActionKind::Original, ProcessEmissionClass::Inline, original, + module->getOperation(), {lhs}, {rhs}, ProcessCalleeId(0), nullptr)); + actions.push_back(makeAction( + 2, ProcessActionKind::Original, ProcessEmissionClass::Invoke, original, + module->getOperation(), {}, {lhs}, ProcessCalleeId(1), nullptr)); + actions.push_back( + makeAction(3, ProcessActionKind::ScalarWrap, ProcessEmissionClass::Wrap, + makeWrapper(ProcessWrapperDirection::Wrap), nullptr, {lhs}, + {rhs}, ProcessCalleeId(2), nullptr)); + actions.push_back(makeAction( + 4, ProcessActionKind::ScalarUnwrap, ProcessEmissionClass::Unwrap, + makeWrapper(ProcessWrapperDirection::Unwrap), nullptr, {rhs}, {lhs}, + ProcessCalleeId(3), nullptr)); + actions.push_back(makeAction( + 5, ProcessActionKind::ForInitialize, ProcessEmissionClass::ForwardOnly, + makeLoop(ProcessLoopPhase::Initialize), loopOperation, {lower}, {lower}, + std::nullopt, nullptr)); + actions.push_back(makeAction( + 6, ProcessActionKind::ForIncrement, ProcessEmissionClass::CopyScalar, + makeLoop(ProcessLoopPhase::Increment), loopOperation, {lower, step}, + {step}, std::nullopt, addi)); + + const ProcessEmissionClass emissions[] = { + ProcessEmissionClass::CopyScalar, ProcessEmissionClass::Inline, + ProcessEmissionClass::Invoke, ProcessEmissionClass::Wrap, + ProcessEmissionClass::Unwrap, ProcessEmissionClass::ForwardOnly, + ProcessEmissionClass::CopyScalar}; + const ProcessActionKind kinds[] = { + ProcessActionKind::ForCondition, ProcessActionKind::Original, + ProcessActionKind::Original, ProcessActionKind::ScalarWrap, + ProcessActionKind::ScalarUnwrap, ProcessActionKind::ForInitialize, + ProcessActionKind::ForIncrement}; + for (auto [index, action] : llvm::enumerate(actions)) { + expect(action.id() == index); + expect(action.kind() == kinds[index]); + expect(action.emission() == emissions[index]); + expect(action.iterationVector() == llvm::ArrayRef({100 + index})); + expect(action.cost() == + (emissions[index] == ProcessEmissionClass::ForwardOnly ? 0U : 1U)); + expect(action.resultTypes().size() == action.results().size()); + expect(llvm::all_of(llvm::zip_equal(action.resultTypes(), action.results()), + [](auto pair) { + return std::get<0>(pair) == std::get<1>(pair).type(); + })); + } + expect(actions[0].sourceOperation() == loopOperation); + expect(actions[0].occurrence().syntheticLoop().phase() == + ProcessLoopPhase::Condition); + expect(actions[0].operands().size() == 2); + expect(actions[0].results()[0].type() == i1); + expect(!actions[0].callee()); + expect(actions[0].scalarOp() && + actions[0].scalarOp()->name() == "arith.cmpi"); + expect(actions[0].scalarOp()->attributes()[0].name() == "predicate"); + expect(actions[0].scalarOp()->attributes()[0].value() == "2 : i64"); + expect(actions[0].scalarOp()->properties() == "{}"); + expect(actions[1].sourceOperation() == module->getOperation()); + expect(actions[1].callee() == ProcessCalleeId(0)); + expect(callees[actions[1].callee()->value()].role() == + ProcessHelperRole::TraceDecode); + expect(!actions[1].scalarOp()); + expect(actions[2].sourceOperation() == module->getOperation()); + expect(actions[2].callee() == ProcessCalleeId(1)); + expect(callees[actions[2].callee()->value()].role() == + ProcessHelperRole::Probe); + expect(!actions[2].scalarOp()); + expect(actions[3].sourceOperation() == nullptr); + expect(actions[3].occurrence().syntheticWrapper().direction() == + ProcessWrapperDirection::Wrap); + expect(actions[3].callee() == ProcessCalleeId(2)); + expect(callees[actions[3].callee()->value()].role() == + ProcessHelperRole::ScalarWrap); + expect(!actions[3].scalarOp()); + expect(actions[4].sourceOperation() == nullptr); + expect(actions[4].occurrence().syntheticWrapper().direction() == + ProcessWrapperDirection::Unwrap); + expect(actions[4].callee() == ProcessCalleeId(3)); + expect(callees[actions[4].callee()->value()].role() == + ProcessHelperRole::ScalarUnwrap); + expect(!actions[4].scalarOp()); + expect(actions[5].sourceOperation() == loopOperation); + expect(actions[5].occurrence().syntheticLoop().phase() == + ProcessLoopPhase::Initialize); + expect(!actions[5].callee()); + expect(!actions[5].scalarOp()); + expect(actions[6].sourceOperation() == loopOperation); + expect(actions[6].occurrence().syntheticLoop().phase() == + ProcessLoopPhase::Increment); + expect(actions[6].operands().size() == 2); + expect(mlir::isa(actions[6].results()[0].type())); + expect(!actions[6].callee()); + expect(actions[6].scalarOp() && + actions[6].scalarOp()->name() == "arith.addi"); + expect(actions[6].scalarOp()->attributes().empty()); + expect(actions[6].scalarOp()->properties() == "{}"); + return valid; +} + +llvm::StringRef detail::PlanSetBuilder::specializationBytes( + const ProcessGeneratedCalleePlan &callee) { + return callee.impl_->specializationBytes; +} + +llvm::StringRef detail::PlanSetBuilder::descriptorBytes( + const ProcessGeneratedCalleePlan &callee) { + return callee.impl_->descriptorBytes; +} + +llvm::StringRef +detail::PlanSetBuilder::specializationBytes(const ProcessValueTypePlan &type) { + return type.impl_->specializationBytes; +} + +bool detail::PlanSetBuilder::validEdgeShape( + const ProcessControlEdgePlan &edge) { + if (!edge.impl_) + return false; + switch (edge.impl_->kind) { + case ProcessControlEdgeKind::Branch: + return edge.impl_->condition && edge.impl_->trueBlock && + edge.impl_->falseBlock && !edge.impl_->targetBlock && + !edge.impl_->transition && edge.impl_->bindings.empty(); + case ProcessControlEdgeKind::LocalContinue: + return edge.impl_->targetBlock && !edge.impl_->condition && + !edge.impl_->trueBlock && !edge.impl_->falseBlock && + !edge.impl_->transition && edge.impl_->trueBindings.empty() && + edge.impl_->falseBindings.empty(); + case ProcessControlEdgeKind::Suspend: + return edge.impl_->transition && !edge.impl_->condition && + !edge.impl_->trueBlock && !edge.impl_->falseBlock && + !edge.impl_->targetBlock && edge.impl_->trueBindings.empty() && + edge.impl_->falseBindings.empty() && edge.impl_->bindings.empty(); + case ProcessControlEdgeKind::Terminate: + return !edge.impl_->condition && !edge.impl_->trueBlock && + !edge.impl_->falseBlock && !edge.impl_->targetBlock && + !edge.impl_->transition && edge.impl_->trueBindings.empty() && + edge.impl_->falseBindings.empty() && edge.impl_->bindings.empty(); + } + return false; +} + +llvm::StringRef +detail::PlanSetBuilder::structuralError(const ProcessStatePlanSet &plans) { + if (!plans.impl_) + return "process-state plan invariant violated: missing plan-set storage"; + + auto validOccurrence = [](const ProcessOccurrenceId &root) { + llvm::SmallVector worklist{&root}; + llvm::SmallPtrSet visited; + while (!worklist.empty()) { + const ProcessOccurrenceId *occurrence = worklist.pop_back_val(); + if (!occurrence->impl_) + return false; + if (!visited.insert(occurrence->impl_.get()).second) + return false; + auto &impl = *occurrence->impl_; + unsigned active = + static_cast(impl.original.has_value()) + + static_cast(impl.syntheticLoop.has_value()) + + static_cast(impl.syntheticWrapper.has_value()) + + static_cast(impl.syntheticConstant.has_value()); + if (active != 1) + return false; + switch (impl.kind) { + case ProcessOccurrenceKind::Original: + if (!impl.original || !impl.original->impl_) + return false; + for (const ProcessCallSitePlan &site : impl.original->impl_->callSites) + if (!site.impl_) + return false; + break; + case ProcessOccurrenceKind::SyntheticLoop: + if (!impl.syntheticLoop || !impl.syntheticLoop->impl_ || + !impl.syntheticLoop->impl_->anchor) + return false; + worklist.push_back(&*impl.syntheticLoop->impl_->anchor); + break; + case ProcessOccurrenceKind::SyntheticWrapper: + if (!impl.syntheticWrapper || !impl.syntheticWrapper->impl_ || + !impl.syntheticWrapper->impl_->anchor || + !impl.syntheticWrapper->impl_->transition || + !impl.syntheticWrapper->impl_->slot) + return false; + worklist.push_back(&*impl.syntheticWrapper->impl_->anchor); + break; + case ProcessOccurrenceKind::SyntheticConstant: + if (!impl.syntheticConstant || !impl.syntheticConstant->impl_ || + !impl.syntheticConstant->impl_->anchor) + return false; + worklist.push_back(&*impl.syntheticConstant->impl_->anchor); + break; + } + } + return true; + }; + + auto validPlannedValue = [&](const ProcessPlannedValue &value) { + if (!value.impl_ || !value.impl_->type) + return false; + unsigned active = + static_cast(value.impl_->original.has_value()) + + static_cast(value.impl_->capture.has_value()) + + static_cast(value.impl_->liveSlot.has_value()) + + static_cast(value.impl_->synthetic.has_value()) + + static_cast(value.impl_->constant.has_value()); + if (active != 1) + return false; + switch (value.impl_->kind) { + case ProcessPlannedValueKind::Original: + return value.impl_->original && value.impl_->original->impl_ && + value.impl_->original->impl_->occurrence && + value.impl_->original->impl_->coordinate && + value.impl_->original->impl_->coordinate->impl_ && + validOccurrence(*value.impl_->original->impl_->occurrence); + case ProcessPlannedValueKind::Capture: + return value.impl_->capture && value.impl_->capture->impl_ && + value.impl_->capture->impl_->capture; + case ProcessPlannedValueKind::LiveSlot: + return value.impl_->liveSlot && value.impl_->liveSlot->impl_ && + value.impl_->liveSlot->impl_->slot; + case ProcessPlannedValueKind::Synthetic: + return value.impl_->synthetic && value.impl_->synthetic->impl_ && + value.impl_->synthetic->impl_->occurrence && + value.impl_->synthetic->impl_->coordinate && + value.impl_->synthetic->impl_->coordinate->impl_ && + validOccurrence(*value.impl_->synthetic->impl_->occurrence); + case ProcessPlannedValueKind::Constant: + return value.impl_->constant && value.impl_->constant->impl_; + } + return false; + }; + + auto validPayloadArm = [](const ProcessGeneratedCalleePayload &payload) { + if (!payload.impl_) + return false; + auto &p = *payload.impl_; + unsigned active = static_cast(p.recordCreate.has_value()) + + static_cast(p.recordGet.has_value()) + + static_cast(p.recordWith.has_value()) + + static_cast(p.packetSerialize.has_value()) + + static_cast(p.packetDeserialize.has_value()) + + static_cast(p.traceDecode.has_value()) + + static_cast(p.queueTrySend.has_value()) + + static_cast(p.queueTryRecv.has_value()) + + static_cast(p.eventSchedule.has_value()) + + static_cast(p.traceOpen.has_value()) + + static_cast(p.traceNext.has_value()) + + static_cast(p.traceEof.has_value()) + + static_cast(p.tracePosition.has_value()) + + static_cast(p.contractRequire.has_value()) + + static_cast(p.contractEnsure.has_value()) + + static_cast(p.contractAssert.has_value()) + + static_cast(p.probe.has_value()) + + static_cast(p.statAdd.has_value()) + + static_cast(p.wakeCondition.has_value()) + + static_cast(p.wakeResource.has_value()) + + static_cast(p.wakeEventQueue.has_value()) + + static_cast(p.wakeNextDelta.has_value()) + + static_cast(p.scalarWrap.has_value()) + + static_cast(p.scalarUnwrap.has_value()); + if (active != 1) + return false; + auto present = [](const auto &arm) { return arm && arm->impl_; }; + switch (p.role) { + case ProcessHelperRole::RecordCreate: + return present(p.recordCreate) && + llvm::all_of(p.recordCreate->impl_->fields, + [](const ProcessRecordFieldDescriptor &field) { + return static_cast(field.impl_); + }); + case ProcessHelperRole::RecordGet: + return present(p.recordGet); + case ProcessHelperRole::RecordWith: + return present(p.recordWith); + case ProcessHelperRole::PacketSerialize: + return present(p.packetSerialize); + case ProcessHelperRole::PacketDeserialize: + return present(p.packetDeserialize); + case ProcessHelperRole::TraceDecode: + return present(p.traceDecode); + case ProcessHelperRole::QueueTrySend: + return present(p.queueTrySend); + case ProcessHelperRole::QueueTryRecv: + return present(p.queueTryRecv); + case ProcessHelperRole::EventSchedule: + return present(p.eventSchedule); + case ProcessHelperRole::TraceOpen: + return present(p.traceOpen); + case ProcessHelperRole::TraceNext: + return present(p.traceNext); + case ProcessHelperRole::TraceEof: + return present(p.traceEof); + case ProcessHelperRole::TracePosition: + return present(p.tracePosition); + case ProcessHelperRole::ContractRequire: + return present(p.contractRequire); + case ProcessHelperRole::ContractEnsure: + return present(p.contractEnsure); + case ProcessHelperRole::ContractAssert: + return present(p.contractAssert); + case ProcessHelperRole::Probe: + return present(p.probe); + case ProcessHelperRole::StatAdd: + return present(p.statAdd); + case ProcessHelperRole::WakeCondition: + return present(p.wakeCondition); + case ProcessHelperRole::WakeResource: + return present(p.wakeResource); + case ProcessHelperRole::WakeEventQueue: + return present(p.wakeEventQueue); + case ProcessHelperRole::WakeNextDelta: + return present(p.wakeNextDelta); + case ProcessHelperRole::ScalarWrap: + return present(p.scalarWrap); + case ProcessHelperRole::ScalarUnwrap: + return present(p.scalarUnwrap); + } + return false; + }; + + for (const ProcessGeneratedCalleePlan &callee : plans.impl_->callees) + if (!callee.impl_ || !callee.impl_->id || !callee.impl_->payload || + !validPayloadArm(*callee.impl_->payload)) + return "process-state plan invariant violated: callee specialization " + "mismatch"; + for (const ProcessValueTypePlan &type : plans.impl_->valueTypes) { + if (!type.impl_ || !type.impl_->id || !type.impl_->payload) + return "process-state plan invariant violated: value-type specialization " + "mismatch"; + if (!type.impl_->payload->impl_) + return "process-state plan invariant violated: value-type specialization " + "mismatch"; + auto &payload = *type.impl_->payload->impl_; + unsigned active = static_cast(payload.value.has_value()) + + static_cast(payload.packet.has_value()); + if (active != 1 || + (payload.kind == ProcessValueTypeKind::Value && + (!payload.value || !payload.value->impl_)) || + (payload.kind == ProcessValueTypeKind::Packet && + (!payload.packet || !payload.packet->impl_))) + return "process-state plan invariant violated: value-type specialization " + "mismatch"; + auto members = payload.kind == ProcessValueTypeKind::Value + ? llvm::ArrayRef(payload.value->impl_->members) + : llvm::ArrayRef(payload.packet->impl_->members); + if (llvm::any_of(members, [](const ProcessValueTypeMemberPlan &member) { + return !member.impl_; + })) + return "process-state plan invariant violated: value-type specialization " + "mismatch"; + } + + for (const ProcessStatePlan &plan : plans.impl_->processes) { + if (!plan.impl_ || !plan.impl_->process || !plan.impl_->entryPc || + plan.impl_->entryPc->value() >= plan.impl_->pcs.size()) + return "process-state plan invariant violated: definition key mismatch"; + for (const ProcessCapturePlan &capture : plan.impl_->captures) + if (!capture.impl_ || !capture.impl_->id || !capture.impl_->type || + !capture.impl_->operand || !capture.impl_->entryArgument) + return "process-state plan invariant violated: dangling reference"; + for (const ProcessPcPlan &pc : plan.impl_->pcs) { + if (!pc.impl_ || !pc.impl_->id) + return "process-state plan invariant violated: non-dense ordinal"; + if (llvm::any_of(pc.impl_->blocks, [&](ProcessBlockId block) { + return block.value() >= plan.impl_->blocks.size(); + })) + return "process-state plan invariant violated: ID kind mismatch"; + } + for (const ProcessBlockPlan &block : plan.impl_->blocks) { + if (!block.impl_ || !block.impl_->id || !block.impl_->pc || + block.impl_->pc->value() >= plan.impl_->pcs.size() || + !block.impl_->edge) + return "process-state plan invariant violated: dangling reference"; + if (!block.impl_->edge->impl_) + return "process-state plan invariant violated: invalid edge binding"; + if (!validEdgeShape(*block.impl_->edge)) + return "process-state plan invariant violated: invalid edge binding"; + auto &edge = *block.impl_->edge->impl_; + auto validBinding = [&](const ProcessForwardingBindingPlan &binding) { + return binding.impl_ && binding.impl_->from && binding.impl_->to && + validPlannedValue(*binding.impl_->from) && + validPlannedValue(*binding.impl_->to); + }; + if ((edge.condition && !validPlannedValue(*edge.condition)) || + llvm::any_of( + edge.trueBindings, + [&](const auto &binding) { return !validBinding(binding); }) || + llvm::any_of( + edge.falseBindings, + [&](const auto &binding) { return !validBinding(binding); }) || + llvm::any_of(edge.bindings, [&](const auto &binding) { + return !validBinding(binding); + })) + return "process-state plan invariant violated: invalid edge binding"; + if (edge.kind == ProcessControlEdgeKind::Suspend && + edge.transition->value() >= plan.impl_->transitions.size()) + return "process-state plan invariant violated: dangling reference"; + if ((edge.kind == ProcessControlEdgeKind::Branch && + (edge.trueBlock->value() >= plan.impl_->blocks.size() || + edge.falseBlock->value() >= plan.impl_->blocks.size())) || + (edge.kind == ProcessControlEdgeKind::LocalContinue && + edge.targetBlock->value() >= plan.impl_->blocks.size())) + return "process-state plan invariant violated: dangling reference"; + for (const ProcessActionPlan &action : block.impl_->actions) { + if (!action.impl_ || !action.impl_->occurrence || + !validOccurrence(*action.impl_->occurrence)) + return "process-state plan invariant violated: invalid action arm"; + bool calleeRequired = + action.impl_->emission == ProcessEmissionClass::Inline || + action.impl_->emission == ProcessEmissionClass::Invoke || + action.impl_->emission == ProcessEmissionClass::Wrap || + action.impl_->emission == ProcessEmissionClass::Unwrap; + bool scalarRequired = + action.impl_->emission == ProcessEmissionClass::CopyScalar; + if (action.impl_->callee.has_value() != calleeRequired || + action.impl_->scalarOp.has_value() != scalarRequired || + (action.impl_->callee && + action.impl_->callee->value() >= plans.impl_->callees.size()) || + (action.impl_->scalarOp && !action.impl_->scalarOp->impl_)) + return "process-state plan invariant violated: invalid action arm"; + if (action.impl_->scalarOp) { + auto &scalar = *action.impl_->scalarOp->impl_; + if (scalar.name.empty() || scalar.properties.empty() || + llvm::any_of(scalar.attributes, + [](const ProcessScalarAttribute &attribute) { + return !attribute.impl_; + })) + return "process-state plan invariant violated: invalid action arm"; + for (size_t index = 1; index < scalar.attributes.size(); ++index) { + auto &previous = *scalar.attributes[index - 1].impl_; + auto ¤t = *scalar.attributes[index].impl_; + if (std::tie(previous.name, previous.value) >= + std::tie(current.name, current.value)) + return "process-state plan invariant violated: invalid action " + "arm"; + } + } + bool sourceRequired = + action.impl_->kind == ProcessActionKind::Original || + action.impl_->kind == ProcessActionKind::ForInitialize || + action.impl_->kind == ProcessActionKind::ForCondition || + action.impl_->kind == ProcessActionKind::ForIncrement; + if (static_cast(action.impl_->sourceOperation) != sourceRequired) + return "process-state plan invariant violated: invalid action arm"; + bool isLoopAction = + action.impl_->kind == ProcessActionKind::ForInitialize || + action.impl_->kind == ProcessActionKind::ForCondition || + action.impl_->kind == ProcessActionKind::ForIncrement; + if (isLoopAction) { + if (!mlir::isa(action.impl_->sourceOperation)) + return "process-state plan invariant violated: invalid action arm"; + const ProcessOccurrenceId::Impl &occurrence = + *action.impl_->occurrence->impl_; + if (occurrence.kind != ProcessOccurrenceKind::SyntheticLoop || + !occurrence.syntheticLoop || + !occurrence.syntheticLoop->impl_->anchor) + return "process-state plan invariant violated: invalid action arm"; + const ProcessOccurrenceId &anchor = + *occurrence.syntheticLoop->impl_->anchor; + if (anchor.impl_->kind != ProcessOccurrenceKind::Original || + !anchor.impl_->original || + anchor.impl_->original->impl_->operation != + action.impl_->sourceOperation) + return "process-state plan invariant violated: invalid action arm"; + } + for (const ProcessPlannedValue &value : action.impl_->operands) + if (!validPlannedValue(value)) + return "process-state plan invariant violated: invalid planned " + "value arm"; + for (const ProcessPlannedValue &value : action.impl_->results) + if (!validPlannedValue(value)) + return "process-state plan invariant violated: invalid planned " + "value arm"; + } + for (const ProcessTransitionLoadPlan &load : block.impl_->loads) { + if (!load.impl_ || !load.impl_->slot || + load.impl_->slot->value() >= plan.impl_->liveSlots.size()) + return "process-state plan invariant violated: dangling reference"; + for (const ProcessPlannedValue &value : load.impl_->replacements) + if (!validPlannedValue(value)) + return "process-state plan invariant violated: invalid planned " + "value arm"; + } + for (const ProcessControlFramePlan &frame : block.impl_->frames) { + if (!frame.impl_) + return "process-state plan invariant violated: invalid frame phase"; + for (const ProcessForwardingBindingPlan &binding : + frame.impl_->bindings) + if (!binding.impl_ || !binding.impl_->from || !binding.impl_->to || + !validPlannedValue(*binding.impl_->from) || + !validPlannedValue(*binding.impl_->to)) + return "process-state plan invariant violated: dangling reference"; + } + } + for (const ProcessLiveSlotPlan &slot : plan.impl_->liveSlots) { + if (!slot.impl_ || !slot.impl_->id || !slot.impl_->storageType || + slot.impl_->storageType->value() >= plans.impl_->valueTypes.size() || + (slot.impl_->wrapCallee && + slot.impl_->wrapCallee->value() >= plans.impl_->callees.size()) || + (slot.impl_->unwrapCallee && + slot.impl_->unwrapCallee->value() >= plans.impl_->callees.size())) + return "process-state plan invariant violated: dangling reference"; + if (slot.impl_->wrapCallee.has_value() != + slot.impl_->unwrapCallee.has_value()) + return "process-state plan invariant violated: invalid live-slot " + "wrapper pair"; + for (const ProcessPlannedValue &value : slot.impl_->memberValues) + if (!validPlannedValue(value)) + return "process-state plan invariant violated: invalid planned value " + "arm"; + } + for (const ProcessWakePlan &wake : plan.impl_->wakes) { + if (!wake.impl_ || !wake.impl_->id || !wake.impl_->callee || + wake.impl_->callee->value() >= plans.impl_->callees.size() || + !wake.impl_->occurrence || !validOccurrence(*wake.impl_->occurrence)) + return "process-state plan invariant violated: invalid wake callee"; + for (const ProcessSubscriptionSourcePlan &source : wake.impl_->sources) { + if (!source.impl_) + return "process-state plan invariant violated: dangling reference"; + switch (source.impl_->kind) { + case ProcessSubscriptionSourceKind::Capture: + if (!source.impl_->capture || source.impl_->value || + source.impl_->declaration || !source.impl_->symbol.empty() || + !source.impl_->ownerPath.empty()) + return "process-state plan invariant violated: dangling reference"; + break; + case ProcessSubscriptionSourceKind::Value: + if (!source.impl_->value || source.impl_->capture || + source.impl_->declaration || !source.impl_->symbol.empty()) + return "process-state plan invariant violated: dangling reference"; + break; + case ProcessSubscriptionSourceKind::Symbol: + if (!source.impl_->declaration || source.impl_->capture || + source.impl_->value || source.impl_->symbol.empty()) + return "process-state plan invariant violated: dangling reference"; + break; + } + } + } + for (const ProcessTransitionPlan &transition : plan.impl_->transitions) { + if (!transition.impl_ || !transition.impl_->id || + !transition.impl_->sourcePc || !transition.impl_->targetPc || + !transition.impl_->wake || + transition.impl_->sourcePc->value() >= plan.impl_->pcs.size() || + transition.impl_->targetPc->value() >= plan.impl_->pcs.size() || + transition.impl_->wake->value() >= plan.impl_->wakes.size()) + return "process-state plan invariant violated: dangling reference"; + for (const ProcessTransitionStorePlan &store : transition.impl_->stores) + if (!store.impl_ || !store.impl_->slot || + store.impl_->slot->value() >= plan.impl_->liveSlots.size() || + !store.impl_->source || !validPlannedValue(*store.impl_->source)) + return "process-state plan invariant violated: dangling reference"; + for (const ProcessTransitionLoadPlan &load : transition.impl_->loads) + if (!load.impl_ || !load.impl_->slot || + load.impl_->slot->value() >= plan.impl_->liveSlots.size() || + llvm::any_of(load.impl_->replacements, + [&](const ProcessPlannedValue &value) { + return !validPlannedValue(value); + })) + return "process-state plan invariant violated: dangling reference"; + } + } + return {}; +} + +ProcessStatePlanSet cloneProcessStatePlanWithCorruptionForTest( + const ProcessStatePlanSet &plan, + ProcessStatePlanCorruptionForTest corruption) { + return detail::PlanSetBuilder::cloneWithCorruption(plan, corruption); +} + +llvm::StringRef detail::generatedCalleeSpecializationBytes( + const ProcessGeneratedCalleePlan &callee) { + return PlanSetBuilder::specializationBytes(callee); +} + +llvm::StringRef detail::generatedCalleeDescriptorBytes( + const ProcessGeneratedCalleePlan &callee) { + return PlanSetBuilder::descriptorBytes(callee); +} + +llvm::StringRef detail::lastProcessStatePlanDiagnosticForTest() { + return lastDiagnostic; +} + +mlir::LogicalResult verifyProcessStatePlan(const ProcessStatePlanSet &plans, + const ProcessStateLimits &limits) { + lastDiagnostic.clear(); + if (llvm::StringRef error = detail::PlanSetBuilder::structuralError(plans); + !error.empty()) + return reject(plans, error); + if (plans.processes().size() > limits.maxProcesses) + return reject(plans, "process-state plan capability maxProcesses exceeded"); + if (plans.callees().size() > limits.maxCalleeDescriptors) + return reject( + plans, "process-state plan capability maxCalleeDescriptors exceeded"); + llvm::StringRef previousKey; + uint64_t pcs = 0, slots = 0, wakes = 0, transitions = 0, actions = 0; + for (const ProcessStatePlan &plan : plans.processes()) { + if (!validDefinitionKey(plan.definitionKey())) + return reject( + plans, + "process-state plan invariant violated: definition key mismatch"); + ac::ModuleOp owner = plan.process()->getParentOfType(); + auto ownerName = owner ? mlir::SymbolTable::getSymbolName(owner) : nullptr; + auto processName = mlir::SymbolTable::getSymbolName(plan.process()); + if (!ownerName || !processName || + plan.definitionKey() != + ("@" + ownerName.str() + "::@" + processName.str())) + return reject( + plans, + "process-state plan invariant violated: definition key mismatch"); + if (!previousKey.empty() && previousKey.compare(plan.definitionKey()) >= 0) + return reject( + plans, + "process-state plan invariant violated: unsorted canonical order"); + previousKey = plan.definitionKey(); + auto occurrenceReferencesClose = [&](const ProcessOccurrenceId &root) { + llvm::SmallVector worklist{&root}; + while (!worklist.empty()) { + const ProcessOccurrenceId &occurrence = *worklist.pop_back_val(); + switch (occurrence.kind()) { + case ProcessOccurrenceKind::Original: + break; + case ProcessOccurrenceKind::SyntheticLoop: + worklist.push_back(&occurrence.syntheticLoop().anchor()); + break; + case ProcessOccurrenceKind::SyntheticWrapper: + if (occurrence.syntheticWrapper().transition().value() >= + plan.transitions().size() || + occurrence.syntheticWrapper().slot().value() >= + plan.liveSlots().size()) + return false; + worklist.push_back(&occurrence.syntheticWrapper().anchor()); + break; + case ProcessOccurrenceKind::SyntheticConstant: + worklist.push_back(&occurrence.syntheticConstant().anchor()); + break; + } + } + return true; + }; + auto plannedReferencesClose = [&](const ProcessPlannedValue &value) { + switch (value.kind()) { + case ProcessPlannedValueKind::Original: + return occurrenceReferencesClose(value.original().occurrence()); + case ProcessPlannedValueKind::Capture: + return value.capture().capture().value() < plan.captures().size(); + case ProcessPlannedValueKind::LiveSlot: + return value.liveSlot().slot().value() < plan.liveSlots().size(); + case ProcessPlannedValueKind::Synthetic: + return occurrenceReferencesClose(value.synthetic().occurrence()); + case ProcessPlannedValueKind::Constant: + return true; + } + return false; + }; + auto bindingsClose = + [&](llvm::ArrayRef bindings) { + return llvm::all_of( + bindings, [&](const ProcessForwardingBindingPlan &binding) { + return plannedReferencesClose(binding.from()) && + plannedReferencesClose(binding.to()) && + binding.from().type() == binding.to().type(); + }); + }; + if (plan.pcs().empty() || plan.entryPc().value() != 0 || + plan.pcs().front().id().value() != 0 || + plan.pcs().front().name() != "entry") + return reject(plans, + "process-state plan invariant violated: non-dense ordinal"); + uint32_t expectedWidth = 1; + uint32_t largestPc = static_cast(plan.pcs().size() - 1); + while (largestPc >>= 1) + ++expectedWidth; + if (plan.pcBitWidth() != expectedWidth) + return reject(plans, "process-state plan invariant violated: PC width"); + for (auto [index, pc] : llvm::enumerate(plan.pcs())) + if (pc.id().value() != index || + pc.name() != + (index == 0 ? "entry" : llvm::formatv("pc{0:D8}", index).str())) + return reject( + plans, "process-state plan invariant violated: non-dense ordinal"); + llvm::SmallVector listedBlocks(plan.blocks().size()); + for (const ProcessPcPlan &pc : plan.pcs()) { + std::optional previousBlock; + for (ProcessBlockId block : pc.blocks()) { + if (block.value() >= plan.blocks().size() || + plan.blocks()[block.value()].pc() != pc.id() || + listedBlocks[block.value()] || + (previousBlock && *previousBlock >= block.value())) + return reject( + plans, "process-state plan invariant violated: ID kind mismatch"); + listedBlocks[block.value()] = true; + previousBlock = block.value(); + } + } + if (llvm::is_contained(listedBlocks, false)) + return reject(plans, + "process-state plan invariant violated: ID kind mismatch"); + for (auto [index, capture] : llvm::enumerate(plan.captures())) + if (capture.id().value() != index || !capture.type() || + capture.name() != llvm::formatv("capture{0:D8}", index).str() || + capture.operand().getType() != capture.type() || + capture.entryArgument().getType() != capture.type()) + return reject( + plans, "process-state plan invariant violated: non-dense ordinal"); + std::set> + wrapperActions; + for (auto [index, slot] : llvm::enumerate(plan.liveSlots())) { + if (slot.id().value() != index || + slot.storageType().value() >= plans.valueTypes().size() || + !slot.type() || + slot.name() != llvm::formatv("live{0:D8}", index).str()) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + if (slot.wrapCallee()) { + if (slot.wrapCallee()->value() >= plans.callees().size() || + slot.unwrapCallee()->value() >= plans.callees().size() || + plans.callees()[slot.wrapCallee()->value()].role() != + ProcessHelperRole::ScalarWrap || + plans.callees()[slot.unwrapCallee()->value()].role() != + ProcessHelperRole::ScalarUnwrap) + return reject(plans, "process-state plan invariant violated: invalid " + "live-slot wrapper pair"); + } + bool builtinScalar = + mlir::isa(slot.type()); + if (builtinScalar != slot.wrapCallee().has_value()) + return reject(plans, "process-state plan invariant violated: invalid " + "live-slot wrapper pair"); + if (llvm::any_of(slot.memberValues(), [&](const auto &value) { + return !plannedReferencesClose(value); + })) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + } + for (auto [index, block] : llvm::enumerate(plan.blocks())) { + std::string expectedBlockPath = + (plan.definitionKey() + "/plan/pc/" + + plan.pcs()[block.pc().value()].name() + "/" + + llvm::formatv("b{0:D8}", index).str()) + .str(); + if (block.id().value() != index || + block.pc().value() >= plan.pcs().size() || + block.path() != expectedBlockPath) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + actions += block.actions().size(); + for (auto [actionIndex, action] : llvm::enumerate(block.actions())) { + if (action.id() != actionIndex) + return reject( + plans, + "process-state plan invariant violated: non-dense ordinal"); + uint32_t expectedActionCost = + action.emission() == ProcessEmissionClass::ForwardOnly ? 0 : 1; + if (action.cost() != expectedActionCost) + return reject(plans, + "process-state plan invariant violated: cost mismatch"); + if (action.callee() && + action.callee()->value() >= plans.callees().size()) + return reject( + plans, + "process-state plan invariant violated: dangling reference"); + if (action.callee()) { + ProcessHelperRole role = + plans.callees()[action.callee()->value()].role(); + ProcessEmissionClass expectedEmission = + role <= ProcessHelperRole::TraceDecode + ? ProcessEmissionClass::Inline + : role == ProcessHelperRole::ScalarWrap + ? ProcessEmissionClass::Wrap + : role == ProcessHelperRole::ScalarUnwrap + ? ProcessEmissionClass::Unwrap + : ProcessEmissionClass::Invoke; + if ((role >= ProcessHelperRole::WakeCondition && + role <= ProcessHelperRole::WakeNextDelta) || + action.emission() != expectedEmission) + return reject( + plans, + "process-state plan invariant violated: invalid action arm"); + } + if ((action.kind() == ProcessActionKind::ScalarWrap) != + (action.emission() == ProcessEmissionClass::Wrap) || + (action.kind() == ProcessActionKind::ScalarUnwrap) != + (action.emission() == ProcessEmissionClass::Unwrap) || + (action.kind() == ProcessActionKind::ForInitialize && + action.emission() != ProcessEmissionClass::ForwardOnly)) + return reject( + plans, + "process-state plan invariant violated: invalid action arm"); + if (action.kind() == ProcessActionKind::ForCondition) { + const ProcessScalarOperationPlan *scalar = action.scalarOp(); + if (action.emission() != ProcessEmissionClass::CopyScalar || + !scalar || scalar->name() != "arith.cmpi" || + scalar->properties() != "{}" || + scalar->attributes().size() != 1 || + scalar->attributes()[0].name() != "predicate" || + scalar->attributes()[0].value() != "2 : i64" || + action.operands().size() != 2 || + !llvm::all_of(action.operands(), + [](const auto &operand) { + return mlir::isa(operand.type()); + }) || + action.results().size() != 1 || + !action.results()[0].type().isInteger(1)) + return reject( + plans, + "process-state plan invariant violated: invalid action arm"); + } + if (action.kind() == ProcessActionKind::ForIncrement) { + const ProcessScalarOperationPlan *scalar = action.scalarOp(); + if (action.emission() != ProcessEmissionClass::CopyScalar || + !scalar || scalar->name() != "arith.addi" || + scalar->properties() != "{}" || !scalar->attributes().empty()) + return reject( + plans, + "process-state plan invariant violated: invalid action arm"); + if (action.operands().size() != 2 || + !llvm::all_of(action.operands(), + [](const auto &operand) { + return mlir::isa(operand.type()); + }) || + action.results().size() != 1 || + !mlir::isa(action.results()[0].type())) + return reject( + plans, + "process-state plan invariant violated: invalid action arm"); + } + ProcessOccurrenceKind expectedOccurrence = + ProcessOccurrenceKind::Original; + std::optional expectedLoopPhase; + std::optional expectedWrapperDirection; + switch (action.kind()) { + case ProcessActionKind::Original: + break; + case ProcessActionKind::Constant: + expectedOccurrence = ProcessOccurrenceKind::SyntheticConstant; + break; + case ProcessActionKind::ForInitialize: + expectedOccurrence = ProcessOccurrenceKind::SyntheticLoop; + expectedLoopPhase = ProcessLoopPhase::Initialize; + break; + case ProcessActionKind::ForCondition: + expectedOccurrence = ProcessOccurrenceKind::SyntheticLoop; + expectedLoopPhase = ProcessLoopPhase::Condition; + break; + case ProcessActionKind::ForIncrement: + expectedOccurrence = ProcessOccurrenceKind::SyntheticLoop; + expectedLoopPhase = ProcessLoopPhase::Increment; + break; + case ProcessActionKind::ScalarWrap: + expectedOccurrence = ProcessOccurrenceKind::SyntheticWrapper; + expectedWrapperDirection = ProcessWrapperDirection::Wrap; + break; + case ProcessActionKind::ScalarUnwrap: + expectedOccurrence = ProcessOccurrenceKind::SyntheticWrapper; + expectedWrapperDirection = ProcessWrapperDirection::Unwrap; + break; + } + if (action.occurrence().kind() != expectedOccurrence || + (expectedLoopPhase && action.occurrence().syntheticLoop().phase() != + *expectedLoopPhase) || + (expectedWrapperDirection && + action.occurrence().syntheticWrapper().direction() != + *expectedWrapperDirection)) + return reject( + plans, + "process-state plan invariant violated: invalid action arm"); + if (expectedWrapperDirection) { + const ProcessSyntheticWrapperOccurrence &wrapper = + action.occurrence().syntheticWrapper(); + const ProcessLiveSlotPlan &slot = + plan.liveSlots()[wrapper.slot().value()]; + std::optional expectedCallee = + *expectedWrapperDirection == ProcessWrapperDirection::Wrap + ? slot.wrapCallee() + : slot.unwrapCallee(); + if (!expectedCallee || action.callee() != expectedCallee || + action.operands().size() != 1 || action.results().size() != 1) + return reject( + plans, + "process-state plan invariant violated: invalid action arm"); + wrapperActions.emplace(wrapper.transition().value(), + wrapper.slot().value(), + *expectedWrapperDirection); + } + if (action.resultTypes().size() != action.results().size()) + return reject( + plans, "process-state plan invariant violated: wrong type key"); + for (auto [type, result] : + llvm::zip_equal(action.resultTypes(), action.results())) + if (type != result.type()) + return reject( + plans, "process-state plan invariant violated: wrong type key"); + if (llvm::any_of(action.operands(), + [&](const auto &value) { + return !plannedReferencesClose(value); + }) || + llvm::any_of(action.results(), [&](const auto &value) { + return !plannedReferencesClose(value); + })) + return reject( + plans, + "process-state plan invariant violated: dangling reference"); + } + for (const ProcessControlFramePlan &frame : block.frames()) { + bool legal = (frame.kind() == ProcessFrameKind::Entry && + frame.phase() == ProcessFramePhase::Entry) || + (frame.kind() == ProcessFrameKind::ScfIf && + (frame.phase() == ProcessFramePhase::Then || + frame.phase() == ProcessFramePhase::Else || + frame.phase() == ProcessFramePhase::Merge)) || + (frame.kind() == ProcessFrameKind::ScfFor && + (frame.phase() == ProcessFramePhase::Header || + frame.phase() == ProcessFramePhase::Body || + frame.phase() == ProcessFramePhase::Exit)) || + (frame.kind() == ProcessFrameKind::ScfWhile && + (frame.phase() == ProcessFramePhase::Before || + frame.phase() == ProcessFramePhase::After || + frame.phase() == ProcessFramePhase::Exit)); + if (!legal) + return reject( + plans, + "process-state plan invariant violated: invalid frame phase"); + if (!bindingsClose(frame.bindings())) + return reject( + plans, + "process-state plan invariant violated: dangling reference"); + } + if (!detail::PlanSetBuilder::validEdgeShape(block.edge())) + return reject( + plans, + "process-state plan invariant violated: invalid edge binding"); + const ProcessControlEdgePlan &edge = block.edge(); + if ((edge.kind() == ProcessControlEdgeKind::Branch && + (edge.trueBlock().value() >= plan.blocks().size() || + edge.falseBlock().value() >= plan.blocks().size() || + !plannedReferencesClose(edge.condition()))) || + (edge.kind() == ProcessControlEdgeKind::LocalContinue && + edge.targetBlock().value() >= plan.blocks().size())) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + if (!bindingsClose(edge.trueBindings()) || + !bindingsClose(edge.falseBindings()) || + !bindingsClose(edge.bindings())) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + for (const ProcessTransitionLoadPlan &load : block.loads()) + if (load.slot().value() >= plan.liveSlots().size() || + llvm::any_of(load.replacements(), [&](const auto &value) { + return !plannedReferencesClose(value); + })) + return reject( + plans, + "process-state plan invariant violated: dangling reference"); + for (size_t i = 1; i < block.loads().size(); ++i) + if (block.loads()[i - 1].slot().value() >= + block.loads()[i].slot().value()) + return reject(plans, "process-state plan invariant violated: " + "unsorted canonical order"); + uint64_t expectedCost = block.loads().size(); + for (const ProcessActionPlan &action : block.actions()) + expectedCost += action.cost(); + if (block.edge().kind() == ProcessControlEdgeKind::Suspend) { + const ProcessTransitionPlan &transition = + plan.transitions()[block.edge().transition().value()]; + expectedCost += transition.stores().size() + 2; + } else { + ++expectedCost; + } + if (block.cost() != expectedCost) + return reject(plans, + "process-state plan invariant violated: cost mismatch"); + } + for (auto [index, wake] : llvm::enumerate(plan.wakes())) { + if (wake.id().value() != index || + wake.callee().value() >= plans.callees().size()) + return reject( + plans, + "process-state plan invariant violated: invalid wake callee"); + if (wake.typeKey() != wakeTypeKey(wake.kind())) + return reject(plans, + "process-state plan invariant violated: wrong type key"); + if (!occurrenceReferencesClose(wake.occurrence()) || + llvm::any_of(wake.sources(), [&](const auto &source) { + return source.capture() && + source.capture()->value() >= plan.captures().size(); + })) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + bool rawHandlesMatch = false; + switch (wake.kind()) { + case ProcessWakeKind::Condition: + rawHandlesMatch = static_cast(wake.triggeringValue()) && + wake.declaration() == nullptr && + !wake.target().empty(); + break; + case ProcessWakeKind::Resource: + case ProcessWakeKind::EventQueue: + rawHandlesMatch = !wake.triggeringValue() && + wake.declaration() != nullptr && + !wake.target().empty(); + break; + case ProcessWakeKind::NextDelta: + rawHandlesMatch = !wake.triggeringValue() && + wake.declaration() == nullptr && + wake.target().empty(); + break; + } + if (!rawHandlesMatch) + return reject(plans, + "process-state plan invariant violated: wrong type key"); + const ProcessGeneratedCalleePlan &callee = + plans.callees()[wake.callee().value()]; + ProcessHelperRole expectedRole = static_cast( + static_cast(ProcessHelperRole::WakeCondition) + + static_cast(wake.kind())); + if (callee.role() != expectedRole || !callee.inputTypeKeys().empty() || + callee.resultTypeKeys().size() != 1 || + callee.resultTypeKeys().front() != wake.typeKey()) + return reject( + plans, + "process-state plan invariant violated: invalid wake callee"); + } + for (auto [index, transition] : llvm::enumerate(plan.transitions())) { + if (transition.id().value() != index || + transition.sourcePc().value() >= plan.pcs().size() || + transition.targetPc().value() >= plan.pcs().size() || + transition.wake().value() >= plan.wakes().size()) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + for (const ProcessTransitionStorePlan &store : transition.stores()) + if (store.slot().value() >= plan.liveSlots().size() || + !plannedReferencesClose(store.source())) + return reject( + plans, + "process-state plan invariant violated: dangling reference"); + else if (plan.liveSlots()[store.slot().value()].wrapCallee() && + !wrapperActions.contains({transition.id().value(), + store.slot().value(), + ProcessWrapperDirection::Wrap})) + return reject(plans, "process-state plan invariant violated: missing " + "scalar wrapper action"); + for (const ProcessTransitionLoadPlan &load : transition.loads()) + if (load.slot().value() >= plan.liveSlots().size() || + llvm::any_of(load.replacements(), [&](const auto &value) { + return !plannedReferencesClose(value); + })) + return reject( + plans, + "process-state plan invariant violated: dangling reference"); + else if (plan.liveSlots()[load.slot().value()].unwrapCallee() && + !wrapperActions.contains({transition.id().value(), + load.slot().value(), + ProcessWrapperDirection::Unwrap})) + return reject(plans, "process-state plan invariant violated: missing " + "scalar wrapper action"); + for (size_t i = 1; i < transition.stores().size(); ++i) + if (transition.stores()[i - 1].slot().value() >= + transition.stores()[i].slot().value()) + return reject(plans, "process-state plan invariant violated: " + "unsorted canonical order"); + for (size_t i = 1; i < transition.loads().size(); ++i) + if (transition.loads()[i - 1].slot().value() >= + transition.loads()[i].slot().value()) + return reject(plans, "process-state plan invariant violated: " + "unsorted canonical order"); + } + uint64_t expectedFairness = 0; + for (const ProcessPcPlan &pc : plan.pcs()) { + if (pc.blocks().empty() || + plan.blocks()[pc.blocks().front().value()].path() != pc.entryPath()) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + llvm::SmallVector indegree(plan.blocks().size()); + llvm::SmallVector> distance(plan.blocks().size()); + bool invalidGraph = false; + auto successors = [](const ProcessControlEdgePlan &edge) { + llvm::SmallVector result; + if (edge.kind() == ProcessControlEdgeKind::Branch) { + result.push_back(edge.trueBlock()); + result.push_back(edge.falseBlock()); + } else if (edge.kind() == ProcessControlEdgeKind::LocalContinue) { + result.push_back(edge.targetBlock()); + } + return result; + }; + for (ProcessBlockId id : pc.blocks()) { + for (ProcessBlockId successor : + successors(plan.blocks()[id.value()].edge())) { + if (plan.blocks()[successor.value()].pc() != pc.id()) { + invalidGraph = true; + continue; + } + ++indegree[successor.value()]; + } + } + llvm::SmallVector ready; + for (ProcessBlockId id : pc.blocks()) + if (indegree[id.value()] == 0) + ready.push_back(id); + distance[pc.blocks().front().value()] = + plan.blocks()[pc.blocks().front().value()].cost(); + size_t processed = 0; + uint64_t pcWork = 0; + for (size_t cursor = 0; cursor < ready.size(); ++cursor) { + ProcessBlockId id = ready[cursor]; + ++processed; + if (distance[id.value()]) + pcWork = std::max(pcWork, *distance[id.value()]); + for (ProcessBlockId successor : + successors(plan.blocks()[id.value()].edge())) { + if (distance[id.value()]) { + uint64_t cost = plan.blocks()[successor.value()].cost(); + if (*distance[id.value()] > + std::numeric_limits::max() - cost) { + invalidGraph = true; + } else { + uint64_t candidate = *distance[id.value()] + cost; + if (!distance[successor.value()] || + candidate > *distance[successor.value()]) + distance[successor.value()] = candidate; + } + } + if (--indegree[successor.value()] == 0) + ready.push_back(successor); + } + } + if (processed != pc.blocks().size()) + invalidGraph = true; + for (ProcessBlockId id : pc.blocks()) + if (!distance[id.value()]) + invalidGraph = true; + if (invalidGraph) + return reject(plans, + "process-state plan invariant violated: cost mismatch"); + expectedFairness = std::max(expectedFairness, pcWork); + } + if (plan.fairnessWork() != expectedFairness || expectedFairness == 0) + return reject(plans, + "process-state plan invariant violated: cost mismatch"); + if (plan.fairnessWork() > limits.maxFairnessWork) + return reject(plans, + "process-state plan capability maxFairnessWork exceeded"); + pcs += plan.pcs().size(); + slots += plan.liveSlots().size(); + wakes += plan.wakes().size(); + transitions += plan.transitions().size(); + } + if (pcs > limits.maxProgramCounters) + return reject(plans, + "process-state plan capability maxProgramCounters exceeded"); + if (slots > limits.maxLiveSlots) + return reject(plans, "process-state plan capability maxLiveSlots exceeded"); + if (wakes > limits.maxWakeRecords) + return reject(plans, + "process-state plan capability maxWakeRecords exceeded"); + if (transitions > limits.maxTransitions) + return reject(plans, + "process-state plan capability maxTransitions exceeded"); + if (actions > limits.maxPlannedOperations) + return reject( + plans, "process-state plan capability maxPlannedOperations exceeded"); + llvm::StringRef previousSpecialization; + for (auto [index, callee] : llvm::enumerate(plans.callees())) { + if (callee.id().value() < index) + return reject(plans, + "process-state plan invariant violated: duplicate ordinal"); + if (callee.id().value() > index) + return reject(plans, + "process-state plan invariant violated: non-dense ordinal"); + if (!previousSpecialization.empty() && + previousSpecialization.compare( + detail::generatedCalleeSpecializationBytes(callee)) >= 0) + return reject( + plans, + previousSpecialization == + detail::generatedCalleeSpecializationBytes(callee) + ? "process-state plan invariant violated: duplicate identity" + : "process-state plan invariant violated: unsorted canonical " + "order"); + previousSpecialization = detail::generatedCalleeSpecializationBytes(callee); + ProcessEffectKind expectedEffect = + callee.role() <= ProcessHelperRole::TraceDecode || + callee.role() >= ProcessHelperRole::ScalarWrap + ? ProcessEffectKind::Pure + : ProcessEffectKind::Stateful; + if (callee.effect() != expectedEffect) + return reject(plans, + "process-state plan invariant violated: effect mismatch"); + if (callee.kind() != "implementation" || !validCalleeSemantics(callee) || + llvm::any_of(callee.inputTypeKeys(), + [](llvm::StringRef key) { return !validTypeKey(key); }) || + llvm::any_of(callee.resultTypeKeys(), + [](llvm::StringRef key) { return !validTypeKey(key); }) || + !llvm::is_sorted(callee.sourcePaths()) || + std::adjacent_find(callee.sourcePaths().begin(), + callee.sourcePaths().end()) != + callee.sourcePaths().end() || + callee.sourceOperations().size() != callee.sourcePaths().size()) + return reject(plans, "process-state plan invariant violated: callee " + "specialization mismatch"); + bool ownsDeclaration = + callee.role() <= ProcessHelperRole::PacketDeserialize || + callee.role() == ProcessHelperRole::QueueTrySend || + callee.role() == ProcessHelperRole::QueueTryRecv || + callee.role() == ProcessHelperRole::EventSchedule || + callee.role() == ProcessHelperRole::Probe || + callee.role() == ProcessHelperRole::StatAdd; + llvm::SmallPtrSet uniqueDeclarations; + if ((ownsDeclaration && callee.declarations().empty()) || + (!ownsDeclaration && !callee.declarations().empty()) || + llvm::any_of(callee.sourceOperations(), + [](mlir::Operation *op) { return op == nullptr; }) || + llvm::any_of(callee.declarations(), [&](mlir::Operation *op) { + return op == nullptr || !uniqueDeclarations.insert(op).second; + })) + return reject(plans, "process-state plan invariant violated: callee " + "specialization mismatch"); + auto canonical = detail::canonicalGeneratedCalleeSpecialization(callee); + if (!canonical) { + llvm::consumeError(canonical.takeError()); + return reject(plans, "process-state plan invariant violated: callee " + "specialization mismatch"); + } + std::string fingerprint = bindings::sha256Fingerprint(*canonical); + llvm::StringRef digest = llvm::StringRef(fingerprint).drop_front(7); + std::string expectedSymbol = + ("@acir_impl_" + helperRoleSpelling(callee.role()) + "_" + digest) + .str(); + std::string expectedCpp = ("acir::generated::impl_" + + helperRoleSpelling(callee.role()) + "_" + digest) + .str(); + if (*canonical != detail::generatedCalleeSpecializationBytes(callee) || + fingerprint != callee.fingerprint() || + callee.symbol() != expectedSymbol || callee.cpp() != expectedCpp) + return reject(plans, "process-state plan invariant violated: callee " + "specialization mismatch"); + } + previousSpecialization = {}; + llvm::StringSet<> generatedTypeKeys; + for (auto [index, type] : llvm::enumerate(plans.valueTypes())) { + if (type.id().value() != index) + return reject(plans, + "process-state plan invariant violated: non-dense ordinal"); + llvm::StringRef specialization = + detail::PlanSetBuilder::specializationBytes(type); + if (!previousSpecialization.empty() && + previousSpecialization.compare(specialization) >= 0) + return reject( + plans, + previousSpecialization == specialization + ? "process-state plan invariant violated: duplicate identity" + : "process-state plan invariant violated: unsorted canonical " + "order"); + previousSpecialization = specialization; + auto canonical = detail::canonicalValueTypeSpecialization(type); + if (!canonical) { + llvm::consumeError(canonical.takeError()); + return reject(plans, "process-state plan invariant violated: value-type " + "specialization mismatch"); + } + std::string fingerprint = bindings::sha256Fingerprint(*canonical); + llvm::StringRef digest = llvm::StringRef(fingerprint).drop_front(7); + llvm::StringRef stem = + type.kind() == ProcessValueTypeKind::Value ? "value" : "packet"; + std::string expectedSymbol = ("@acir_" + stem + "_" + digest).str(); + std::string expectedCpp = ("acir::generated::" + stem + "_" + digest).str(); + if (type.kind() != type.payload().kind() || !type.acirType() || + *canonical != specialization || fingerprint != type.fingerprint() || + type.symbol() != expectedSymbol || type.cpp() != expectedCpp) + return reject( + plans, + "process-state plan invariant violated: value-type specialization " + "mismatch"); + generatedTypeKeys.insert(("storage:" + stem + ":" + digest).str()); + uint64_t width = type.kind() == ProcessValueTypeKind::Value + ? type.payload().value().widthBits() + : type.payload().packet().widthBits(); + auto members = type.kind() == ProcessValueTypeKind::Value + ? type.payload().value().members() + : type.payload().packet().members(); + if ((type.kind() == ProcessValueTypeKind::Packet && + (type.payload().packet().bytes() > UINT64_MAX / 8 || + type.payload().packet().bytes() * 8 != width)) || + llvm::any_of(members, [&](const ProcessValueTypeMemberPlan &member) { + bool shape = + member.kind() == ProcessValueTypeMemberKind::Field + ? !member.name().empty() && !member.index() + : member.name().empty() && member.index().has_value(); + return !shape || member.offsetBits() > width || + member.widthBits() > width - member.offsetBits() || + member.encoding().empty() || !validTypeKey(member.typeKey()); + })) + return reject(plans, "process-state plan invariant violated: value-type " + "specialization mismatch"); + } + auto referenceCloses = [&](llvm::StringRef key) { + return !key.starts_with("storage:") || generatedTypeKeys.contains(key); + }; + for (const ProcessGeneratedCalleePlan &callee : plans.callees()) + if (llvm::any_of( + callee.inputTypeKeys(), + [&](llvm::StringRef key) { return !referenceCloses(key); }) || + llvm::any_of(callee.resultTypeKeys(), [&](llvm::StringRef key) { + return !referenceCloses(key); + })) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + for (const ProcessValueTypePlan &type : plans.valueTypes()) { + auto members = type.kind() == ProcessValueTypeKind::Value + ? type.payload().value().members() + : type.payload().packet().members(); + if (llvm::any_of(members, [&](const ProcessValueTypeMemberPlan &member) { + return !referenceCloses(member.typeKey()); + })) + return reject( + plans, "process-state plan invariant violated: dangling reference"); + } + return mlir::success(); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Analysis/ProcessStatePlanInternal.h b/components/agentic-circuit/lib/Analysis/ProcessStatePlanInternal.h new file mode 100644 index 00000000..766a3a7a --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStatePlanInternal.h @@ -0,0 +1,554 @@ +#ifndef ACIR_ANALYSIS_PROCESSSTATEPLANINTERNAL_H +#define ACIR_ANALYSIS_PROCESSSTATEPLANINTERNAL_H + +#include "Dialect/ACIR/ProcessLowerability.h" +#include "ProcessStatePlanTestHooks.h" + +#include + +namespace acir { + +struct ProcessCallSitePlan::Impl { + mlir::Operation *operation = nullptr; + std::string operationPath; + std::vector iterationVector; +}; +struct ProcessOriginalOccurrence::Impl { + mlir::Operation *operation = nullptr; + std::string operationPath; + std::vector callSites; + std::vector iterationVector; +}; +struct ProcessSyntheticLoopOccurrence::Impl { + std::optional anchor; + ProcessLoopPhase phase = ProcessLoopPhase::Initialize; +}; +struct ProcessSyntheticWrapperOccurrence::Impl { + std::optional anchor; + std::optional transition; + std::optional slot; + ProcessWrapperDirection direction = ProcessWrapperDirection::Wrap; +}; +struct ProcessSyntheticConstantOccurrence::Impl { + std::optional anchor; + uint32_t constant = 0; +}; +struct ProcessOccurrenceId::Impl { + ProcessOccurrenceKind kind = ProcessOccurrenceKind::Original; + std::optional original; + std::optional syntheticLoop; + std::optional syntheticWrapper; + std::optional syntheticConstant; +}; +struct ProcessValueCoordinate::Impl { + ProcessValueCoordinateKind kind = ProcessValueCoordinateKind::Result; + std::string ownerPath; + uint32_t index = 0; +}; +struct ProcessOriginalPlannedValue::Impl { + mlir::Value value; + std::optional occurrence; + std::optional coordinate; + std::string path; +}; +struct ProcessCapturePlannedValue::Impl { + std::optional capture; +}; +struct ProcessLiveSlotPlannedValue::Impl { + std::optional slot; +}; +struct ProcessSyntheticPlannedValue::Impl { + std::optional occurrence; + std::optional coordinate; +}; +struct ProcessConstantPlannedValue::Impl { + std::string value; +}; +struct ProcessPlannedValue::Impl { + ProcessPlannedValueKind kind = ProcessPlannedValueKind::Constant; + mlir::Type type; + std::optional original; + std::optional capture; + std::optional liveSlot; + std::optional synthetic; + std::optional constant; +}; +struct ProcessScalarAttribute::Impl { + std::string name; + std::string value; +}; +struct ProcessScalarOperationPlan::Impl { + std::string name; + std::vector attributes; + std::string properties; +}; +struct ProcessCapturePlan::Impl { + std::optional id; + std::string name; + mlir::Value operand; + mlir::Value entryArgument; + mlir::Type type; + std::string operandPath; + std::string argumentPath; +}; +struct ProcessActionPlan::Impl { + uint32_t id = 0; + ProcessActionKind kind = ProcessActionKind::Original; + ProcessEmissionClass emission = ProcessEmissionClass::ForwardOnly; + std::optional occurrence; + mlir::Operation *sourceOperation = nullptr; + std::vector iterationVector; + std::vector operands; + std::vector results; + uint32_t cost = 0; + std::vector resultTypes; + std::optional callee; + std::optional scalarOp; +}; +struct ProcessLiveSlotPlan::Impl { + std::optional id; + std::string name; + mlir::Type type; + std::optional storageType; + std::vector memberValues; + std::optional wrapCallee; + std::optional unwrapCallee; +}; +struct ProcessSubscriptionSourcePlan::Impl { + ProcessSubscriptionSourceKind kind = ProcessSubscriptionSourceKind::Value; + mlir::Value value; + mlir::Operation *owner = nullptr; + mlir::Operation *declaration = nullptr; + std::optional capture; + std::string symbol; + std::string path; + std::string ownerPath; +}; +struct ProcessWakePlan::Impl { + std::optional id; + ProcessWakeKind kind = ProcessWakeKind::NextDelta; + mlir::Operation *operation = nullptr; + mlir::Value triggeringValue; + mlir::Operation *declaration = nullptr; + std::optional callee; + std::string typeKey; + std::string operationPath; + std::string target; + std::optional occurrence; + std::vector iterationVector; + std::vector sources; +}; +struct ProcessTransitionStorePlan::Impl { + std::optional slot; + std::optional source; + mlir::Value sourceValue; +}; +struct ProcessTransitionLoadPlan::Impl { + std::optional slot; + std::vector replacements; +}; +struct ProcessTransitionPlan::Impl { + std::optional id; + std::optional sourcePc; + std::optional targetPc; + std::optional wake; + std::vector iterationVector; + std::vector stores; + std::vector loads; +}; +struct ProcessForwardingBindingPlan::Impl { + std::optional from; + std::optional to; +}; +struct ProcessControlFramePlan::Impl { + ProcessFrameKind kind = ProcessFrameKind::Entry; + ProcessFramePhase phase = ProcessFramePhase::Entry; + mlir::Operation *operation = nullptr; + std::string operationPath; + std::vector bindings; +}; +struct ProcessControlEdgePlan::Impl { + ProcessControlEdgeKind kind = ProcessControlEdgeKind::Terminate; + std::optional condition; + std::optional trueBlock; + std::optional falseBlock; + std::vector trueBindings; + std::vector falseBindings; + std::optional targetBlock; + std::vector bindings; + std::optional transition; + ProcessTerminateStatus status = ProcessTerminateStatus::Success; +}; +struct ProcessBlockPlan::Impl { + std::optional id; + std::optional pc; + mlir::Region *originRegion = nullptr; + mlir::Block *originBlock = nullptr; + std::string path; + std::vector frames; + std::vector loads; + std::vector actions; + std::optional edge; + uint64_t cost = 0; +}; +struct ProcessPcPlan::Impl { + std::optional id; + std::string name; + std::string entryPath; + std::vector blocks; +}; +struct ProcessStatePlan::Impl { + std::string definitionKey; + ac::ProcessOp process; + std::vector captures; + std::optional entryPc; + std::vector pcs; + std::vector blocks; + std::vector liveSlots; + std::vector wakes; + std::vector transitions; + uint32_t pcBitWidth = 1; + uint64_t fairnessWork = 1; +}; +struct ProcessRecordFieldDescriptor::Impl { + std::string name; + std::string typeKey; +}; +struct ProcessRecordCreatePayload::Impl { + std::vector fields; + std::string recordType; +}; +struct ProcessRecordGetPayload::Impl { + std::string field, record, result; +}; +struct ProcessRecordWithPayload::Impl { + std::string field, record, value; +}; +struct ProcessPacketSerializePayload::Impl { + uint64_t bytes = 0; + std::string packet, packetType; +}; +struct ProcessPacketDeserializePayload::Impl { + uint64_t bytes = 0; + std::string packet, packetType; +}; +struct ProcessTraceDecodePayload::Impl { + std::string entry, result, source; +}; +struct ProcessQueueTrySendPayload::Impl { + std::string element, queue; +}; +struct ProcessQueueTryRecvPayload::Impl { + std::string element, queue; +}; +struct ProcessEventSchedulePayload::Impl { + std::string delay, target, value; +}; +struct ProcessTraceOpenPayload::Impl { + std::string source; +}; +struct ProcessTraceNextPayload::Impl { + std::string entry, source; +}; +struct ProcessTraceEofPayload::Impl { + std::string source; +}; +struct ProcessTracePositionPayload::Impl { + std::string source; +}; +struct ProcessContractRequirePayload::Impl { + std::string message; +}; +struct ProcessContractEnsurePayload::Impl { + std::string message; +}; +struct ProcessContractAssertPayload::Impl { + std::string message; +}; +struct ProcessProbePayload::Impl { + std::string kind, result, target; +}; +struct ProcessStatAddPayload::Impl { + std::string stat, valueType; +}; +struct ProcessWakeConditionPayload::Impl { + ProcessWakeKind wakeKind = ProcessWakeKind::Condition; + std::string wakeType; +}; +struct ProcessWakeResourcePayload::Impl { + ProcessWakeKind wakeKind = ProcessWakeKind::Resource; + std::string wakeType; +}; +struct ProcessWakeEventQueuePayload::Impl { + ProcessWakeKind wakeKind = ProcessWakeKind::EventQueue; + std::string wakeType; +}; +struct ProcessWakeNextDeltaPayload::Impl { + ProcessWakeKind wakeKind = ProcessWakeKind::NextDelta; + std::string wakeType; +}; +struct ProcessScalarWrapPayload::Impl { + ProcessWrapperDirection direction = ProcessWrapperDirection::Wrap; + std::string scalar, valueType; +}; +struct ProcessScalarUnwrapPayload::Impl { + ProcessWrapperDirection direction = ProcessWrapperDirection::Unwrap; + std::string scalar, valueType; +}; +struct ProcessGeneratedCalleePayload::Impl { + ProcessHelperRole role = ProcessHelperRole::WakeNextDelta; + std::optional recordCreate; + std::optional recordGet; + std::optional recordWith; + std::optional packetSerialize; + std::optional packetDeserialize; + std::optional traceDecode; + std::optional queueTrySend; + std::optional queueTryRecv; + std::optional eventSchedule; + std::optional traceOpen; + std::optional traceNext; + std::optional traceEof; + std::optional tracePosition; + std::optional contractRequire; + std::optional contractEnsure; + std::optional contractAssert; + std::optional probe; + std::optional statAdd; + std::optional wakeCondition; + std::optional wakeResource; + std::optional wakeEventQueue; + std::optional wakeNextDelta; + std::optional scalarWrap; + std::optional scalarUnwrap; +}; +struct ProcessValueTypeMemberPlan::Impl { + ProcessValueTypeMemberKind kind = ProcessValueTypeMemberKind::Field; + std::string name; + std::optional index; + uint64_t offsetBits = 0; + uint64_t widthBits = 0; + std::optional signedness; + std::string encoding; + std::string typeKey; +}; +struct ProcessStorageValuePayload::Impl { + std::vector members; + uint64_t widthBits = 0; + std::string encoding; +}; +struct ProcessStoragePacketPayload::Impl { + std::vector members; + uint64_t widthBits = 0; + uint64_t bytes = 0; + std::string encoding; +}; +struct ProcessValueTypePayload::Impl { + ProcessValueTypeKind kind = ProcessValueTypeKind::Value; + std::optional value; + std::optional packet; +}; +struct ProcessGeneratedCalleePlan::Impl { + std::optional id; + std::string symbol, cpp, kind = "implementation", fingerprint; + ProcessEffectKind effect = ProcessEffectKind::Pure; + std::vector inputTypeKeyStorage; + std::vector inputTypeKeys; + std::vector resultTypeKeyStorage; + std::vector resultTypeKeys; + ProcessHelperRole role = ProcessHelperRole::WakeNextDelta; + std::optional payload; + std::vector sourceOperations; + std::vector declarations; + std::vector sourcePathStorage; + std::vector sourcePaths; + std::string specializationBytes; + std::string descriptorBytes; +}; +struct ProcessValueTypePlan::Impl { + std::optional id; + std::string symbol, cpp, fingerprint; + ProcessValueTypeKind kind = ProcessValueTypeKind::Value; + mlir::Type acirType; + std::optional payload; + std::string specializationBytes; +}; +struct ProcessStatePlanSet::Impl { + std::vector processes; + std::vector callees; + std::vector valueTypes; +}; + +namespace detail { + +struct ExpandedForwarding { + ProcessPlannedValue from; + ProcessPlannedValue to; +}; + +struct ExpandedAction { + ProcessActionKind kind = ProcessActionKind::Original; + mlir::Operation *operation = nullptr; + std::string operationPath; + std::optional occurrence; + std::vector callSites; + std::vector iterationVector; + std::vector operands; + std::vector results; + std::optional scalarOperation; +}; + +struct ExpandedProcess { + ac::ProcessOp process; + std::string definitionKey; + std::vector actions; + std::vector forwarding; + uint64_t expandedNodes = 0; + uint64_t expandedEdges = 0; + uint64_t valueLookupProbes = 0; + uint64_t maxValueLookupProbes = 0; +}; + +mlir::FailureOr expandProcessForPlanning( + ac::ProcessOp process, + const ac::RawModelStructureLimits &limits = ac::RawModelStructureLimits()); + +llvm::Expected +canonicalProcessOccurrenceJSON(const ProcessOccurrenceId &occurrence); +llvm::Expected +hashProcessOccurrence(const ProcessOccurrenceId &occurrence); + +class PlanSetBuilder { +public: + enum class LoopActionMutationForTest { + WrongOwningLoopSource, + NonLoopSource, + WrongResultType, + InactiveCallee, + InactiveScalar, + WrongEmission, + WrongScalarName, + MissingScalar, + WrongScalarProperties, + WrongScalarAttributeCount, + WrongScalarAttributeName, + WrongScalarAttributeValue, + WrongOperandCount, + WrongOperandType, + WrongResultCount, + }; + + static mlir::FailureOr buildEmpty(mlir::ModuleOp module); + static mlir::FailureOr + buildYieldOnly(mlir::ModuleOp module); + static mlir::FailureOr + buildLoopActionFixture(mlir::ModuleOp module); + static ProcessStatePlanSet + cloneWithCorruption(const ProcessStatePlanSet &plans, + ProcessStatePlanCorruptionForTest corruption); + static ProcessStatePlanSet + cloneWithMissingWakeCallee(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithDanglingSuspendTransition(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithUnpairedLiveSlotCallee(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithMissingValueTypePayload(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithNullEdgeStorage(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithInactiveEdgeField(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithDoubleValueTypePayload(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithMissingOriginalActionSource(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithUnexpectedConstantActionSource(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithNonLoopForActionSource(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForInitializeWrongOwningLoopSource(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForConditionWrongOwningLoopSource(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForIncrementWrongOwningLoopSource(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForConditionWrongResultType(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForIncrementWrongResultType(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForConditionInactiveCallee(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForInitializeInactiveScalar(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForConditionWrongEmission(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForConditionWrongScalarOp(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForIncrementWrongEmission(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithForIncrementWrongScalarOp(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneLoopActionWithMutationForTest(const ProcessStatePlanSet &plans, + uint32_t actionIndex, + LoopActionMutationForTest mutation); + static bool exerciseCompleteApiFixture(mlir::MLIRContext &context); + static bool exerciseAllActionArmsFixture(mlir::MLIRContext &context); + static ProcessStatePlanSet + cloneWithLongLocalChain(const ProcessStatePlanSet &plans, uint32_t blocks); + static ProcessStatePlanSet + cloneWithLocalCycle(const ProcessStatePlanSet &plans); + static ProcessStatePlanSet + cloneWithUnreachableBlock(const ProcessStatePlanSet &plans); + static llvm::StringRef + specializationBytes(const ProcessGeneratedCalleePlan &callee); + static llvm::StringRef + descriptorBytes(const ProcessGeneratedCalleePlan &callee); + static llvm::StringRef specializationBytes(const ProcessValueTypePlan &type); + static bool validEdgeShape(const ProcessControlEdgePlan &edge); + static llvm::StringRef structuralError(const ProcessStatePlanSet &plans); + static mlir::FailureOr + expandProcess(ac::ProcessOp process, + const ac::RawModelStructureLimits &limits); + + struct ControlPlan { + std::vector> captures; + std::vector> pcs; + std::vector> blocks; + std::vector> wakes; + std::vector> transitions; + std::vector> liveSlots; + uint64_t fairnessWork = 0; + }; + static mlir::FailureOr + buildProduction(mlir::ModuleOp module, const ProcessStateLimits &limits); + static mlir::FailureOr> + planProcessContinuation(const ExpandedProcess &expanded, + const ProcessStateLimits &limits); + static mlir::FailureOr> + planProcessWakes(std::unique_ptr control, + const ProcessStateLimits &limits); + static mlir::LogicalResult + planProcessLiveness(ControlPlan &control, const ProcessStateLimits &limits); + static mlir::LogicalResult planProcessCost(ControlPlan &control, + const ProcessStateLimits &limits); + +private: + static mlir::FailureOr + buildFrozenFixture(mlir::ModuleOp module, bool requireYieldOnly); +}; + +llvm::StringRef +generatedCalleeSpecializationBytes(const ProcessGeneratedCalleePlan &callee); +llvm::StringRef +generatedCalleeDescriptorBytes(const ProcessGeneratedCalleePlan &callee); +llvm::StringRef lastProcessStatePlanDiagnosticForTest(); +llvm::Expected canonicalGeneratedCalleeSpecialization( + const ProcessGeneratedCalleePlan &callee); +llvm::Expected +canonicalValueTypeSpecialization(const ProcessValueTypePlan &type); + +} // namespace detail +} // namespace acir + +#endif diff --git a/components/agentic-circuit/lib/Analysis/ProcessStatePlanTestHooks.h b/components/agentic-circuit/lib/Analysis/ProcessStatePlanTestHooks.h new file mode 100644 index 00000000..8f675ae3 --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStatePlanTestHooks.h @@ -0,0 +1,32 @@ +#ifndef ACIR_ANALYSIS_PROCESSSTATEPLANTESTHOOKS_H +#define ACIR_ANALYSIS_PROCESSSTATEPLANTESTHOOKS_H + +#include "acir/Analysis/ProcessStatePlan.h" + +namespace acir { + +enum class ProcessStatePlanCorruptionForTest { + DuplicateOrdinal, + NonDenseOrdinal, + DanglingReference, + DuplicateIdentity, + UnsortedCanonicalOrder, + CostMismatch, + DefinitionKeyMismatch, + CalleeSpecializationMismatch, + ValueTypeSpecializationMismatch, + EffectMismatch, + IdKindMismatch, + WrongTypeKey, + InvalidFramePhase, + InvalidEdgeBinding, + InvalidWakeCallee +}; + +ProcessStatePlanSet cloneProcessStatePlanWithCorruptionForTest( + const ProcessStatePlanSet &plan, + ProcessStatePlanCorruptionForTest corruption); + +} // namespace acir + +#endif diff --git a/components/agentic-circuit/lib/Analysis/ProcessStateReport.cpp b/components/agentic-circuit/lib/Analysis/ProcessStateReport.cpp new file mode 100644 index 00000000..c7a87cef --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStateReport.cpp @@ -0,0 +1,717 @@ +#include "ProcessStatePlanInternal.h" + +#include "acir/Bindings/Binding.h" + +#include "llvm/ADT/StringExtras.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/SHA256.h" +#include "llvm/Support/raw_ostream.h" + +namespace acir { +namespace { + +using llvm::json::Array; +using llvm::json::Object; +using llvm::json::Value; + +template +Array mapArray(Range range, Convert convert) { + Array result; + for (const auto &item : range) + result.push_back(convert(item)); + return result; +} + +Array integers(llvm::ArrayRef values) { + return mapArray(values, [](uint64_t value) { return Value(value); }); +} + +std::string typeSpelling(mlir::Type type) { + std::string storage; + llvm::raw_string_ostream stream(storage); + stream << type; + return storage; +} + +llvm::StringRef spelling(ProcessWakeKind value) { + switch (value) { + case ProcessWakeKind::Condition: + return "condition"; + case ProcessWakeKind::Resource: + return "resource"; + case ProcessWakeKind::EventQueue: + return "event_queue"; + case ProcessWakeKind::NextDelta: + return "next_delta"; + } + llvm_unreachable("unknown wake kind"); +} +llvm::StringRef spelling(ProcessSubscriptionSourceKind value) { + switch (value) { + case ProcessSubscriptionSourceKind::Capture: + return "capture"; + case ProcessSubscriptionSourceKind::Value: + return "value"; + case ProcessSubscriptionSourceKind::Symbol: + return "symbol"; + } + llvm_unreachable("unknown source kind"); +} +llvm::StringRef spelling(ProcessActionKind value) { + switch (value) { + case ProcessActionKind::Original: + return "original"; + case ProcessActionKind::Constant: + return "constant"; + case ProcessActionKind::ForInitialize: + return "for_initialize"; + case ProcessActionKind::ForCondition: + return "for_condition"; + case ProcessActionKind::ForIncrement: + return "for_increment"; + case ProcessActionKind::ScalarWrap: + return "scalar_wrap"; + case ProcessActionKind::ScalarUnwrap: + return "scalar_unwrap"; + } + llvm_unreachable("unknown action kind"); +} +llvm::StringRef spelling(ProcessEmissionClass value) { + switch (value) { + case ProcessEmissionClass::CopyScalar: + return "copy_scalar"; + case ProcessEmissionClass::Inline: + return "inline"; + case ProcessEmissionClass::Invoke: + return "invoke"; + case ProcessEmissionClass::Wrap: + return "wrap"; + case ProcessEmissionClass::Unwrap: + return "unwrap"; + case ProcessEmissionClass::ForwardOnly: + return "forward_only"; + } + llvm_unreachable("unknown emission class"); +} +llvm::StringRef spelling(ProcessLoopPhase value) { + switch (value) { + case ProcessLoopPhase::Initialize: + return "initialize"; + case ProcessLoopPhase::Condition: + return "condition"; + case ProcessLoopPhase::Increment: + return "increment"; + } + llvm_unreachable("unknown loop phase"); +} +llvm::StringRef spelling(ProcessWrapperDirection value) { + return value == ProcessWrapperDirection::Wrap ? "wrap" : "unwrap"; +} +llvm::StringRef spelling(ProcessFrameKind value) { + switch (value) { + case ProcessFrameKind::Entry: + return "entry"; + case ProcessFrameKind::ScfIf: + return "scf.if"; + case ProcessFrameKind::ScfFor: + return "scf.for"; + case ProcessFrameKind::ScfWhile: + return "scf.while"; + } + llvm_unreachable("unknown frame kind"); +} +llvm::StringRef spelling(ProcessFramePhase value) { + switch (value) { + case ProcessFramePhase::Entry: + return "entry"; + case ProcessFramePhase::Then: + return "then"; + case ProcessFramePhase::Else: + return "else"; + case ProcessFramePhase::Merge: + return "merge"; + case ProcessFramePhase::Header: + return "header"; + case ProcessFramePhase::Body: + return "body"; + case ProcessFramePhase::Before: + return "before"; + case ProcessFramePhase::After: + return "after"; + case ProcessFramePhase::Exit: + return "exit"; + } + llvm_unreachable("unknown frame phase"); +} +llvm::StringRef spelling(ProcessValueCoordinateKind value) { + return value == ProcessValueCoordinateKind::Result ? "result" + : "block_argument"; +} +llvm::StringRef spelling(ProcessControlEdgeKind value) { + switch (value) { + case ProcessControlEdgeKind::Branch: + return "branch"; + case ProcessControlEdgeKind::LocalContinue: + return "local_continue"; + case ProcessControlEdgeKind::Suspend: + return "suspend"; + case ProcessControlEdgeKind::Terminate: + return "terminate"; + } + llvm_unreachable("unknown edge kind"); +} +llvm::StringRef spelling(ProcessTerminateStatus value) { + return value == ProcessTerminateStatus::Success ? "success" : "failure"; +} +llvm::StringRef spelling(ProcessEffectKind value) { + return value == ProcessEffectKind::Pure ? "pure" : "stateful"; +} +llvm::StringRef spelling(ProcessValueTypeKind value) { + return value == ProcessValueTypeKind::Value ? "value" : "packet"; +} +llvm::StringRef spelling(ProcessValueTypeMemberKind value) { + return value == ProcessValueTypeMemberKind::Field ? "field" : "element"; +} +llvm::StringRef spelling(ProcessStorageSignedness value) { + switch (value) { + case ProcessStorageSignedness::Signless: + return "signless"; + case ProcessStorageSignedness::Signed: + return "signed"; + case ProcessStorageSignedness::Unsigned: + return "unsigned"; + } + llvm_unreachable("unknown signedness"); +} +llvm::StringRef spelling(ProcessHelperRole value) { + static constexpr llvm::StringLiteral names[] = {"record_create", + "record_get", + "record_with", + "packet_serialize", + "packet_deserialize", + "trace_decode", + "queue_try_send", + "queue_try_recv", + "event_schedule", + "trace_open", + "trace_next", + "trace_eof", + "trace_position", + "contract_require", + "contract_ensure", + "contract_assert", + "probe", + "stat_add", + "wake_condition", + "wake_resource", + "wake_event_queue", + "wake_next_delta", + "scalar_wrap", + "scalar_unwrap"}; + return names[static_cast(value)]; +} + +Value json(const ProcessOccurrenceId &occurrence); +Value json(const ProcessPlannedValue &planned); + +Value json(const ProcessValueCoordinate &coordinate) { + Object object; + object["index"] = coordinate.index(); + object["kind"] = spelling(coordinate.kind()); + object["owner_path"] = coordinate.ownerPath(); + return object; +} +Value json(const ProcessCallSitePlan &site) { + Object object; + object["iteration_vector"] = integers(site.iterationVector()); + object["operation_path"] = site.operationPath(); + return object; +} +Value json(const ProcessOccurrenceId &occurrence) { + Object object; + switch (occurrence.kind()) { + case ProcessOccurrenceKind::Original: + object["call_sites"] = mapArray(occurrence.original().callSites(), + [](const auto &x) { return json(x); }); + object["iteration_vector"] = + integers(occurrence.original().iterationVector()); + object["kind"] = "original"; + object["operation_path"] = occurrence.original().operationPath(); + break; + case ProcessOccurrenceKind::SyntheticLoop: + object["anchor"] = json(occurrence.syntheticLoop().anchor()); + object["kind"] = "synthetic"; + object["phase"] = spelling(occurrence.syntheticLoop().phase()); + break; + case ProcessOccurrenceKind::SyntheticWrapper: + object["anchor"] = json(occurrence.syntheticWrapper().anchor()); + object["direction"] = spelling(occurrence.syntheticWrapper().direction()); + object["kind"] = "synthetic"; + object["slot"] = occurrence.syntheticWrapper().slot().value(); + object["transition"] = occurrence.syntheticWrapper().transition().value(); + break; + case ProcessOccurrenceKind::SyntheticConstant: + object["anchor"] = json(occurrence.syntheticConstant().anchor()); + object["constant"] = occurrence.syntheticConstant().constant(); + object["kind"] = "synthetic"; + break; + } + return object; +} +Value json(const ProcessPlannedValue &planned) { + Object object; + switch (planned.kind()) { + case ProcessPlannedValueKind::Original: + object["coordinate"] = json(planned.original().coordinate()); + object["kind"] = "original"; + object["occurrence"] = json(planned.original().occurrence()); + object["path"] = planned.original().path(); + break; + case ProcessPlannedValueKind::Capture: + object["capture"] = planned.capture().capture().value(); + object["kind"] = "capture"; + break; + case ProcessPlannedValueKind::LiveSlot: + object["kind"] = "live_slot"; + object["slot"] = planned.liveSlot().slot().value(); + break; + case ProcessPlannedValueKind::Synthetic: + object["coordinate"] = json(planned.synthetic().coordinate()); + object["kind"] = "synthetic"; + object["occurrence"] = json(planned.synthetic().occurrence()); + break; + case ProcessPlannedValueKind::Constant: + object["kind"] = "constant"; + object["value"] = planned.constant().value(); + break; + } + object["type"] = typeSpelling(planned.type()); + return object; +} +Value json(const ProcessForwardingBindingPlan &binding) { + Object object; + object["from"] = json(binding.from()); + object["to"] = json(binding.to()); + return object; +} +Value json(const ProcessControlEdgePlan &edge) { + Object object; + object["kind"] = spelling(edge.kind()); + switch (edge.kind()) { + case ProcessControlEdgeKind::Branch: + object["condition"] = json(edge.condition()); + object["false_bindings"] = + mapArray(edge.falseBindings(), [](const auto &x) { return json(x); }); + object["false_block"] = edge.falseBlock().value(); + object["true_bindings"] = + mapArray(edge.trueBindings(), [](const auto &x) { return json(x); }); + object["true_block"] = edge.trueBlock().value(); + break; + case ProcessControlEdgeKind::LocalContinue: + object["bindings"] = + mapArray(edge.bindings(), [](const auto &x) { return json(x); }); + object["target_block"] = edge.targetBlock().value(); + break; + case ProcessControlEdgeKind::Suspend: + object["transition"] = edge.transition().value(); + break; + case ProcessControlEdgeKind::Terminate: + object["status"] = spelling(edge.status()); + break; + } + return object; +} +Value json(const ProcessTransitionLoadPlan &load) { + Object object; + object["replacements"] = + mapArray(load.replacements(), [](const auto &x) { return json(x); }); + object["slot"] = load.slot().value(); + return object; +} +Value json(const ProcessTransitionStorePlan &store) { + Object object; + object["slot"] = store.slot().value(); + object["source"] = json(store.source()); + return object; +} +Value json(const ProcessGeneratedCalleePayload &payload) { + Object object; + auto two = [&](llvm::StringRef a, llvm::StringRef av, llvm::StringRef b, + llvm::StringRef bv) { + object[a] = av; + object[b] = bv; + }; + switch (payload.role()) { + case ProcessHelperRole::RecordCreate: + object["fields"] = + mapArray(payload.recordCreate().fields(), [](const auto &field) { + Object value; + value["name"] = field.name(); + value["type_key"] = field.typeKey(); + return Value(std::move(value)); + }); + object["record_type"] = payload.recordCreate().recordType(); + break; + case ProcessHelperRole::RecordGet: + object["field"] = payload.recordGet().field(); + object["record"] = payload.recordGet().record(); + object["result"] = payload.recordGet().result(); + break; + case ProcessHelperRole::RecordWith: + object["field"] = payload.recordWith().field(); + object["record"] = payload.recordWith().record(); + object["value"] = payload.recordWith().value(); + break; + case ProcessHelperRole::PacketSerialize: + object["bytes"] = payload.packetSerialize().bytes(); + two("packet", payload.packetSerialize().packet(), "packet_type", + payload.packetSerialize().packetType()); + break; + case ProcessHelperRole::PacketDeserialize: + object["bytes"] = payload.packetDeserialize().bytes(); + two("packet", payload.packetDeserialize().packet(), "packet_type", + payload.packetDeserialize().packetType()); + break; + case ProcessHelperRole::TraceDecode: + object["entry"] = payload.traceDecode().entry(); + object["result"] = payload.traceDecode().result(); + object["source"] = payload.traceDecode().source(); + break; + case ProcessHelperRole::QueueTrySend: + two("element", payload.queueTrySend().element(), "queue", + payload.queueTrySend().queue()); + break; + case ProcessHelperRole::QueueTryRecv: + two("element", payload.queueTryRecv().element(), "queue", + payload.queueTryRecv().queue()); + break; + case ProcessHelperRole::EventSchedule: + object["delay"] = payload.eventSchedule().delay(); + object["target"] = payload.eventSchedule().target(); + object["value"] = payload.eventSchedule().value(); + break; + case ProcessHelperRole::TraceOpen: + object["source"] = payload.traceOpen().source(); + break; + case ProcessHelperRole::TraceNext: + two("entry", payload.traceNext().entry(), "source", + payload.traceNext().source()); + break; + case ProcessHelperRole::TraceEof: + object["source"] = payload.traceEof().source(); + break; + case ProcessHelperRole::TracePosition: + object["source"] = payload.tracePosition().source(); + break; + case ProcessHelperRole::ContractRequire: + object["message"] = payload.contractRequire().message(); + break; + case ProcessHelperRole::ContractEnsure: + object["message"] = payload.contractEnsure().message(); + break; + case ProcessHelperRole::ContractAssert: + object["message"] = payload.contractAssert().message(); + break; + case ProcessHelperRole::Probe: + object["kind"] = payload.probe().kind(); + object["result"] = payload.probe().result(); + object["target"] = payload.probe().target(); + break; + case ProcessHelperRole::StatAdd: + two("stat", payload.statAdd().stat(), "value_type", + payload.statAdd().valueType()); + break; + case ProcessHelperRole::WakeCondition: + two("wake_kind", spelling(payload.wakeCondition().wakeKind()), "wake_type", + payload.wakeCondition().wakeType()); + break; + case ProcessHelperRole::WakeResource: + two("wake_kind", spelling(payload.wakeResource().wakeKind()), "wake_type", + payload.wakeResource().wakeType()); + break; + case ProcessHelperRole::WakeEventQueue: + two("wake_kind", spelling(payload.wakeEventQueue().wakeKind()), "wake_type", + payload.wakeEventQueue().wakeType()); + break; + case ProcessHelperRole::WakeNextDelta: + two("wake_kind", spelling(payload.wakeNextDelta().wakeKind()), "wake_type", + payload.wakeNextDelta().wakeType()); + break; + case ProcessHelperRole::ScalarWrap: + object["direction"] = spelling(payload.scalarWrap().direction()); + object["scalar"] = payload.scalarWrap().scalar(); + object["value_type"] = payload.scalarWrap().valueType(); + break; + case ProcessHelperRole::ScalarUnwrap: + object["direction"] = spelling(payload.scalarUnwrap().direction()); + object["scalar"] = payload.scalarUnwrap().scalar(); + object["value_type"] = payload.scalarUnwrap().valueType(); + break; + } + return object; +} +Value json(const ProcessGeneratedCalleePlan &callee) { + Object object; + object["cpp"] = callee.cpp(); + object["effect"] = spelling(callee.effect()); + object["fingerprint"] = callee.fingerprint(); + object["inputs"] = mapArray(callee.inputTypeKeys(), + [](llvm::StringRef x) { return Value(x); }); + object["kind"] = callee.kind(); + object["ordinal"] = callee.id().value(); + object["payload"] = json(callee.payload()); + object["results"] = mapArray(callee.resultTypeKeys(), + [](llvm::StringRef x) { return Value(x); }); + object["role"] = spelling(callee.role()); + object["source_paths"] = mapArray(callee.sourcePaths(), + [](llvm::StringRef x) { return Value(x); }); + object["symbol"] = callee.symbol(); + return object; +} +Value json(const ProcessValueTypeMemberPlan &member) { + Object object; + object["encoding"] = member.encoding(); + object["kind"] = spelling(member.kind()); + if (member.kind() == ProcessValueTypeMemberKind::Field) + object["name"] = member.name(); + else + object["index"] = *member.index(); + object["offset_bits"] = member.offsetBits(); + if (member.signedness()) + object["signedness"] = spelling(*member.signedness()); + object["type_key"] = member.typeKey(); + object["width_bits"] = member.widthBits(); + return object; +} +Value json(const ProcessValueTypePlan &type) { + Object payload; + const auto &value = type.payload(); + if (value.kind() == ProcessValueTypeKind::Value) { + payload["encoding"] = value.value().encoding(); + payload["members"] = mapArray(value.value().members(), + [](const auto &x) { return json(x); }); + payload["width_bits"] = value.value().widthBits(); + } else { + payload["bytes"] = value.packet().bytes(); + payload["encoding"] = value.packet().encoding(); + payload["members"] = mapArray(value.packet().members(), + [](const auto &x) { return json(x); }); + payload["width_bits"] = value.packet().widthBits(); + } + Object object; + object["acir_type"] = typeSpelling(type.acirType()); + object["cpp"] = type.cpp(); + object["fingerprint"] = type.fingerprint(); + object["kind"] = spelling(type.kind()); + object["ordinal"] = type.id().value(); + object["payload"] = std::move(payload); + object["symbol"] = type.symbol(); + return object; +} +Value json(const ProcessStatePlan &plan) { + Object object; + object["blocks"] = mapArray(plan.blocks(), [](const ProcessBlockPlan &block) { + Object value; + value["actions"] = + mapArray(block.actions(), [](const ProcessActionPlan &action) { + Object a; + a["cost"] = action.cost(); + a["emission"] = spelling(action.emission()); + a["iteration_vector"] = integers(action.iterationVector()); + a["kind"] = spelling(action.kind()); + a["occurrence"] = json(action.occurrence()); + a["operands"] = mapArray(action.operands(), + [](const auto &x) { return json(x); }); + a["ordinal"] = action.id(); + a["result_types"] = mapArray(action.resultTypes(), [](mlir::Type x) { + return Value(typeSpelling(x)); + }); + a["results"] = + mapArray(action.results(), [](const auto &x) { return json(x); }); + if (action.callee()) + a["callee"] = action.callee()->value(); + if (action.scalarOp()) { + Object scalar; + scalar["attributes"] = + mapArray(action.scalarOp()->attributes(), [](const auto &x) { + Object attr; + attr["name"] = x.name(); + attr["value"] = x.value(); + return Value(std::move(attr)); + }); + scalar["name"] = action.scalarOp()->name(); + scalar["properties"] = action.scalarOp()->properties(); + a["scalar_op"] = std::move(scalar); + } + return Value(std::move(a)); + }); + value["cost"] = block.cost(); + value["edge"] = json(block.edge()); + value["frames"] = + mapArray(block.frames(), [](const ProcessControlFramePlan &frame) { + Object f; + f["bindings"] = + mapArray(frame.bindings(), [](const auto &x) { return json(x); }); + f["kind"] = spelling(frame.kind()); + f["operation_path"] = frame.operationPath(); + f["phase"] = spelling(frame.phase()); + return Value(std::move(f)); + }); + value["loads"] = + mapArray(block.loads(), [](const auto &x) { return json(x); }); + value["ordinal"] = block.id().value(); + value["path"] = block.path(); + value["pc"] = block.pc().value(); + return Value(std::move(value)); + }); + object["captures"] = + mapArray(plan.captures(), [](const ProcessCapturePlan &capture) { + Object value; + value["argument_path"] = capture.argumentPath(); + value["name"] = capture.name(); + value["operand_path"] = capture.operandPath(); + value["ordinal"] = capture.id().value(); + value["type"] = typeSpelling(capture.type()); + return Value(std::move(value)); + }); + object["definition_key"] = plan.definitionKey(); + object["entry_pc"] = plan.entryPc().value(); + object["fairness_work"] = plan.fairnessWork(); + object["live_slots"] = + mapArray(plan.liveSlots(), [](const ProcessLiveSlotPlan &slot) { + Object value; + value["member_values"] = mapArray( + slot.memberValues(), [](const auto &x) { return json(x); }); + value["name"] = slot.name(); + value["ordinal"] = slot.id().value(); + value["storage_type"] = slot.storageType().value(); + value["type"] = typeSpelling(slot.type()); + if (slot.wrapCallee()) { + value["wrap_callee"] = slot.wrapCallee()->value(); + value["unwrap_callee"] = slot.unwrapCallee()->value(); + } + return Value(std::move(value)); + }); + object["pc_bit_width"] = plan.pcBitWidth(); + object["pcs"] = mapArray(plan.pcs(), [](const ProcessPcPlan &pc) { + Object value; + value["blocks"] = mapArray( + pc.blocks(), [](ProcessBlockId id) { return Value(id.value()); }); + value["entry_path"] = pc.entryPath(); + value["name"] = pc.name(); + value["ordinal"] = pc.id().value(); + return Value(std::move(value)); + }); + object["transitions"] = + mapArray(plan.transitions(), [](const ProcessTransitionPlan &transition) { + Object value; + value["iteration_vector"] = integers(transition.iterationVector()); + value["loads"] = + mapArray(transition.loads(), [](const auto &x) { return json(x); }); + value["ordinal"] = transition.id().value(); + value["source_pc"] = transition.sourcePc().value(); + value["stores"] = mapArray(transition.stores(), + [](const auto &x) { return json(x); }); + value["target_pc"] = transition.targetPc().value(); + value["wake"] = transition.wake().value(); + return Value(std::move(value)); + }); + object["wakes"] = mapArray(plan.wakes(), [](const ProcessWakePlan &wake) { + Object value; + value["callee"] = wake.callee().value(); + value["iteration_vector"] = integers(wake.iterationVector()); + value["kind"] = spelling(wake.kind()); + value["occurrence"] = json(wake.occurrence()); + value["operation_path"] = wake.operationPath(); + value["ordinal"] = wake.id().value(); + value["sources"] = mapArray( + wake.sources(), [](const ProcessSubscriptionSourcePlan &source) { + Object s; + if (source.capture()) + s["capture"] = source.capture()->value(); + s["kind"] = spelling(source.kind()); + if (!source.ownerPath().empty()) + s["owner_path"] = source.ownerPath(); + s["path"] = source.path(); + if (!source.symbol().empty()) + s["symbol"] = source.symbol(); + return Value(std::move(s)); + }); + value["target"] = wake.target(); + value["type_key"] = wake.typeKey(); + return Value(std::move(value)); + }); + return object; +} + +} // namespace + +llvm::Expected +detail::canonicalProcessOccurrenceJSON(const ProcessOccurrenceId &occurrence) { + return bindings::canonicalizeJson(json(occurrence)); +} + +llvm::Expected +detail::hashProcessOccurrence(const ProcessOccurrenceId &occurrence) { + auto canonical = canonicalProcessOccurrenceJSON(occurrence); + if (!canonical) + return canonical.takeError(); + llvm::SHA256 hasher; + hasher.update(*canonical); + return llvm::toHex(hasher.final(), /*LowerCase=*/true); +} + +llvm::Expected detail::canonicalGeneratedCalleeSpecialization( + const ProcessGeneratedCalleePlan &callee) { + Object object; + object["contract_epoch"] = "0.3"; + object["effect"] = spelling(callee.effect()); + object["inputs"] = mapArray(callee.inputTypeKeys(), + [](llvm::StringRef key) { return Value(key); }); + object["kind"] = callee.kind(); + object["payload"] = json(callee.payload()); + object["results"] = mapArray(callee.resultTypeKeys(), + [](llvm::StringRef key) { return Value(key); }); + object["role"] = spelling(callee.role()); + object["schema"] = "acir-generated-implementation-0.1"; + object["source_paths"] = mapArray( + callee.sourcePaths(), [](llvm::StringRef path) { return Value(path); }); + return bindings::canonicalizeJson(Value(std::move(object))); +} + +llvm::Expected +detail::canonicalValueTypeSpecialization(const ProcessValueTypePlan &type) { + Object descriptor = *json(type).getAsObject(); + descriptor.erase("cpp"); + descriptor.erase("fingerprint"); + descriptor.erase("ordinal"); + descriptor.erase("symbol"); + descriptor["contract_epoch"] = "0.3"; + descriptor["schema"] = "acir-generated-value-type-0.1"; + return bindings::canonicalizeJson(Value(std::move(descriptor))); +} + +llvm::Expected +serializeProcessStatePlan(const ProcessStatePlanSet &plans, + const ProcessStateLimits &limits) { + if (mlir::failed(verifyProcessStatePlan(plans, limits))) + return llvm::createStringError("process-state plan verification failed"); + Object report; + report["callees"] = + mapArray(plans.callees(), [](const auto &x) { return json(x); }); + report["contract_epoch"] = "0.3"; + report["processes"] = + mapArray(plans.processes(), [](const auto &x) { return json(x); }); + report["schema"] = "acir-process-state-plan-0.1"; + report["value_types"] = + mapArray(plans.valueTypes(), [](const auto &x) { return json(x); }); + auto canonical = bindings::canonicalizeJson(Value(std::move(report))); + if (!canonical) + return canonical.takeError(); + if (canonical->size() > limits.maxCanonicalReportBytes) + return llvm::createStringError( + "process-state plan capability maxCanonicalReportBytes exceeded"); + return canonical; +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Analysis/ProcessStateWake.cpp b/components/agentic-circuit/lib/Analysis/ProcessStateWake.cpp new file mode 100644 index 00000000..aa6bae54 --- /dev/null +++ b/components/agentic-circuit/lib/Analysis/ProcessStateWake.cpp @@ -0,0 +1,46 @@ +#include "ProcessStatePlanInternal.h" + +#include "acir/Dialect/ACIR/ACIROps.h" + +#include "mlir/IR/SymbolTable.h" +#include "llvm/ADT/SmallVector.h" + +using namespace mlir; + +namespace acir::detail { + +// Wake planning is primarily done inline during continuation planning +// (see ProcessStateContinuation.cpp). This file exists as a separate +// compilation unit for the wake-specific validation and declaration +// resolution that can be called after the initial control plan is built. +// +FailureOr> +PlanSetBuilder::planProcessWakes(std::unique_ptr control, + const ProcessStateLimits &limits) { + for (auto &wake : control->wakes) { + wake->callee = ProcessCalleeId(static_cast(wake->kind)); + if (auto wait = dyn_cast(wake->operation)) { + wake->triggeringValue = wait.getCondition(); + wake->target = wake->operationPath; + continue; + } + mlir::FlatSymbolRefAttr target; + if (auto wait = dyn_cast(wake->operation)) + target = wait.getResourceAttr(); + else if (auto await = dyn_cast(wake->operation)) + target = await.getEventQueueAttr(); + if (!target) + continue; + wake->target = target.getValue().str(); + wake->declaration = + SymbolTable::lookupNearestSymbolFrom(wake->operation, target); + if (!wake->declaration) { + wake->operation->emitOpError("cannot resolve suspension target ") + << target; + return failure(); + } + } + return control; +} + +} // namespace acir::detail diff --git a/components/agentic-circuit/lib/Bindings/Binding.cpp b/components/agentic-circuit/lib/Bindings/Binding.cpp new file mode 100644 index 00000000..3d745cc8 --- /dev/null +++ b/components/agentic-circuit/lib/Bindings/Binding.cpp @@ -0,0 +1,1394 @@ +#include "acir/Bindings/Binding.h" + +#include "BindingInternal.h" +#include "BindingTestHooks.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/ConvertUTF.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/Format.h" +#include "llvm/Support/SHA256.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace acir::bindings { +namespace { + +constexpr int64_t MaxSafeInteger = 9007199254740991LL; + +llvm::Error jsonError(const llvm::Twine &message) { + return llvm::createStringError(llvm::errc::invalid_argument, + "ACLOWER-BINDING-JSON: %s", + message.str().c_str()); +} + +llvm::Error metadataError(const llvm::Twine &message) { + return llvm::createStringError(llvm::errc::invalid_argument, + "ACLOWER-BINDING-METADATA: %s", + message.str().c_str()); +} + +class IJsonPreflight { +public: + IJsonPreflight(llvm::StringRef input, const JsonParseLimits &limits) + : input(input), limits(limits) {} + + llvm::Error run() { + if (input.size() > limits.maxInputBytes) + return jsonError("input byte limit exceeded"); + skipWhitespace(); + if (llvm::Error error = scanValue(1)) + return error; + skipWhitespace(); + if (position != input.size()) + return jsonError(llvm::Twine("trailing input at byte ") + + llvm::Twine(position)); + return llvm::Error::success(); + } + +private: + llvm::Error spendWork() { + if (++structuralWork > limits.maxStructuralWork) + return jsonError("structural work limit exceeded"); + return llvm::Error::success(); + } + + void skipWhitespace() { + while (position < input.size() && + (input[position] == ' ' || input[position] == '\t' || + input[position] == '\n' || input[position] == '\r')) + ++position; + } + + llvm::Error scanValue(size_t depth) { + if (depth > limits.maxDepth) + return jsonError("maximum depth exceeded"); + if (llvm::Error error = spendWork()) + return error; + skipWhitespace(); + if (position == input.size()) + return jsonError("unexpected end of input"); + switch (input[position]) { + case 'n': + return scanLiteral("null"); + case 't': + return scanLiteral("true"); + case 'f': + return scanLiteral("false"); + case '"': { + auto string = scanString(); + if (!string) + return string.takeError(); + return llvm::Error::success(); + } + case '[': + return scanArray(depth); + case '{': + return scanObject(depth); + default: + if (input[position] == '-' || llvm::isDigit(input[position])) + return scanNumber(); + return jsonError(llvm::Twine("unexpected token at byte ") + + llvm::Twine(position)); + } + } + + llvm::Error scanLiteral(llvm::StringRef literal) { + if (!input.substr(position).starts_with(literal)) + return jsonError(llvm::Twine("invalid literal at byte ") + + llvm::Twine(position)); + position += literal.size(); + return llvm::Error::success(); + } + + llvm::Expected scanString() { + size_t start = position++; + while (position < input.size()) { + unsigned char byte = static_cast(input[position++]); + if (byte == '"') { + llvm::StringRef token = input.slice(start, position); + auto parsed = llvm::json::parse(token); + if (!parsed) + return jsonError(llvm::Twine("invalid Unicode string at byte ") + + llvm::Twine(start)); + auto value = parsed->getAsString(); + if (!value) + return jsonError("internal string parse failure"); + if (value->size() > limits.maxStringBytes) + return jsonError("string byte limit exceeded"); + totalStringBytes += value->size(); + if (totalStringBytes > limits.maxTotalStringBytes) + return jsonError("total string byte limit exceeded"); + return value->str(); + } + if (byte < 0x20) + return jsonError(llvm::Twine("unescaped control character at byte ") + + llvm::Twine(position - 1)); + if (byte != '\\') + continue; + if (position == input.size()) + return jsonError("unterminated escape sequence"); + char escape = input[position++]; + if (escape == '"' || escape == '\\' || escape == '/' || escape == 'b' || + escape == 'f' || escape == 'n' || escape == 'r' || escape == 't') + continue; + if (escape != 'u' || position + 4 > input.size()) + return jsonError(llvm::Twine("invalid string escape at byte ") + + llvm::Twine(position - 1)); + uint16_t codeUnit = 0; + for (size_t index = 0; index < 4; ++index) + if (!llvm::isHexDigit(input[position + index])) { + return jsonError(llvm::Twine("invalid Unicode escape at byte ") + + llvm::Twine(position)); + } else { + codeUnit = static_cast( + (codeUnit << 4) | llvm::hexDigitValue(input[position + index])); + } + position += 4; + if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) + return jsonError("lone low UTF-16 surrogate is invalid Unicode"); + if (codeUnit < 0xd800 || codeUnit > 0xdbff) + continue; + if (position + 6 > input.size() || input[position] != '\\' || + input[position + 1] != 'u') + return jsonError("lone high UTF-16 surrogate is invalid Unicode"); + uint16_t lowSurrogate = 0; + for (size_t index = 0; index < 4; ++index) { + char digit = input[position + 2 + index]; + if (!llvm::isHexDigit(digit)) + return jsonError("invalid low UTF-16 surrogate escape"); + lowSurrogate = static_cast((lowSurrogate << 4) | + llvm::hexDigitValue(digit)); + } + if (lowSurrogate < 0xdc00 || lowSurrogate > 0xdfff) + return jsonError("high UTF-16 surrogate requires a low surrogate"); + position += 6; + } + return jsonError("unterminated string"); + } + + llvm::Error scanArray(size_t depth) { + ++position; + skipWhitespace(); + if (position < input.size() && input[position] == ']') { + ++position; + return llvm::Error::success(); + } + size_t elements = 0; + while (true) { + if (++elements > limits.maxArrayElements) + return jsonError("array element limit exceeded"); + if (llvm::Error error = scanValue(depth + 1)) + return error; + skipWhitespace(); + if (position == input.size()) + return jsonError("unterminated array"); + if (input[position] == ']') { + ++position; + return llvm::Error::success(); + } + if (input[position++] != ',') + return jsonError("array entries must be comma-separated"); + skipWhitespace(); + } + } + + llvm::Error scanObject(size_t depth) { + ++position; + skipWhitespace(); + if (position < input.size() && input[position] == '}') { + ++position; + return llvm::Error::success(); + } + llvm::StringSet<> names; + size_t members = 0; + while (true) { + if (++members > limits.maxObjectMembers) + return jsonError("object member limit exceeded"); + if (position == input.size() || input[position] != '"') + return jsonError("object property name must be a string"); + auto name = scanString(); + if (!name) + return name.takeError(); + if (!names.insert(*name).second) + return jsonError(llvm::Twine("duplicate object property '") + *name + + "'"); + skipWhitespace(); + if (position == input.size()) + return jsonError("object property requires ':'"); + const char separator = input[position]; + ++position; + if (separator != ':') + return jsonError("object property requires ':'"); + if (llvm::Error error = scanValue(depth + 1)) + return error; + skipWhitespace(); + if (position == input.size()) + return jsonError("unterminated object"); + if (input[position] == '}') { + ++position; + return llvm::Error::success(); + } + if (input[position++] != ',') + return jsonError("object entries must be comma-separated"); + skipWhitespace(); + } + } + + llvm::Error scanNumber() { + size_t start = position; + bool negative = false; + if (input[position] == '-') { + negative = true; + ++position; + if (position == input.size()) + return jsonError("incomplete number"); + } + if (input[position] == '0') { + ++position; + if (position < input.size() && llvm::isDigit(input[position])) + return jsonError("number has a leading zero"); + } else { + if (!llvm::isDigit(input[position]) || input[position] == '0') + return jsonError("number requires an integer part"); + while (position < input.size() && llvm::isDigit(input[position])) + ++position; + } + if (position < input.size() && input[position] == '.') { + ++position; + size_t fraction = position; + while (position < input.size() && llvm::isDigit(input[position])) + ++position; + if (fraction == position) + return jsonError("number fraction requires a digit"); + } + if (position < input.size() && + (input[position] == 'e' || input[position] == 'E')) { + ++position; + if (position < input.size() && + (input[position] == '+' || input[position] == '-')) + ++position; + size_t exponent = position; + while (position < input.size() && llvm::isDigit(input[position])) + ++position; + if (exponent == position) + return jsonError("number exponent requires a digit"); + } + llvm::StringRef token = input.slice(start, position); + double value = 0.0; + auto converted = std::from_chars(token.begin(), token.end(), value, + std::chars_format::general); + if (converted.ec != std::errc() || converted.ptr != token.end() || + !std::isfinite(value)) + return jsonError("number is not a finite IEEE-754 binary64 value"); + if (negative && value == 0.0) + return jsonError("negative zero is forbidden by RFC 8785 errata 7920"); + return llvm::Error::success(); + } + + llvm::StringRef input; + const JsonParseLimits &limits; + size_t position = 0; + size_t structuralWork = 0; + size_t totalStringBytes = 0; +}; + +llvm::Expected> utf16Units(llvm::StringRef string); +llvm::Expected ecmascriptNumber(double value); + +llvm::Expected canonicalStringSize(llvm::StringRef string) { + if (auto units = utf16Units(string); !units) + return units.takeError(); + size_t size = 2; + for (unsigned char byte : string.bytes()) { + if (byte == '"' || byte == '\\' || byte == '\b' || byte == '\t' || + byte == '\n' || byte == '\f' || byte == '\r') { + size += 2; + } else if (byte < 0x20) { + size += 6; + } else { + ++size; + } + } + return size; +} + +class ConstructedJsonPreflight { +public: + explicit ConstructedJsonPreflight(const JsonParseLimits &limits) + : limits(limits) {} + + llvm::Expected run(const llvm::json::Value &value) { + pending.push_back({&value, 1}); + if (llvm::Error error = drain()) + return std::move(error); + return canonicalBytes; + } + + llvm::Expected run(const llvm::json::Object &object) { + if (llvm::Error error = enter(1)) + return std::move(error); + if (llvm::Error error = visitObject(object, 1)) + return std::move(error); + if (llvm::Error error = drain()) + return std::move(error); + return canonicalBytes; + } + +private: + struct Frame { + const llvm::json::Value *value; + size_t depth; + }; + + llvm::Error enter(size_t depth) { + if (depth > limits.maxDepth) + return jsonError("maximum depth exceeded"); + if (++structuralWork > limits.maxStructuralWork) + return jsonError("structural work limit exceeded"); + return llvm::Error::success(); + } + + llvm::Error accountCanonicalBytes(size_t bytes) { + if (bytes > limits.maxInputBytes - canonicalBytes) + return jsonError("canonical output byte limit exceeded"); + canonicalBytes += bytes; + return llvm::Error::success(); + } + + llvm::Error accountRawString(llvm::StringRef string) { + if (string.size() > limits.maxStringBytes) + return jsonError("string byte limit exceeded"); + if (string.size() > limits.maxTotalStringBytes - totalStringBytes) + return jsonError("total string byte limit exceeded"); + totalStringBytes += string.size(); + return llvm::Error::success(); + } + + llvm::Error accountCanonicalString(llvm::StringRef string) { + auto size = canonicalStringSize(string); + if (!size) + return size.takeError(); + if (llvm::Error error = accountCanonicalBytes(*size)) + return error; + return llvm::Error::success(); + } + + llvm::Error visitObject(const llvm::json::Object &object, size_t depth) { + if (object.size() > limits.maxObjectMembers) + return jsonError("object member limit exceeded"); + if (llvm::Error error = accountCanonicalBytes(2)) + return error; + if (!object.empty()) { + if (llvm::Error error = accountCanonicalBytes(object.size() - 1)) + return error; + if (llvm::Error error = accountCanonicalBytes(object.size())) + return error; + } + for (const auto &entry : object) { + if (llvm::Error error = accountRawString(entry.first)) + return error; + if (llvm::Error error = accountCanonicalString(entry.first)) + return error; + pending.push_back({&entry.second, depth + 1}); + } + return llvm::Error::success(); + } + + llvm::Error drain() { + while (!pending.empty()) { + Frame frame = pending.pop_back_val(); + if (llvm::Error error = enter(frame.depth)) + return error; + const llvm::json::Value &value = *frame.value; + if (value.getAsNull()) { + if (llvm::Error error = accountCanonicalBytes(4)) + return error; + continue; + } + if (auto boolean = value.getAsBoolean()) { + if (llvm::Error error = accountCanonicalBytes(*boolean ? 4 : 5)) + return error; + continue; + } + if (auto string = value.getAsString()) { + if (llvm::Error error = accountRawString(*string)) + return error; + if (llvm::Error error = accountCanonicalString(*string)) + return error; + continue; + } + if (const auto *array = value.getAsArray()) { + if (array->size() > limits.maxArrayElements) + return jsonError("array element limit exceeded"); + if (llvm::Error error = accountCanonicalBytes(2)) + return error; + if (!array->empty()) + if (llvm::Error error = accountCanonicalBytes(array->size() - 1)) + return error; + for (const llvm::json::Value &element : *array) + pending.push_back({&element, frame.depth + 1}); + continue; + } + if (const auto *object = value.getAsObject()) { + if (llvm::Error error = visitObject(*object, frame.depth)) + return error; + continue; + } + auto number = value.getAsNumber(); + if (!number || !std::isfinite(*number) || + (std::signbit(*number) && *number == 0.0)) + return jsonError("constructed number is not canonical I-JSON"); + auto serialized = ecmascriptNumber(*number); + if (!serialized) + return serialized.takeError(); + if (llvm::Error error = accountCanonicalBytes(serialized->size())) + return error; + } + return llvm::Error::success(); + } + + const JsonParseLimits &limits; + llvm::SmallVector pending; + size_t structuralWork = 0; + size_t totalStringBytes = 0; + size_t canonicalBytes = 0; +}; + +llvm::Expected> utf16Units(llvm::StringRef string) { + std::vector units; + for (size_t index = 0; index < string.size();) { + unsigned char first = static_cast(string[index]); + uint32_t codePoint = 0; + size_t count = 0; + if (first < 0x80) { + codePoint = first; + count = 1; + } else if ((first & 0xe0) == 0xc0) { + codePoint = first & 0x1f; + count = 2; + } else if ((first & 0xf0) == 0xe0) { + codePoint = first & 0x0f; + count = 3; + } else if ((first & 0xf8) == 0xf0) { + codePoint = first & 0x07; + count = 4; + } else { + return jsonError("invalid UTF-8 lead byte"); + } + if (index + count > string.size()) + return jsonError("truncated UTF-8 sequence"); + for (size_t offset = 1; offset < count; ++offset) { + unsigned char continuation = + static_cast(string[index + offset]); + if ((continuation & 0xc0) != 0x80) + return jsonError("invalid UTF-8 continuation byte"); + codePoint = (codePoint << 6) | (continuation & 0x3f); + } + if ((count == 2 && codePoint < 0x80) || (count == 3 && codePoint < 0x800) || + (count == 4 && codePoint < 0x10000) || codePoint > 0x10ffff || + (codePoint >= 0xd800 && codePoint <= 0xdfff)) + return jsonError("invalid Unicode scalar value"); + if (codePoint <= 0xffff) { + units.push_back(static_cast(codePoint)); + } else { + codePoint -= 0x10000; + units.push_back(static_cast(0xd800 + (codePoint >> 10))); + units.push_back(static_cast(0xdc00 + (codePoint & 0x3ff))); + } + index += count; + } + return units; +} + +llvm::Error writeEscapedString(llvm::StringRef string, + llvm::raw_ostream &output) { + if (auto units = utf16Units(string); !units) + return units.takeError(); + output << '"'; + static constexpr char Hex[] = "0123456789abcdef"; + for (unsigned char byte : string.bytes()) { + switch (byte) { + case '"': + output << "\\\""; + break; + case '\\': + output << "\\\\"; + break; + case '\b': + output << "\\b"; + break; + case '\t': + output << "\\t"; + break; + case '\n': + output << "\\n"; + break; + case '\f': + output << "\\f"; + break; + case '\r': + output << "\\r"; + break; + default: + if (byte < 0x20) + output << "\\u00" << Hex[byte >> 4] << Hex[byte & 0xf]; + else + output << static_cast(byte); + break; + } + } + output << '"'; + return llvm::Error::success(); +} + +llvm::Expected ecmascriptNumber(double value) { + if (!std::isfinite(value)) + return jsonError("non-finite number cannot be canonicalized"); + if (value == 0.0) { + if (std::signbit(value)) + return jsonError("negative zero cannot be canonicalized"); + return std::string("0"); + } + + std::array buffer{}; + auto converted = std::to_chars(buffer.data(), buffer.data() + buffer.size(), + value, std::chars_format::general); + if (converted.ec != std::errc()) + return jsonError("binary64 conversion failed"); + std::string shortest(buffer.data(), converted.ptr); + bool negative = shortest.front() == '-'; + llvm::StringRef magnitude(shortest); + if (negative) + magnitude = magnitude.drop_front(); + + auto [mantissa, exponentText] = magnitude.split('e'); + if (exponentText.empty()) { + auto split = magnitude.split('E'); + mantissa = split.first; + exponentText = split.second; + } + int explicitExponent = 0; + if (!exponentText.empty()) { + llvm::StringRef digits = exponentText; + bool exponentNegative = digits.consume_front("-"); + digits.consume_front("+"); + if (digits.getAsInteger(10, explicitExponent)) + return jsonError("binary64 exponent conversion failed"); + if (exponentNegative) + explicitExponent = -explicitExponent; + } + + size_t decimal = mantissa.find('.'); + if (decimal == llvm::StringRef::npos) + decimal = mantissa.size(); + std::string digits; + digits.reserve(mantissa.size()); + for (char character : mantissa) + if (character != '.') + digits.push_back(character); + size_t firstNonZero = digits.find_first_not_of('0'); + if (firstNonZero == std::string::npos) + return std::string("0"); + int scientificExponent = explicitExponent + static_cast(decimal) - + static_cast(firstNonZero) - 1; + digits.erase(0, firstNonZero); + while (digits.size() > 1 && digits.back() == '0') + digits.pop_back(); + + std::string result; + if (negative) + result.push_back('-'); + if (scientificExponent >= 0 && scientificExponent < 21) { + size_t integerDigits = static_cast(scientificExponent) + 1; + if (digits.size() <= integerDigits) { + result.append(digits); + result.append(integerDigits - digits.size(), '0'); + } else { + result.append(digits.substr(0, integerDigits)); + result.push_back('.'); + result.append(digits.substr(integerDigits)); + } + } else if (scientificExponent >= -6 && scientificExponent < 0) { + result.append("0."); + result.append(static_cast(-scientificExponent - 1), '0'); + result.append(digits); + } else { + result.push_back(digits.front()); + if (digits.size() > 1) { + result.push_back('.'); + result.append(digits.substr(1)); + } + result.push_back('e'); + if (scientificExponent >= 0) + result.push_back('+'); + result.append(std::to_string(scientificExponent)); + } + return result; +} + +llvm::Error writeCanonical(const llvm::json::Value &value, + llvm::raw_ostream &output) { + if (value.getAsNull()) { + output << "null"; + return llvm::Error::success(); + } + if (auto boolean = value.getAsBoolean()) { + output << (*boolean ? "true" : "false"); + return llvm::Error::success(); + } + if (auto string = value.getAsString()) + return writeEscapedString(*string, output); + if (const auto *array = value.getAsArray()) { + output << '['; + for (size_t index = 0; index < array->size(); ++index) { + if (index) + output << ','; + if (llvm::Error error = writeCanonical((*array)[index], output)) + return error; + } + output << ']'; + return llvm::Error::success(); + } + if (const auto *object = value.getAsObject()) { + struct Property { + llvm::StringRef name; + const llvm::json::Value *value; + std::vector sortKey; + }; + std::vector properties; + properties.reserve(object->size()); + for (const auto &entry : *object) { + llvm::StringRef name = entry.first; + auto key = utf16Units(name); + if (!key) + return key.takeError(); + properties.push_back({name, &entry.second, std::move(*key)}); + } + llvm::sort(properties, [](const Property &left, const Property &right) { + return std::lexicographical_compare( + left.sortKey.begin(), left.sortKey.end(), right.sortKey.begin(), + right.sortKey.end()); + }); + output << '{'; + for (size_t index = 0; index < properties.size(); ++index) { + if (index) + output << ','; + if (llvm::Error error = + writeEscapedString(properties[index].name, output)) + return error; + output << ':'; + if (llvm::Error error = writeCanonical(*properties[index].value, output)) + return error; + } + output << '}'; + return llvm::Error::success(); + } + auto number = value.getAsNumber(); + if (!number) + return jsonError("unsupported JSON value kind"); + if (std::signbit(*number) && *number == 0.0) + return jsonError("negative zero cannot be canonicalized"); + auto serialized = ecmascriptNumber(*number); + if (!serialized) + return serialized.takeError(); + output << *serialized; + return llvm::Error::success(); +} + +bool hasExactKeys(const llvm::json::Object &object, + llvm::ArrayRef expected) { + if (object.size() != expected.size()) + return false; + return llvm::all_of(expected, + [&](llvm::StringRef key) { return object.get(key); }); +} + +llvm::Expected requireString(const llvm::json::Object &object, + llvm::StringRef key) { + auto value = object.getString(key); + if (!value) + return metadataError(llvm::Twine("field '") + key + "' must be a string"); + return value->str(); +} + +bool isName(llvm::StringRef value) { + if (value.empty() || !(llvm::isAlpha(value.front()) || value.front() == '_')) + return false; + return llvm::all_of(value.drop_front(), [](char character) { + return llvm::isAlnum(character) || character == '_'; + }); +} + +bool isIdentity(llvm::StringRef value) { + if (value.empty() || !isName(value.split('.').first)) + return false; + while (value.contains('.')) { + value = value.split('.').second; + if (!isName(value.split('.').first)) + return false; + } + return true; +} + +bool isCppSymbol(llvm::StringRef value) { + if (value.empty() || value.starts_with("::") || value.ends_with("::")) + return false; + while (!value.empty()) { + auto [segment, remainder] = value.split("::"); + if (!isName(segment)) + return false; + value = remainder; + } + return true; +} + +bool hasRawCppFragment(llvm::StringRef value) { + return value.contains(';') || value.contains('{') || value.contains('}') || + value.contains('(') || value.contains(')') || value.contains('=') || + value.contains('%') || value.contains('#') || value.contains('\n') || + value.contains('\r') || value.contains('`'); +} + +bool isSha256(llvm::StringRef value) { + if (!value.starts_with("sha256:") || value.size() != 71) + return false; + return llvm::all_of(value.drop_front(7), [](char character) { + return llvm::isDigit(character) || (character >= 'a' && character <= 'f'); + }); +} + +llvm::Error validateStaticValue(const llvm::json::Value &value) { + if (value.getAsNull() || value.getAsBoolean()) + return llvm::Error::success(); + if (auto string = value.getAsString()) { + if (hasRawCppFragment(*string)) + return metadataError("static metadata contains raw C++ fragments"); + return llvm::Error::success(); + } + if (auto integer = value.getAsInteger()) { + if (*integer < -MaxSafeInteger || *integer > MaxSafeInteger) + return metadataError("static integer is outside the safe exact range"); + return llvm::Error::success(); + } + if (auto number = value.getAsNumber()) { + if (!std::isfinite(*number) || (std::signbit(*number) && *number == 0.0)) + return metadataError("static number is not canonical I-JSON"); + return llvm::Error::success(); + } + if (const auto *array = value.getAsArray()) { + for (const llvm::json::Value &element : *array) + if (llvm::Error error = validateStaticValue(element)) + return error; + return llvm::Error::success(); + } + if (const auto *object = value.getAsObject()) { + for (const auto &entry : *object) { + if (!isName(entry.first)) + return metadataError( + "static metadata object keys must be canonical identifiers"); + if (llvm::Error error = validateStaticValue(entry.second)) + return error; + } + return llvm::Error::success(); + } + return metadataError("static metadata is not canonical I-JSON data"); +} + +template +llvm::Expected> +parseRecordArray(const llvm::json::Object &object, llvm::StringRef key, + Parser parse) { + const auto *array = object.getArray(key); + if (!array) + return metadataError(llvm::Twine("field '") + key + "' must be an array"); + std::vector records; + records.reserve(array->size()); + for (const llvm::json::Value &value : *array) { + const auto *record = value.getAsObject(); + if (!record) + return metadataError(llvm::Twine("field '") + key + + "' must contain records"); + auto parsed = parse(*record); + if (!parsed) + return parsed.takeError(); + records.push_back(std::move(*parsed)); + } + return records; +} + +} // namespace + +namespace detail { + +llvm::Expected preflightConstructedJson(const llvm::json::Value &value, + const JsonParseLimits &limits) { + ConstructedJsonPreflight preflight(limits); + return preflight.run(value); +} + +llvm::Expected +preflightConstructedJson(const llvm::json::Object &object, + const JsonParseLimits &limits) { + ConstructedJsonPreflight preflight(limits); + return preflight.run(object); +} + +} // namespace detail + +struct BindingRecord::Storage { + llvm::json::Object object; + std::string bindingSchema; + std::string contractEpoch; + std::string binding; + std::string componentSchema; + std::string componentSchemaFingerprint; + std::string availability; + std::string effect; + std::string cppType; + std::string implementation; + std::string provider; + std::string providerImplementationFingerprint; + std::string fingerprint; + CppBinding cpp; + ConstructionBinding construction; + OwnershipBinding ownership; + std::vector parameters; + std::vector ports; + std::vector resources; + std::vector results; + std::vector activationSources; +}; + +llvm::Expected parseIJson(llvm::StringRef input, + const JsonParseLimits &limits) { + IJsonPreflight preflight(input, limits); + if (llvm::Error error = preflight.run()) + return std::move(error); + auto parsed = llvm::json::parse(input); + if (!parsed) + return jsonError(llvm::Twine("invalid JSON: ") + + llvm::toString(parsed.takeError())); + return std::move(*parsed); +} + +llvm::Expected canonicalizeJson(const llvm::json::Value &value, + const JsonParseLimits &limits) { + auto canonicalSize = detail::preflightConstructedJson(value, limits); + if (!canonicalSize) + return canonicalSize.takeError(); + if (detail::shouldFailCanonicalEmission()) + return jsonError("canonical emission failure injected"); + std::string storage; + storage.reserve(*canonicalSize); + llvm::raw_string_ostream output(storage); + if (llvm::Error error = writeCanonical(value, output)) + return std::move(error); + output.flush(); + return storage; +} + +llvm::Expected +canonicalizeJsonText(llvm::StringRef input, const JsonParseLimits &limits) { + auto parsed = parseIJson(input, limits); + if (!parsed) + return parsed.takeError(); + return canonicalizeJson(*parsed, limits); +} + +std::string sha256Fingerprint(llvm::StringRef canonicalBytes) { + llvm::SHA256 hasher; + hasher.update(canonicalBytes); + return "sha256:" + llvm::toHex(hasher.final(), true); +} + +BindingRecord::BindingRecord(std::shared_ptr storage) + : storage(std::move(storage)) {} + +BindingRecord::~BindingRecord() = default; + +llvm::Expected +BindingRecord::parse(const llvm::json::Object &object, + const JsonParseLimits &limits) { + auto canonicalSize = detail::preflightConstructedJson(object, limits); + if (!canonicalSize) + return canonicalSize.takeError(); + static constexpr std::array TopKeys = { + "activation_sources", + "availability", + "binding", + "binding_schema", + "component_schema", + "component_schema_fingerprint", + "construction", + "contract_epoch", + "cpp", + "cpp_type", + "effect", + "fingerprint", + "implementation", + "ownership", + "parameters", + "ports", + "provider", + "provider_implementation_fingerprint", + "resources", + "results", + }; + if (!hasExactKeys(object, TopKeys)) + return metadataError( + "binding lock must contain exactly the acsim-binding-0.1 fields"); + + auto result = std::make_shared(); + result->object = object; +#define ACIR_REQUIRE_STRING(member, key) \ + do { \ + auto value = requireString(object, key); \ + if (!value) \ + return value.takeError(); \ + result->member = std::move(*value); \ + } while (false) + ACIR_REQUIRE_STRING(bindingSchema, "binding_schema"); + ACIR_REQUIRE_STRING(contractEpoch, "contract_epoch"); + ACIR_REQUIRE_STRING(binding, "binding"); + ACIR_REQUIRE_STRING(componentSchema, "component_schema"); + ACIR_REQUIRE_STRING(componentSchemaFingerprint, + "component_schema_fingerprint"); + ACIR_REQUIRE_STRING(availability, "availability"); + ACIR_REQUIRE_STRING(effect, "effect"); + ACIR_REQUIRE_STRING(cppType, "cpp_type"); + ACIR_REQUIRE_STRING(implementation, "implementation"); + ACIR_REQUIRE_STRING(provider, "provider"); + ACIR_REQUIRE_STRING(providerImplementationFingerprint, + "provider_implementation_fingerprint"); + ACIR_REQUIRE_STRING(fingerprint, "fingerprint"); +#undef ACIR_REQUIRE_STRING + + if (result->bindingSchema != BindingSchema || + result->contractEpoch != ContractEpoch || + result->availability != "available" || !isName(result->binding) || + !isIdentity(result->componentSchema) || + !isIdentity(result->implementation) || !isIdentity(result->provider) || + !isIdentity(result->cppType) || + (result->effect != "pure" && result->effect != "stateful")) + return metadataError( + "binding lock identity, epoch, availability, and effect are invalid"); + for (llvm::StringRef fingerprint : + {llvm::StringRef(result->componentSchemaFingerprint), + llvm::StringRef(result->providerImplementationFingerprint), + llvm::StringRef(result->fingerprint)}) + if (!isSha256(fingerprint)) + return metadataError( + "binding lock requires lowercase sha256 fingerprints"); + + const auto *cpp = object.getObject("cpp"); + static constexpr std::array CppKeys = { + "concept", "entry_points", "header", "symbol", "target"}; + static constexpr std::array EntryKeys = { + "pure", "reset", "validate", "work", "xfer"}; + if (!cpp || !hasExactKeys(*cpp, CppKeys)) + return metadataError("binding lock C++ record must have exact fields"); + const auto *entries = cpp->getObject("entry_points"); + if (!entries || !hasExactKeys(*entries, EntryKeys)) + return metadataError("binding lock entry points must have exact fields"); + auto assignCpp = [&](std::string &destination, const llvm::json::Object &from, + llvm::StringRef key) -> llvm::Error { + auto value = requireString(from, key); + if (!value) + return value.takeError(); + destination = std::move(*value); + return llvm::Error::success(); + }; + if (llvm::Error error = assignCpp(result->cpp.conceptName, *cpp, "concept")) + return std::move(error); + if (llvm::Error error = assignCpp(result->cpp.header, *cpp, "header")) + return std::move(error); + if (llvm::Error error = assignCpp(result->cpp.symbol, *cpp, "symbol")) + return std::move(error); + if (llvm::Error error = assignCpp(result->cpp.target, *cpp, "target")) + return std::move(error); + if (llvm::Error error = + assignCpp(result->cpp.entryPoints.pure, *entries, "pure")) + return std::move(error); + if (llvm::Error error = + assignCpp(result->cpp.entryPoints.reset, *entries, "reset")) + return std::move(error); + if (llvm::Error error = + assignCpp(result->cpp.entryPoints.validate, *entries, "validate")) + return std::move(error); + if (llvm::Error error = + assignCpp(result->cpp.entryPoints.work, *entries, "work")) + return std::move(error); + if (llvm::Error error = + assignCpp(result->cpp.entryPoints.xfer, *entries, "xfer")) + return std::move(error); + for (llvm::StringRef value : + {llvm::StringRef(result->cpp.conceptName), + llvm::StringRef(result->cpp.header), + llvm::StringRef(result->cpp.symbol), + llvm::StringRef(result->cpp.target), + llvm::StringRef(result->cpp.entryPoints.pure), + llvm::StringRef(result->cpp.entryPoints.reset), + llvm::StringRef(result->cpp.entryPoints.validate), + llvm::StringRef(result->cpp.entryPoints.work), + llvm::StringRef(result->cpp.entryPoints.xfer)}) + if (hasRawCppFragment(value)) + return metadataError( + "binding metadata cannot contain raw C++ or emitter behavior"); + llvm::StringRef header = result->cpp.header; + if (!isCppSymbol(result->cpp.conceptName) || + !isCppSymbol(result->cpp.symbol) || !isName(result->cpp.target) || + header.empty() || header.starts_with('/') || header.contains('\\') || + header == ".." || header.starts_with("../") || header.contains("/../") || + header.ends_with("/..")) + return metadataError("binding C++ names and header path are invalid"); + for (llvm::StringRef entry : + {llvm::StringRef(result->cpp.entryPoints.pure), + llvm::StringRef(result->cpp.entryPoints.reset), + llvm::StringRef(result->cpp.entryPoints.validate), + llvm::StringRef(result->cpp.entryPoints.work), + llvm::StringRef(result->cpp.entryPoints.xfer)}) + if (!entry.empty() && !isCppSymbol(entry)) + return metadataError( + "binding entry points must be empty or qualified C++ symbols"); + if ((result->effect == "pure" && (result->cpp.entryPoints.pure.empty() || + !result->cpp.entryPoints.reset.empty() || + !result->cpp.entryPoints.validate.empty() || + !result->cpp.entryPoints.work.empty() || + !result->cpp.entryPoints.xfer.empty())) || + (result->effect == "stateful" && (!result->cpp.entryPoints.pure.empty() || + result->cpp.entryPoints.work.empty() || + result->cpp.entryPoints.xfer.empty()))) + return metadataError( + "binding effect requires exact executable entry points"); + + const auto *construction = object.getObject("construction"); + const auto *ownership = object.getObject("ownership"); + static constexpr std::array ConstructionKeys = { + "arguments", "kind"}; + static constexpr std::array OwnershipKeys = {"kind", + "placement"}; + if (!construction || !hasExactKeys(*construction, ConstructionKeys) || + !ownership || !hasExactKeys(*ownership, OwnershipKeys)) + return metadataError( + "binding construction and ownership records are exact"); + auto constructionKind = requireString(*construction, "kind"); + auto ownershipKind = requireString(*ownership, "kind"); + auto ownershipPlacement = requireString(*ownership, "placement"); + const auto *arguments = construction->getArray("arguments"); + if (!constructionKind || !ownershipKind || !ownershipPlacement || !arguments) + return metadataError( + "binding construction and ownership fields are invalid"); + result->construction.kind = std::move(*constructionKind); + result->construction.arguments.assign(arguments->begin(), arguments->end()); + result->ownership.kind = std::move(*ownershipKind); + result->ownership.placement = std::move(*ownershipPlacement); + if (result->construction.kind != "constructor") + return metadataError("construction kind must be exactly constructor"); + for (const llvm::json::Value &argument : result->construction.arguments) + if (llvm::Error error = validateStaticValue(argument)) + return std::move(error); + if ((result->effect == "pure" && (result->ownership.kind != "none" || + result->ownership.placement != "inline")) || + (result->effect == "stateful" && + (result->ownership.kind != "unique" || + (result->ownership.placement != "member_or_array" && + result->ownership.placement != "root_or_process")))) + return metadataError("binding ownership does not match its effect"); + + auto parameters = parseRecordArray( + object, "parameters", + [](const llvm::json::Object ¶meter) + -> llvm::Expected { + static constexpr std::array Keys = { + "acir_type", "cpp_type", "mapping", "name", "ordinal", "value"}; + if (!hasExactKeys(parameter, Keys)) + return metadataError( + "binding parameter must have exact static fields"); + ParameterBinding result; + auto acirType = requireString(parameter, "acir_type"); + auto cppType = requireString(parameter, "cpp_type"); + auto mapping = requireString(parameter, "mapping"); + auto name = requireString(parameter, "name"); + auto ordinal = parameter.getInteger("ordinal"); + const llvm::json::Value *value = parameter.get("value"); + if (!acirType || !cppType || !mapping || !name || !ordinal || !value) + return metadataError("binding parameter fields have invalid types"); + result.acirType = std::move(*acirType); + result.cppType = std::move(*cppType); + result.mapping = std::move(*mapping); + result.name = std::move(*name); + result.ordinal = *ordinal; + result.value = *value; + if (hasRawCppFragment(result.acirType) || + hasRawCppFragment(result.cppType)) + return metadataError( + "binding metadata cannot contain raw C++ or emitter behavior"); + if (result.acirType.empty() || result.cppType.empty() || + !isName(result.name) || result.ordinal < 0 || + (result.mapping != "template_argument" && + result.mapping != "constexpr_argument" && + result.mapping != "constructor_constant")) + return metadataError("binding parameter domain is invalid"); + if (llvm::Error error = validateStaticValue(result.value)) + return std::move(error); + return result; + }); + if (!parameters) + return parameters.takeError(); + llvm::StringSet<> parameterNames; + for (size_t index = 0; index < parameters->size(); ++index) + if ((*parameters)[index].ordinal != static_cast(index) || + !parameterNames.insert((*parameters)[index].name).second) + return metadataError( + "binding parameters require contiguous ordinals and unique names"); + result->parameters = std::move(*parameters); + + auto ports = parseRecordArray( + object, "ports", + [](const llvm::json::Object &port) -> llvm::Expected { + static constexpr std::array Keys = { + "accessor", "cardinality", "delegation", "direction", + "interface", "ownership", "payload", "protocol", + "role", "time_domain"}; + if (!hasExactKeys(port, Keys)) + return metadataError( + "binding port records require exact closed fields"); + PortBinding result; +#define ACIR_PORT_STRING(member, key) \ + do { \ + auto value = requireString(port, key); \ + if (!value) \ + return value.takeError(); \ + result.member = std::move(*value); \ + } while (false) + ACIR_PORT_STRING(accessor, "accessor"); + ACIR_PORT_STRING(cardinality, "cardinality"); + ACIR_PORT_STRING(delegation, "delegation"); + ACIR_PORT_STRING(direction, "direction"); + ACIR_PORT_STRING(interface, "interface"); + ACIR_PORT_STRING(ownership, "ownership"); + ACIR_PORT_STRING(payload, "payload"); + ACIR_PORT_STRING(protocol, "protocol"); + ACIR_PORT_STRING(role, "role"); + ACIR_PORT_STRING(timeDomain, "time_domain"); +#undef ACIR_PORT_STRING + if (!isName(result.accessor) || !isIdentity(result.interface) || + !isIdentity(result.payload) || !isIdentity(result.protocol) || + !isIdentity(result.role) || !isIdentity(result.timeDomain) || + (result.cardinality != "exclusive" && + result.cardinality != "shared") || + (result.direction != "input" && result.direction != "output") || + (result.delegation != "forbidden" && + result.delegation != "allowed" && + result.delegation != "required") || + (result.ownership != "owned" && result.ownership != "borrowed" && + result.ownership != "shared")) + return metadataError("binding port domain is invalid"); + return result; + }); + if (!ports) + return ports.takeError(); + llvm::StringSet<> portAccessors; + for (const PortBinding &port : *ports) + if (!portAccessors.insert(port.accessor).second) + return metadataError("binding port accessors must be unique"); + result->ports = std::move(*ports); + + auto resources = parseRecordArray( + object, "resources", + [](const llvm::json::Object &resource) + -> llvm::Expected { + static constexpr std::array Keys = { + "accessor", "delegation", "mode", "ownership", + "resource", "role", "time_domain"}; + if (!hasExactKeys(resource, Keys)) + return metadataError( + "binding resource records require exact closed fields"); + ResourceBinding result; +#define ACIR_RESOURCE_STRING(member, key) \ + do { \ + auto value = requireString(resource, key); \ + if (!value) \ + return value.takeError(); \ + result.member = std::move(*value); \ + } while (false) + ACIR_RESOURCE_STRING(accessor, "accessor"); + ACIR_RESOURCE_STRING(delegation, "delegation"); + ACIR_RESOURCE_STRING(mode, "mode"); + ACIR_RESOURCE_STRING(ownership, "ownership"); + ACIR_RESOURCE_STRING(resource, "resource"); + ACIR_RESOURCE_STRING(role, "role"); + ACIR_RESOURCE_STRING(timeDomain, "time_domain"); +#undef ACIR_RESOURCE_STRING + if (!isName(result.accessor) || !isIdentity(result.resource) || + !isIdentity(result.role) || !isIdentity(result.timeDomain) || + (result.mode != "initiator" && result.mode != "target") || + (result.delegation != "forbidden" && + result.delegation != "allowed" && + result.delegation != "required") || + (result.ownership != "owned" && result.ownership != "borrowed" && + result.ownership != "shared")) + return metadataError("binding resource domain is invalid"); + return result; + }); + if (!resources) + return resources.takeError(); + llvm::StringSet<> resourceAccessors; + for (const ResourceBinding &resource : *resources) + if (!resourceAccessors.insert(resource.accessor).second) + return metadataError("binding resource accessors must be unique"); + result->resources = std::move(*resources); + + auto results = parseRecordArray( + object, "results", + [](const llvm::json::Object &entry) -> llvm::Expected { + static constexpr std::array Keys = {"cpp_type", + "name"}; + if (!hasExactKeys(entry, Keys)) + return metadataError("binding result records require exact fields"); + auto cppType = requireString(entry, "cpp_type"); + auto name = requireString(entry, "name"); + if (!cppType || !name) + return metadataError("binding result fields have invalid types"); + if (!isIdentity(*cppType) || !isName(*name)) + return metadataError("binding result domain is invalid"); + return ResultBinding{std::move(*cppType), std::move(*name)}; + }); + if (!results) + return results.takeError(); + llvm::StringSet<> resultNames; + for (const ResultBinding &entry : *results) + if (!resultNames.insert(entry.name).second) + return metadataError("binding result names must be unique"); + result->results = std::move(*results); + + auto activations = parseRecordArray( + object, "activation_sources", + [](const llvm::json::Object &entry) + -> llvm::Expected { + static constexpr std::array Keys = {"kind", "name"}; + if (!hasExactKeys(entry, Keys)) + return metadataError( + "binding activation source records require exact fields"); + auto kind = requireString(entry, "kind"); + auto name = requireString(entry, "name"); + if (!kind || !name) + return metadataError( + "binding activation source fields have invalid types"); + if (!isIdentity(*kind) || !isName(*name)) + return metadataError("binding activation source domain is invalid"); + return ActivationSourceBinding{std::move(*kind), std::move(*name)}; + }); + if (!activations) + return activations.takeError(); + if (result->effect == "pure" && !activations->empty()) + return metadataError( + "pure binding cannot contain activation or wakeup metadata"); + llvm::StringSet<> activationNames; + for (const ActivationSourceBinding &activation : *activations) + if (!activationNames.insert(activation.name).second) + return metadataError("binding activation-source names must be unique"); + result->activationSources = std::move(*activations); + + return BindingRecord(std::move(result)); +} + +llvm::StringRef BindingRecord::bindingSchema() const { + return storage->bindingSchema; +} +llvm::StringRef BindingRecord::contractEpoch() const { + return storage->contractEpoch; +} +llvm::StringRef BindingRecord::binding() const { return storage->binding; } +llvm::StringRef BindingRecord::componentSchema() const { + return storage->componentSchema; +} +llvm::StringRef BindingRecord::componentSchemaFingerprint() const { + return storage->componentSchemaFingerprint; +} +llvm::StringRef BindingRecord::availability() const { + return storage->availability; +} +llvm::StringRef BindingRecord::effect() const { return storage->effect; } +llvm::StringRef BindingRecord::cppType() const { return storage->cppType; } +llvm::StringRef BindingRecord::implementation() const { + return storage->implementation; +} +llvm::StringRef BindingRecord::provider() const { return storage->provider; } +llvm::StringRef BindingRecord::providerImplementationFingerprint() const { + return storage->providerImplementationFingerprint; +} +llvm::StringRef BindingRecord::fingerprint() const { + return storage->fingerprint; +} +const CppBinding &BindingRecord::cpp() const { return storage->cpp; } +const ConstructionBinding &BindingRecord::construction() const { + return storage->construction; +} +const OwnershipBinding &BindingRecord::ownership() const { + return storage->ownership; +} +llvm::ArrayRef BindingRecord::parameters() const { + return storage->parameters; +} +llvm::ArrayRef BindingRecord::ports() const { + return storage->ports; +} +llvm::ArrayRef BindingRecord::resources() const { + return storage->resources; +} +llvm::ArrayRef BindingRecord::results() const { + return storage->results; +} +llvm::ArrayRef +BindingRecord::activationSources() const { + return storage->activationSources; +} +const llvm::json::Object &BindingRecord::json() const { + return storage->object; +} + +llvm::Expected BindingRecord::canonicalJson() const { + return canonicalizeJson( + llvm::json::Value(llvm::json::Object(storage->object))); +} + +llvm::Expected BindingRecord::canonicalJsonForFingerprint() const { + llvm::json::Object object(storage->object); + object.erase("fingerprint"); + return canonicalizeJson(llvm::json::Value(std::move(object))); +} + +llvm::Error BindingRecord::validateFingerprint() const { + auto expected = computeBindingRecordFingerprint(*this); + if (!expected) + return expected.takeError(); + if (*expected != fingerprint()) + return llvm::createStringError( + llvm::errc::invalid_argument, + "ACLOWER-FINGERPRINT: binding=%s expected=%s actual=%s", + binding().str().c_str(), expected->c_str(), + fingerprint().str().c_str()); + return llvm::Error::success(); +} + +llvm::Expected +computeBindingRecordFingerprint(const BindingRecord &record) { + auto canonical = record.canonicalJsonForFingerprint(); + if (!canonical) + return canonical.takeError(); + return sha256Fingerprint(*canonical); +} + +} // namespace acir::bindings diff --git a/components/agentic-circuit/lib/Bindings/BindingInternal.h b/components/agentic-circuit/lib/Bindings/BindingInternal.h new file mode 100644 index 00000000..c476d743 --- /dev/null +++ b/components/agentic-circuit/lib/Bindings/BindingInternal.h @@ -0,0 +1,16 @@ +#ifndef ACIR_LIB_BINDINGS_BINDINGINTERNAL_H +#define ACIR_LIB_BINDINGS_BINDINGINTERNAL_H + +#include "acir/Bindings/Binding.h" + +namespace acir::bindings::detail { + +llvm::Expected preflightConstructedJson(const llvm::json::Value &value, + const JsonParseLimits &limits); +llvm::Expected +preflightConstructedJson(const llvm::json::Object &object, + const JsonParseLimits &limits); + +} // namespace acir::bindings::detail + +#endif // ACIR_LIB_BINDINGS_BINDINGINTERNAL_H diff --git a/components/agentic-circuit/lib/Bindings/BindingTestHooks.h b/components/agentic-circuit/lib/Bindings/BindingTestHooks.h new file mode 100644 index 00000000..338c344b --- /dev/null +++ b/components/agentic-circuit/lib/Bindings/BindingTestHooks.h @@ -0,0 +1,47 @@ +#ifndef ACIR_LIB_BINDINGS_BINDINGTESTHOOKS_H +#define ACIR_LIB_BINDINGS_BINDINGTESTHOOKS_H + +namespace acir::bindings::detail { + +inline thread_local bool failBindingPublish = false; +inline thread_local bool failCanonicalEmission = false; + +class ScopedCanonicalEmissionFailure { +public: + ScopedCanonicalEmissionFailure() : previous(failCanonicalEmission) { + failCanonicalEmission = true; + } + + ~ScopedCanonicalEmissionFailure() { failCanonicalEmission = previous; } + + ScopedCanonicalEmissionFailure(const ScopedCanonicalEmissionFailure &) = + delete; + ScopedCanonicalEmissionFailure & + operator=(const ScopedCanonicalEmissionFailure &) = delete; + +private: + bool previous; +}; + +class ScopedBindingPublishFailure { +public: + ScopedBindingPublishFailure() : previous(failBindingPublish) { + failBindingPublish = true; + } + + ~ScopedBindingPublishFailure() { failBindingPublish = previous; } + + ScopedBindingPublishFailure(const ScopedBindingPublishFailure &) = delete; + ScopedBindingPublishFailure & + operator=(const ScopedBindingPublishFailure &) = delete; + +private: + bool previous; +}; + +inline bool shouldFailBindingPublish() { return failBindingPublish; } +inline bool shouldFailCanonicalEmission() { return failCanonicalEmission; } + +} // namespace acir::bindings::detail + +#endif // ACIR_LIB_BINDINGS_BINDINGTESTHOOKS_H diff --git a/components/agentic-circuit/lib/Bindings/CMakeLists.txt b/components/agentic-circuit/lib/Bindings/CMakeLists.txt new file mode 100644 index 00000000..e9d94f78 --- /dev/null +++ b/components/agentic-circuit/lib/Bindings/CMakeLists.txt @@ -0,0 +1,10 @@ +add_acir_library(ACIRBindings SOURCES + Binding.cpp + Registry.cpp +) +target_link_libraries(ACIRBindings PUBLIC LLVMSupport) +target_include_directories(ACIRBindings PUBLIC + "$" + "$" +) +add_library(AgenticCircuit::ACIRBindings ALIAS ACIRBindings) diff --git a/components/agentic-circuit/lib/Bindings/Registry.cpp b/components/agentic-circuit/lib/Bindings/Registry.cpp new file mode 100644 index 00000000..d41ff898 --- /dev/null +++ b/components/agentic-circuit/lib/Bindings/Registry.cpp @@ -0,0 +1,804 @@ +#include "acir/Bindings/Registry.h" + +#include "BindingInternal.h" +#include "BindingTestHooks.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include + +namespace acir::bindings { +namespace { + +llvm::Error registryError(const llvm::Twine &message) { + return llvm::createStringError(llvm::errc::invalid_argument, + "ACLOWER-BINDING-REGISTRY: %s", + message.str().c_str()); +} + +llvm::Error resolutionError(llvm::StringRef code, const BindingRequest &request, + const llvm::Twine &detail) { + return llvm::createStringError(llvm::errc::invalid_argument, + "%s: key=%s binding=%s %s", code.str().c_str(), + request.resolutionKey.c_str(), + request.binding.c_str(), detail.str().c_str()); +} + +llvm::Error outputError(const llvm::Twine &message) { + return llvm::createStringError(llvm::errc::io_error, + "ACLOWER-BINDING-OUTPUT: %s", + message.str().c_str()); +} + +bool hasExactKeys(const llvm::json::Object &object, + llvm::ArrayRef keys) { + return object.size() == keys.size() && + llvm::all_of(keys, + [&](llvm::StringRef key) { return object.get(key); }); +} + +llvm::Expected requestString(const llvm::json::Object &object, + llvm::StringRef key) { + auto value = object.getString(key); + if (!value) + return registryError(llvm::Twine("request field '") + key + + "' must be a string"); + return value->str(); +} + +bool isSha256(llvm::StringRef value) { + return value.starts_with("sha256:") && value.size() == 71 && + llvm::all_of(value.drop_front(7), [](char character) { + return llvm::isDigit(character) || + (character >= 'a' && character <= 'f'); + }); +} + +bool isName(llvm::StringRef value) { + if (value.empty() || !(llvm::isAlpha(value.front()) || value.front() == '_')) + return false; + return llvm::all_of(value.drop_front(), [](char character) { + return llvm::isAlnum(character) || character == '_'; + }); +} + +bool isIdentity(llvm::StringRef value) { + if (value.empty()) + return false; + while (true) { + auto [segment, remainder] = value.split('.'); + if (!isName(segment)) + return false; + if (remainder.empty()) + return true; + value = remainder; + } +} + +bool isResolutionKey(llvm::StringRef value) { + while (true) { + if (!value.consume_front("@")) + return false; + auto [segment, remainder] = value.split("::"); + if (!isName(segment)) + return false; + if (remainder.empty()) + return true; + value = remainder; + } +} + +bool isSelectionToken(llvm::StringRef value) { + return !value.empty() && llvm::all_of(value, [](char character) { + return llvm::isAlnum(character) || character == '_' || character == '.' || + character == '+' || character == '-'; + }); +} + +template +llvm::Expected> +parseRequestArray(const llvm::json::Object &object, llvm::StringRef key, + Parser parse) { + const auto *array = object.getArray(key); + if (!array) + return registryError(llvm::Twine("request field '") + key + + "' must be an array"); + std::vector records; + records.reserve(array->size()); + for (const llvm::json::Value &value : *array) { + const auto *entry = value.getAsObject(); + if (!entry) + return registryError(llvm::Twine("request field '") + key + + "' must contain records"); + auto record = parse(*entry); + if (!record) + return record.takeError(); + records.push_back(std::move(*record)); + } + return records; +} + +bool matchesParameters(const BindingRecord &record, + const BindingRequest &request) { + if (record.parameters().size() != request.parameters.size()) + return false; + for (auto [candidate, required] : + llvm::zip_equal(record.parameters(), request.parameters)) + if (candidate.acirType != required.acirType || + candidate.name != required.name || + candidate.ordinal != required.ordinal || + candidate.value != required.value) + return false; + return true; +} + +bool matchesResults(const BindingRecord &record, + const BindingRequest &request) { + if (record.results().size() != request.results.size()) + return false; + for (auto [candidate, required] : + llvm::zip_equal(record.results(), request.results)) + if (candidate.name != required.name) + return false; + return true; +} + +bool matchesPorts(const BindingRecord &record, const BindingRequest &request) { + if (record.ports().size() != request.ports.size()) + return false; + for (auto [candidate, required] : + llvm::zip_equal(record.ports(), request.ports)) + if (candidate.cardinality != required.cardinality || + candidate.delegation != required.delegation || + candidate.direction != required.direction || + candidate.interface != required.interface || + candidate.ownership != required.ownership || + candidate.payload != required.payload || + candidate.protocol != required.protocol || + candidate.role != required.role || + candidate.timeDomain != required.timeDomain) + return false; + return true; +} + +bool matchesResources(const BindingRecord &record, + const BindingRequest &request) { + if (record.resources().size() != request.resources.size()) + return false; + for (auto [candidate, required] : + llvm::zip_equal(record.resources(), request.resources)) + if (candidate.delegation != required.delegation || + candidate.mode != required.mode || + candidate.ownership != required.ownership || + candidate.resource != required.resource || + candidate.role != required.role || + candidate.timeDomain != required.timeDomain) + return false; + return true; +} + +bool matchesMetadata(const BindingRecord &record, + const BindingRequest &request) { + return record.bindingSchema() == request.bindingSchema && + record.contractEpoch() == request.contractEpoch && + record.binding() == request.binding && + record.componentSchema() == request.componentSchema && + record.componentSchemaFingerprint() == + request.componentSchemaFingerprint && + record.effect() == request.effect && + record.provider() == request.provider && + record.providerImplementationFingerprint() == + request.providerImplementationFingerprint && + matchesParameters(record, request) && matchesPorts(record, request) && + matchesResources(record, request) && matchesResults(record, request) && + record.activationSources() == + llvm::ArrayRef(request.activationSources); +} + +llvm::Error validateRequest(const BindingRequest &request, + llvm::StringRef profile, llvm::StringRef target) { + if (request.contractEpoch != ContractEpoch) + return resolutionError("ACLOWER-EPOCH-MISMATCH", request, + llvm::Twine("expected=") + ContractEpoch); + if (request.bindingSchema != BindingSchema) + return resolutionError("ACLOWER-SCHEMA-MISMATCH", request, + llvm::Twine("expected=") + BindingSchema); + if (request.effect != "pure" && request.effect != "stateful") + return resolutionError("ACLOWER-INLINE-EFFECT", request, + "effect must be pure or stateful"); + if (!isSha256(request.componentSchemaFingerprint) || + !isSha256(request.providerImplementationFingerprint)) + return resolutionError("ACLOWER-FINGERPRINT", request, + "request fingerprints are malformed"); + if (!isSelectionToken(profile) || !isSelectionToken(target)) + return resolutionError("ACLOWER-PROFILE", request, + "profile or target syntax is invalid"); + if (!isResolutionKey(request.resolutionKey) || !isName(request.binding) || + !isIdentity(request.componentSchema) || !isIdentity(request.provider) || + request.functionType.empty()) + return resolutionError("ACLOWER-BINDING-MISSING", request, + "request identity is malformed"); + + llvm::StringSet<> parameterNames; + for (size_t index = 0; index < request.parameters.size(); ++index) { + const ParameterRequirement ¶meter = request.parameters[index]; + if (parameter.ordinal != static_cast(index) || + !isName(parameter.name) || parameter.acirType.empty() || + !parameterNames.insert(parameter.name).second) + return resolutionError("ACLOWER-PARAM-PHASE", request, + "request parameter metadata is malformed"); + if (auto canonical = canonicalizeJson(parameter.value); !canonical) + return resolutionError("ACLOWER-PARAM-PHASE", request, + llvm::toString(canonical.takeError())); + } + + llvm::StringSet<> resultNames; + for (const ResultRequirement &result : request.results) + if (result.acirType.empty() || !isName(result.name) || + !resultNames.insert(result.name).second) + return resolutionError("ACLOWER-TYPE-MISMATCH", request, + "request result metadata is malformed"); + llvm::StringSet<> activationNames; + for (const ActivationSourceBinding &activation : request.activationSources) + if (!isIdentity(activation.kind) || !isName(activation.name) || + !activationNames.insert(activation.name).second) + return resolutionError("ACLOWER-TYPE-MISMATCH", request, + "request activation metadata is malformed"); + return llvm::Error::success(); +} + +llvm::Expected +parseBindingRequest(const llvm::json::Object &object) { + static constexpr std::array Keys = { + "activation_sources", + "binding", + "binding_schema", + "component_schema", + "component_schema_fingerprint", + "contract_epoch", + "effect", + "function_type", + "parameters", + "ports", + "provider", + "provider_implementation_fingerprint", + "resolution_key", + "resources", + "results", + }; + if (!hasExactKeys(object, Keys)) + return registryError( + "request must contain exactly the frozen architecture fields"); + BindingRequest request; +#define ACIR_REQUEST_STRING(member, key) \ + do { \ + auto value = requestString(object, key); \ + if (!value) \ + return value.takeError(); \ + request.member = std::move(*value); \ + } while (false) + ACIR_REQUEST_STRING(resolutionKey, "resolution_key"); + ACIR_REQUEST_STRING(functionType, "function_type"); + ACIR_REQUEST_STRING(bindingSchema, "binding_schema"); + ACIR_REQUEST_STRING(contractEpoch, "contract_epoch"); + ACIR_REQUEST_STRING(binding, "binding"); + ACIR_REQUEST_STRING(componentSchema, "component_schema"); + ACIR_REQUEST_STRING(componentSchemaFingerprint, + "component_schema_fingerprint"); + ACIR_REQUEST_STRING(effect, "effect"); + ACIR_REQUEST_STRING(provider, "provider"); + ACIR_REQUEST_STRING(providerImplementationFingerprint, + "provider_implementation_fingerprint"); +#undef ACIR_REQUEST_STRING + if (!request.resolutionKey.starts_with('@') || + request.resolutionKey.size() == 1 || request.functionType.empty() || + request.binding.empty() || request.componentSchema.empty() || + request.provider.empty() || + (request.effect != "pure" && request.effect != "stateful") || + !isSha256(request.componentSchemaFingerprint) || + !isSha256(request.providerImplementationFingerprint)) + return registryError("request identity, fingerprint, or effect is invalid"); + + auto parameters = parseRequestArray( + object, "parameters", + [](const llvm::json::Object &entry) + -> llvm::Expected { + static constexpr std::array Keys = { + "acir_type", "name", "ordinal", "value"}; + if (!hasExactKeys(entry, Keys)) + return registryError( + "request parameter must contain exact architecture fields"); + auto acirType = requestString(entry, "acir_type"); + auto name = requestString(entry, "name"); + auto ordinal = entry.getInteger("ordinal"); + const llvm::json::Value *value = entry.get("value"); + if (!acirType || acirType->empty() || !name || name->empty() || + !ordinal || *ordinal < 0 || !value) + return registryError("request parameter is invalid"); + return ParameterRequirement{std::move(*acirType), std::move(*name), + *ordinal, *value}; + }); + if (!parameters) + return parameters.takeError(); + for (size_t index = 0; index < parameters->size(); ++index) + if ((*parameters)[index].ordinal != static_cast(index)) + return registryError("request parameter ordinals must be contiguous"); + request.parameters = std::move(*parameters); + + auto ports = parseRequestArray( + object, "ports", + [](const llvm::json::Object &entry) -> llvm::Expected { + static constexpr std::array Keys = { + "cardinality", "delegation", "direction", "interface", "ownership", + "payload", "protocol", "role", "time_domain"}; + if (!hasExactKeys(entry, Keys)) + return registryError( + "request port must contain exact architecture fields"); + PortRequirement port; +#define ACIR_PORT_STRING(member, key) \ + do { \ + auto value = requestString(entry, key); \ + if (!value) \ + return value.takeError(); \ + port.member = std::move(*value); \ + } while (false) + ACIR_PORT_STRING(cardinality, "cardinality"); + ACIR_PORT_STRING(delegation, "delegation"); + ACIR_PORT_STRING(direction, "direction"); + ACIR_PORT_STRING(interface, "interface"); + ACIR_PORT_STRING(ownership, "ownership"); + ACIR_PORT_STRING(payload, "payload"); + ACIR_PORT_STRING(protocol, "protocol"); + ACIR_PORT_STRING(role, "role"); + ACIR_PORT_STRING(timeDomain, "time_domain"); +#undef ACIR_PORT_STRING + return port; + }); + if (!ports) + return ports.takeError(); + request.ports = std::move(*ports); + + auto resources = parseRequestArray( + object, "resources", + [](const llvm::json::Object &entry) + -> llvm::Expected { + static constexpr std::array Keys = { + "delegation", "mode", "ownership", + "resource", "role", "time_domain"}; + if (!hasExactKeys(entry, Keys)) + return registryError( + "request resource must contain exact architecture fields"); + ResourceRequirement resource; +#define ACIR_RESOURCE_STRING(member, key) \ + do { \ + auto value = requestString(entry, key); \ + if (!value) \ + return value.takeError(); \ + resource.member = std::move(*value); \ + } while (false) + ACIR_RESOURCE_STRING(delegation, "delegation"); + ACIR_RESOURCE_STRING(mode, "mode"); + ACIR_RESOURCE_STRING(ownership, "ownership"); + ACIR_RESOURCE_STRING(resource, "resource"); + ACIR_RESOURCE_STRING(role, "role"); + ACIR_RESOURCE_STRING(timeDomain, "time_domain"); +#undef ACIR_RESOURCE_STRING + return resource; + }); + if (!resources) + return resources.takeError(); + request.resources = std::move(*resources); + + auto results = parseRequestArray( + object, "results", + [](const llvm::json::Object &entry) -> llvm::Expected { + static constexpr std::array Keys = {"acir_type", + "name"}; + if (!hasExactKeys(entry, Keys)) + return registryError( + "request result must contain exact architecture fields"); + auto acirType = requestString(entry, "acir_type"); + auto name = requestString(entry, "name"); + if (!acirType || acirType->empty() || !name || name->empty()) + return registryError("request result is invalid"); + return ResultRequirement{std::move(*acirType), std::move(*name)}; + }); + if (!results) + return results.takeError(); + request.results = std::move(*results); + + auto activations = parseRequestArray( + object, "activation_sources", + [](const llvm::json::Object &entry) + -> llvm::Expected { + static constexpr std::array Keys = {"kind", "name"}; + if (!hasExactKeys(entry, Keys)) + return registryError( + "request activation source must contain exact fields"); + auto kind = requestString(entry, "kind"); + auto name = requestString(entry, "name"); + if (!kind || kind->empty() || !name || name->empty()) + return registryError("request activation source is invalid"); + return ActivationSourceBinding{std::move(*kind), std::move(*name)}; + }); + if (!activations) + return activations.takeError(); + request.activationSources = std::move(*activations); + return request; +} + +} // namespace + +struct BindingCandidate::Storage { + bool available = false; + std::string profile; + std::string target; + BindingRecord record; + + Storage(bool available, std::string profile, std::string target, + BindingRecord record) + : available(available), profile(std::move(profile)), + target(std::move(target)), record(std::move(record)) {} +}; + +BindingCandidate::BindingCandidate(std::shared_ptr storage) + : storage(std::move(storage)) {} + +BindingCandidate::~BindingCandidate() = default; + +llvm::Expected +BindingCandidate::parse(const llvm::json::Object &object, + const JsonParseLimits &limits) { + auto canonicalSize = detail::preflightConstructedJson(object, limits); + if (!canonicalSize) + return canonicalSize.takeError(); + static constexpr std::array Keys = { + "available", "profile", "record", "target"}; + if (object.size() != Keys.size() || + !llvm::all_of(Keys, [&](llvm::StringRef key) { return object.get(key); })) + return registryError("candidate must contain exactly available, profile, " + "record, and target"); + auto available = object.getBoolean("available"); + auto profile = object.getString("profile"); + auto target = object.getString("target"); + const auto *recordObject = object.getObject("record"); + if (!available || !profile || profile->empty() || !target || + target->empty() || !recordObject) + return registryError("candidate selection metadata has invalid types"); + auto record = BindingRecord::parse(*recordObject, limits); + if (!record) + return record.takeError(); + return BindingCandidate(std::make_shared( + *available, profile->str(), target->str(), std::move(*record))); +} + +llvm::StringRef BindingCandidate::profile() const { return storage->profile; } +llvm::StringRef BindingCandidate::target() const { return storage->target; } +bool BindingCandidate::available() const { return storage->available; } +const BindingRecord &BindingCandidate::record() const { + return storage->record; +} + +llvm::Expected BindingCandidate::deterministicKey() const { + auto canonical = record().canonicalJson(); + if (!canonical) + return canonical.takeError(); + return (llvm::Twine(profile()) + "\n" + target() + "\n" + + (available() ? "1\n" : "0\n") + *canonical) + .str(); +} + +llvm::Expected +parseBindingRegistry(llvm::StringRef input, const JsonParseLimits &limits) { + auto parsed = parseIJson(input, limits); + if (!parsed) + return parsed.takeError(); + const auto *document = parsed->getAsObject(); + static constexpr std::array Keys = {"candidates", + "requests"}; + if (!document || !hasExactKeys(*document, Keys)) + return registryError( + "registry must contain exactly candidates and requests arrays"); + const auto *candidateArray = document->getArray("candidates"); + const auto *requestArray = document->getArray("requests"); + if (!candidateArray || !requestArray) + return registryError("registry candidates and requests must be arrays"); + BindingRegistryDocument result; + result.candidates.reserve(candidateArray->size()); + for (const llvm::json::Value &value : *candidateArray) { + const auto *object = value.getAsObject(); + if (!object) + return registryError("registry candidate must be an object"); + auto candidate = BindingCandidate::parse(*object, limits); + if (!candidate) + return candidate.takeError(); + result.candidates.push_back(std::move(*candidate)); + } + result.requests.reserve(requestArray->size()); + for (const llvm::json::Value &value : *requestArray) { + const auto *object = value.getAsObject(); + if (!object) + return registryError("registry request must be an object"); + auto request = parseBindingRequest(*object); + if (!request) + return request.takeError(); + result.requests.push_back(std::move(*request)); + } + return result; +} + +ResolvedBinding::ResolvedBinding(std::string resolutionKey, + BindingCandidate candidate) + : key(std::move(resolutionKey)), selected(std::move(candidate)) {} + +BindingRegistry::BindingRegistry(std::vector candidates) + : orderedCandidates(std::move(candidates)) {} + +llvm::Expected +BindingRegistry::create(std::vector candidates) { + struct KeyedCandidate { + std::string key; + BindingCandidate candidate; + }; + std::vector keyed; + keyed.reserve(candidates.size()); + for (BindingCandidate &candidate : candidates) { + if (llvm::Error error = candidate.record().validateFingerprint()) + return std::move(error); + auto key = candidate.deterministicKey(); + if (!key) + return key.takeError(); + keyed.push_back({std::move(*key), std::move(candidate)}); + } + llvm::sort(keyed, + [](const KeyedCandidate &left, const KeyedCandidate &right) { + return left.key < right.key; + }); + candidates.clear(); + candidates.reserve(keyed.size()); + for (KeyedCandidate &entry : keyed) + candidates.push_back(std::move(entry.candidate)); + return BindingRegistry(std::move(candidates)); +} + +llvm::Expected +BindingRegistry::resolve(const BindingRequest &request, llvm::StringRef profile, + llvm::StringRef target) const { + if (llvm::Error error = validateRequest(request, profile, target)) + return std::move(error); + + std::vector exactMatches; + for (const BindingCandidate &candidate : orderedCandidates) + if (candidate.profile() == profile && candidate.target() == target && + candidate.available() && matchesMetadata(candidate.record(), request)) + exactMatches.push_back(&candidate); + if (exactMatches.size() == 1) + return ResolvedBinding(request.resolutionKey, *exactMatches.front()); + if (exactMatches.size() > 1) { + std::string candidates; + for (const BindingCandidate *candidate : exactMatches) { + if (!candidates.empty()) + candidates.push_back(','); + candidates.append(candidate->record().fingerprint()); + } + return resolutionError("ACLOWER-BINDING-AMBIGUOUS", request, + llvm::Twine("candidates=") + candidates); + } + + std::vector diagnosticCandidates; + for (const BindingCandidate &candidate : orderedCandidates) + if (candidate.record().binding() == request.binding) + diagnosticCandidates.push_back(&candidate); + if (diagnosticCandidates.empty()) + return resolutionError("ACLOWER-BINDING-MISSING", request, + "no candidate has the exact binding identity"); + + auto reasonIfEmpty = [&](llvm::StringRef reason, + auto predicate) -> std::optional { + std::vector matches; + for (const BindingCandidate *candidate : diagnosticCandidates) + if (predicate(*candidate)) + matches.push_back(candidate); + diagnosticCandidates = std::move(matches); + if (diagnosticCandidates.empty()) + return (llvm::Twine("reason=") + reason).str(); + return std::nullopt; + }; + +#define ACIR_RETURN_MISSING_REASON(code, predicate) \ + do { \ + if (auto reason = reasonIfEmpty(code, predicate)) \ + return resolutionError("ACLOWER-BINDING-MISSING", request, *reason); \ + } while (false) + ACIR_RETURN_MISSING_REASON( + "ACLOWER-EPOCH-MISMATCH", [&](const BindingCandidate &candidate) { + return candidate.record().contractEpoch() == request.contractEpoch; + }); + ACIR_RETURN_MISSING_REASON( + "ACLOWER-SCHEMA-MISMATCH", [&](const BindingCandidate &candidate) { + return candidate.record().bindingSchema() == request.bindingSchema && + candidate.record().componentSchema() == + request.componentSchema && + candidate.record().componentSchemaFingerprint() == + request.componentSchemaFingerprint; + }); + ACIR_RETURN_MISSING_REASON( + "ACLOWER-INLINE-EFFECT", [&](const BindingCandidate &candidate) { + return candidate.record().effect() == request.effect; + }); + ACIR_RETURN_MISSING_REASON( + "ACLOWER-PROFILE", [&](const BindingCandidate &candidate) { + return candidate.profile() == profile && candidate.target() == target; + }); + ACIR_RETURN_MISSING_REASON( + "ACLOWER-PARAM-PHASE", [&](const BindingCandidate &candidate) { + return matchesParameters(candidate.record(), request); + }); + ACIR_RETURN_MISSING_REASON( + "ACLOWER-TYPE-MISMATCH", [&](const BindingCandidate &candidate) { + return matchesPorts(candidate.record(), request) && + matchesResources(candidate.record(), request) && + matchesResults(candidate.record(), request) && + candidate.record().activationSources() == + llvm::ArrayRef( + request.activationSources); + }); + ACIR_RETURN_MISSING_REASON( + "ACLOWER-FINGERPRINT", [&](const BindingCandidate &candidate) { + return candidate.record().provider() == request.provider && + candidate.record().providerImplementationFingerprint() == + request.providerImplementationFingerprint; + }); +#undef ACIR_RETURN_MISSING_REASON + return resolutionError("ACLOWER-BINDING-MISSING", request, + "exact candidate is unavailable"); +} + +llvm::ArrayRef BindingRegistry::candidates() const { + return orderedCandidates; +} + +BindingResolutionResult::BindingResolutionResult( + std::vector selections, std::string canonicalLock, + std::string lockFingerprint) + : selected(std::move(selections)), lock(std::move(canonicalLock)), + fingerprint(std::move(lockFingerprint)) {} + +const ResolvedBinding *BindingResolutionResult::selectionForResolutionKey( + llvm::StringRef resolutionKey) const { + for (const ResolvedBinding &selection : selected) + if (selection.resolutionKey() == resolutionKey) + return &selection; + return nullptr; +} + +llvm::Expected +resolveBindings(llvm::ArrayRef candidates, + llvm::ArrayRef requests, + llvm::StringRef profile, llvm::StringRef target) { + std::vector ownedCandidates(candidates.begin(), + candidates.end()); + auto registry = BindingRegistry::create(std::move(ownedCandidates)); + if (!registry) + return registry.takeError(); + + std::vector orderedRequests(requests.begin(), requests.end()); + llvm::sort(orderedRequests, + [](const BindingRequest &left, const BindingRequest &right) { + return std::tie(left.resolutionKey, left.binding) < + std::tie(right.resolutionKey, right.binding); + }); + for (size_t index = 1; index < orderedRequests.size(); ++index) + if (orderedRequests[index - 1].resolutionKey == + orderedRequests[index].resolutionKey) + return resolutionError("ACLOWER-BINDING-AMBIGUOUS", + orderedRequests[index], + "duplicate attempted resolution key"); + + std::vector selections; + selections.reserve(orderedRequests.size()); + for (const BindingRequest &request : orderedRequests) { + auto selected = registry->resolve(request, profile, target); + if (!selected) + return selected.takeError(); + selections.push_back(std::move(*selected)); + } + + struct LockRecord { + std::string binding; + std::string canonical; + const BindingRecord *record; + }; + std::vector lockRecords; + llvm::StringMap byBinding; + for (const ResolvedBinding &selection : selections) { + auto canonical = selection.record().canonicalJson(); + if (!canonical) + return canonical.takeError(); + auto [iterator, inserted] = + byBinding.try_emplace(selection.record().binding(), *canonical); + if (!inserted && iterator->second != *canonical) + return llvm::createStringError( + llvm::errc::invalid_argument, + "ACLOWER-BINDING-AMBIGUOUS: key=%s binding=%s one binding identity " + "selected distinct records", + selection.resolutionKey().str().c_str(), + selection.record().binding().str().c_str()); + if (inserted) + lockRecords.push_back({selection.record().binding().str(), + std::move(*canonical), &selection.record()}); + } + llvm::sort(lockRecords, [](const LockRecord &left, const LockRecord &right) { + return std::tie(left.binding, left.canonical) < + std::tie(right.binding, right.canonical); + }); + llvm::json::Array lockArray; + for (const LockRecord &entry : lockRecords) + lockArray.push_back(llvm::json::Object(entry.record->json())); + auto canonicalLock = + canonicalizeJson(llvm::json::Value(std::move(lockArray))); + if (!canonicalLock) + return canonicalLock.takeError(); + std::string fingerprint = sha256Fingerprint(*canonicalLock); + return BindingResolutionResult( + std::move(selections), std::move(*canonicalLock), std::move(fingerprint)); +} + +llvm::Error emitBindingLock(const BindingResolutionResult &result, + llvm::raw_ostream &output) { + output << result.canonicalLock(); + return llvm::Error::success(); +} + +llvm::Error emitBindingLockAtomically(const BindingResolutionResult &result, + llvm::StringRef outputPath) { + if (outputPath.empty()) + return outputError("output path must be non-empty"); + auto temporary = + llvm::sys::fs::TempFile::create(llvm::Twine(outputPath) + ".tmp-%%%%%%"); + if (!temporary) + return outputError(llvm::Twine("cannot create temporary lock: ") + + llvm::toString(temporary.takeError())); + { + llvm::raw_fd_ostream output(temporary->FD, false); + if (llvm::Error error = emitBindingLock(result, output)) { + llvm::consumeError(temporary->discard()); + return error; + } + output.flush(); + if (output.has_error()) { + llvm::consumeError(temporary->discard()); + return outputError("failed to flush canonical binding lock bytes"); + } + } + if (detail::shouldFailBindingPublish()) { + llvm::consumeError(temporary->discard()); + return outputError("binding lock publication failed"); + } + if (llvm::Error error = temporary->keep(outputPath)) + return outputError(llvm::Twine("cannot publish binding lock: ") + + llvm::toString(std::move(error))); + return llvm::Error::success(); +} + +llvm::Error +resolveAndWriteBindingLock(llvm::ArrayRef candidates, + llvm::ArrayRef requests, + llvm::StringRef profile, llvm::StringRef target, + llvm::StringRef outputPath) { + auto result = resolveBindings(candidates, requests, profile, target); + if (!result) + return result.takeError(); + return emitBindingLockAtomically(*result, outputPath); +} + +} // namespace acir::bindings diff --git a/components/agentic-circuit/lib/CMakeLists.txt b/components/agentic-circuit/lib/CMakeLists.txt new file mode 100644 index 00000000..5862066c --- /dev/null +++ b/components/agentic-circuit/lib/CMakeLists.txt @@ -0,0 +1,37 @@ +# LLVM's own build defaults to no RTTI. When linking such an LLVM build (CI +# builds LLVM from source with default flags), our MLIR-facing targets must +# compile with -fno-rtti too: derived-class vtables otherwise reference +# typeinfo symbols (mlir::Dialect, mlir::Pass, llvm::cl::Option, ...) that +# no-RTTI LLVM libraries never export. gfsim keeps RTTI for its dynamic_cast +# tree walks, so it is added before the flag is scoped in. +add_subdirectory(gfsim) + +if(NOT LLVM_ENABLE_RTTI) + add_compile_options($<$:-fno-rtti>) +endif() + +add_subdirectory(Dialect) +add_subdirectory(Analysis) +add_subdirectory(Bindings) +add_subdirectory(Transforms) +add_subdirectory(Conversion) +add_subdirectory(CodeGen) +add_subdirectory(Compiler) + +# Install every library that owns symbols declared by the installed public +# headers, including the generated-model runtime. +install( + TARGETS + gfsim + ACIRDialect + ACSimDialect + ACIRBindings + ACIRAnalysis + ACIRTransforms + ACIRToACSim + ACIRCodeGen + ACIRCompiler + EXPORT AgenticCircuitTargets + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib +) diff --git a/components/agentic-circuit/lib/CodeGen/Build.cpp b/components/agentic-circuit/lib/CodeGen/Build.cpp new file mode 100644 index 00000000..3cd22ddb --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/Build.cpp @@ -0,0 +1,521 @@ +#include "BuildInternal.h" + +#include "acir/Bindings/Binding.h" + +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Program.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +llvm::Error buildError(const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "ACLOWER-FINGERPRINT: " + message); +} + +llvm::Error injectedFailure(BuildFailurePoint requested, + BuildFailurePoint actual) { + if (requested != actual) + return llvm::Error::success(); + return buildError("injected build-stage failure"); +} + +llvm::SmallString<256> stagedPath(llvm::StringRef root, + llvm::StringRef relative) { + llvm::SmallString<256> path(root); + llvm::sys::path::append(path, relative); + return path; +} + +llvm::Expected +publicationFingerprint(const BuildRequest &request, const SourceBundle &bundle, + const CompilePlan &compilePlan) { + std::vector layers = request.instrumentationLayers; + std::sort(layers.begin(), layers.end()); + layers.erase(std::unique(layers.begin(), layers.end()), layers.end()); + llvm::json::Array instrumentation; + for (const std::string &layer : layers) + instrumentation.push_back(layer); + std::vector providerIdentities = request.providerInputs; + std::sort(providerIdentities.begin(), providerIdentities.end()); + providerIdentities.erase( + std::unique(providerIdentities.begin(), providerIdentities.end()), + providerIdentities.end()); + llvm::json::Array providers; + for (const std::string &provider : providerIdentities) + providers.push_back(provider); + std::vector sourceFiles = request.frontend.sourceFiles; + std::sort(sourceFiles.begin(), sourceFiles.end(), + [](const FileHash &lhs, const FileHash &rhs) { + return lhs.path < rhs.path; + }); + llvm::json::Array frontendSources; + for (const FileHash &source : sourceFiles) + frontendSources.push_back( + llvm::json::Object{{"path", source.path}, {"sha256", source.sha256}}); + llvm::json::Array helpers; + for (const NamedFingerprint &helper : request.frontend.helperIdentities) + helpers.push_back(llvm::json::Object{{"name", helper.name}, + {"fingerprint", helper.fingerprint}}); + llvm::json::Object frontend{ + {"source_files", std::move(frontendSources)}, + {"acpy", llvm::json::Object{{"path", request.frontend.acpy.path}, + {"sha256", request.frontend.acpy.sha256}}}, + {"canonical_acir", + llvm::json::Object{{"path", request.frontend.canonicalAcir.path}, + {"sha256", request.frontend.canonicalAcir.sha256}}}, + {"python_version", request.frontend.pythonVersion}, + {"helpers", std::move(helpers)}}; + llvm::json::Object preimage{ + {"domain", "agentic-circuit-generated-build-0.1"}, + {"source_bundle", bundle.sourceFingerprint}, + {"compile_plan", compilePlan.fingerprint}, + {"project", llvm::json::Object{{"name", request.project.name}, + {"identity", request.project.identity}}}, + {"system", llvm::json::Object{{"name", request.system.name}, + {"identity", request.system.identity}}}, + {"frontend", std::move(frontend)}, + {"profile", request.profile}, + {"pass_pipeline", + [&] { + llvm::json::Array pipeline; + for (const std::string &pass : request.passPipeline) + pipeline.push_back(pass); + return pipeline; + }()}, + {"instrumentation_layers", std::move(instrumentation)}, + {"providers", std::move(providers)}}; + return fingerprintCanonicalJson(llvm::json::Value(std::move(preimage))); +} + +llvm::Error retargetBundle(SourceBundle &bundle, llvm::StringRef fingerprint) { + const std::string previous = bundle.buildFingerprint; + size_t replacements = 0; + for (GeneratedFile &file : bundle.files) { + size_t position = file.content.find(previous); + if (position == std::string::npos) + continue; + if (file.content.find(previous, position + previous.size()) != + std::string::npos) + return buildError("generated fingerprint occurs more than once"); + file.content.replace(position, previous.size(), fingerprint); + file.fingerprint = computeFingerprint(file.content); + ++replacements; + } + if (replacements != 1) + return buildError("generated model has no unique embedded fingerprint"); + bundle.buildFingerprint = fingerprint.str(); + return llvm::Error::success(); +} + +ArtifactKind sourceArtifactKind(llvm::StringRef path) { + return path.ends_with(".h") ? ArtifactKind::CppHeader + : ArtifactKind::CppSource; +} + +llvm::Error addArtifact(llvm::StringRef stage, llvm::StringRef path, + llvm::StringRef bytes, ArtifactKind kind, + std::vector &artifacts) { + if (auto error = writeFileExclusive(stage, path, bytes)) + return error; + artifacts.push_back({path.str(), kind, computeFingerprint(bytes)}); + return llvm::Error::success(); +} + +std::vector +resolveArguments(const CompilePlan &plan, llvm::StringRef stage, + llvm::ArrayRef arguments) { + std::set stagedFiles(plan.sourceUnits.begin(), + plan.sourceUnits.end()); + stagedFiles.insert(plan.objectOutputs.begin(), plan.objectOutputs.end()); + stagedFiles.insert(plan.executablePath); + for (const PrebuiltInput &input : plan.prebuiltInputs) + stagedFiles.insert(input.path); + + std::vector result; + result.reserve(arguments.size()); + for (const std::string &argument : arguments) { + if (stagedFiles.contains(argument)) { + result.push_back(stagedPath(stage, argument).str().str()); + continue; + } + llvm::StringRef value(argument); + if (value.starts_with("-I")) { + llvm::StringRef root = value.drop_front(2); + if (!llvm::sys::path::is_absolute(root)) + result.push_back("-I" + stagedPath(stage, root).str().str()); + else + result.push_back(argument); + continue; + } + result.push_back(argument); + } + return result; +} + +llvm::Error execute(BuildServices &services, + const std::vector &ownedArguments) { + llvm::SmallVector arguments; + for (const std::string &argument : ownedArguments) + arguments.push_back(argument); + if (!services.execute) + return buildError("build execution service is absent"); + return services.execute(arguments); +} + +llvm::Expected queryFingerprint(llvm::StringRef executable, + llvm::StringRef reportPath) { + const std::array arguments = {executable, + "--build-fingerprint"}; + const std::array, 3> redirects = { + std::nullopt, reportPath, reportPath}; + const int status = llvm::sys::ExecuteAndWait(executable, arguments, + std::nullopt, redirects, 30); + if (status != 0) { + auto report = readFileBytes(reportPath); + if (!report) + return report.takeError(); + constexpr size_t maxDiagnosticBytes = size_t{64} * 1024; + if (report->size() > maxDiagnosticBytes) + report->resize(maxDiagnosticBytes); + return buildError(llvm::Twine("embedded fingerprint query failed: ") + + *report); + } + auto bytes = readFileBytes(reportPath); + if (!bytes) + return bytes.takeError(); + return llvm::StringRef(*bytes).trim().str(); +} + +llvm::Expected makeManifest(const BuildRequest &request, + const ModelPlan &model, + const SourceBundle &bundle, + llvm::ArrayRef artifacts) { + BuildManifest manifest; + manifest.project = request.project; + manifest.system = request.system; + manifest.normalizedAcirSha256 = computeFingerprint(request.frozenAcirBytes); + manifest.compiler = {request.toolchain.compilerName, + request.toolchain.compilerBuildId, + request.toolchain.targetTriple}; + manifest.passPipeline = request.passPipeline; + std::map providerNamespaces; + std::map typeIdentities; + for (const TypePlan &type : model.types) + if (type.kind == TypeKind::Provider) + providerNamespaces.emplace(type.symbol, type.cppType); + else + typeIdentities.emplace(type.symbol, type.cppType); + std::map providerImplementations; + for (const BindingPlan &binding : model.bindings) { + auto provider = providerNamespaces.find(binding.provider); + if (provider == providerNamespaces.end()) + return buildError("binding references an unknown provider identity"); + auto [iterator, inserted] = providerImplementations.emplace( + provider->second, binding.providerImplementationFingerprint); + if (!inserted && + iterator->second != binding.providerImplementationFingerprint) + return buildError( + "one provider has conflicting implementation identities"); + } + for (const auto &[nameSpace, implementation] : providerImplementations) + manifest.providers.push_back( + {nameSpace, model.schemaSetFingerprint, implementation}); + std::map specializations; + for (const ModulePlan &module : model.modules) + specializations.emplace( + module.symbol, + ComponentSpecialization{module.symbol, model.schemaSetFingerprint, + module.specializationFingerprint}); + for (const BindingPlan &binding : model.bindings) { + auto schema = typeIdentities.find(binding.componentSchema); + if (schema == typeIdentities.end()) + return buildError("binding references an unknown component schema"); + ComponentSpecialization specialization{schema->second, + binding.componentSchemaFingerprint, + binding.recordFingerprint}; + auto [iterator, inserted] = + specializations.emplace(specialization.canonicalName, specialization); + if (!inserted && (iterator->second.schemaFingerprint != + specialization.schemaFingerprint || + iterator->second.specializationFingerprint != + specialization.specializationFingerprint)) + return buildError( + "one component has conflicting specialization identities"); + for (const ParameterPlan ¶meter : binding.parameters) + manifest.specializationInputs.push_back( + {binding.symbol + "." + parameter.name, parameter.acirType, + parameter.canonicalValue}); + } + for (const auto &[name, specialization] : specializations) + manifest.componentSpecializations.push_back(specialization); + for (const TypePlan &type : model.types) + if (type.kind == TypeKind::Protocol) + manifest.protocolIdentities.push_back({type.cppType, type.fingerprint}); + manifest.artifacts.assign(artifacts.begin(), artifacts.end()); + manifest.validationGates = { + {"generated_source_contract", ValidationStatus::Passed, std::nullopt}, + {"compile", ValidationStatus::Passed, std::nullopt}, + {"link", ValidationStatus::Passed, std::nullopt}, + {"embedded_fingerprint", ValidationStatus::Passed, std::nullopt}}; + manifest.buildProfile = request.profile; + manifest.instrumentationLayers = request.instrumentationLayers; + manifest.buildFingerprint = bundle.buildFingerprint; + manifest.sourceFiles = request.frontend.sourceFiles; + for (const GeneratedFile &file : bundle.files) + manifest.sourceFiles.push_back({file.relativePath, file.fingerprint}); + return manifest; +} + +} // namespace + +BuildServices makeRealBuildServices() { + BuildServices services; + services.execute = + [](llvm::ArrayRef arguments) -> llvm::Error { + if (arguments.empty()) + return buildError("cannot execute an empty command vector"); + llvm::SmallString<256> outputPath; + if (std::error_code error = llvm::sys::fs::createTemporaryFile( + "acir-build-command", "log", outputPath)) + return llvm::createStringError(error, + "cannot create command report capture"); + struct RemoveCapture { + llvm::SmallString<256> path; + ~RemoveCapture() { llvm::sys::fs::remove(path); } + } cleanup{outputPath}; + const std::array, 3> redirects = { + std::nullopt, outputPath.str(), outputPath.str()}; + const int status = llvm::sys::ExecuteAndWait(arguments.front(), arguments, + std::nullopt, redirects, 120); + if (status != 0) { + auto report = readFileBytes(outputPath); + if (!report) + return report.takeError(); + constexpr size_t maxDiagnosticBytes = size_t{64} * 1024; + if (report->size() > maxDiagnosticBytes) + report->resize(maxDiagnosticBytes); + return buildError(llvm::Twine("compiler or linker command failed: ") + + *report); + } + return llvm::Error::success(); + }; + return services; +} + +llvm::Expected +buildGeneratedModelForTesting(const BuildRequest &request, + BuildServices &services) { + if (auto error = preflightBuildRequest(request)) + return std::move(error); + if (!request.canonicalACSim || request.outputRoot.empty()) + return buildError("build requires canonical ACSim and an output root"); + if (auto error = injectedFailure(services.failurePoint, + BuildFailurePoint::AfterInputValidation)) + return std::move(error); + + auto model = buildModelPlan(request.canonicalACSim); + if (!model) + return model.takeError(); + auto bundle = generateModelSources(*model); + if (!bundle) + return bundle.takeError(); + auto preliminaryPlan = createCompilePlan(request, *bundle); + if (!preliminaryPlan) + return preliminaryPlan.takeError(); + auto buildFingerprint = + publicationFingerprint(request, *bundle, *preliminaryPlan); + if (!buildFingerprint) + return buildFingerprint.takeError(); + if (auto error = retargetBundle(*bundle, *buildFingerprint)) + return std::move(error); + auto compilePlan = createCompilePlan(request, *bundle); + if (!compilePlan) + return compilePlan.takeError(); + + if (std::error_code error = + llvm::sys::fs::create_directories(request.outputRoot)) + return llvm::createStringError(error, "cannot create build output root"); + llvm::SmallString<256> stagePrefix(request.outputRoot); + llvm::sys::path::append(stagePrefix, ".stage"); + llvm::SmallString<256> stage; + if (std::error_code error = + llvm::sys::fs::createUniqueDirectory(stagePrefix, stage)) + return llvm::createStringError(error, "cannot create private build stage"); + struct Cleanup { + llvm::SmallString<256> path; + ~Cleanup() { + if (llvm::sys::fs::exists(path)) + llvm::sys::fs::remove_directories(path); + } + } cleanup{stage}; + + std::vector artifacts; + std::string canonicalACSimBytes = request.canonicalACSimBytes; + if (canonicalACSimBytes.empty()) { + llvm::raw_string_ostream output(canonicalACSimBytes); + mlir::ModuleOp canonical = request.canonicalACSim; + canonical.print(output); + output.flush(); + } + if (!request.frontend.sourceFiles.empty()) { + if (auto error = addArtifact(stage, request.frontend.acpy.path, + request.frontend.acpyBytes, ArtifactKind::Acpy, + artifacts)) + return std::move(error); + if (auto error = addArtifact(stage, request.frontend.canonicalAcir.path, + request.frontend.canonicalAcirBytes, + ArtifactKind::Acir, artifacts)) + return std::move(error); + } + if (auto error = + addArtifact(stage, "input/frozen.acir", request.frozenAcirBytes, + ArtifactKind::Acir, artifacts)) + return std::move(error); + if (auto error = addArtifact(stage, "input/model.acsim", canonicalACSimBytes, + ArtifactKind::Acsim, artifacts)) + return std::move(error); + if (auto error = addArtifact(stage, "input/binding-lock.json", + request.bindingLockBytes, ArtifactKind::Report, + artifacts)) + return std::move(error); + for (const GeneratedFile &file : bundle->files) + if (auto error = + addArtifact(stage, file.relativePath, file.content, + sourceArtifactKind(file.relativePath), artifacts)) + return std::move(error); + auto compilePlanBytes = compilePlan->canonicalJson(); + if (!compilePlanBytes) + return compilePlanBytes.takeError(); + if (auto error = addArtifact(stage, "compile-plan.json", *compilePlanBytes, + ArtifactKind::Report, artifacts)) + return std::move(error); + if (auto error = injectedFailure(services.failurePoint, + BuildFailurePoint::AfterSourceWrite)) + return std::move(error); + + if (auto error = validateSourceBundle(*model, *bundle)) + return std::move(error); + if (auto error = compilePlan->validate()) + return std::move(error); + if (auto error = addArtifact(stage, "reports/source-contract.txt", "passed\n", + ArtifactKind::Report, artifacts)) + return std::move(error); + if (auto error = injectedFailure(services.failurePoint, + BuildFailurePoint::AfterContractCheck)) + return std::move(error); + + for (const std::string &output : compilePlan->objectOutputs) { + llvm::SmallString<256> parent = stagedPath(stage, output); + llvm::sys::path::remove_filename(parent); + if (std::error_code error = llvm::sys::fs::create_directories(parent)) + return llvm::createStringError(error, "cannot create object directory"); + } + for (const CompileCommand &command : compilePlan->compileCommands) { + auto arguments = resolveArguments(*compilePlan, stage, command.arguments); + if (auto error = execute(services, arguments)) + return std::move(error); + } + if (auto error = addArtifact(stage, "reports/compile.txt", "passed\n", + ArtifactKind::Report, artifacts)) + return std::move(error); + if (auto error = injectedFailure(services.failurePoint, + BuildFailurePoint::AfterCompile)) + return std::move(error); + + llvm::SmallString<256> executableParent = + stagedPath(stage, compilePlan->executablePath); + llvm::sys::path::remove_filename(executableParent); + if (std::error_code error = + llvm::sys::fs::create_directories(executableParent)) + return llvm::createStringError(error, "cannot create executable directory"); + auto linkArguments = + resolveArguments(*compilePlan, stage, compilePlan->linkCommand.arguments); + if (auto error = execute(services, linkArguments)) + return std::move(error); + if (auto error = addArtifact(stage, "reports/link.txt", "passed\n", + ArtifactKind::Report, artifacts)) + return std::move(error); + if (auto error = + injectedFailure(services.failurePoint, BuildFailurePoint::AfterLink)) + return std::move(error); + + llvm::SmallString<256> fingerprintReport = + stagedPath(stage, "reports/embedded-fingerprint.txt"); + auto observed = queryFingerprint( + stagedPath(stage, compilePlan->executablePath), fingerprintReport); + if (!observed) + return observed.takeError(); + if (*observed != *buildFingerprint) + return buildError("embedded build fingerprint does not match plan"); + artifacts.push_back({"reports/embedded-fingerprint.txt", ArtifactKind::Report, + computeFingerprint(*observed + "\n")}); + auto executableBytes = + readFileBytes(stagedPath(stage, compilePlan->executablePath)); + if (!executableBytes) + return executableBytes.takeError(); + artifacts.push_back({compilePlan->executablePath, ArtifactKind::Executable, + computeFingerprint(*executableBytes)}); + for (const std::string &object : compilePlan->objectOutputs) { + if (std::error_code error = + llvm::sys::fs::remove(stagedPath(stage, object))) + return llvm::createStringError(error, + "cannot remove staged object output"); + } + if (auto error = injectedFailure(services.failurePoint, + BuildFailurePoint::AfterFingerprintQuery)) + return std::move(error); + + auto manifest = makeManifest(request, *model, *bundle, artifacts); + if (!manifest) + return manifest.takeError(); + auto manifestBytes = manifest->canonicalJson(); + if (!manifestBytes) + return manifestBytes.takeError(); + if (auto error = + writeFileExclusive(stage, "build-manifest.json", *manifestBytes)) + return std::move(error); + if (auto error = injectedFailure(services.failurePoint, + BuildFailurePoint::AfterManifestWrite)) + return std::move(error); + + auto published = publishImmutableStage( + stage, request.outputRoot, *buildFingerprint, artifacts, *manifestBytes); + if (!published) + return published.takeError(); + if (auto error = injectedFailure(services.failurePoint, + BuildFailurePoint::AfterImmutableRename)) + return std::move(error); + if (auto error = injectedFailure(services.failurePoint, + BuildFailurePoint::BeforeCurrentRename)) + return std::move(error); + if (auto error = writeCurrentPointer(request.outputRoot, *buildFingerprint)) + return std::move(error); + + return BuildResult{published->path, + published->path + "/" + compilePlan->executablePath, + *buildFingerprint, published->cacheHit}; +} + +llvm::Expected buildGeneratedModel(const BuildRequest &request) { + BuildServices services = makeRealBuildServices(); + return buildGeneratedModelForTesting(request, services); +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/BuildInternal.h b/components/agentic-circuit/lib/CodeGen/BuildInternal.h new file mode 100644 index 00000000..581c45ce --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/BuildInternal.h @@ -0,0 +1,55 @@ +#ifndef ACIR_CODEGEN_BUILDINTERNAL_H +#define ACIR_CODEGEN_BUILDINTERNAL_H + +#include "acir/CodeGen/Build.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/FunctionExtras.h" +#include "llvm/ADT/StringRef.h" + +namespace acir::codegen { + +enum class BuildFailurePoint { + None, + AfterInputValidation, + AfterSourceWrite, + AfterContractCheck, + AfterCompile, + AfterLink, + AfterFingerprintQuery, + AfterManifestWrite, + AfterImmutableRename, + BeforeCurrentRename, +}; + +struct BuildServices { + BuildFailurePoint failurePoint = BuildFailurePoint::None; + llvm::unique_function)> execute; +}; + +struct PublishedStage { + std::string path; + bool cacheHit = false; +}; + +llvm::Expected normalizeArtifactPath(llvm::StringRef path); +llvm::Error writeFileExclusive(llvm::StringRef stageRoot, + llvm::StringRef relativePath, + llvm::StringRef bytes); +llvm::Expected readFileBytes(llvm::StringRef path); +llvm::Expected +publishImmutableStage(llvm::StringRef stageRoot, llvm::StringRef outputRoot, + llvm::StringRef buildFingerprint, + llvm::ArrayRef artifacts, + llvm::StringRef manifestBytes); +llvm::Error writeCurrentPointer(llvm::StringRef outputRoot, + llvm::StringRef buildFingerprint); + +BuildServices makeRealBuildServices(); +llvm::Expected +buildGeneratedModelForTesting(const BuildRequest &request, + BuildServices &services); + +} // namespace acir::codegen + +#endif // ACIR_CODEGEN_BUILDINTERNAL_H diff --git a/components/agentic-circuit/lib/CodeGen/CMakeLists.txt b/components/agentic-circuit/lib/CodeGen/CMakeLists.txt new file mode 100644 index 00000000..373ac0da --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/CMakeLists.txt @@ -0,0 +1,33 @@ +add_acir_library(ACIRCodeGen SOURCES + Build.cpp + CompilePlan.cpp + Manifest.cpp + Emitter.cpp + Generator.cpp + ModelPlan.cpp + ModelPlanDetails.cpp + ProcessGenerator.cpp + QueueBlockContract.cpp + QueueGraphPlan.cpp + QueueGraphGenerator.cpp + QueueGraphPyc.cpp + Staging.cpp +) +string(JOIN "|" ACIR_CODEGEN_LLVM_INCLUDE_DIRS ${LLVM_INCLUDE_DIRS}) +target_compile_definitions(ACIRCodeGen PRIVATE + ACIR_CODEGEN_LLVM_INCLUDE_DIRS="${ACIR_CODEGEN_LLVM_INCLUDE_DIRS}" +) +target_link_libraries(ACIRCodeGen PUBLIC + ACIRBindings + ACIRDialect + ACSimDialect + MLIRArithDialect + MLIRIndexDialect + LLVMSupport +) +target_include_directories(ACIRCodeGen PUBLIC + "$" + "$" + "$" +) +add_library(AgenticCircuit::ACIRCodeGen ALIAS ACIRCodeGen) diff --git a/components/agentic-circuit/lib/CodeGen/CompilePlan.cpp b/components/agentic-circuit/lib/CodeGen/CompilePlan.cpp new file mode 100644 index 00000000..75901edf --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/CompilePlan.cpp @@ -0,0 +1,578 @@ +#include "acir/CodeGen/Build.h" + +#include "acir/Bindings/Binding.h" + +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Program.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +llvm::Error buildError(llvm::StringRef code, const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + llvm::Twine(code) + ": " + message); +} + +bool isNormalizedRelativePath(llvm::StringRef path) { + if (path.empty() || path.starts_with('/') || path.ends_with('/') || + path.contains('\\') || path.contains('\0')) + return false; + while (!path.empty()) { + auto [component, remainder] = path.split('/'); + if (component.empty() || component == "." || component == "..") + return false; + path = remainder; + } + return true; +} + +bool isCanonicalIncludeRoot(llvm::StringRef path) { + if (isNormalizedRelativePath(path)) + return true; + if (!llvm::sys::path::is_absolute(path) || path.contains('\0') || + path.contains('\\') || path.contains("//") || path.ends_with('/')) + return false; + llvm::SmallString<256> normalized(path); + llvm::sys::path::remove_dots(normalized, true); + return normalized == path; +} + +bool isSortedUnique(const std::vector &values) { + return std::adjacent_find(values.begin(), values.end(), + std::greater_equal<>()) == values.end(); +} + +std::string trimOutput(llvm::StringRef value) { return value.trim().str(); } + +llvm::Expected +runAndCapture(llvm::StringRef program, + const std::vector &ownedArguments) { + llvm::SmallString<256> outputPath; + if (std::error_code error = llvm::sys::fs::createTemporaryFile( + "acir-toolchain", "txt", outputPath)) + return llvm::createStringError(error, "cannot create toolchain probe file"); + struct Cleanup { + llvm::SmallString<256> path; + ~Cleanup() { llvm::sys::fs::remove(path); } + } cleanup{outputPath}; + + llvm::SmallVector arguments; + for (const std::string &argument : ownedArguments) + arguments.push_back(argument); + const std::array, 3> redirects = { + std::nullopt, outputPath.str(), outputPath.str()}; + const int status = llvm::sys::ExecuteAndWait(program, arguments, std::nullopt, + redirects, 30); + if (status != 0) + return buildError("ACLOWER-FINGERPRINT", "compiler identity probe failed"); + auto buffer = llvm::MemoryBuffer::getFile(outputPath); + if (!buffer) + return llvm::createStringError(buffer.getError(), + "cannot read toolchain probe output"); + return trimOutput(buffer.get()->getBuffer()); +} + +llvm::json::Array stringsJson(const std::vector &values) { + llvm::json::Array result; + for (const std::string &value : values) + result.push_back(value); + return result; +} + +llvm::json::Object provenanceJson(const PrebuiltProvenance &provenance) { + return llvm::json::Object{ + {"compiler_build_id", provenance.compilerBuildId}, + {"target_triple", provenance.targetTriple}, + {"standard_library", provenance.standardLibrary}, + {"abi_mode", provenance.abiMode}, + {"object_format", provenance.objectFormat}, + {"contract_epoch", provenance.contractEpoch}, + {"contract_flags", stringsJson(provenance.contractFlags)}, + {"toolchain_fingerprint", provenance.toolchainFingerprint}, + {"source_fingerprint", provenance.sourceFingerprint}}; +} + +llvm::json::Object prebuiltJson(const PrebuiltInput &input) { + return llvm::json::Object{{"path", input.path}, + {"kind", input.kind}, + {"provenance", provenanceJson(input.provenance)}, + {"source_available", input.sourceAvailable}}; +} + +llvm::json::Object commandJson(const CompileCommand &command) { + return llvm::json::Object{{"arguments", stringsJson(command.arguments)}, + {"output", command.output}}; +} + +llvm::json::Object linkInputJson(const LinkInputIdentity &input) { + return llvm::json::Object{{"path", input.path}, + {"fingerprint", input.fingerprint}}; +} + +llvm::json::Object planJson(const CompilePlan &plan, + llvm::StringRef fingerprint) { + llvm::json::Array prebuilts; + for (const PrebuiltInput &input : plan.prebuiltInputs) + prebuilts.push_back(prebuiltJson(input)); + llvm::json::Array commands; + for (const CompileCommand &command : plan.compileCommands) + commands.push_back(commandJson(command)); + llvm::json::Array linkInputs; + for (const LinkInputIdentity &input : plan.linkInputs) + linkInputs.push_back(linkInputJson(input)); + return llvm::json::Object{ + {"schema", plan.schema}, + {"source_units", stringsJson(plan.sourceUnits)}, + {"object_outputs", stringsJson(plan.objectOutputs)}, + {"include_roots", stringsJson(plan.includeRoots)}, + {"definitions", stringsJson(plan.definitions)}, + {"compiler_flags", stringsJson(plan.compilerFlags)}, + {"linker_flags", stringsJson(plan.linkerFlags)}, + {"link_inputs", std::move(linkInputs)}, + {"prebuilt_inputs", std::move(prebuilts)}, + {"compile_commands", std::move(commands)}, + {"link_command", commandJson(plan.linkCommand)}, + {"executable_path", plan.executablePath}, + {"source_fingerprint", plan.sourceFingerprint}, + {"toolchain_fingerprint", plan.toolchainFingerprint}, + {"fingerprint", fingerprint}}; +} + +llvm::Expected +toolchainFingerprint(const ToolchainIdentity &toolchain) { + std::vector flags = toolchain.contractFlags; + std::sort(flags.begin(), flags.end()); + llvm::json::Object identity{{"domain", "acsim-toolchain-0.1"}, + {"compiler_path", toolchain.compilerPath}, + {"compiler_name", toolchain.compilerName}, + {"compiler_build_id", toolchain.compilerBuildId}, + {"target_triple", toolchain.targetTriple}, + {"standard_library", toolchain.standardLibrary}, + {"abi_mode", toolchain.abiMode}, + {"object_format", toolchain.objectFormat}, + {"contract_flags", stringsJson(flags)}}; + return fingerprintCanonicalJson(llvm::json::Value(std::move(identity))); +} + +bool provenanceMatches(const PrebuiltProvenance &provenance, + const ToolchainIdentity &toolchain) { + std::vector expectedFlags = toolchain.contractFlags; + std::vector actualFlags = provenance.contractFlags; + std::sort(expectedFlags.begin(), expectedFlags.end()); + std::sort(actualFlags.begin(), actualFlags.end()); + return provenance.compilerBuildId == toolchain.compilerBuildId && + provenance.targetTriple == toolchain.targetTriple && + provenance.standardLibrary == toolchain.standardLibrary && + provenance.abiMode == toolchain.abiMode && + provenance.objectFormat == toolchain.objectFormat && + provenance.contractEpoch == "0.3" && actualFlags == expectedFlags && + provenance.toolchainFingerprint == toolchain.fingerprint && + isValidFingerprint(provenance.sourceFingerprint); +} + +llvm::Expected fingerprintJsonValue(llvm::json::Value value) { + auto canonical = bindings::canonicalizeJson(value); + if (!canonical) + return canonical.takeError(); + return computeFingerprint(*canonical); +} + +std::string objectPath(llvm::StringRef source) { + std::string result = "obj/"; + for (char character : source) { + if (std::isalnum(static_cast(character)) || + character == '_' || character == '-') + result.push_back(character); + else + result.push_back('_'); + } + result.append(".o"); + return result; +} + +} // namespace + +llvm::Expected +identifyToolchain(std::string compilerPath, std::string standardLibrary, + std::string abiMode, std::string objectFormat, + std::vector contractFlags) { + if (compilerPath.empty() || standardLibrary.empty() || abiMode.empty() || + objectFormat.empty()) + return buildError("ACLOWER-FINGERPRINT", + "toolchain fields must be explicit"); + auto version = runAndCapture(compilerPath, {compilerPath, "--version"}); + if (!version) + return version.takeError(); + auto target = runAndCapture(compilerPath, {compilerPath, "-dumpmachine"}); + if (!target) + return target.takeError(); + + const llvm::StringRef versionText(*version); + ToolchainIdentity result; + result.compilerPath = std::move(compilerPath); + result.compilerBuildId = versionText.split('\n').first.str(); + result.compilerName = versionText.split(" version ").first.trim().str(); + result.targetTriple = llvm::StringRef(*target).split('\n').first.trim().str(); + result.standardLibrary = std::move(standardLibrary); + result.abiMode = std::move(abiMode); + result.objectFormat = std::move(objectFormat); + result.contractFlags = std::move(contractFlags); + auto fingerprint = toolchainFingerprint(result); + if (!fingerprint) + return fingerprint.takeError(); + result.fingerprint = std::move(*fingerprint); + return result; +} + +llvm::Error preflightBuildRequest(const BuildRequest &request) { + if (request.project.name.empty() || request.project.identity.empty() || + request.system.name.empty() || request.system.identity.empty()) + return buildError("ACLOWER-FINGERPRINT", + "project and system identities must be explicit"); + if (request.profile != "fast" && request.profile != "validated" && + request.profile != "custom") + return buildError("ACLOWER-PROFILE", + "build profile is outside the closed set"); + if (request.canonicalACSim && + (request.passPipeline.empty() || + std::any_of(request.passPipeline.begin(), request.passPipeline.end(), + [](const std::string &pass) { return pass.empty(); }))) + return buildError("ACLOWER-PROFILE", "build pass pipeline is incomplete"); + const FrontendProvenance &frontend = request.frontend; + const bool hasFrontend = + !frontend.sourceFiles.empty() || !frontend.acpy.path.empty() || + !frontend.acpyBytes.empty() || !frontend.canonicalAcir.path.empty() || + !frontend.canonicalAcirBytes.empty() || !frontend.pythonVersion.empty() || + !frontend.helperIdentities.empty(); + if (hasFrontend) { + if (frontend.sourceFiles.empty() || frontend.pythonVersion.empty()) + return buildError("ACLOWER-FINGERPRINT", + "frontend source and Python identities are required"); + std::set frontendPaths; + for (const FileHash &source : frontend.sourceFiles) + if (!isNormalizedRelativePath(source.path) || + !isValidFingerprint(source.sha256) || + !frontendPaths.insert(source.path).second) + return buildError("ACLOWER-FINGERPRINT", + "frontend source identity is invalid"); + auto validFrontendArtifact = [](const Artifact &artifact, + ArtifactKind expected, + llvm::StringRef bytes) { + return artifact.kind == expected && + isNormalizedRelativePath(artifact.path) && + isValidFingerprint(artifact.sha256) && + artifact.sha256 == computeFingerprint(bytes); + }; + if (!validFrontendArtifact(frontend.acpy, ArtifactKind::Acpy, + frontend.acpyBytes) || + !validFrontendArtifact(frontend.canonicalAcir, ArtifactKind::Acir, + frontend.canonicalAcirBytes) || + frontend.acpy.path == frontend.canonicalAcir.path) + return buildError("ACLOWER-FINGERPRINT", + "frontend artifacts are invalid or inconsistent"); + llvm::StringRef previousHelper; + for (const NamedFingerprint &helper : frontend.helperIdentities) { + if (helper.name.empty() || !isValidFingerprint(helper.fingerprint) || + (!previousHelper.empty() && previousHelper >= helper.name)) + return buildError("ACLOWER-FINGERPRINT", + "frontend helper identities are not canonical"); + previousHelper = helper.name; + } + } + const ToolchainIdentity &toolchain = request.toolchain; + if (toolchain.compilerPath.empty() || toolchain.compilerName.empty() || + toolchain.compilerBuildId.empty() || toolchain.targetTriple.empty() || + toolchain.standardLibrary.empty() || toolchain.abiMode.empty() || + toolchain.objectFormat.empty() || toolchain.contractFlags.empty() || + !isValidFingerprint(toolchain.fingerprint)) + return buildError("ACLOWER-FINGERPRINT", + "toolchain identity is incomplete"); + if (std::none_of(toolchain.contractFlags.begin(), + toolchain.contractFlags.end(), [](llvm::StringRef flag) { + return flag == "-std=c++20" || flag == "-std=gnu++20"; + })) + return buildError("ACLOWER-FINGERPRINT", + "toolchain does not require C++20"); + auto expectedFingerprint = toolchainFingerprint(toolchain); + if (!expectedFingerprint || *expectedFingerprint != toolchain.fingerprint) + return buildError("ACLOWER-FINGERPRINT", + "toolchain fingerprint does not match its fields"); + + auto observed = identifyToolchain( + toolchain.compilerPath, toolchain.standardLibrary, toolchain.abiMode, + toolchain.objectFormat, toolchain.contractFlags); + if (!observed || observed->compilerBuildId != toolchain.compilerBuildId || + observed->targetTriple != toolchain.targetTriple || + observed->fingerprint != toolchain.fingerprint) + return buildError("ACLOWER-FINGERPRINT", + "compiler probe does not match declared toolchain"); + + std::set paths; + for (const PrebuiltInput &input : request.prebuiltInputs) { + if (!isNormalizedRelativePath(input.path) || input.kind.empty() || + !paths.insert(input.path).second) + return buildError("ACLOWER-FINGERPRINT", + "prebuilt input identity is invalid"); + if (!provenanceMatches(input.provenance, toolchain) && + !input.sourceAvailable) + return buildError("ACLOWER-FINGERPRINT", + "prebuilt provenance does not match toolchain"); + } + + if (request.canonicalACSim) { + if (request.frozenAcirBytes.empty() || request.bindingLockBytes.empty()) + return buildError("ACLOWER-FINGERPRINT", + "frozen ACIR and binding lock bytes are required"); + auto model = buildModelPlan(request.canonicalACSim); + if (!model) + return model.takeError(); + if (!request.frozenAcirBytes.empty() && + computeFingerprint(request.frozenAcirBytes) != + model->frozenAcirFingerprint) + return buildError("ACLOWER-FINGERPRINT", + "frozen ACIR bytes do not match ACSim"); + if (!request.bindingLockBytes.empty()) { + auto canonicalLock = + bindings::canonicalizeJsonText(request.bindingLockBytes); + if (!canonicalLock) + return canonicalLock.takeError(); + if (computeFingerprint(*canonicalLock) != model->bindingLockFingerprint) + return buildError("ACLOWER-FINGERPRINT", + "binding lock bytes do not match ACSim"); + } + auto profileFingerprint = + fingerprintJsonValue(llvm::json::Value(request.profile)); + auto targetFingerprint = + fingerprintJsonValue(llvm::json::Value(toolchain.targetTriple)); + if (!profileFingerprint) + return profileFingerprint.takeError(); + if (!targetFingerprint) + return targetFingerprint.takeError(); + if (*profileFingerprint != model->profileFingerprint || + *targetFingerprint != model->toolchainFingerprint) + return buildError("ACLOWER-PROFILE", + "profile or target identity does not match ACSim"); + + std::vector providers = request.providerInputs; + std::sort(providers.begin(), providers.end()); + providers.erase(std::unique(providers.begin(), providers.end()), + providers.end()); + llvm::json::Array providerArray; + for (const std::string &provider : providers) + providerArray.push_back(provider); + auto providerFingerprint = + fingerprintJsonValue(llvm::json::Value(std::move(providerArray))); + if (!providerFingerprint) + return providerFingerprint.takeError(); + if (*providerFingerprint != model->providerFingerprint) + return buildError("ACLOWER-FINGERPRINT", + "provider identity set does not match ACSim"); + + if (!request.canonicalACSimBytes.empty()) { + std::string printed; + llvm::raw_string_ostream stream(printed); + mlir::ModuleOp canonicalACSim = request.canonicalACSim; + canonicalACSim.print(stream); + stream.flush(); + if (printed != request.canonicalACSimBytes) + return buildError("ACLOWER-FINGERPRINT", + "canonical ACSim bytes do not match the module"); + } + } + return llvm::Error::success(); +} + +llvm::Error CompilePlan::validate() const { + if (schema != "acsim-compile-plan-0.1") + return buildError("ACLOWER-FINGERPRINT", "compile plan schema is invalid"); + if (!isValidFingerprint(sourceFingerprint) || + !isValidFingerprint(toolchainFingerprint) || + !isValidFingerprint(fingerprint) || + sourceUnits.size() != objectOutputs.size() || + sourceUnits.size() != compileCommands.size() || executablePath.empty()) + return buildError("ACLOWER-FINGERPRINT", + "compile plan identity or command arity is invalid"); + if (!isSortedUnique(includeRoots) || !isSortedUnique(definitions)) + return buildError("ACLOWER-FINGERPRINT", + "compile plan set-like fields are not canonical"); + for (llvm::StringRef root : includeRoots) + if (!isCanonicalIncludeRoot(root)) + return buildError("ACLOWER-FINGERPRINT", + "compile plan include root is not canonical"); + for (llvm::StringRef path : sourceUnits) + if (!isNormalizedRelativePath(path)) + return buildError("ACLOWER-FINGERPRINT", + "compile plan contains a non-normalized source path"); + for (llvm::StringRef path : objectOutputs) + if (!isNormalizedRelativePath(path)) + return buildError("ACLOWER-FINGERPRINT", + "compile plan contains a non-normalized object path"); + for (const LinkInputIdentity &input : linkInputs) + if (!isCanonicalIncludeRoot(input.path) || + !isValidFingerprint(input.fingerprint)) + return buildError("ACLOWER-FINGERPRINT", + "compile plan link input identity is invalid"); + if (!isNormalizedRelativePath(executablePath) || + linkCommand.arguments.empty() || linkCommand.output != executablePath) + return buildError("ACLOWER-FINGERPRINT", + "compile plan link command is invalid"); + for (auto [index, command] : llvm::enumerate(compileCommands)) { + if (command.arguments.empty() || command.output != objectOutputs[index] || + command.arguments.back() != command.output) + return buildError("ACLOWER-FINGERPRINT", + "compile command is not a closed argument vector"); + } + return llvm::Error::success(); +} + +llvm::Expected CompilePlan::canonicalJson() const { + if (auto error = validate()) + return std::move(error); + return bindings::canonicalizeJson( + llvm::json::Value(planJson(*this, fingerprint))); +} + +llvm::Expected createCompilePlan(const BuildRequest &request, + const SourceBundle &bundle) { + if (auto error = preflightBuildRequest(request)) + return std::move(error); + if (!isValidFingerprint(bundle.sourceFingerprint) || + !isValidFingerprint(bundle.buildFingerprint)) + return buildError("ACLOWER-FINGERPRINT", + "source bundle build identity is invalid"); + llvm::StringRef previousPath; + for (const GeneratedFile &file : bundle.files) { + if (!isNormalizedRelativePath(file.relativePath) || + (!previousPath.empty() && previousPath >= file.relativePath) || + file.fingerprint != computeFingerprint(file.content) || + file.content.find('\r') != std::string::npos) + return buildError("ACLOWER-FINGERPRINT", + "source bundle is not canonical and complete"); + previousPath = file.relativePath; + } + + CompilePlan plan; + plan.includeRoots = request.includeRoots; + llvm::StringRef llvmIncludeRoots(ACIR_CODEGEN_LLVM_INCLUDE_DIRS); + while (!llvmIncludeRoots.empty()) { + auto [root, remainder] = llvmIncludeRoots.split('|'); + if (!root.empty()) + plan.includeRoots.push_back(root.str()); + llvmIncludeRoots = remainder; + } + plan.includeRoots.push_back("include"); + plan.definitions = request.definitions; + std::sort(plan.includeRoots.begin(), plan.includeRoots.end()); + plan.includeRoots.erase( + std::unique(plan.includeRoots.begin(), plan.includeRoots.end()), + plan.includeRoots.end()); + std::sort(plan.definitions.begin(), plan.definitions.end()); + plan.definitions.erase( + std::unique(plan.definitions.begin(), plan.definitions.end()), + plan.definitions.end()); + for (llvm::StringRef path : plan.includeRoots) + if (!isCanonicalIncludeRoot(path)) + return buildError("ACLOWER-FINGERPRINT", + "include root is not a normalized relative path"); + plan.compilerFlags = request.compilerFlags; + plan.linkerFlags = request.linkerFlags; + plan.toolchainFingerprint = request.toolchain.fingerprint; + plan.sourceFingerprint = bundle.sourceFingerprint; + plan.executablePath = "bin/model"; + + plan.prebuiltInputs = request.prebuiltInputs; + std::erase_if(plan.prebuiltInputs, [&](const PrebuiltInput &input) { + return input.sourceAvailable && + !provenanceMatches(input.provenance, request.toolchain); + }); + std::sort(plan.prebuiltInputs.begin(), plan.prebuiltInputs.end(), + [](const PrebuiltInput &left, const PrebuiltInput &right) { + return left.path < right.path; + }); + + for (const std::string &path : request.linkInputs) { + if (!isCanonicalIncludeRoot(path)) + return buildError("ACLOWER-FINGERPRINT", + "link input path is not canonical"); + auto buffer = llvm::MemoryBuffer::getFile(path); + if (!buffer) + return llvm::createStringError( + buffer.getError(), "cannot read link input '%s'", path.c_str()); + plan.linkInputs.push_back( + {.path = path, + .fingerprint = computeFingerprint(buffer.get()->getBuffer())}); + } + + for (const GeneratedFile &file : bundle.files) { + if (!llvm::StringRef(file.relativePath).ends_with(".cpp")) + continue; + plan.sourceUnits.push_back(file.relativePath); + plan.objectOutputs.push_back(objectPath(file.relativePath)); + } + for (auto [source, output] : + llvm::zip_equal(plan.sourceUnits, plan.objectOutputs)) { + CompileCommand command; + command.arguments.push_back(request.toolchain.compilerPath); + command.arguments.insert(command.arguments.end(), + request.toolchain.contractFlags.begin(), + request.toolchain.contractFlags.end()); + command.arguments.insert(command.arguments.end(), + plan.compilerFlags.begin(), + plan.compilerFlags.end()); + for (const std::string &definition : plan.definitions) + command.arguments.push_back("-D" + definition); + for (const std::string &include : plan.includeRoots) + command.arguments.push_back("-I" + include); + command.arguments.insert(command.arguments.end(), + {"-c", source, "-o", output}); + command.output = output; + plan.compileCommands.push_back(std::move(command)); + } + + plan.linkCommand.arguments.push_back(request.toolchain.compilerPath); + plan.linkCommand.arguments.insert(plan.linkCommand.arguments.end(), + plan.objectOutputs.begin(), + plan.objectOutputs.end()); + for (const PrebuiltInput &input : plan.prebuiltInputs) + plan.linkCommand.arguments.push_back(input.path); + plan.linkCommand.arguments.insert(plan.linkCommand.arguments.end(), + request.linkInputs.begin(), + request.linkInputs.end()); + plan.linkCommand.arguments.insert(plan.linkCommand.arguments.end(), + plan.linkerFlags.begin(), + plan.linkerFlags.end()); + plan.linkCommand.arguments.insert(plan.linkCommand.arguments.end(), + {"-o", plan.executablePath}); + plan.linkCommand.output = plan.executablePath; + + llvm::json::Object preimage{{"domain", "acsim-compile-plan-0.1"}, + {"plan", planJson(plan, "")}}; + auto fingerprint = + fingerprintCanonicalJson(llvm::json::Value(std::move(preimage))); + if (!fingerprint) + return fingerprint.takeError(); + plan.fingerprint = std::move(*fingerprint); + if (auto error = plan.validate()) + return std::move(error); + return plan; +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/Emitter.cpp b/components/agentic-circuit/lib/CodeGen/Emitter.cpp new file mode 100644 index 00000000..d88b8dd9 --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/Emitter.cpp @@ -0,0 +1,375 @@ +#include "acir/CodeGen/Emitter.h" + +#include +#include +#include +#include +#include +#include + +namespace acir::codegen { + +// ── CppEmitter ───────────────────────────────────────────────────────── + +void CppEmitter::writeIndent() { + for (int i = 0; i < indent_; ++i) + os_ << " "; +} + +void CppEmitter::emitPragmaOnce() { + os_ << "#pragma once\n\n"; + atLineStart_ = true; +} + +void CppEmitter::emitInclude(const std::string &header, bool system) { + if (system) + os_ << "#include <" << header << ">\n"; + else + os_ << "#include \"" << header << "\"\n"; + atLineStart_ = true; +} + +void CppEmitter::emitNewline() { + os_ << "\n"; + atLineStart_ = true; +} + +void CppEmitter::emitComment(const std::string &text) { + writeIndent(); + os_ << "// " << text << "\n"; + atLineStart_ = true; +} + +void CppEmitter::beginNamespace(const std::string &name) { + os_ << "namespace " << name << " {\n\n"; + atLineStart_ = true; +} + +void CppEmitter::endNamespace() { + os_ << "} // namespace\n\n"; + atLineStart_ = true; +} + +void CppEmitter::beginClass(const std::string &name, const std::string &base, + bool isStruct, bool isFinal) { + writeIndent(); + os_ << (isStruct ? "struct " : "class ") << name; + if (isFinal) + os_ << " final"; + if (!base.empty()) + os_ << " : public " << base; + os_ << " {\n"; + indent(); + atLineStart_ = true; +} + +void CppEmitter::endClass() { + dedent(); + writeIndent(); + os_ << "};\n\n"; + atLineStart_ = true; +} + +void CppEmitter::emitPublic() { + dedent(); + writeIndent(); + os_ << "public:\n"; + indent(); + atLineStart_ = true; +} + +void CppEmitter::emitPrivate() { + dedent(); + writeIndent(); + os_ << "private:\n"; + indent(); + atLineStart_ = true; +} + +void CppEmitter::emitProtected() { + dedent(); + writeIndent(); + os_ << "protected:\n"; + indent(); + atLineStart_ = true; +} + +void CppEmitter::emitMember(const std::string &type, const std::string &name, + const std::string &init) { + writeIndent(); + os_ << type << " " << name; + if (!init.empty()) + os_ << " = " << init; + os_ << ";\n"; + atLineStart_ = true; +} + +void CppEmitter::emitStaticMember(const std::string &type, + const std::string &name, + const std::string &init) { + writeIndent(); + os_ << "static " << type << " " << name; + if (!init.empty()) + os_ << " = " << init; + os_ << ";\n"; + atLineStart_ = true; +} + +void CppEmitter::emitMethod( + const std::string &returnType, const std::string &name, + const std::vector> ¶ms, + bool isConst, bool isOverride, bool isVirtual, bool isStatic) { + writeIndent(); + if (isStatic) + os_ << "static "; + if (isVirtual) + os_ << "virtual "; + os_ << returnType << " " << name << "("; + for (size_t i = 0; i < params.size(); ++i) { + os_ << params[i].first << " " << params[i].second; + if (i + 1 < params.size()) + os_ << ", "; + } + os_ << ")"; + if (isConst) + os_ << " const"; + if (isOverride) + os_ << " override"; + os_ << ";\n"; + atLineStart_ = true; +} + +void CppEmitter::emitConstructor( + const std::string &className, + const std::vector> ¶ms, + const std::vector &initializers, const std::string &body) { + writeIndent(); + os_ << className << "("; + for (size_t i = 0; i < params.size(); ++i) { + os_ << params[i].first << " " << params[i].second; + if (i + 1 < params.size()) + os_ << ", "; + } + os_ << ")"; + if (body.empty()) { + os_ << ";\n"; + atLineStart_ = true; + return; + } + if (!initializers.empty()) { + os_ << "\n"; + writeIndent(); + for (size_t i = 0; i < initializers.size(); ++i) { + os_ << " : " << initializers[i]; + if (i + 1 < initializers.size()) { + os_ << "\n"; + writeIndent(); + } + } + } + os_ << " {\n"; + os_ << body; + writeIndent(); + os_ << "}\n"; + atLineStart_ = true; +} + +void CppEmitter::beginMethodBody() { + os_ << " {\n"; + indent(); + atLineStart_ = true; +} + +void CppEmitter::endMethodBody() { + dedent(); + writeIndent(); + os_ << "}\n\n"; + atLineStart_ = true; +} + +void CppEmitter::emitReturn(const std::string &expr) { + writeIndent(); + if (expr.empty()) + os_ << "return;\n"; + else + os_ << "return " << expr << ";\n"; + atLineStart_ = true; +} + +void CppEmitter::emitStatement(const std::string &stmt) { + writeIndent(); + os_ << stmt << ";\n"; + atLineStart_ = true; +} + +void CppEmitter::emitEnum(const std::string &name, + const std::vector &values, + const std::string &underlyingType) { + writeIndent(); + os_ << "enum class " << name << " : " << underlyingType << " {\n"; + indent(); + for (size_t i = 0; i < values.size(); ++i) { + writeIndent(); + os_ << values[i]; + if (i + 1 < values.size()) + os_ << ","; + os_ << "\n"; + } + dedent(); + writeIndent(); + os_ << "};\n\n"; + atLineStart_ = true; +} + +void CppEmitter::emitSwitch(const std::string &expr) { + writeIndent(); + os_ << "switch (" << expr << ") {\n"; + indent(); + atLineStart_ = true; +} + +void CppEmitter::emitCase(const std::string &value) { + dedent(); + writeIndent(); + os_ << "case " << value << ":\n"; + indent(); + atLineStart_ = true; +} + +void CppEmitter::emitDefault() { + dedent(); + writeIndent(); + os_ << "default:\n"; + indent(); + atLineStart_ = true; +} + +void CppEmitter::emitBreak() { + writeIndent(); + os_ << "break;\n"; + atLineStart_ = true; +} + +void CppEmitter::endSwitch() { + dedent(); + writeIndent(); + os_ << "}\n"; + atLineStart_ = true; +} + +void CppEmitter::emitTemplate(const std::string ¶ms) { + writeIndent(); + os_ << "template <" << params << ">\n"; + atLineStart_ = true; +} + +void CppEmitter::emitRaw(const std::string &code) { + os_ << code; + atLineStart_ = false; +} + +void CppEmitter::indent() { ++indent_; } +void CppEmitter::dedent() { + if (indent_ > 0) + --indent_; +} + +// ── Deterministic code generation ───────────────────────────────────── + +SourceFile generateDispatchHeader(const std::string &namespaceName, + const std::string &modelType, + std::vector entries, + std::vector activationEdges) { + if (namespaceName.empty() || modelType.empty()) + throw std::invalid_argument( + "dispatch namespace and model type must be non-empty"); + if (entries.size() > std::numeric_limits::max()) + throw std::invalid_argument("dispatch table exceeds the uint32_t ID space"); + + std::sort(entries.begin(), entries.end(), + [](const DispatchEntry &left, const DispatchEntry &right) { + return left.objectId < right.objectId; + }); + for (size_t index = 0; index < entries.size(); ++index) { + if (entries[index].objectId != index || + entries[index].objectExpression.empty()) + throw std::invalid_argument( + "dispatch object IDs must be dense and expressions non-empty"); + } + + std::sort(activationEdges.begin(), activationEdges.end(), + [](const ActivationEdge &left, const ActivationEdge &right) { + return std::tie(left.sourceId, left.targetId) < + std::tie(right.sourceId, right.targetId); + }); + activationEdges.erase( + std::unique(activationEdges.begin(), activationEdges.end(), + [](const ActivationEdge &left, const ActivationEdge &right) { + return left.sourceId == right.sourceId && + left.targetId == right.targetId; + }), + activationEdges.end()); + if (activationEdges.size() > std::numeric_limits::max()) + throw std::invalid_argument( + "activation adjacency exceeds the uint32_t offset space"); + std::vector activationOffsets(entries.size() + 1, 0); + std::vector activationTargets; + activationTargets.reserve(activationEdges.size()); + for (const ActivationEdge &edge : activationEdges) { + if (edge.sourceId >= entries.size() || edge.targetId >= entries.size()) + throw std::invalid_argument( + "activation edge endpoint is outside the dense dispatch table"); + ++activationOffsets[edge.sourceId + 1]; + activationTargets.push_back(edge.targetId); + } + for (size_t index = 1; index < activationOffsets.size(); ++index) + activationOffsets[index] += activationOffsets[index - 1]; + + std::ostringstream os; + os << "#pragma once\n\n" + "#include \"gfsim/dispatch.h\"\n\n" + "#include \n" + "#include \n\n" + << "namespace " << namespaceName << " {\n\n" + << "inline std::array\n" + << "makeDispatchTable(" << modelType << " &model) {\n" + << " return {\n"; + for (const DispatchEntry &entry : entries) + os << " gfsim::makeDispatchRow(&" << entry.objectExpression << "),\n"; + os << " };\n" + "}\n\n"; + os << "inline constexpr std::array kActivationOffsets = {"; + for (size_t index = 0; index < activationOffsets.size(); ++index) { + if (index != 0) + os << ", "; + os << activationOffsets[index]; + } + os << "};\n"; + os << "inline constexpr std::array kActivationTargets = {"; + for (size_t index = 0; index < activationTargets.size(); ++index) { + if (index != 0) + os << ", "; + os << activationTargets[index]; + } + os << "};\n\n" + << "} // namespace " << namespaceName << "\n"; + + std::string pathNamespace = namespaceName; + constexpr std::string_view generatedPrefix = "generated::"; + if (pathNamespace.starts_with(generatedPrefix)) + pathNamespace.erase(0, generatedPrefix.size()); + for (size_t separator = pathNamespace.find("::"); + separator != std::string::npos; + separator = pathNamespace.find("::", separator + 1)) + pathNamespace.replace(separator, 2, "/"); + + SourceFile sf; + sf.relativePath = "include/generated/" + pathNamespace + "/dispatch.h"; + sf.content = os.str(); + sf.fingerprint = computeFingerprint(sf.content); + return sf; +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/Generator.cpp b/components/agentic-circuit/lib/CodeGen/Generator.cpp new file mode 100644 index 00000000..748f39ea --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/Generator.cpp @@ -0,0 +1,967 @@ +#include "acir/CodeGen/Generator.h" + +#include "ProcessGenerator.h" + +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/FormatVariadic.h" + +#include +#include +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +llvm::Error generatorError(const llvm::Twine &code, + const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), code + ": " + message); +} + +bool isIdentifier(llvm::StringRef value) { + if (value.empty() || + !(std::isalpha(static_cast(value.front())) || + value.front() == '_')) + return false; + return std::all_of(value.drop_front().begin(), value.end(), [](char value) { + return std::isalnum(static_cast(value)) || value == '_'; + }); +} + +bool isQualifiedName(llvm::StringRef value) { + if (value.empty() || value.starts_with(':') || value.ends_with(':')) + return false; + while (!value.empty()) { + auto split = value.split("::"); + if (!isIdentifier(split.first)) + return false; + if (split.second.empty()) + return true; + value = split.second; + } + return true; +} + +bool isIncludePath(llvm::StringRef value) { + return !value.empty() && !value.starts_with('/') && !value.contains('\\') && + !value.contains('"') && !value.contains("..") && + std::all_of(value.begin(), value.end(), [](char character) { + return character >= 0x20 && character != 0x7f; + }); +} + +bool isNormalizedRelativePath(llvm::StringRef value) { + if (value.empty() || value.starts_with('/') || value.ends_with('/') || + value.contains('\\')) + return false; + while (!value.empty()) { + auto [component, rest] = value.split('/'); + if (component.empty() || component == "." || component == "..") + return false; + value = rest; + } + return true; +} + +const BindingPlan *findBinding(const ModelPlan &plan, llvm::StringRef symbol) { + auto found = std::find_if( + plan.bindings.begin(), plan.bindings.end(), + [&](const BindingPlan &binding) { return binding.symbol == symbol; }); + return found == plan.bindings.end() ? nullptr : &*found; +} + +const ModulePlan *findModule(const ModelPlan &plan, llvm::StringRef symbol) { + auto found = std::find_if( + plan.modules.begin(), plan.modules.end(), + [&](const ModulePlan &module) { return module.symbol == symbol; }); + return found == plan.modules.end() ? nullptr : &*found; +} + +const TypePlan *findType(const ModelPlan &plan, llvm::StringRef symbol) { + auto found = + std::find_if(plan.types.begin(), plan.types.end(), + [&](const TypePlan &type) { return type.symbol == symbol; }); + return found == plan.types.end() ? nullptr : &*found; +} + +const ModulePlan *rootModule(const ModelPlan &plan) { + return findModule(plan, plan.rootSymbol); +} + +llvm::Expected cppLiteral(const llvm::json::Value &value) { + if (value.kind() == llvm::json::Value::Null) + return std::string("{}"); + if (auto boolean = value.getAsBoolean()) + return std::string(*boolean ? "true" : "false"); + if (auto integer = value.getAsInteger()) + return std::to_string(*integer); + if (auto number = value.getAsNumber()) + return llvm::formatv("{0}", *number).str(); + if (value.getAsString()) + return llvm::formatv("{0}", value).str(); + if (const llvm::json::Array *array = value.getAsArray()) { + std::string result = "{"; + for (auto [index, element] : llvm::enumerate(*array)) { + auto literal = cppLiteral(element); + if (!literal) + return literal.takeError(); + if (index != 0) + result.append(", "); + result.append(*literal); + } + return result.append("}"); + } + if (const llvm::json::Object *object = value.getAsObject()) { + std::vector> fields; + for (const auto &field : *object) + fields.emplace_back(field.first.str(), &field.second); + std::sort(fields.begin(), fields.end()); + std::string result = "{"; + for (auto [index, field] : llvm::enumerate(fields)) { + auto literal = cppLiteral(*field.second); + if (!literal) + return literal.takeError(); + if (index != 0) + result.append(", "); + result.append(*literal); + } + return result.append("}"); + } + return generatorError("ACLOWER-PARAM-PHASE", + "static value cannot be represented in C++"); +} + +llvm::Expected bindingType(const ModelPlan &plan, + const BindingPlan &binding) { + std::vector templateArguments; + for (const ParameterPlan ¶meter : binding.parameters) + if (parameter.mapping == ParameterMappingKind::TemplateArgument) + templateArguments.push_back(¶meter); + std::sort(templateArguments.begin(), templateArguments.end(), + [](const ParameterPlan *left, const ParameterPlan *right) { + return left->ordinal < right->ordinal; + }); + if (templateArguments.empty()) + return binding.cppSymbol; + std::string result = binding.cppSymbol + "<"; + for (auto [index, parameter] : llvm::enumerate(templateArguments)) { + std::string argument; + if (auto identity = parameter->canonicalValue.getAsString()) { + const TypePlan *type = findType(plan, *identity); + if (!type) + type = findType(plan, parameter->cppType); + if (!type) + return generatorError( + "ACLOWER-TYPE-MISMATCH", + "template type identity has no concrete C++ realization"); + argument = type->cppType; + } else { + auto literal = cppLiteral(parameter->canonicalValue); + if (!literal) + return literal.takeError(); + argument = std::move(*literal); + } + if (index != 0) + result.append(", "); + result.append(argument); + } + return result.append(">"); +} + +llvm::Expected constructorSuffix(const BindingPlan &binding) { + std::string result; + for (const llvm::json::Value &argument : binding.constructorArguments) { + auto literal = cppLiteral(argument); + if (!literal) + return literal.takeError(); + result.append(", ").append(*literal); + } + return result; +} + +constexpr llvm::StringLiteral kGeneratedFingerprintPlaceholder = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + +llvm::Expected +sourceBundleFingerprint(const std::vector &files) { + llvm::json::Array sources; + for (const GeneratedFile &file : files) + sources.push_back(llvm::json::Object{{"path", file.relativePath}, + {"content", file.content}}); + llvm::json::Object preimage{{"domain", "acsim-source-bundle-0.1"}, + {"sources", std::move(sources)}}; + return fingerprintCanonicalJson(llvm::json::Value(std::move(preimage))); +} + +llvm::Error embedSourceBundleFingerprint(SourceBundle &bundle) { + auto fingerprint = sourceBundleFingerprint(bundle.files); + if (!fingerprint) + return fingerprint.takeError(); + size_t replacements = 0; + for (GeneratedFile &file : bundle.files) { + size_t position = file.content.find(kGeneratedFingerprintPlaceholder); + if (position == std::string::npos) + continue; + if (file.content.find(kGeneratedFingerprintPlaceholder, + position + kGeneratedFingerprintPlaceholder.size()) != + std::string::npos) + return generatorError("ACLOWER-FINGERPRINT", + "generated fingerprint placeholder is repeated"); + file.content.replace(position, kGeneratedFingerprintPlaceholder.size(), + *fingerprint); + file.fingerprint = computeFingerprint(file.content); + ++replacements; + } + if (replacements != 1) + return generatorError("ACLOWER-FINGERPRINT", + "generated fingerprint placeholder is incomplete"); + bundle.buildFingerprint = std::move(*fingerprint); + bundle.sourceFingerprint = bundle.buildFingerprint; + return llvm::Error::success(); +} + +llvm::Expected placementType(const ModelPlan &plan, + const PlacementPlan &placement) { + llvm::StringRef target = placement.target; + std::string base; + if (const BindingPlan *binding = findBinding(plan, target)) { + auto type = bindingType(plan, *binding); + if (!type) + return type.takeError(); + base = std::move(*type); + } else if (const ModulePlan *module = findModule(plan, target)) + base = module->className; + else + return generatorError("ACLOWER-TYPE-MISMATCH", + "placement target has no typed realization"); + + for (auto extent = placement.shape.rbegin(); extent != placement.shape.rend(); + ++extent) { + std::string wrapped = "std::array<"; + wrapped.append(base) + .append(", ") + .append(std::to_string(*extent)) + .append(">"); + base = std::move(wrapped); + } + return base; +} + +GeneratedFile makeFile(std::string path, std::string content) { + GeneratedFile file{std::move(path), std::move(content), {}}; + file.fingerprint = computeFingerprint(file.content); + return file; +} + +llvm::Expected moduleHeader(const ModelPlan &plan, + const ModulePlan &module) { + std::set headers; + std::set moduleHeaders; + std::set> conceptChecks; + for (const PlacementPlan &placement : module.placements) { + llvm::StringRef target = placement.target; + target = target.take_until([](char value) { return value == ':'; }); + if (const BindingPlan *binding = findBinding(plan, target)) { + headers.insert(binding->header); + auto type = bindingType(plan, *binding); + if (!type) + return type.takeError(); + conceptChecks.emplace(binding->conceptName, std::move(*type)); + } else if (const ModulePlan *nested = findModule(plan, target)) { + moduleHeaders.insert("generated/modules/" + nested->className + ".h"); + } + } + for (const ExpressionPlan &expression : module.expressions) { + const BindingPlan *binding = findBinding(plan, expression.callee); + if (!binding) + continue; + headers.insert(binding->header); + auto type = bindingType(plan, *binding); + if (!type) + return type.takeError(); + conceptChecks.emplace(binding->conceptName, std::move(*type)); + } + + std::ostringstream output; + output << "#pragma once\n\n#include \"gfsim/object.h\"\n"; + if (std::any_of(module.placements.begin(), module.placements.end(), + [](const PlacementPlan &placement) { + return !placement.shape.empty(); + })) + output << "#include \n"; + for (const std::string &header : headers) + output << "#include \"" << header << "\"\n"; + for (const std::string &header : moduleHeaders) + output << "#include \"" << header << "\"\n"; + for (const ProcessPlan &process : module.processes) + output << "#include \"generated/processes/" << process.className + << ".h\"\n"; + output << "\nnamespace acsim_generated {\n\n"; + for (const auto &[conceptName, type] : conceptChecks) + output << "static_assert(" << conceptName << '<' << type << ">);\n"; + if (!conceptChecks.empty()) + output << '\n'; + output << "class " << module.className + << " final : public gfsim::Module {\npublic:\n " << module.className + << "(std::string name, gfsim::ObjectId id, gfsim::SimObject " + "*parent, gfsim::ObjectId &nextObjectId);\n"; + for (const ExportPlan &exported : module.exports) + output << " decltype(auto) " << exported.symbol << "();\n" + << " decltype(auto) " << exported.symbol << "() const;\n"; + output + << "\nprivate:\n friend class Model;\n friend struct DispatchAccess;\n"; + for (const PlacementPlan &placement : module.placements) { + auto type = placementType(plan, placement); + if (!type) + return type.takeError(); + output << " " << *type << ' ' << placement.memberName << ";\n"; + } + for (const ProcessPlan &process : module.processes) + output << " " << process.className << ' ' << process.symbol << "_;\n"; + output << "};\n\n} // namespace acsim_generated\n"; + return makeFile("include/generated/modules/" + module.className + ".h", + output.str()); +} + +llvm::Expected moduleValueExpression(const ModelPlan &plan, + const ModulePlan &module, + llvm::StringRef value) { + auto placement = + std::find_if(module.placements.begin(), module.placements.end(), + [&](const PlacementPlan &candidate) { + return candidate.resultValue == value; + }); + if (placement != module.placements.end()) + return placement->memberName; + + auto projection = + std::find_if(module.projections.begin(), module.projections.end(), + [&](const ProjectionPlan &candidate) { + return candidate.resultValue == value; + }); + if (projection != module.projections.end()) { + auto base = moduleValueExpression(plan, module, projection->baseValue); + if (!base) + return base.takeError(); + for (uint64_t index : projection->indices) + base->append("[").append(std::to_string(index)).append("]"); + if (projection->kind != ProjectionKind::Element) { + const TypePlan *accessor = findType(plan, projection->accessor); + if (!accessor || !isIdentifier(accessor->cppType)) + return generatorError("ACLOWER-TYPE-MISMATCH", + "projection accessor has no safe C++ name"); + base->append(".").append(accessor->cppType).append("()"); + } + return base; + } + + auto expression = + std::find_if(module.expressions.begin(), module.expressions.end(), + [&](const ExpressionPlan &candidate) { + return candidate.resultValue == value; + }); + if (expression != module.expressions.end()) { + const BindingPlan *binding = findBinding(plan, expression->callee); + if (!binding || binding->effect != BindingEffect::Pure || + binding->entryPoints.pure.empty()) + return generatorError("ACLOWER-INLINE-EFFECT", + "pure expression has no exact entry point"); + std::string result = binding->entryPoints.pure + "("; + for (auto [index, argument] : llvm::enumerate(expression->arguments)) { + auto emitted = moduleValueExpression(plan, module, argument); + if (!emitted) + return emitted.takeError(); + if (index != 0) + result.append(", "); + result.append(*emitted); + } + return result.append(")"); + } + + auto exported = std::find_if(module.exports.begin(), module.exports.end(), + [&](const ExportPlan &candidate) { + return candidate.resultValue == value; + }); + if (exported != module.exports.end()) + return moduleValueExpression(plan, module, exported->sourceValue); + return generatorError("ACLOWER-OWNERSHIP", + "module value has no structured realization"); +} + +llvm::Expected arrayInitializer(const ModelPlan &plan, + const PlacementPlan &placement, + size_t dimension, + std::vector &indices) { + std::string result = "{"; + const uint64_t extent = placement.shape[dimension]; + for (uint64_t index = 0; index < extent; ++index) { + if (index != 0) + result.append(", "); + indices.push_back(index); + if (dimension + 1 != placement.shape.size()) { + auto nested = arrayInitializer(plan, placement, dimension + 1, indices); + if (!nested) + return nested.takeError(); + result.append(*nested); + } else { + llvm::StringRef target = placement.target; + target = target.take_until([](char value) { return value == ':'; }); + const BindingPlan *binding = findBinding(plan, target); + const ModulePlan *nestedModule = findModule(plan, target); + if (!binding && !nestedModule) + return generatorError("ACLOWER-TYPE-MISMATCH", + "array target has no typed realization"); + auto type = placementType(plan, PlacementPlan{.target = target.str()}); + if (!type) + return type.takeError(); + result.append(*type).append("(\"").append(placement.symbol); + for (uint64_t element : indices) + result.append("[").append(std::to_string(element)).append("]"); + if (binding) { + auto suffix = constructorSuffix(*binding); + if (!suffix) + return suffix.takeError(); + result.append("\", nextObjectId++, this").append(*suffix).append(")"); + } else { + result.append("\", gfsim::kInvalidObjectId, this, nextObjectId)"); + } + } + indices.pop_back(); + } + result.append("}"); + return result; +} + +llvm::Expected moduleSource(const ModelPlan &plan, + const ModulePlan &module) { + std::vector initializers; + for (const PlacementPlan &placement : module.placements) { + if (placement.kind == PlacementKind::GeneratedModule) { + initializers.push_back(placement.memberName + "(\"" + placement.symbol + + "\", gfsim::kInvalidObjectId, this, " + "nextObjectId)"); + continue; + } + llvm::StringRef target = placement.target; + target = target.take_until([](char value) { return value == ':'; }); + const BindingPlan *binding = findBinding(plan, target); + const ModulePlan *nestedModule = findModule(plan, target); + if (!binding && !nestedModule) + return generatorError("ACLOWER-BINDING-MISSING", + "placement has no selected binding"); + if (placement.shape.empty()) { + if (!binding) + return generatorError("ACLOWER-OWNERSHIP", + "generated module placement kind is invalid"); + auto suffix = constructorSuffix(*binding); + if (!suffix) + return suffix.takeError(); + initializers.push_back(placement.memberName + "(\"" + placement.symbol + + "\", nextObjectId++, this" + *suffix + ")"); + } else { + std::vector indices; + auto initializer = arrayInitializer(plan, placement, 0, indices); + if (!initializer) + return initializer.takeError(); + initializers.push_back(placement.memberName + *initializer); + } + } + for (const ProcessPlan &process : module.processes) { + std::string initializer = + process.symbol + "_(\"" + process.symbol + "\", nextObjectId++, this"; + for (const CapturePlan &capture : process.captures) { + auto expression = + moduleValueExpression(plan, module, capture.sourceValue); + if (!expression) + return expression.takeError(); + initializer.append(", ").append(*expression); + } + initializer.append(")"); + initializers.push_back(std::move(initializer)); + } + + std::ostringstream output; + output << "#include \"generated/modules/" << module.className + << ".h\"\n\n#include \n\nnamespace acsim_generated {\n\n"; + if (std::any_of( + module.binds.begin(), module.binds.end(), + [](const BindPlan &bind) { return bind.kind != "pure_view"; })) + output << "namespace {\ntemplate \n" + "void bindStatic(Source &&source, Target &&target) {\n" + " if constexpr (requires { source.bind(target); })\n" + " source.bind(target);\n" + " else if constexpr (requires { target.bind(source); })\n" + " target.bind(source);\n" + " else\n" + " static_assert(sizeof(Source) == 0, " + "\"ACLOWER-TYPE-MISMATCH\");\n" + "}\n} // namespace\n\n"; + output << module.className << "::" << module.className + << "(std::string name, gfsim::ObjectId id, gfsim::SimObject *parent, " + "gfsim::ObjectId &nextObjectId)\n" + " : gfsim::Module(std::move(name), id, parent)"; + for (const std::string &initializer : initializers) + output << ",\n " << initializer; + output << " {\n"; + for (const PlacementPlan &placement : module.placements) { + if (placement.shape.empty()) { + output << " if (!attachChild(" << placement.memberName << "))\n" + << " throw std::logic_error(\"ACLOWER-OWNERSHIP\");\n"; + } else { + std::string range = placement.memberName; + for (size_t dimension = 0; dimension < placement.shape.size(); + ++dimension) { + output << std::string(2 + dimension * 2, ' ') << "for (auto &element" + << dimension << " : " << range << ") {\n"; + range = "element" + std::to_string(dimension); + } + output << std::string(2 + placement.shape.size() * 2, ' ') + << "if (!attachChild(" << range << "))\n" + << std::string(4 + placement.shape.size() * 2, ' ') + << "throw std::logic_error(\"ACLOWER-OWNERSHIP\");\n"; + for (size_t dimension = placement.shape.size(); dimension > 0; + --dimension) + output << std::string(2 + (dimension - 1) * 2, ' ') << "}\n"; + } + } + for (const ProcessPlan &process : module.processes) + output << " if (!attachChild(" << process.symbol << "_))\n" + << " throw std::logic_error(\"ACLOWER-OWNERSHIP\");\n"; + for (const BindPlan &bind : module.binds) { + if (bind.kind == "pure_view") + continue; + auto source = moduleValueExpression(plan, module, bind.sourceValue); + auto target = moduleValueExpression(plan, module, bind.targetValue); + if (!source) + return source.takeError(); + if (!target) + return target.takeError(); + output << " bindStatic(" << *source << ", " << *target << ");\n"; + } + output << "}\n"; + for (const ExportPlan &exported : module.exports) { + auto expression = moduleValueExpression(plan, module, exported.sourceValue); + if (!expression) + return expression.takeError(); + output << "\ndecltype(auto) " << module.className << "::" << exported.symbol + << "() { return " << *expression << "; }\n" + << "decltype(auto) " << module.className << "::" << exported.symbol + << "() const { return " << *expression << "; }\n"; + } + output << "\n} // namespace acsim_generated\n"; + return makeFile("src/generated/modules/" + module.className + ".cpp", + output.str()); +} + +llvm::Expected +runtimeObjectExpression(const ModelPlan &plan, + const RuntimeObjectPlan &runtimeObject) { + const ModulePlan *module = rootModule(plan); + if (!module) + return generatorError("ACLOWER-OWNERSHIP", + "root module has no generated specialization"); + + llvm::SmallVector segments; + llvm::StringRef(runtimeObject.hierarchyPath).split(segments, '.'); + if (segments.size() < 2) + return generatorError("ACLOWER-DISPATCH", + "runtime path has no hierarchy segments"); + std::string expression = "model.top_"; + for (size_t segmentIndex = 1; segmentIndex < segments.size(); + ++segmentIndex) { + llvm::StringRef segment = segments[segmentIndex]; + llvm::StringRef symbol = + segment.take_until([](char value) { return value == '['; }); + auto placement = + std::find_if(module->placements.begin(), module->placements.end(), + [&](const PlacementPlan &candidate) { + return candidate.symbol == symbol; + }); + if (placement != module->placements.end()) { + expression.append(".").append(placement->memberName); + llvm::StringRef indices = segment.drop_front(symbol.size()); + while (!indices.empty()) { + if (!indices.consume_front("[")) + return generatorError("ACLOWER-ARRAY", + "runtime path index is malformed"); + if (!indices.contains(']')) + return generatorError("ACLOWER-ARRAY", + "runtime path index is unterminated"); + auto [index, rest] = indices.split(']'); + expression.append("[").append(index.str()).append("]"); + indices = rest; + } + if (segmentIndex + 1 != segments.size()) { + llvm::StringRef target = placement->target; + target = target.take_until([](char value) { return value == ':'; }); + module = findModule(plan, target); + if (!module) + return generatorError("ACLOWER-DISPATCH", + "runtime path crosses a non-module member"); + } + continue; + } + auto process = + std::find_if(module->processes.begin(), module->processes.end(), + [&](const ProcessPlan &candidate) { + return candidate.symbol == symbol; + }); + if (process == module->processes.end() || + segmentIndex + 1 != segments.size()) + return generatorError("ACLOWER-DISPATCH", + "runtime path has no generated member"); + expression.append(".").append(process->symbol).append("_"); + } + return expression; +} + +llvm::Expected dispatchHeader(const ModelPlan &plan) { + std::vector offsets(plan.runtimeObjects.size() + 1, 0); + std::vector targets; + size_t edgeIndex = 0; + for (size_t source = 0; source < plan.runtimeObjects.size(); ++source) { + while (edgeIndex < plan.activationEdges.size() && + plan.activationEdges[edgeIndex].sourceId == source) { + targets.push_back(plan.activationEdges[edgeIndex].targetId); + ++edgeIndex; + } + offsets[source + 1] = static_cast(targets.size()); + } + + std::ostringstream output; + output << "#pragma once\n\n#include \"generated/model.h\"\n" + "#include \"gfsim/dispatch.h\"\n\n#include \n\n" + "namespace acsim_generated {\n\nstruct DispatchAccess {\n" + " static std::array makeRows(Model &model) {\n" + << " return {"; + for (auto [index, runtimeObject] : llvm::enumerate(plan.runtimeObjects)) { + auto expression = runtimeObjectExpression(plan, runtimeObject); + if (!expression) + return expression.takeError(); + if (index != 0) + output << ", "; + output << "gfsim::makeDispatchRow(&" << *expression << ")"; + } + output << "};\n }\n};\n\ninline constexpr std::array kActivationOffsets = {"; + for (auto [index, offset] : llvm::enumerate(offsets)) { + if (index != 0) + output << ", "; + output << offset; + } + output << "};\ninline constexpr std::array kActivationTargets = {"; + for (auto [index, target] : llvm::enumerate(targets)) { + if (index != 0) + output << ", "; + output << target; + } + output << "};\n\n} // namespace acsim_generated\n"; + return makeFile("include/generated/dispatch.h", output.str()); +} + +llvm::Expected modelHeader(const ModelPlan &plan, + llvm::StringRef fingerprint) { + const ModulePlan *root = rootModule(plan); + if (!root) + return generatorError("ACLOWER-OWNERSHIP", + "root module has no generated specialization"); + std::ostringstream output; + output + << "#pragma once\n\n#include \"generated/modules/" << root->className + << ".h\"\n#include \"gfsim/dispatch.h\"\n#include \"gfsim/harness.h\"\n" + "#include \"gfsim/object.h\"\n\n" + "#include \n#include \n#include \n\n" + "namespace acsim_generated {\n\ninline constexpr std::string_view " + "kBuildFingerprint = \"" + << fingerprint.str() + << "\";\n\ninline const std::array kTimeDomains = {{"; + for (auto [index, domain] : llvm::enumerate(plan.timeDomains)) { + if (index != 0) + output << ", "; + output << "gfsim::TimeDomainRuntime{\"" << domain.name << "\", " + << domain.period << ", " << domain.phase << ", " << domain.tickScale + << "}"; + } + output + << "}};\n\nstruct DispatchAccess;\n\nclass Model final {\n" + "public:\n Model();\n void configure(const gfsim::RuntimeLimits " + "&limits);\n bool loadTrace(gfsim::PtoTraceDocument document);\n " + "gfsim::TerminationResult run();\n std::string_view " + "buildFingerprint() const { return kBuildFingerprint; }\n " + "std::span timeDomains() const { " + "return kTimeDomains; }\n std::vector " + "statistics() const { return system_.statistics(); }\n " + "std::span observations() const { " + "return system_.observations(); }\n\nprivate:\n " + " " + "friend struct " + "DispatchAccess;\n static constexpr std::size_t kTraceOwnerCount = " + << std::count_if( + plan.runtimeObjects.begin(), plan.runtimeObjects.end(), + [](const RuntimeObjectPlan &object) { return object.traceOwner; }) + << ";\n " + << "gfsim::SimSystem system_;\n gfsim::ObjectId nextObjectId_ = 0;\n " + << root->className << " top_;\n" + << " std::array dispatch_;\n};\n\n} // namespace acsim_generated\n"; + return makeFile("include/generated/model.h", output.str()); +} + +llvm::Expected modelSource(const ModelPlan &plan) { + std::ostringstream output; + output << "#include \"generated/dispatch.h\"\n\n#include \n" + "#include \n\n" + "namespace acsim_generated {\n\nModel::Model()\n" + " : system_(\"generated\"),\n" + " nextObjectId_(0),\n" + " top_(\"root-model\", gfsim::kRootObjectId - 1, " + "&system_.root(), nextObjectId_),\n " + "dispatch_(DispatchAccess::makeRows(*this)) {\n" + " if (!system_.root().attachChild(top_))\n" + " throw std::logic_error(\"ACLOWER-OWNERSHIP\");\n" + " for (gfsim::DispatchRow &row : dispatch_) {\n" + " auto *object = static_cast(row.object);\n" + " object->bindSystem(&system_);\n" + " object->setObservationSink(&system_);\n" + " }\n" + " if (!system_.setDispatchTable(dispatch_))\n" + " throw std::logic_error(\"ACLOWER-DISPATCH\");\n" + " if (!system_.setActivationPlan(kActivationOffsets, " + "kActivationTargets))\n" + " throw std::logic_error(\"ACLOWER-ACTIVATION\");\n" + " if (!system_.setTimeDomains(kTimeDomains))\n" + " throw std::logic_error(\"ACLOWER-TIME-DOMAIN\");\n" + "}\n\nvoid Model::configure(const gfsim::RuntimeLimits &limits) {\n" + " if (!system_.setRuntimeLimits(limits))\n" + " throw std::logic_error(\"ACRUN-LIMITS\");\n" + "}\n\nbool Model::loadTrace(gfsim::PtoTraceDocument document) {\n"; + const auto traceOwnerCount = std::count_if( + plan.runtimeObjects.begin(), plan.runtimeObjects.end(), + [](const RuntimeObjectPlan &object) { return object.traceOwner; }); + if (traceOwnerCount == 0) { + output << " return document.records.empty();\n"; + } else if (traceOwnerCount == 1) { + const RuntimeObjectPlan &traceOwner = *std::find_if( + plan.runtimeObjects.begin(), plan.runtimeObjects.end(), + [](const RuntimeObjectPlan &object) { return object.traceOwner; }); + auto expression = runtimeObjectExpression(plan, traceOwner); + if (!expression) + return expression.takeError(); + llvm::StringRef memberExpression(*expression); + if (!memberExpression.consume_front("model.")) + return generatorError("ACLOWER-DISPATCH", + "trace owner expression is not model-relative"); + output << " return " << memberExpression.str() + << ".loadDocument(std::move(document));\n"; + } else { + output << " return false;\n"; + } + output << "}\n\ngfsim::TerminationResult Model::run() {\n" + " return system_.run();\n}\n\n} // namespace acsim_generated\n"; + return makeFile("src/generated/model.cpp", output.str()); +} + +GeneratedFile mainSource() { + return makeFile( + "src/generated/main.cpp", + "#include \"generated/model.h\"\n\n#include \"llvm/Support/Error.h\"\n\n" + "#include \n#include \n#include " + "\n#include " + "\n#include \n#include \n\nnamespace " + "{\n\nint " + "exitCode(gfsim::RunStatus status) {\n switch (status) {\n case " + "gfsim::RunStatus::Completed:\n return 0;\n case " + "gfsim::RunStatus::Incomplete:\n return 7;\n case " + "gfsim::RunStatus::Failed:\n return 6;\n }\n return 6;\n}\n\nint " + "exitCode(gfsim::TerminationClass classification) {\n switch " + "(classification) {\n case gfsim::TerminationClass::Completed:\n " + "return " + "0;\n case gfsim::TerminationClass::Incomplete:\n return 7;\n case " + "gfsim::TerminationClass::Failed:\n return 6;\n }\n return " + "6;\n}\n\n} " + "// namespace\n\nint main(int argc, char **argv) {\n if (argc == 2 && " + "std::string_view(argv[1]) == \"--build-fingerprint\") {\n std::cout " + "<< acsim_generated::kBuildFingerprint << '\\n';\n return 0;\n }\n\n " + " " + "acsim_generated::Model model;\n if (argc == 1) {\n " + "model.configure({});\n return " + "exitCode(model.run().classification);\n " + "}\n if (argc != 5 || std::string_view(argv[1]) != \"--run-manifest\" " + "||\n" + " std::string_view(argv[3]) != \"--run-result-stage\")\n return " + "2;\n\n std::ifstream input(argv[2], std::ios::binary);\n if " + "(!input)\n " + "return 5;\n std::string " + "bytes((std::istreambuf_iterator(input)),\n" + " std::istreambuf_iterator());\n " + "std::filesystem::path " + "manifestPath(argv[2]);\n auto manifest = gfsim::loadRunManifest(\n " + " " + "bytes, manifestPath.parent_path().string());\n if (!manifest) {\n " + "std::cerr << llvm::toString(manifest.takeError()) << '\\n';\n return " + "5;\n }\n auto result =\n gfsim::runGeneratedModel(model, " + "*manifest, " + "argv[4]);\n if (!result) {\n std::cerr << " + "llvm::toString(result.takeError()) << '\\n';\n return 5;\n }\n " + "return " + "exitCode(result->status);\n}\n"); +} + +std::vector expectedPaths(const ModelPlan &plan) { + std::vector paths = { + "include/generated/dispatch.h", "include/generated/model.h", + "src/generated/main.cpp", "src/generated/model.cpp"}; + for (const ModulePlan &module : plan.modules) { + paths.push_back("include/generated/modules/" + module.className + ".h"); + paths.push_back("src/generated/modules/" + module.className + ".cpp"); + for (const ProcessPlan &process : module.processes) { + paths.push_back("include/generated/processes/" + process.className + + ".h"); + paths.push_back("src/generated/processes/" + process.className + ".cpp"); + } + } + std::sort(paths.begin(), paths.end()); + paths.erase(std::unique(paths.begin(), paths.end()), paths.end()); + return paths; +} + +} // namespace + +llvm::Expected generateModelSources(const ModelPlan &plan) { + if (auto error = validateModelPlan(plan)) + return std::move(error); + for (const BindingPlan &binding : plan.bindings) { + const std::array entryPoints = { + llvm::StringRef(binding.entryPoints.pure), + llvm::StringRef(binding.entryPoints.reset), + llvm::StringRef(binding.entryPoints.validate), + llvm::StringRef(binding.entryPoints.work), + llvm::StringRef(binding.entryPoints.xfer)}; + if (!isIncludePath(binding.header) || !isQualifiedName(binding.cppSymbol) || + !isQualifiedName(binding.conceptName) || + std::any_of(entryPoints.begin(), entryPoints.end(), + [](llvm::StringRef entryPoint) { + return !entryPoint.empty() && + !isQualifiedName(entryPoint); + })) + return generatorError("ACLOWER-PARAM-PHASE", + "binding contains an unsafe C++ token"); + } + + SourceBundle bundle; + bundle.buildFingerprint = kGeneratedFingerprintPlaceholder.str(); + auto generatedDispatch = dispatchHeader(plan); + if (!generatedDispatch) + return generatedDispatch.takeError(); + auto generatedModel = modelHeader(plan, bundle.buildFingerprint); + if (!generatedModel) + return generatedModel.takeError(); + bundle.files.push_back(std::move(*generatedDispatch)); + bundle.files.push_back(std::move(*generatedModel)); + bundle.files.push_back(mainSource()); + auto generatedModelSource = modelSource(plan); + if (!generatedModelSource) + return generatedModelSource.takeError(); + bundle.files.push_back(std::move(*generatedModelSource)); + for (const ModulePlan &module : plan.modules) { + auto header = moduleHeader(plan, module); + if (!header) + return header.takeError(); + bundle.files.push_back(std::move(*header)); + auto source = moduleSource(plan, module); + if (!source) + return source.takeError(); + bundle.files.push_back(std::move(*source)); + for (const ProcessPlan &process : module.processes) { + auto header = detail::generateProcessHeader(plan, process); + if (!header) + return header.takeError(); + auto source = detail::generateProcessSource(plan, process); + if (!source) + return source.takeError(); + bundle.files.push_back(std::move(*header)); + bundle.files.push_back(std::move(*source)); + } + } + std::sort(bundle.files.begin(), bundle.files.end(), + [](const GeneratedFile &left, const GeneratedFile &right) { + return left.relativePath < right.relativePath; + }); + if (auto error = embedSourceBundleFingerprint(bundle)) + return std::move(error); + if (auto error = validateSourceBundle(plan, bundle)) + return std::move(error); + return bundle; +} + +llvm::Error validateSourceBundle(const ModelPlan &plan, + const SourceBundle &bundle) { + static constexpr std::array forbiddenTokens = { + "Python", + "pybind", + "dlopen", + "co_await", + "std::function", + "dynamic_cast", + "runtime_factory", + "descriptor_interpreter", + "schema_walker", + "schema_catalog", + "catalog_walker", + "catalog_lookup", + "topology_mutation", + "topology_builder"}; + const std::vector required = expectedPaths(plan); + if (!isValidFingerprint(bundle.sourceFingerprint) || + !isValidFingerprint(bundle.buildFingerprint)) + return generatorError("ACLOWER-FINGERPRINT", + "source bundle build fingerprint is invalid"); + if (bundle.files.size() != required.size()) + return generatorError("ACLOWER-FINGERPRINT", + "source bundle has an incomplete file set"); + for (size_t index = 0; index < bundle.files.size(); ++index) { + const GeneratedFile &file = bundle.files[index]; + if (file.relativePath != required[index] || + !isNormalizedRelativePath(file.relativePath)) + return generatorError("ACLOWER-FINGERPRINT", + "source paths are not canonical and complete"); + if (file.fingerprint != computeFingerprint(file.content) || + file.content.find('\r') != std::string::npos) + return generatorError("ACLOWER-FINGERPRINT", + "source content fingerprint is invalid"); + for (llvm::StringRef token : forbiddenTokens) + if (llvm::StringRef(file.content).contains(token)) + return generatorError("ACLOWER-UNSUPPORTED-CONSTRUCT", + "generated source contains forbidden token '" + + token + "'"); + } + std::vector preimageFiles = bundle.files; + size_t embeddedIdentityCount = 0; + for (GeneratedFile &file : preimageFiles) { + size_t position = file.content.find(bundle.buildFingerprint); + while (position != std::string::npos) { + file.content.replace(position, bundle.buildFingerprint.size(), + kGeneratedFingerprintPlaceholder); + ++embeddedIdentityCount; + position = file.content.find(bundle.buildFingerprint, position + 1); + } + } + if (embeddedIdentityCount != 1) + return generatorError("ACLOWER-FINGERPRINT", + "source bundle embedded identity is incomplete"); + auto expectedFingerprint = sourceBundleFingerprint(preimageFiles); + if (!expectedFingerprint || *expectedFingerprint != bundle.sourceFingerprint) + return generatorError("ACLOWER-FINGERPRINT", + "source bundle identity does not match its content"); + return llvm::Error::success(); +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/Manifest.cpp b/components/agentic-circuit/lib/CodeGen/Manifest.cpp new file mode 100644 index 00000000..6c3d8dac --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/Manifest.cpp @@ -0,0 +1,389 @@ +#include "acir/CodeGen/Manifest.h" + +#include "acir/Bindings/Binding.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/Twine.h" + +#include +#include +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +llvm::Error manifestError(const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "ACLOWER-FINGERPRINT: " + message); +} + +bool isNormalizedRelativePath(llvm::StringRef path) { + if (path.empty() || path.starts_with('/') || path.ends_with('/') || + path.contains('\\') || path.contains('\0')) + return false; + + while (!path.empty()) { + auto [component, remainder] = path.split('/'); + if (component.empty() || component == "." || component == "..") + return false; + path = remainder; + } + return true; +} + +template +llvm::Error validateUniqueKeys(const Range &range, Key key, + llvm::StringRef field) { + std::set seen; + for (const auto &value : range) { + llvm::StringRef current = key(value); + if (current.empty()) + return manifestError(field + " contains an empty stable key"); + if (!seen.insert(current.str()).second) + return manifestError(field + " contains duplicate key '" + current + "'"); + } + return llvm::Error::success(); +} + +template +std::vector sortedPointers(const std::vector &values, Key key) { + std::vector sorted; + sorted.reserve(values.size()); + for (const auto &value : values) + sorted.push_back(&value); + std::sort(sorted.begin(), sorted.end(), + [&](const T *lhs, const T *rhs) { return key(*lhs) < key(*rhs); }); + return sorted; +} + +llvm::StringRef artifactKindName(ArtifactKind kind) { + switch (kind) { + case ArtifactKind::Acpy: + return "acpy"; + case ArtifactKind::Acir: + return "acir"; + case ArtifactKind::Acsim: + return "acsim"; + case ArtifactKind::CppSource: + return "cpp_source"; + case ArtifactKind::CppHeader: + return "cpp_header"; + case ArtifactKind::Executable: + return "executable"; + case ArtifactKind::Report: + return "report"; + } + llvm_unreachable("closed ArtifactKind is exhaustive"); +} + +llvm::StringRef validationStatusName(ValidationStatus status) { + switch (status) { + case ValidationStatus::Passed: + return "passed"; + case ValidationStatus::Failed: + return "failed"; + } + llvm_unreachable("closed ValidationStatus is exhaustive"); +} + +llvm::json::Object identityJson(const Identity &identity) { + return llvm::json::Object{{"name", identity.name}, + {"identity", identity.identity}}; +} + +llvm::json::Object manifestJson(const BuildManifest &manifest, + llvm::StringRef buildFingerprint) { + llvm::json::Array sources; + for (const FileHash *source : + sortedPointers(manifest.sourceFiles, [](const FileHash &value) { + return llvm::StringRef(value.path); + })) + sources.push_back( + llvm::json::Object{{"path", source->path}, {"sha256", source->sha256}}); + + llvm::json::Object compiler{ + {"name", manifest.compiler.name}, + {"build_id", manifest.compiler.buildId}, + {"toolchain_target", manifest.compiler.toolchainTarget}}; + + llvm::json::Array pipeline; + for (const auto &pass : manifest.passPipeline) + pipeline.push_back(pass); + + llvm::json::Array providers; + for (const ProviderIdentity *provider : + sortedPointers(manifest.providers, [](const ProviderIdentity &value) { + return llvm::StringRef(value.nameSpace); + })) { + providers.push_back(llvm::json::Object{ + {"namespace", provider->nameSpace}, + {"schema_fingerprint", provider->schemaFingerprint}, + {"implementation_fingerprint", provider->implementationFingerprint}}); + } + + llvm::json::Array specializations; + for (const ComponentSpecialization *specialization : + sortedPointers(manifest.componentSpecializations, + [](const ComponentSpecialization &value) { + return llvm::StringRef(value.canonicalName); + })) { + specializations.push_back(llvm::json::Object{ + {"canonical_name", specialization->canonicalName}, + {"schema_fingerprint", specialization->schemaFingerprint}, + {"specialization_fingerprint", + specialization->specializationFingerprint}}); + } + + llvm::json::Array protocols; + for (const NamedFingerprint *protocol : sortedPointers( + manifest.protocolIdentities, [](const NamedFingerprint &value) { + return llvm::StringRef(value.name); + })) { + protocols.push_back(llvm::json::Object{ + {"name", protocol->name}, {"fingerprint", protocol->fingerprint}}); + } + + llvm::json::Array artifacts; + for (const Artifact *artifact : + sortedPointers(manifest.artifacts, [](const Artifact &value) { + return llvm::StringRef(value.path); + })) { + artifacts.push_back( + llvm::json::Object{{"path", artifact->path}, + {"kind", artifactKindName(artifact->kind)}, + {"sha256", artifact->sha256}}); + } + + llvm::json::Array gates; + for (const ValidationGate *gate : sortedPointers( + manifest.validationGates, [](const ValidationGate &value) { + return llvm::StringRef(value.name); + })) { + llvm::json::Object object{{"name", gate->name}, + {"status", validationStatusName(gate->status)}}; + if (gate->reportSha256) + object["report_sha256"] = *gate->reportSha256; + else + object["report_sha256"] = nullptr; + gates.push_back(std::move(object)); + } + + std::vector layers = manifest.instrumentationLayers; + std::sort(layers.begin(), layers.end()); + llvm::json::Array instrumentation; + for (const auto &layer : layers) + instrumentation.push_back(layer); + + llvm::json::Array specializationInputs; + for (const SpecializationInput *input : sortedPointers( + manifest.specializationInputs, [](const SpecializationInput &value) { + return llvm::StringRef(value.name); + })) { + specializationInputs.push_back( + llvm::json::Object{{"name", input->name}, + {"acir_type", input->acirType}, + {"canonical_value", input->canonicalValue}}); + } + + return llvm::json::Object{ + {"schema", manifest.schema}, + {"version", manifest.version}, + {"contract_epoch", manifest.contractEpoch}, + {"project", identityJson(manifest.project)}, + {"system", identityJson(manifest.system)}, + {"source_files", std::move(sources)}, + {"normalized_acir_sha256", manifest.normalizedAcirSha256}, + {"compiler", std::move(compiler)}, + {"pass_pipeline", std::move(pipeline)}, + {"providers", std::move(providers)}, + {"component_specializations", std::move(specializations)}, + {"protocol_identities", std::move(protocols)}, + {"artifacts", std::move(artifacts)}, + {"validation_gates", std::move(gates)}, + {"build_profile", manifest.buildProfile}, + {"instrumentation_layers", std::move(instrumentation)}, + {"specialization_inputs", std::move(specializationInputs)}, + {"build_fingerprint", buildFingerprint}, + }; +} + +llvm::Error validateManifest(const BuildManifest &manifest, + bool requireBuildFingerprint) { + if (manifest.schema != "agentic-circuit-build-manifest") + return manifestError( + "manifest schema must be agentic-circuit-build-manifest"); + if (manifest.version != "0.1" || manifest.contractEpoch != "0.3") + return manifestError( + "manifest version must be 0.1 and contract epoch must be 0.3"); + if (manifest.project.name.empty() || manifest.project.identity.empty() || + manifest.system.name.empty() || manifest.system.identity.empty()) + return manifestError("project and system identities must be non-empty"); + if (!isValidFingerprint(manifest.normalizedAcirSha256)) + return manifestError("normalized_acir_sha256 is invalid"); + if (manifest.compiler.name.empty() || manifest.compiler.buildId.empty() || + manifest.compiler.toolchainTarget.empty()) + return manifestError("compiler identity must be complete"); + if (manifest.buildProfile != "fast" && manifest.buildProfile != "validated" && + manifest.buildProfile != "custom") + return manifestError("build_profile is not a closed v0.1 value"); + if (requireBuildFingerprint && !isValidFingerprint(manifest.buildFingerprint)) + return manifestError("build_fingerprint is invalid"); + + if (auto error = validateUniqueKeys( + manifest.sourceFiles, + [](const FileHash &value) { return llvm::StringRef(value.path); }, + "source_files")) + return error; + for (const auto &source : manifest.sourceFiles) { + if (!isNormalizedRelativePath(source.path)) + return manifestError("source_files contains a non-normalized path"); + if (!isValidFingerprint(source.sha256)) + return manifestError("source_files contains an invalid fingerprint"); + } + + for (const auto &pass : manifest.passPipeline) + if (pass.empty()) + return manifestError("pass_pipeline contains an empty pass name"); + + if (auto error = validateUniqueKeys( + manifest.providers, + [](const ProviderIdentity &value) { + return llvm::StringRef(value.nameSpace); + }, + "providers")) + return error; + for (const auto &provider : manifest.providers) { + if (!isValidFingerprint(provider.schemaFingerprint) || + !isValidFingerprint(provider.implementationFingerprint)) + return manifestError("providers contains an invalid fingerprint"); + } + + if (auto error = validateUniqueKeys( + manifest.componentSpecializations, + [](const ComponentSpecialization &value) { + return llvm::StringRef(value.canonicalName); + }, + "component_specializations")) + return error; + for (const auto &specialization : manifest.componentSpecializations) { + if (!isValidFingerprint(specialization.schemaFingerprint) || + !isValidFingerprint(specialization.specializationFingerprint)) + return manifestError( + "component_specializations contains an invalid fingerprint"); + } + + if (auto error = validateUniqueKeys( + manifest.protocolIdentities, + [](const NamedFingerprint &value) { + return llvm::StringRef(value.name); + }, + "protocol_identities")) + return error; + for (const auto &protocol : manifest.protocolIdentities) + if (!isValidFingerprint(protocol.fingerprint)) + return manifestError( + "protocol_identities contains an invalid fingerprint"); + + if (auto error = validateUniqueKeys( + manifest.artifacts, + [](const Artifact &value) { return llvm::StringRef(value.path); }, + "artifacts")) + return error; + for (const auto &artifact : manifest.artifacts) { + if (!isNormalizedRelativePath(artifact.path)) + return manifestError("artifacts contains a non-normalized path"); + if (!isValidFingerprint(artifact.sha256)) + return manifestError("artifacts contains an invalid fingerprint"); + } + + if (auto error = validateUniqueKeys( + manifest.validationGates, + [](const ValidationGate &value) { + return llvm::StringRef(value.name); + }, + "validation_gates")) + return error; + for (const auto &gate : manifest.validationGates) + if (gate.reportSha256 && !isValidFingerprint(*gate.reportSha256)) + return manifestError("validation_gates contains an invalid report hash"); + + if (auto error = validateUniqueKeys( + manifest.instrumentationLayers, + [](const std::string &value) { return llvm::StringRef(value); }, + "instrumentation_layers")) + return error; + + if (auto error = validateUniqueKeys( + manifest.specializationInputs, + [](const SpecializationInput &value) { + return llvm::StringRef(value.name); + }, + "specialization_inputs")) + return error; + for (const auto &input : manifest.specializationInputs) { + if (input.acirType.empty()) + return manifestError("specialization_inputs contains an empty ACIR type"); + auto canonical = bindings::canonicalizeJson(input.canonicalValue); + if (!canonical) + return canonical.takeError(); + } + + return llvm::Error::success(); +} + +} // namespace + +bool isValidFingerprint(llvm::StringRef value) { + constexpr llvm::StringLiteral Prefix = "sha256:"; + if (!value.consume_front(Prefix) || value.size() != 64) + return false; + return std::all_of(value.begin(), value.end(), [](char character) { + return (character >= '0' && character <= '9') || + (character >= 'a' && character <= 'f'); + }); +} + +Fingerprint computeFingerprint(llvm::StringRef content) { + return bindings::sha256Fingerprint(content); +} + +llvm::Expected +fingerprintCanonicalJson(const llvm::json::Value &value) { + auto canonical = bindings::canonicalizeJson(value); + if (!canonical) + return canonical.takeError(); + return computeFingerprint(*canonical); +} + +llvm::Error BuildManifest::validate() const { + return validateManifest(*this, true); +} + +llvm::Expected BuildManifest::canonicalJson() const { + if (auto error = validate()) + return std::move(error); + return bindings::canonicalizeJson( + llvm::json::Value(manifestJson(*this, buildFingerprint))); +} + +llvm::Error BuildManifest::finalizeBuildFingerprint() { + if (auto error = validateManifest(*this, false)) + return error; + + llvm::json::Object preimage{ + {"domain", "agentic-circuit-build-0.1"}, + {"manifest", manifestJson(*this, "")}, + }; + auto fingerprint = + fingerprintCanonicalJson(llvm::json::Value(std::move(preimage))); + if (!fingerprint) + return fingerprint.takeError(); + buildFingerprint = std::move(*fingerprint); + return validate(); +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/ModelPlan.cpp b/components/agentic-circuit/lib/CodeGen/ModelPlan.cpp new file mode 100644 index 00000000..638e0626 --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/ModelPlan.cpp @@ -0,0 +1,395 @@ +#include "acir/CodeGen/ModelPlan.h" + +#include "ModelPlanInternal.h" + +#include "acir/Dialect/ACSim/ACSimOps.h" + +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/Error.h" + +#include +#include +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +llvm::Error planError(const llvm::Twine &code, const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), code + ": " + message); +} + +std::optional parseTypeKind(llvm::StringRef kind) { + return llvm::StringSwitch>(kind) + .Case("accessor", TypeKind::Accessor) + .Case("implementation", TypeKind::Implementation) + .Case("interface", TypeKind::Interface) + .Case("packet", TypeKind::Packet) + .Case("policy", TypeKind::Policy) + .Case("protocol", TypeKind::Protocol) + .Case("provider", TypeKind::Provider) + .Case("resource", TypeKind::Resource) + .Case("role", TypeKind::Role) + .Case("schema", TypeKind::Schema) + .Case("time_domain", TypeKind::TimeDomain) + .Case("value", TypeKind::Value) + .Case("wake", TypeKind::Wake) + .Case("payload", TypeKind::Payload) + .Default(std::nullopt); +} + +std::string symbolRefString(mlir::SymbolRefAttr symbol) { + std::string result = symbol.getRootReference().getValue().str(); + for (mlir::FlatSymbolRefAttr nested : symbol.getNestedReferences()) { + result += "::"; + result += nested.getValue(); + } + return result; +} + +RuntimeObjectKind inferRuntimeObjectKind(acsim::ModelOp model, + mlir::SymbolRefAttr target) { + auto nested = target.getNestedReferences(); + if (nested.size() != 1) + return RuntimeObjectKind::External; + + const llvm::StringRef moduleName = target.getRootReference().getValue(); + const llvm::StringRef memberName = nested.front().getValue(); + for (mlir::Operation &operation : model.getBody().front()) { + auto module = mlir::dyn_cast(operation); + if (!module || module.getSymName() != moduleName) + continue; + for (mlir::Operation &member : module.getBody().front()) { + auto process = mlir::dyn_cast(member); + if (process && process.getSymName() == memberName) + return RuntimeObjectKind::Process; + } + } + return RuntimeObjectKind::External; +} + +const BindingPlan *findBinding(const ModelPlan &plan, llvm::StringRef symbol) { + auto found = std::find_if( + plan.bindings.begin(), plan.bindings.end(), + [&](const BindingPlan &binding) { return binding.symbol == symbol; }); + return found == plan.bindings.end() ? nullptr : &*found; +} + +const ModulePlan *findModule(const ModelPlan &plan, llvm::StringRef symbol) { + auto found = std::find_if( + plan.modules.begin(), plan.modules.end(), + [&](const ModulePlan &module) { return module.symbol == symbol; }); + return found == plan.modules.end() ? nullptr : &*found; +} + +bool isTraceSourceBinding(const ModelPlan &plan, const BindingPlan &binding) { + if (binding.cppSymbol == "gfsim::TraceSource" || + llvm::StringRef(binding.cppSymbol).starts_with("gfsim::TraceSource<") || + binding.cppSymbol == "gfsim::ShowcaseTraceSource" || + binding.cppSymbol == "gfsim::NpuTraceSource") + return true; + auto schema = std::find_if(plan.types.begin(), plan.types.end(), + [&](const TypePlan &type) { + return type.symbol == binding.componentSchema; + }); + return schema != plan.types.end() && schema->cppType == "ac.TraceSource"; +} + +bool isTraceOwner(const ModelPlan &plan, + const RuntimeObjectPlan &runtimeObject) { + const ModulePlan *module = findModule(plan, plan.rootSymbol); + if (!module) + return false; + llvm::SmallVector segments; + llvm::StringRef(runtimeObject.hierarchyPath).split(segments, '.'); + if (segments.size() < 2) + return false; + for (size_t index = 1; index < segments.size(); ++index) { + const llvm::StringRef symbol = + segments[index].take_until([](char value) { return value == '['; }); + auto placement = + std::find_if(module->placements.begin(), module->placements.end(), + [&](const PlacementPlan &candidate) { + return candidate.symbol == symbol; + }); + if (placement == module->placements.end()) + return false; + llvm::StringRef target = placement->target; + target = target.take_until([](char value) { return value == ':'; }); + if (index + 1 == segments.size()) { + const BindingPlan *binding = findBinding(plan, target); + return binding && isTraceSourceBinding(plan, *binding); + } + module = findModule(plan, target); + if (!module) + return false; + } + return false; +} + +llvm::Expected checkedId(uint64_t value, llvm::StringRef field) { + if (value > std::numeric_limits::max()) + return planError("ACLOWER-DISPATCH", field + " exceeds uint32_t"); + return static_cast(value); +} + +llvm::Expected> stringArray(mlir::ArrayAttr values, + llvm::StringRef field) { + std::vector result; + result.reserve(values.size()); + for (mlir::Attribute value : values) { + auto string = mlir::dyn_cast(value); + if (!string) + return planError("ACLOWER-FINGERPRINT", + field + " contains a non-string value"); + result.push_back(string.getValue().str()); + } + return result; +} + +llvm::Expected fingerprintField(mlir::DictionaryAttr fields, + llvm::StringRef name) { + auto value = fields.getAs(name); + if (!value || !isValidFingerprint(value.getValue())) + return planError("ACLOWER-FINGERPRINT", + "model fingerprint '" + name + "' is invalid"); + return value.getValue().str(); +} + +} // namespace + +llvm::Error validateModelPlan(const ModelPlan &plan) { + if (plan.modelSymbol.empty() || plan.rootSymbol.empty()) + return planError("ACLOWER-FINGERPRINT", + "model and root symbols must be non-empty"); + if (plan.contractEpoch != "0.3") + return planError("ACLOWER-FINGERPRINT", "model contract epoch must be 0.3"); + for (const Fingerprint *fingerprint : + {&plan.frozenAcirFingerprint, &plan.bindingLockFingerprint, + &plan.providerFingerprint, &plan.profileFingerprint, + &plan.toolchainFingerprint, &plan.schemaSetFingerprint}) { + if (!isValidFingerprint(*fingerprint)) + return planError("ACLOWER-FINGERPRINT", + "model contains an invalid input fingerprint"); + } + if (plan.constructionOrder.size() != plan.destructionOrder.size()) + return planError("ACLOWER-OWNERSHIP", + "construction and destruction orders differ in size"); + if (!std::equal(plan.constructionOrder.rbegin(), + plan.constructionOrder.rend(), plan.destructionOrder.begin())) + return planError("ACLOWER-OWNERSHIP", + "destruction order must reverse construction order"); + + llvm::StringRef previousType; + for (const TypePlan &type : plan.types) { + if (type.symbol.empty() || type.cppType.empty() || + !isValidFingerprint(type.fingerprint)) + return planError("ACLOWER-TYPE-MISMATCH", "type plan is incomplete"); + if (!previousType.empty() && previousType >= type.symbol) + return planError("ACLOWER-TYPE-MISMATCH", + "type plans are not strictly symbol-sorted"); + previousType = type.symbol; + } + + auto isDomainName = [](llvm::StringRef name) { + if (name.empty() || + !(std::isalpha(static_cast(name.front())) || + name.front() == '_')) + return false; + return llvm::all_of(name.drop_front(), [](char character) { + return std::isalnum(static_cast(character)) || + character == '_' || character == '.' || character == '-'; + }); + }; + llvm::StringRef previousDomain; + for (const TimeDomainPlan &domain : plan.timeDomains) { + if (!isDomainName(domain.name) || domain.period == 0 || + domain.tickScale == 0 || + (!previousDomain.empty() && previousDomain >= domain.name)) + return planError("ACLOWER-TIME-DOMAIN", + "time-domain plans are not canonical and complete"); + previousDomain = domain.name; + } + + for (size_t index = 0; index < plan.runtimeObjects.size(); ++index) { + const RuntimeObjectPlan &object = plan.runtimeObjects[index]; + if (object.objectId != index || object.activationId != index) + return planError("ACLOWER-DISPATCH", + "runtime object and activation IDs must be dense"); + if (object.targetSymbol.empty() || object.hierarchyPath.empty() || + object.workThunk.empty() || object.xferThunk.empty() || + object.resetThunk.empty() || object.validateThunk.empty()) + return planError("ACLOWER-DISPATCH", + "runtime dispatch row is incomplete"); + } + + std::optional previousEdge; + for (const ActivationEdgePlan &edge : plan.activationEdges) { + if (edge.sourceId >= plan.runtimeObjects.size() || + edge.targetId >= plan.runtimeObjects.size()) + return planError("ACLOWER-ACTIVATION", + "activation edge references an unknown dense ID"); + if (previousEdge && *previousEdge >= edge) + return planError("ACLOWER-ACTIVATION", + "activation edges are not sorted and unique"); + previousEdge = edge; + } + return detail::validateModelDetails(plan); +} + +llvm::Expected buildModelPlan(mlir::ModuleOp canonicalACSim) { + if (!canonicalACSim || + mlir::failed(acsim::verifyCanonicalACSimFile(canonicalACSim))) + return planError("ACLOWER-FINGERPRINT", + "input is not verified canonical ACSim"); + + acsim::ModelOp model; + unsigned modelCount = 0; + canonicalACSim.walk([&](acsim::ModelOp candidate) { + model = candidate; + ++modelCount; + }); + if (modelCount != 1) + return planError("ACLOWER-FINGERPRINT", + "input must contain exactly one ACSim model"); + + ModelPlan plan; + plan.modelSymbol = model.getSymName().str(); + plan.rootSymbol = model.getRoot().str(); + plan.contractEpoch = model.getContractEpoch().str(); + + auto construction = + stringArray(model.getConstructionOrder(), "construction_order"); + if (!construction) + return construction.takeError(); + plan.constructionOrder = std::move(*construction); + auto destruction = + stringArray(model.getDestructionOrder(), "destruction_order"); + if (!destruction) + return destruction.takeError(); + plan.destructionOrder = std::move(*destruction); + + auto readFingerprint = [&](llvm::StringRef name, + Fingerprint &destination) -> llvm::Error { + auto value = fingerprintField(model.getFingerprints(), name); + if (!value) + return value.takeError(); + destination = std::move(*value); + return llvm::Error::success(); + }; + if (auto error = readFingerprint("frozen_acir", plan.frozenAcirFingerprint)) + return std::move(error); + if (auto error = readFingerprint("binding_lock", plan.bindingLockFingerprint)) + return std::move(error); + if (auto error = readFingerprint("provider", plan.providerFingerprint)) + return std::move(error); + if (auto error = readFingerprint("profile", plan.profileFingerprint)) + return std::move(error); + if (auto error = readFingerprint("toolchain", plan.toolchainFingerprint)) + return std::move(error); + if (auto error = readFingerprint("schema_set", plan.schemaSetFingerprint)) + return std::move(error); + + for (mlir::Operation &operation : model.getBody().front()) { + if (auto type = mlir::dyn_cast(operation)) { + auto kind = parseTypeKind(type.getKind()); + if (!kind) + return planError("ACLOWER-TYPE-MISMATCH", + "unknown closed ACSim type kind"); + plan.types.push_back({type.getSymName().str(), *kind, + type.getCppName().str(), + type.getFingerprint().str()}); + if (*kind == TypeKind::TimeDomain) { + mlir::IntegerAttr period = type.getPeriodAttr(); + mlir::IntegerAttr phase = type.getPhaseAttr(); + mlir::IntegerAttr tickScale = type.getTickScaleAttr(); + if (period || phase || tickScale) { + if (!period || !phase || !tickScale || period.getInt() <= 0 || + phase.getInt() < 0 || tickScale.getInt() <= 0) + return planError("ACLOWER-TIME-DOMAIN", + "time-domain runtime metadata is incomplete"); + plan.timeDomains.push_back( + {type.getSymName().str(), static_cast(period.getInt()), + static_cast(phase.getInt()), + static_cast(tickScale.getInt())}); + } + } + continue; + } + + if (auto dispatch = mlir::dyn_cast(operation)) { + auto objectId = checkedId(dispatch.getObjectId(), "object_id"); + if (!objectId) + return objectId.takeError(); + auto activationId = + checkedId(dispatch.getActivationId(), "activation_id"); + if (!activationId) + return activationId.takeError(); + RuntimeObjectPlan object; + object.objectId = *objectId; + object.activationId = *activationId; + object.targetSymbol = symbolRefString(dispatch.getTargetAttr()); + object.hierarchyPath = dispatch.getPath().str(); + object.objectKind = + inferRuntimeObjectKind(model, dispatch.getTargetAttr()); + object.workThunk = dispatch.getWork().str(); + object.xferThunk = dispatch.getXfer().str(); + object.resetThunk = dispatch.getReset().str(); + object.validateThunk = dispatch.getValidate().str(); + for (int64_t index : dispatch.getIndices()) { + if (index < 0) + return planError("ACLOWER-ARRAY", + "dispatch index cannot be negative"); + object.indices.push_back(static_cast(index)); + } + plan.runtimeObjects.push_back(std::move(object)); + continue; + } + + if (auto activate = mlir::dyn_cast(operation)) { + auto source = activate.getSource().getDefiningOp(); + auto target = activate.getTarget().getDefiningOp(); + if (!source || !target) + return planError("ACLOWER-ACTIVATION", + "activation operands must resolve to dispatch rows"); + auto sourceId = checkedId(source.getActivationId(), "activation source"); + if (!sourceId) + return sourceId.takeError(); + auto targetId = checkedId(target.getObjectId(), "activation target"); + if (!targetId) + return targetId.takeError(); + plan.activationEdges.push_back({*sourceId, *targetId}); + } + } + + std::sort(plan.types.begin(), plan.types.end(), + [](const TypePlan &lhs, const TypePlan &rhs) { + return lhs.symbol < rhs.symbol; + }); + std::sort(plan.runtimeObjects.begin(), plan.runtimeObjects.end(), + [](const RuntimeObjectPlan &lhs, const RuntimeObjectPlan &rhs) { + return lhs.objectId < rhs.objectId; + }); + std::sort(plan.timeDomains.begin(), plan.timeDomains.end(), + [](const TimeDomainPlan &lhs, const TimeDomainPlan &rhs) { + return lhs.name < rhs.name; + }); + std::sort(plan.activationEdges.begin(), plan.activationEdges.end()); + + if (auto error = detail::populateModelDetails(model, plan)) + return std::move(error); + for (RuntimeObjectPlan &runtimeObject : plan.runtimeObjects) + runtimeObject.traceOwner = isTraceOwner(plan, runtimeObject); + if (auto error = validateModelPlan(plan)) + return std::move(error); + return plan; +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/ModelPlanDetails.cpp b/components/agentic-circuit/lib/CodeGen/ModelPlanDetails.cpp new file mode 100644 index 00000000..e6ff7dc5 --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/ModelPlanDetails.cpp @@ -0,0 +1,660 @@ +#include "ModelPlanInternal.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Index/IR/IndexOps.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include + +namespace acir::codegen::detail { +namespace { + +template struct Overloaded : Ts... { + using Ts::operator()...; +}; +template Overloaded(Ts...) -> Overloaded; + +llvm::Error detailError(const llvm::Twine &code, const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), code + ": " + message); +} + +std::string printType(mlir::Type type) { + std::string result; + llvm::raw_string_ostream(result) << type; + return result; +} + +std::string printAttribute(mlir::Attribute attribute) { + std::string result; + llvm::raw_string_ostream(result) << attribute; + return result; +} + +std::string flatSymbol(mlir::DictionaryAttr record, llvm::StringRef name) { + return record.getAs(name).getValue().str(); +} + +std::string stringField(mlir::DictionaryAttr record, llvm::StringRef name) { + return record.getAs(name).getValue().str(); +} + +std::string symbolRefString(mlir::SymbolRefAttr symbol) { + std::string result = symbol.getRootReference().getValue().str(); + for (mlir::FlatSymbolRefAttr nested : symbol.getNestedReferences()) { + result += "::"; + result += nested.getValue(); + } + return result; +} + +llvm::Expected staticValue(mlir::Attribute attribute) { + if (auto value = mlir::dyn_cast(attribute)) + return llvm::json::Value(value.getValue()); + if (auto value = mlir::dyn_cast(attribute)) { + if (!value.getValue().isSignedIntN(64)) + return detailError("ACLOWER-PARAM-PHASE", "integer exceeds int64_t"); + return llvm::json::Value(value.getInt()); + } + if (auto value = mlir::dyn_cast(attribute)) { + double number = value.getValueAsDouble(); + if (!std::isfinite(number) || (std::signbit(number) && number == 0.0)) + return detailError("ACLOWER-PARAM-PHASE", + "floating value is not canonical I-JSON"); + return llvm::json::Value(number); + } + if (auto value = mlir::dyn_cast(attribute)) + return llvm::json::Value(value.getValue()); + if (auto values = mlir::dyn_cast(attribute)) { + llvm::json::Array result; + for (mlir::Attribute element : values) { + auto converted = staticValue(element); + if (!converted) + return converted.takeError(); + result.push_back(std::move(*converted)); + } + return llvm::json::Value(std::move(result)); + } + if (auto values = mlir::dyn_cast(attribute)) { + llvm::json::Object result; + for (mlir::NamedAttribute element : values) { + auto converted = staticValue(element.getValue()); + if (!converted) + return converted.takeError(); + result[element.getName().getValue()] = std::move(*converted); + } + return llvm::json::Value(std::move(result)); + } + if (mlir::isa(attribute)) + return llvm::json::Value(printAttribute(attribute)); + return detailError("ACLOWER-PARAM-PHASE", + "unsupported canonical static value"); +} + +llvm::Expected> +staticValues(mlir::ArrayAttr values) { + std::vector result; + for (mlir::Attribute value : values) { + auto converted = staticValue(value); + if (!converted) + return converted.takeError(); + result.push_back(std::move(*converted)); + } + return result; +} + +std::string generatedClassName(llvm::StringRef symbol, + llvm::StringRef fingerprint) { + fingerprint.consume_front("sha256:"); + return (symbol + "_s" + fingerprint.take_front(16)).str(); +} + +llvm::Expected extractBinding(acsim::BindingOp binding) { + mlir::DictionaryAttr record = binding.getRecord(); + BindingPlan result; + result.symbol = binding.getSymName().str(); + result.bindingId = stringField(record, "binding"); + result.effect = stringField(record, "effect") == "pure" + ? BindingEffect::Pure + : BindingEffect::Stateful; + result.cppType = flatSymbol(record, "cpp_type"); + result.implementation = flatSymbol(record, "implementation"); + result.provider = flatSymbol(record, "provider"); + result.componentSchema = flatSymbol(record, "component_schema"); + result.recordFingerprint = stringField(record, "fingerprint"); + result.componentSchemaFingerprint = + stringField(record, "component_schema_fingerprint"); + result.providerImplementationFingerprint = + stringField(record, "provider_implementation_fingerprint"); + + auto cpp = record.getAs("cpp"); + result.header = stringField(cpp, "header"); + result.target = stringField(cpp, "target"); + result.cppSymbol = stringField(cpp, "symbol"); + result.conceptName = stringField(cpp, "concept"); + auto entries = cpp.getAs("entry_points"); + result.entryPoints = { + stringField(entries, "pure"), stringField(entries, "reset"), + stringField(entries, "validate"), stringField(entries, "work"), + stringField(entries, "xfer")}; + + auto construction = record.getAs("construction"); + auto arguments = + staticValues(construction.getAs("arguments")); + if (!arguments) + return arguments.takeError(); + result.constructorArguments = std::move(*arguments); + auto ownership = record.getAs("ownership"); + result.ownershipKind = stringField(ownership, "kind"); + result.ownershipPlacement = stringField(ownership, "placement"); + + for (mlir::Attribute attribute : + record.getAs("parameters")) { + auto parameter = mlir::cast(attribute); + auto value = staticValue(parameter.get("value")); + if (!value) + return value.takeError(); + auto mapping = + llvm::StringSwitch( + stringField(parameter, "mapping")) + .Case("template_argument", ParameterMappingKind::TemplateArgument) + .Case("constexpr_argument", ParameterMappingKind::ConstexprArgument) + .Default(ParameterMappingKind::ConstructorConstant); + result.parameters.push_back( + {stringField(parameter, "name"), stringField(parameter, "acir_type"), + stringField(parameter, "cpp_type"), std::move(*value), + static_cast( + parameter.getAs("ordinal").getInt()), + mapping}); + } + for (mlir::Attribute attribute : record.getAs("ports")) { + auto port = mlir::cast(attribute); + result.ports.push_back( + {flatSymbol(port, "accessor"), stringField(port, "cardinality"), + stringField(port, "delegation"), stringField(port, "direction"), + flatSymbol(port, "interface"), stringField(port, "ownership"), + flatSymbol(port, "payload"), flatSymbol(port, "protocol"), + flatSymbol(port, "role"), flatSymbol(port, "time_domain")}); + } + for (mlir::Attribute attribute : record.getAs("resources")) { + auto resource = mlir::cast(attribute); + result.resources.push_back( + {flatSymbol(resource, "accessor"), stringField(resource, "delegation"), + stringField(resource, "mode"), stringField(resource, "ownership"), + flatSymbol(resource, "resource"), flatSymbol(resource, "role"), + flatSymbol(resource, "time_domain")}); + } + for (mlir::Attribute attribute : record.getAs("results")) { + auto value = mlir::cast(attribute); + result.results.push_back( + {stringField(value, "name"), flatSymbol(value, "cpp_type")}); + } + for (mlir::Attribute attribute : + record.getAs("activation_sources")) { + auto source = mlir::cast(attribute); + result.activationSources.push_back( + {stringField(source, "name"), flatSymbol(source, "kind")}); + } + return result; +} + +std::string valueName(llvm::DenseMap &names, + mlir::Value value, uint32_t &next) { + auto found = names.find(value); + if (found != names.end()) + return found->second; + std::string name = (llvm::Twine("v") + llvm::Twine(next++)).str(); + names.try_emplace(value, name); + return name; +} + +std::vector +valueNames(llvm::DenseMap &names, + mlir::ValueRange values, uint32_t &next) { + std::vector result; + for (mlir::Value value : values) + result.push_back(valueName(names, value, next)); + return result; +} + +std::vector resultTypeNames(mlir::ResultRange values) { + std::vector result; + for (mlir::Value value : values) + result.push_back(printType(value.getType())); + return result; +} + +llvm::Expected +extractProcess(acsim::ProcessOp process, + llvm::DenseMap &moduleValues, + uint32_t &nextModuleValue) { + ProcessPlan result; + result.symbol = process.getSymName().str(); + result.className = + generatedClassName(result.symbol, process.getSpecializationFingerprint()); + result.specializationFingerprint = + process.getSpecializationFingerprint().str(); + result.entryPc = process.getEntryPc().str(); + result.fairnessWork = process.getFairnessCap(); + + for (auto [index, capture] : llvm::enumerate(process.getCaptures())) { + auto name = mlir::cast(process.getCaptureNames()[index]); + result.captures.push_back( + {name.getValue().str(), + valueName(moduleValues, capture, nextModuleValue), + printType(capture.getType())}); + } + for (mlir::Attribute attribute : process.getLiveSlots()) { + auto slot = mlir::cast(attribute); + result.liveSlots.push_back( + {stringField(slot, "name"), + printType(slot.getAs("type").getValue())}); + } + + for (auto [stateIndex, region] : llvm::enumerate(process.getStates())) { + PcStatePlan state; + state.ordinal = static_cast(stateIndex); + state.name = + mlir::cast(process.getPcs()[stateIndex]) + .getValue() + .str(); + llvm::DenseMap values; + uint32_t nextValue = 0; + llvm::DenseMap blockOrdinals; + for (auto [blockIndex, block] : llvm::enumerate(region)) + blockOrdinals.try_emplace(&block, static_cast(blockIndex)); + for (mlir::Block &block : region) { + PcBlockPlan blockPlan; + blockPlan.ordinal = blockOrdinals.lookup(&block); + for (auto [index, argument] : llvm::enumerate(block.getArguments())) { + std::string name = + blockPlan.ordinal == 0 + ? (llvm::Twine("arg") + llvm::Twine(index)).str() + : (llvm::Twine("block") + llvm::Twine(blockPlan.ordinal) + + "_value" + llvm::Twine(index)) + .str(); + values.try_emplace(argument, std::move(name)); + } + for (mlir::BlockArgument argument : block.getArguments()) + blockPlan.arguments.push_back({valueName(values, argument, nextValue), + printType(argument.getType())}); + for (mlir::Operation &operation : block) { + for (mlir::Value value : operation.getResults()) + valueName(values, value, nextValue); + if (auto load = mlir::dyn_cast(operation)) { + state.operations.push_back(LiveLoadPlan{ + valueName(values, load.getResult(), nextValue), + load.getSlot().str(), printType(load.getResult().getType())}); + blockPlan.operations.push_back(state.operations.back()); + } else if (auto store = mlir::dyn_cast(operation)) { + state.operations.push_back( + LiveStorePlan{valueName(values, store.getValue(), nextValue), + store.getSlot().str()}); + blockPlan.operations.push_back(state.operations.back()); + } else if (auto call = mlir::dyn_cast(operation)) { + state.operations.push_back( + InlineCallPlan{call.getCallee().str(), + valueNames(values, call.getArgs(), nextValue), + {valueName(values, call.getResult(), nextValue)}, + {printType(call.getResult().getType())}}); + blockPlan.operations.push_back(state.operations.back()); + } else if (auto call = mlir::dyn_cast(operation)) { + state.operations.push_back( + InvokePlan{call.getCallee().str(), + valueNames(values, call.getArgs(), nextValue), + valueNames(values, call.getResults(), nextValue), + resultTypeNames(call.getResults())}); + blockPlan.operations.push_back(state.operations.back()); + } else if (auto branch = + mlir::dyn_cast(operation)) { + blockPlan.terminator = BranchPlan{ + blockOrdinals.lookup(branch.getDest()), + valueNames(values, branch.getDestOperands(), nextValue)}; + } else if (auto branch = + mlir::dyn_cast(operation)) { + blockPlan.terminator = ConditionalBranchPlan{ + valueName(values, branch.getCondition(), nextValue), + blockOrdinals.lookup(branch.getTrueDest()), + valueNames(values, branch.getTrueDestOperands(), nextValue), + blockOrdinals.lookup(branch.getFalseDest()), + valueNames(values, branch.getFalseDestOperands(), nextValue)}; + } else if (auto transition = + mlir::dyn_cast(operation)) { + ContinuePlan plan{transition.getTargetPc().str()}; + state.terminator = plan; + blockPlan.terminator = std::move(plan); + } else if (auto suspend = mlir::dyn_cast(operation)) { + SuspendPlan plan{valueName(values, suspend.getWake(), nextValue), + suspend.getTargetPc().str()}; + state.terminator = plan; + blockPlan.terminator = std::move(plan); + } else if (auto terminate = + mlir::dyn_cast(operation)) { + TerminatePlan plan{terminate.getStatus().str()}; + state.terminator = plan; + blockPlan.terminator = std::move(plan); + } else if (auto constant = + mlir::dyn_cast(operation)) { + auto value = staticValue(constant.getValue()); + if (!value) + return value.takeError(); + state.operations.push_back(ConstantPlan{ + valueName(values, constant.getResult(), nextValue), + printType(constant.getResult().getType()), std::move(*value)}); + blockPlan.operations.push_back(state.operations.back()); + } else if (auto constant = + mlir::dyn_cast(operation)) { + if (!constant.getValue().isSignedIntN(64)) + return detailError("ACLOWER-PROCESS-STATE", + "index constant exceeds int64_t"); + state.operations.push_back(ConstantPlan{ + valueName(values, constant.getResult(), nextValue), + printType(constant.getResult().getType()), + llvm::json::Value(constant.getValue().getSExtValue())}); + blockPlan.operations.push_back(state.operations.back()); + } else if (operation.getName().getStringRef().starts_with("arith.")) { + std::string predicate; + if (auto compare = mlir::dyn_cast(operation)) + predicate = + mlir::arith::stringifyCmpIPredicate(compare.getPredicate()) + .str(); + else if (auto compare = + mlir::dyn_cast(operation)) + predicate = + mlir::arith::stringifyCmpFPredicate(compare.getPredicate()) + .str(); + state.operations.push_back(ArithmeticPlan{ + operation.getName().getStringRef().str(), + valueNames(values, operation.getOperands(), nextValue), + valueNames(values, operation.getResults(), nextValue), + resultTypeNames(operation.getResults()), std::move(predicate)}); + blockPlan.operations.push_back(state.operations.back()); + } else if (operation.getName().getStringRef().starts_with("index.")) { + std::string predicate; + if (auto compare = mlir::dyn_cast(operation)) + predicate = + mlir::index::stringifyIndexCmpPredicate(compare.getPred()) + .str(); + state.operations.push_back(IndexPlan{ + operation.getName().getStringRef().str(), + valueNames(values, operation.getOperands(), nextValue), + valueNames(values, operation.getResults(), nextValue), + resultTypeNames(operation.getResults()), std::move(predicate)}); + blockPlan.operations.push_back(state.operations.back()); + } else { + return detailError("ACLOWER-PROCESS-STATE", + "process operation is outside the closed C++ " + "generation subset"); + } + } + state.blocks.push_back(std::move(blockPlan)); + } + result.states.push_back(std::move(state)); + } + return result; +} + +llvm::Expected +extractModule(acsim::ModuleOp module, + const llvm::DenseSet &moduleSymbols) { + ModulePlan result; + result.symbol = module.getSymName().str(); + result.className = + generatedClassName(result.symbol, module.getSpecializationFingerprint()); + result.specializationFingerprint = + module.getSpecializationFingerprint().str(); + llvm::DenseMap values; + uint32_t nextValue = 0; + + for (mlir::Operation &operation : module.getBody().front()) { + for (mlir::Value value : operation.getResults()) + valueName(values, value, nextValue); + if (auto instance = mlir::dyn_cast(operation)) { + auto staticArgs = staticValues(instance.getStaticArgs()); + if (!staticArgs) + return staticArgs.takeError(); + PlacementKind kind = + moduleSymbols.contains( + instance.getTarget().getRootReference().getValue()) + ? PlacementKind::GeneratedModule + : PlacementKind::ExternalStateful; + result.placements.push_back( + {kind, + instance.getSymName().str(), + (instance.getSymName() + "_").str(), + symbolRefString(instance.getTargetAttr()), + valueName(values, instance.getResult(), nextValue), + instance.getSpecializationFingerprint().str(), + {}, + std::move(*staticArgs)}); + } else if (auto array = mlir::dyn_cast(operation)) { + auto staticArgs = staticValues(array.getStaticArgs()); + if (!staticArgs) + return staticArgs.takeError(); + PlacementPlan placement{PlacementKind::HomogeneousArray, + array.getSymName().str(), + (array.getSymName() + "_").str(), + symbolRefString(array.getTargetAttr()), + valueName(values, array.getResult(), nextValue), + array.getSpecializationFingerprint().str(), + {}, + std::move(*staticArgs)}; + for (int64_t extent : array.getShape()) + placement.shape.push_back(static_cast(extent)); + result.placements.push_back(std::move(placement)); + } else if (auto element = mlir::dyn_cast(operation)) { + ProjectionPlan projection; + projection.kind = ProjectionKind::Element; + projection.resultValue = + valueName(values, element.getResult(), nextValue); + projection.baseValue = valueName(values, element.getArray(), nextValue); + projection.resultType = printType(element.getResult().getType()); + for (int64_t index : element.getIndices()) + projection.indices.push_back(static_cast(index)); + result.projections.push_back(std::move(projection)); + } else if (auto port = mlir::dyn_cast(operation)) { + result.projections.push_back( + {ProjectionKind::Port, + valueName(values, port.getResult(), nextValue), + valueName(values, port.getBase(), nextValue), + {}, + port.getAccessor().str(), + printType(port.getResult().getType())}); + } else if (auto resource = mlir::dyn_cast(operation)) { + result.projections.push_back( + {ProjectionKind::Resource, + valueName(values, resource.getResult(), nextValue), + valueName(values, resource.getBase(), nextValue), + {}, + resource.getAccessor().str(), + printType(resource.getResult().getType())}); + } else if (auto bind = mlir::dyn_cast(operation)) { + result.binds.push_back({valueName(values, bind.getSource(), nextValue), + valueName(values, bind.getTarget(), nextValue), + bind.getKind().str()}); + } else if (auto expression = mlir::dyn_cast(operation)) { + result.expressions.push_back( + {valueName(values, expression.getResult(), nextValue), + expression.getCallee().str(), + valueNames(values, expression.getArgs(), nextValue), + printType(expression.getResult().getType())}); + } else if (auto process = mlir::dyn_cast(operation)) { + auto extracted = extractProcess(process, values, nextValue); + if (!extracted) + return extracted.takeError(); + result.processes.push_back(std::move(*extracted)); + } else if (auto exportOp = mlir::dyn_cast(operation)) { + result.exports.push_back( + {exportOp.getSymName().str(), + valueName(values, exportOp.getValue(), nextValue), + valueName(values, exportOp.getResult(), nextValue), + exportOp.getRole().str(), + printType(exportOp.getResult().getType())}); + } else if (auto returnOp = mlir::dyn_cast(operation)) { + result.returnValues = + valueNames(values, returnOp.getOperands(), nextValue); + } + } + return result; +} + +} // namespace + +llvm::Error populateModelDetails(acsim::ModelOp model, ModelPlan &plan) { + llvm::DenseSet moduleSymbols; + for (mlir::Operation &operation : model.getBody().front()) + if (auto module = mlir::dyn_cast(operation)) + moduleSymbols.insert(module.getSymName()); + + for (mlir::Operation &operation : model.getBody().front()) { + if (auto binding = mlir::dyn_cast(operation)) { + auto extracted = extractBinding(binding); + if (!extracted) + return extracted.takeError(); + plan.bindings.push_back(std::move(*extracted)); + } else if (auto module = mlir::dyn_cast(operation)) { + auto extracted = extractModule(module, moduleSymbols); + if (!extracted) + return extracted.takeError(); + plan.modules.push_back(std::move(*extracted)); + } + } + std::sort(plan.bindings.begin(), plan.bindings.end(), + [](const BindingPlan &left, const BindingPlan &right) { + return left.symbol < right.symbol; + }); + std::sort(plan.modules.begin(), plan.modules.end(), + [](const ModulePlan &left, const ModulePlan &right) { + return left.symbol < right.symbol; + }); + return llvm::Error::success(); +} + +llvm::Error validateModelDetails(const ModelPlan &plan) { + llvm::StringRef prior; + for (const BindingPlan &binding : plan.bindings) { + if (binding.symbol.empty() || (!prior.empty() && prior >= binding.symbol) || + binding.header.empty() || binding.cppSymbol.empty() || + binding.conceptName.empty() || + !isValidFingerprint(binding.recordFingerprint)) + return detailError("ACLOWER-BINDING-MISSING", + "binding plan is incomplete or non-canonical"); + prior = binding.symbol; + } + prior = {}; + for (const ModulePlan &module : plan.modules) { + if (module.symbol.empty() || module.className.empty() || + (!prior.empty() && prior >= module.symbol) || + !isValidFingerprint(module.specializationFingerprint)) + return detailError("ACLOWER-OWNERSHIP", + "module plan is incomplete or non-canonical"); + prior = module.symbol; + for (const ProcessPlan &process : module.processes) { + if (process.symbol.empty() || process.className.empty() || + process.fairnessWork == 0 || process.states.empty() || + !isValidFingerprint(process.specializationFingerprint)) + return detailError("ACLOWER-PROCESS-STATE", + "process plan is incomplete"); + std::set pcs; + for (auto [ordinal, state] : llvm::enumerate(process.states)) { + if (state.ordinal != ordinal || state.name.empty() || + !pcs.insert(state.name).second) + return detailError("ACLOWER-PROCESS-STATE", + "process PCs are not dense and unique"); + } + if (!pcs.contains(process.entryPc)) + return detailError("ACLOWER-PROCESS-STATE", + "entry PC is outside the closed PC set"); + for (const PcStatePlan &state : process.states) { + llvm::Error transitionError = std::visit( + [&](const auto &terminator) -> llvm::Error { + using Terminator = std::decay_t; + if constexpr (std::is_same_v) { + if (!pcs.contains(terminator.targetPc)) + return detailError("ACLOWER-PROCESS-STATE", + "continue target is outside the PC set"); + } else if constexpr (std::is_same_v) { + if (terminator.wakeValue.empty() || + !pcs.contains(terminator.targetPc)) + return detailError("ACLOWER-PROCESS-STATE", + "suspend wake or target is invalid"); + } else if (terminator.status != "success" && + terminator.status != "failure") { + return detailError("ACLOWER-PROCESS-STATE", + "terminate status is not closed"); + } + return llvm::Error::success(); + }, + state.terminator); + if (transitionError) + return transitionError; + for (auto [blockOrdinal, block] : llvm::enumerate(state.blocks)) { + if (block.ordinal != blockOrdinal) + return detailError("ACLOWER-PROCESS-STATE", + "PC blocks are not dense and ordered"); + auto checkSuccessor = + [&](uint32_t target, + const std::vector &arguments) -> llvm::Error { + if (target >= state.blocks.size()) + return detailError("ACLOWER-PROCESS-STATE", + "branch target is outside its PC"); + if (arguments.size() != state.blocks[target].arguments.size()) + return detailError("ACLOWER-PROCESS-STATE", + "branch successor arity is inconsistent"); + return llvm::Error::success(); + }; + llvm::Error blockError = std::visit( + Overloaded{ + [&](const BranchPlan &branch) -> llvm::Error { + return checkSuccessor(branch.targetBlock, branch.arguments); + }, + [&](const ConditionalBranchPlan &branch) -> llvm::Error { + if (branch.condition.empty()) + return detailError("ACLOWER-PROCESS-STATE", + "conditional branch has no condition"); + if (auto error = checkSuccessor(branch.trueBlock, + branch.trueArguments)) + return error; + return checkSuccessor(branch.falseBlock, + branch.falseArguments); + }, + [&](const ContinuePlan &next) -> llvm::Error { + return pcs.contains(next.targetPc) + ? llvm::Error::success() + : detailError( + "ACLOWER-PROCESS-STATE", + "continue target is outside PC set"); + }, + [&](const SuspendPlan &suspend) -> llvm::Error { + return !suspend.wakeValue.empty() && + pcs.contains(suspend.targetPc) + ? llvm::Error::success() + : detailError("ACLOWER-PROCESS-STATE", + "suspend target is invalid"); + }, + [&](const TerminatePlan &terminate) -> llvm::Error { + return terminate.status == "success" || + terminate.status == "failure" + ? llvm::Error::success() + : detailError("ACLOWER-PROCESS-STATE", + "terminate status is not closed"); + }}, + block.terminator); + if (blockError) + return std::move(blockError); + } + } + } + } + return llvm::Error::success(); +} + +} // namespace acir::codegen::detail diff --git a/components/agentic-circuit/lib/CodeGen/ModelPlanInternal.h b/components/agentic-circuit/lib/CodeGen/ModelPlanInternal.h new file mode 100644 index 00000000..9f7ce3f3 --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/ModelPlanInternal.h @@ -0,0 +1,14 @@ +#ifndef ACIR_CODEGEN_MODELPLANINTERNAL_H +#define ACIR_CODEGEN_MODELPLANINTERNAL_H + +#include "acir/CodeGen/ModelPlan.h" +#include "acir/Dialect/ACSim/ACSimOps.h" + +namespace acir::codegen::detail { + +llvm::Error populateModelDetails(acsim::ModelOp model, ModelPlan &plan); +llvm::Error validateModelDetails(const ModelPlan &plan); + +} // namespace acir::codegen::detail + +#endif // ACIR_CODEGEN_MODELPLANINTERNAL_H diff --git a/components/agentic-circuit/lib/CodeGen/ProcessGenerator.cpp b/components/agentic-circuit/lib/CodeGen/ProcessGenerator.cpp new file mode 100644 index 00000000..58783a9b --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/ProcessGenerator.cpp @@ -0,0 +1,1006 @@ +#include "ProcessGenerator.h" + +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/FormatVariadic.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace acir::codegen::detail { +namespace { + +template struct Overloaded : Ts... { + using Ts::operator()...; +}; +template Overloaded(Ts...) -> Overloaded; + +llvm::Error processError(const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + llvm::Twine("ACLOWER-PROCESS-STATE: ") + message); +} + +bool isIdentifier(llvm::StringRef value) { + if (value.empty() || + !(std::isalpha(static_cast(value.front())) || + value.front() == '_')) + return false; + return std::all_of(value.drop_front().begin(), value.end(), [](char value) { + return std::isalnum(static_cast(value)) || value == '_'; + }); +} + +const BindingPlan *findBinding(const ModelPlan &plan, llvm::StringRef symbol) { + auto found = std::find_if( + plan.bindings.begin(), plan.bindings.end(), + [&](const BindingPlan &binding) { return binding.symbol == symbol; }); + return found == plan.bindings.end() ? nullptr : &*found; +} + +const BindingPlan *findImplementationBinding(const ModelPlan &plan, + llvm::StringRef symbol) { + auto found = std::find_if(plan.bindings.begin(), plan.bindings.end(), + [&](const BindingPlan &binding) { + return binding.implementation == symbol; + }); + return found == plan.bindings.end() ? nullptr : &*found; +} + +const TypePlan *findType(const ModelPlan &plan, llvm::StringRef symbol) { + auto found = + std::find_if(plan.types.begin(), plan.types.end(), + [&](const TypePlan &type) { return type.symbol == symbol; }); + return found == plan.types.end() ? nullptr : &*found; +} + +llvm::Expected wakeKind(const TypePlan &implementation) { + llvm::StringRef symbol(implementation.symbol); + if (symbol.starts_with("acir_impl_wake_condition")) + return std::string("Condition"); + if (symbol.starts_with("acir_impl_wake_resource")) + return std::string("Resource"); + if (symbol.starts_with("acir_impl_wake_event_queue")) + return std::string("EventQueue"); + if (symbol.starts_with("acir_impl_wake_next_delta")) + return std::string("NextDelta"); + return processError("generated wake implementation has an unknown role"); +} + +llvm::Error emitWakeHelper(std::ostringstream &output, + const TypePlan &implementation) { + llvm::StringRef name(implementation.cppType); + if (!name.consume_front("acir::generated::") || !isIdentifier(name)) + return processError( + "generated wake implementation has an invalid C++ name"); + auto kind = wakeKind(implementation); + if (!kind) + return kind.takeError(); + output << "inline gfsim::ProcessWake " << name.str() + << "() { return {gfsim::ProcessWakeKind::" << *kind << ", 0}; }\n"; + return llvm::Error::success(); +} + +llvm::Expected cppType(const ModelPlan &plan, + llvm::StringRef type) { + if (type == "i1") + return std::string("bool"); + if (type == "i8") + return std::string("std::int8_t"); + if (type == "i16") + return std::string("std::int16_t"); + if (type == "i32") + return std::string("std::int32_t"); + if (type == "i64") + return std::string("std::int64_t"); + if (type == "index") + return std::string("std::size_t"); + if (type == "f32") + return std::string("float"); + if (type == "f64") + return std::string("double"); + + const size_t symbolStart = type.find('@'); + const size_t symbolEnd = type.find('>', symbolStart); + if (symbolStart == llvm::StringRef::npos || + symbolEnd == llvm::StringRef::npos) + return processError("process value has no C++ type realization"); + const llvm::StringRef symbol = type.slice(symbolStart + 1, symbolEnd); + if (type.starts_with("!acsim.ref<")) { + if (const BindingPlan *binding = findBinding(plan, symbol)) + return binding->cppSymbol; + } else if (type.starts_with("!acsim.wake<")) { + return std::string("gfsim::ProcessWake"); + } else if (const TypePlan *realization = findType(plan, symbol)) { + return realization->cppType; + } + return processError("process value references an unknown type"); +} + +const LiveSlotPlan *findSlot(const ProcessPlan &process, llvm::StringRef name) { + auto found = + std::find_if(process.liveSlots.begin(), process.liveSlots.end(), + [&](const LiveSlotPlan &slot) { return slot.name == name; }); + return found == process.liveSlots.end() ? nullptr : &*found; +} + +const PcStatePlan *findState(const ProcessPlan &process, llvm::StringRef name) { + auto found = std::find_if( + process.states.begin(), process.states.end(), + [&](const PcStatePlan &state) { return state.name == name; }); + return found == process.states.end() ? nullptr : &*found; +} + +llvm::Expected pcType(const ProcessPlan &process) { + const uint32_t maximum = + process.states.empty() ? 0 : process.states.back().ordinal; + if (maximum <= std::numeric_limits::max()) + return std::string("uint8_t"); + if (maximum <= std::numeric_limits::max()) + return std::string("uint16_t"); + return std::string("uint32_t"); +} + +GeneratedFile makeFile(std::string path, std::string content) { + GeneratedFile file{std::move(path), std::move(content), {}}; + file.fingerprint = computeFingerprint(file.content); + return file; +} + +llvm::Expected scalarLiteral(const llvm::json::Value &value, + llvm::StringRef resultType) { + if (auto boolean = value.getAsBoolean()) + return std::string(*boolean ? "true" : "false"); + if (auto integer = value.getAsInteger()) + return std::to_string(*integer); + if (auto number = value.getAsNumber()) { + std::string literal = llvm::formatv("{0}", *number).str(); + if (resultType == "f32") + literal.push_back('f'); + return literal; + } + return processError("scalar constant has no closed C++ literal"); +} + +bool isArithmeticOperation(llvm::StringRef name) { + return llvm::StringSwitch(name) + .Cases({"arith.cmpi", "arith.cmpf", "arith.addi", "arith.subi"}, true) + .Cases({"arith.muli", "arith.divui", "arith.divsi", "arith.remui"}, true) + .Cases({"arith.remsi", "arith.andi", "arith.ori", "arith.xori"}, true) + .Cases({"arith.shli", "arith.shrui", "arith.shrsi", "arith.select"}, true) + .Cases({"arith.index_cast", "arith.extui", "arith.extsi", "arith.trunci"}, + true) + .Cases({"arith.addf", "arith.subf", "arith.mulf", "arith.divf"}, true) + .Case("arith.negf", true) + .Default(false); +} + +bool isIndexOperation(llvm::StringRef name) { + return llvm::StringSwitch(name) + .Cases({"index.add", "index.sub", "index.mul", "index.divs"}, true) + .Cases({"index.divu", "index.rems", "index.remu", "index.cmp"}, true) + .Cases({"index.casts", "index.castu"}, true) + .Default(false); +} + +size_t scalarArity(llvm::StringRef name) { + if (name == "arith.negf" || name == "arith.index_cast" || + name == "arith.extui" || name == "arith.extsi" || + name == "arith.trunci" || name == "index.casts" || name == "index.castu") + return 1; + if (name == "arith.select") + return 3; + return 2; +} + +llvm::Error validateScalarOperation( + const ModelPlan &plan, const std::map &values, + llvm::StringRef operationName, const std::vector &arguments, + const std::vector &results, + const std::vector &resultTypes, llvm::StringRef predicate, + bool arithmetic) { + if ((arithmetic ? !isArithmeticOperation(operationName) + : !isIndexOperation(operationName)) || + arguments.size() != scalarArity(operationName) || results.size() != 1 || + resultTypes.size() != 1) + return processError("scalar operation shape is outside the closed subset"); + for (const std::string &argument : arguments) + if (!values.contains(argument)) + return processError("operation uses a value outside its PC"); + auto resultType = cppType(plan, resultTypes.front()); + if (!resultType) + return resultType.takeError(); + const bool isComparison = operationName == "arith.cmpi" || + operationName == "arith.cmpf" || + operationName == "index.cmp"; + if (isComparison != !predicate.empty()) + return processError("scalar comparison predicate is invalid"); + return llvm::Error::success(); +} + +llvm::Error +validateOperations(const ModelPlan &plan, const ProcessPlan &process, + const std::vector &operations, + std::map &values) { + auto requireValue = [&](llvm::StringRef value) -> llvm::Error { + if (!values.contains(value.str())) + return processError("operation uses a value outside its PC"); + return llvm::Error::success(); + }; + auto addResults = [&](const std::vector &results, + const std::vector &types) -> llvm::Error { + if (results.size() != types.size()) + return processError("operation result arity is inconsistent"); + for (auto [result, type] : llvm::zip_equal(results, types)) { + if (!isIdentifier(result) || values.contains(result)) + return processError("operation result is not a fresh identifier"); + auto realization = cppType(plan, type); + if (!realization) + return realization.takeError(); + values.emplace(result, type); + } + return llvm::Error::success(); + }; + + for (const ProcessOperationPlan &operation : operations) { + llvm::Error error = std::visit( + Overloaded{ + [&](const ConstantPlan &constant) -> llvm::Error { + if (!isIdentifier(constant.resultValue) || + values.contains(constant.resultValue)) + return processError("scalar constant is invalid"); + auto type = cppType(plan, constant.resultType); + if (!type) + return type.takeError(); + auto literal = + scalarLiteral(constant.canonicalValue, constant.resultType); + if (!literal) + return literal.takeError(); + values.emplace(constant.resultValue, constant.resultType); + return llvm::Error::success(); + }, + [&](const ArithmeticPlan &scalar) -> llvm::Error { + if (auto validation = validateScalarOperation( + plan, values, scalar.operationName, scalar.arguments, + scalar.results, scalar.resultTypes, scalar.predicate, + true)) + return validation; + return addResults(scalar.results, scalar.resultTypes); + }, + [&](const IndexPlan &scalar) -> llvm::Error { + if (auto validation = validateScalarOperation( + plan, values, scalar.operationName, scalar.arguments, + scalar.results, scalar.resultTypes, scalar.predicate, + false)) + return validation; + return addResults(scalar.results, scalar.resultTypes); + }, + [&](const LiveLoadPlan &load) -> llvm::Error { + const LiveSlotPlan *slot = findSlot(process, load.slot); + if (!slot || slot->type != load.type) + return processError("live load slot or type is invalid"); + return addResults({load.resultValue}, {load.type}); + }, + [&](const LiveStorePlan &store) -> llvm::Error { + if (auto validation = requireValue(store.sourceValue)) + return validation; + const LiveSlotPlan *slot = findSlot(process, store.slot); + if (!slot || values.at(store.sourceValue) != slot->type) + return processError("live store slot or type is invalid"); + return llvm::Error::success(); + }, + [&](const InlineCallPlan &call) -> llvm::Error { + const BindingPlan *binding = findBinding(plan, call.callee); + const TypePlan *implementation = findType(plan, call.callee); + if ((!binding || binding->effect != BindingEffect::Pure) && + (!implementation || + implementation->kind != TypeKind::Implementation)) + return processError("inline callee is not a pure binding"); + for (const std::string &argument : call.arguments) + if (auto validation = requireValue(argument)) + return validation; + return addResults(call.results, call.resultTypes); + }, + [&](const InvokePlan &call) -> llvm::Error { + const BindingPlan *binding = findBinding(plan, call.callee); + const TypePlan *implementation = findType(plan, call.callee); + if ((!binding || binding->effect != BindingEffect::Stateful) && + (!implementation || + implementation->kind != TypeKind::Implementation)) + return processError("invoke callee is not a stateful binding"); + for (const std::string &argument : call.arguments) + if (auto validation = requireValue(argument)) + return validation; + return addResults(call.results, call.resultTypes); + }}, + operation); + if (error) + return error; + } + return llvm::Error::success(); +} + +llvm::Error validateProcess(const ModelPlan &plan, const ProcessPlan &process) { + if (!isIdentifier(process.className) || !isIdentifier(process.symbol) || + process.states.empty() || process.fairnessWork == 0) + return processError("process identity or fairness is invalid"); + + std::set pcNames; + for (auto [ordinal, state] : llvm::enumerate(process.states)) { + if (state.ordinal != ordinal || !isIdentifier(state.name) || + !pcNames.insert(state.name).second) + return processError("process PCs are not dense identifiers"); + } + if (!pcNames.contains(process.entryPc)) + return processError("entry PC is outside the closed PC set"); + + std::set slots; + for (const LiveSlotPlan &slot : process.liveSlots) { + if (!isIdentifier(slot.name) || !slots.insert(slot.name).second) + return processError("live slots are not unique identifiers"); + auto type = cppType(plan, slot.type); + if (!type) + return type.takeError(); + } + + for (const PcStatePlan &state : process.states) { + std::map values; + for (auto [index, capture] : llvm::enumerate(process.captures)) + values.emplace("arg" + std::to_string(index), capture.type); + if (state.blocks.size() > 1) { + for (const PcBlockPlan &block : state.blocks) { + for (const BlockArgumentPlan &argument : block.arguments) { + if (!isIdentifier(argument.name)) + return processError("block argument is not a fresh process value"); + auto prior = values.find(argument.name); + if (prior != values.end()) { + if (block.ordinal == 0 && prior->second == argument.type) + continue; + return processError("block argument is not a fresh process value"); + } + auto type = cppType(plan, argument.type); + if (!type) + return type.takeError(); + values.emplace(argument.name, argument.type); + } + } + } + + if (auto error = + validateOperations(plan, process, state.operations, values)) + return error; + + if (state.blocks.size() > 1) { + for (const PcBlockPlan &block : state.blocks) { + std::map localValues; + for (auto [index, capture] : llvm::enumerate(process.captures)) + localValues.emplace("arg" + std::to_string(index), capture.type); + for (const BlockArgumentPlan &argument : block.arguments) + localValues.emplace(argument.name, argument.type); + + auto requireLocalValue = [&](llvm::StringRef value) -> llvm::Error { + if (!localValues.contains(value.str())) + return processError( + "multi-block operation uses a value outside its block"); + return llvm::Error::success(); + }; + if (auto error = validateOperations(plan, process, block.operations, + localValues)) + return error; + + auto validateSuccessor = + [&](uint32_t targetOrdinal, + const std::vector &arguments) -> llvm::Error { + if (targetOrdinal >= state.blocks.size()) + return processError("branch target is outside its PC"); + const PcBlockPlan &target = state.blocks[targetOrdinal]; + if (arguments.size() != target.arguments.size()) + return processError("branch successor arity is inconsistent"); + for (auto [argument, targetArgument] : + llvm::zip_equal(arguments, target.arguments)) { + if (auto error = requireLocalValue(argument)) + return error; + if (localValues.at(argument) != targetArgument.type) + return processError("branch successor type is inconsistent"); + } + return llvm::Error::success(); + }; + llvm::Error blockError = std::visit( + Overloaded{ + [&](const BranchPlan &branch) -> llvm::Error { + return validateSuccessor(branch.targetBlock, + branch.arguments); + }, + [&](const ConditionalBranchPlan &branch) -> llvm::Error { + if (auto error = requireLocalValue(branch.condition)) + return error; + if (localValues.at(branch.condition) != "i1") + return processError( + "conditional branch condition is not i1"); + if (auto error = validateSuccessor(branch.trueBlock, + branch.trueArguments)) + return error; + return validateSuccessor(branch.falseBlock, + branch.falseArguments); + }, + [&](const ContinuePlan &next) -> llvm::Error { + return findState(process, next.targetPc) + ? llvm::Error::success() + : processError( + "continue target is outside the PC set"); + }, + [&](const SuspendPlan &suspend) -> llvm::Error { + auto found = localValues.find(suspend.wakeValue); + if (found == localValues.end() || + !llvm::StringRef(found->second) + .starts_with("!acsim.wake<") || + !findState(process, suspend.targetPc)) + return processError("suspend wake or target is invalid"); + return llvm::Error::success(); + }, + [&](const TerminatePlan &terminate) -> llvm::Error { + return terminate.status == "success" || + terminate.status == "failure" + ? llvm::Error::success() + : processError("terminate status is invalid"); + }}, + block.terminator); + if (blockError) + return blockError; + } + } + + llvm::Error terminatorError = std::visit( + Overloaded{ + [&](const ContinuePlan &next) -> llvm::Error { + return findState(process, next.targetPc) + ? llvm::Error::success() + : processError( + "continue target is outside the PC set"); + }, + [&](const SuspendPlan &suspend) -> llvm::Error { + auto found = values.find(suspend.wakeValue); + if (found == values.end() || + !llvm::StringRef(found->second).starts_with("!acsim.wake<") || + !findState(process, suspend.targetPc)) + return processError("suspend wake or target is invalid"); + return llvm::Error::success(); + }, + [&](const TerminatePlan &terminate) -> llvm::Error { + if (terminate.status != "success" && + terminate.status != "failure") + return processError("terminate status is invalid"); + return llvm::Error::success(); + }}, + state.terminator); + if (terminatorError) + return terminatorError; + } + return llvm::Error::success(); +} + +void emitArguments(std::ostringstream &output, + const std::vector &arguments) { + for (auto [index, argument] : llvm::enumerate(arguments)) { + if (index != 0) + output << ", "; + output << argument; + } +} + +void emitResultAssignment(std::ostringstream &output, + const std::vector &results) { + if (results.empty()) + return; + if (results.size() == 1) { + output << "auto " << results.front() << " = "; + return; + } + output << "auto ["; + emitArguments(output, results); + output << "] = "; +} + +std::string unsignedValue(llvm::StringRef value, llvm::StringRef cppTypeName) { + return "static_cast>(" + + value.str() + ")"; +} + +std::string signedValue(llvm::StringRef value, llvm::StringRef cppTypeName) { + return "static_cast>(" + + value.str() + ")"; +} + +llvm::Expected +comparisonExpression(llvm::StringRef operationName, llvm::StringRef predicate, + const std::vector &arguments, + llvm::StringRef operandCppType) { + std::string left = arguments[0]; + std::string right = arguments[1]; + if (predicate.starts_with("u") && operationName != "arith.cmpf") { + left = unsignedValue(left, operandCppType); + right = unsignedValue(right, operandCppType); + } else if (operationName == "index.cmp" && predicate.starts_with("s")) { + left = signedValue(left, operandCppType); + right = signedValue(right, operandCppType); + } + + auto relation = llvm::StringSwitch(predicate) + .Cases({"eq", "oeq", "ueq"}, "==") + .Cases({"ne", "one", "une"}, "!=") + .Cases({"slt", "ult", "olt"}, "<") + .Cases({"sle", "ule", "ole"}, "<=") + .Cases({"sgt", "ugt", "ogt"}, ">") + .Cases({"sge", "uge", "oge"}, ">=") + .Default({}); + const std::string unordered = + "(std::isnan(" + left + ") || std::isnan(" + right + "))"; + const std::string ordered = + "(!std::isnan(" + left + ") && !std::isnan(" + right + "))"; + if (operationName == "arith.cmpf") { + if (predicate == "false") + return std::string("false"); + if (predicate == "true") + return std::string("true"); + if (predicate == "ord") + return ordered; + if (predicate == "uno") + return unordered; + if (relation.empty()) + return processError("floating comparison predicate is unsupported"); + const std::string comparison = + "(" + left + " " + relation.str() + " " + right + ")"; + return predicate.starts_with("u") + ? "(" + comparison + " || " + unordered + ")" + : "(" + comparison + " && " + ordered + ")"; + } + if (relation.empty()) + return processError("integer comparison predicate is unsupported"); + return "(" + left + " " + relation.str() + " " + right + ")"; +} + +llvm::Expected +scalarExpression(const ModelPlan &plan, llvm::StringRef operationName, + const std::vector &arguments, + const std::vector &resultTypes, + llvm::StringRef predicate) { + auto resultCppType = cppType(plan, resultTypes.front()); + if (!resultCppType) + return resultCppType.takeError(); + + if (operationName == "arith.cmpi" || operationName == "arith.cmpf" || + operationName == "index.cmp") { + // Integer comparison results are i1, so use the operand's declared C++ + // expression type instead of the result type for signedness conversions. + const std::string inferredOperandType = + operationName == "index.cmp" + ? "std::size_t" + : "std::remove_cvref_t"; + return comparisonExpression(operationName, predicate, arguments, + inferredOperandType); + } + + if (operationName == "arith.select") + return "(" + arguments[0] + " ? " + arguments[1] + " : " + arguments[2] + + ")"; + if (operationName == "arith.negf") + return "(-" + arguments[0] + ")"; + if (operationName == "arith.index_cast" || operationName == "arith.extui" || + operationName == "arith.extsi" || operationName == "arith.trunci" || + operationName == "index.casts" || operationName == "index.castu") + return "static_cast<" + *resultCppType + ">(" + arguments[0] + ")"; + + llvm::StringRef binaryOperator = + llvm::StringSwitch(operationName) + .Cases({"arith.addi", "arith.addf", "index.add"}, "+") + .Cases({"arith.subi", "arith.subf", "index.sub"}, "-") + .Cases({"arith.muli", "arith.mulf", "index.mul"}, "*") + .Cases({"arith.divsi", "arith.divf", "index.divs"}, "/") + .Cases({"arith.remsi", "index.rems"}, "%") + .Case("arith.andi", "&") + .Case("arith.ori", "|") + .Case("arith.xori", "^") + .Case("arith.shli", "<<") + .Case("arith.shrsi", ">>") + .Default({}); + if (!binaryOperator.empty()) + return "(" + arguments[0] + " " + binaryOperator.str() + " " + + arguments[1] + ")"; + + binaryOperator = llvm::StringSwitch(operationName) + .Cases({"arith.divui", "index.divu"}, "/") + .Cases({"arith.remui", "index.remu"}, "%") + .Case("arith.shrui", ">>") + .Default({}); + if (!binaryOperator.empty()) + return "static_cast<" + *resultCppType + ">(" + + unsignedValue(arguments[0], *resultCppType) + " " + + binaryOperator.str() + " " + + unsignedValue(arguments[1], *resultCppType) + ")"; + return processError("scalar operation has no C++ emission"); +} + +llvm::Error emitOperation(const ModelPlan &plan, const ProcessPlan &process, + std::ostringstream &output, + const ProcessOperationPlan &operation) { + return std::visit( + Overloaded{ + [&](const ConstantPlan &constant) -> llvm::Error { + auto type = cppType(plan, constant.resultType); + if (!type) + return type.takeError(); + auto literal = + scalarLiteral(constant.canonicalValue, constant.resultType); + if (!literal) + return literal.takeError(); + output << " " << *type << ' ' << constant.resultValue << " = " + << *literal << ";\n"; + return llvm::Error::success(); + }, + [&](const ArithmeticPlan &scalar) -> llvm::Error { + auto expression = + scalarExpression(plan, scalar.operationName, scalar.arguments, + scalar.resultTypes, scalar.predicate); + if (!expression) + return expression.takeError(); + emitResultAssignment(output, scalar.results); + output << *expression << ";\n"; + return llvm::Error::success(); + }, + [&](const IndexPlan &scalar) -> llvm::Error { + auto expression = + scalarExpression(plan, scalar.operationName, scalar.arguments, + scalar.resultTypes, scalar.predicate); + if (!expression) + return expression.takeError(); + emitResultAssignment(output, scalar.results); + output << *expression << ";\n"; + return llvm::Error::success(); + }, + [&](const LiveLoadPlan &load) -> llvm::Error { + auto type = cppType(plan, load.type); + if (!type) + return type.takeError(); + output << " const " << *type << " &" << load.resultValue + << " = committed_" << load.slot << "_;\n"; + return llvm::Error::success(); + }, + [&](const LiveStorePlan &store) -> llvm::Error { + output << " proposed_" << store.slot + << "_ = " << store.sourceValue << ";\n"; + return llvm::Error::success(); + }, + [&](const InlineCallPlan &call) -> llvm::Error { + const BindingPlan *binding = findBinding(plan, call.callee); + emitResultAssignment(output, call.results); + if (binding) + output << binding->entryPoints.pure; + else if (const BindingPlan *implementationBinding = + findImplementationBinding(plan, call.callee)) + output << implementationBinding->entryPoints.pure; + else + output << findType(plan, call.callee)->cppType; + output << '('; + emitArguments(output, call.arguments); + output << ");\n"; + return llvm::Error::success(); + }, + [&](const InvokePlan &call) -> llvm::Error { + emitResultAssignment(output, call.results); + const BindingPlan *binding = findBinding(plan, call.callee); + if (!binding) + binding = findImplementationBinding(plan, call.callee); + if (binding) { + auto capture = + std::find_if(process.captures.begin(), process.captures.end(), + [&](const CapturePlan &candidate) { + return llvm::StringRef(candidate.type) + .contains("@" + binding->symbol + ">"); + }); + if (capture == process.captures.end()) + return processError("stateful invoke has no matching capture"); + output << "arg" + << std::distance(process.captures.begin(), capture) + << ".invoke("; + } else { + output << findType(plan, call.callee)->cppType << '('; + } + emitArguments(output, call.arguments); + output << ");\n"; + return llvm::Error::success(); + }}, + operation); +} + +} // namespace + +llvm::Expected +generateProcessHeader(const ModelPlan &plan, const ProcessPlan &process) { + if (auto error = validateProcess(plan, process)) + return std::move(error); + auto underlyingType = pcType(process); + if (!underlyingType) + return underlyingType.takeError(); + + std::set headers; + std::map wakeHelpers; + auto collectHelper = [&](const ProcessOperationPlan &operation) { + std::visit( + Overloaded{ + [&](const InvokePlan &call) { + const TypePlan *type = findType(plan, call.callee); + if (type && type->kind == TypeKind::Implementation && + llvm::StringRef(type->symbol).starts_with("acir_impl_wake_")) + wakeHelpers.emplace(type->symbol, type); + }, + [&](const auto &) {}}, + operation); + }; + for (const CapturePlan &capture : process.captures) { + const size_t start = capture.type.find('@'); + const size_t end = capture.type.find('>', start); + if (start != std::string::npos && end != std::string::npos) + if (const BindingPlan *binding = findBinding( + plan, llvm::StringRef(capture.type).slice(start + 1, end))) + headers.insert(binding->header); + } + for (const PcStatePlan &state : process.states) + for (const ProcessOperationPlan &operation : state.operations) { + collectHelper(operation); + std::visit( + Overloaded{ + [&](const InlineCallPlan &call) { + if (const BindingPlan *binding = findBinding(plan, call.callee)) + headers.insert(binding->header); + else if (const BindingPlan *binding = + findImplementationBinding(plan, call.callee)) + headers.insert(binding->header); + }, + [&](const InvokePlan &call) { + if (const BindingPlan *binding = findBinding(plan, call.callee)) + headers.insert(binding->header); + else if (const BindingPlan *binding = + findImplementationBinding(plan, call.callee)) + headers.insert(binding->header); + }, + [&](const auto &) {}}, + operation); + } + for (const PcStatePlan &state : process.states) + for (const PcBlockPlan &block : state.blocks) + for (const ProcessOperationPlan &operation : block.operations) + collectHelper(operation); + + std::ostringstream output; + output << "#pragma once\n\n#include \"gfsim/process.h\"\n"; + for (const std::string &header : headers) + output << "#include \"" << header << "\"\n"; + output << "\n#include \n#include \n#include \n" + "#include \n\n" + << "namespace acir::generated {\n"; + for (const auto &[symbol, implementation] : wakeHelpers) + if (auto error = emitWakeHelper(output, *implementation)) + return std::move(error); + output << "} // namespace acir::generated\n\n" + << "namespace acsim_generated {\n\nclass " << process.className + << " final : public gfsim::ProcessRuntime<" << process.className + << "> {\npublic:\n enum class Pc : " << *underlyingType << " {\n"; + for (auto [index, state] : llvm::enumerate(process.states)) + output << " " << state.name << " = " << state.ordinal + << (index + 1 == process.states.size() ? "\n" : ",\n"); + output << " };\n\n static constexpr uint64_t kFairnessWork = " + << process.fairnessWork << ";\n\n " << process.className + << "(std::string name, gfsim::ObjectId id, gfsim::SimObject *parent"; + for (const CapturePlan &capture : process.captures) { + auto type = cppType(plan, capture.type); + if (!type) + return type.takeError(); + output << ", " << *type << " &" << capture.name; + } + output + << ");\n\n gfsim::ProcessStep executeProcessStep(uint32_t pc, " + "gfsim::Epoch epoch);\n void doXfer(gfsim::Epoch epoch) override;\n" + " void reset() override;\n\nprivate:\n"; + for (const CapturePlan &capture : process.captures) { + auto type = cppType(plan, capture.type); + if (!type) + return type.takeError(); + output << " " << *type << " *capture_" << capture.name << "_;\n"; + } + for (const LiveSlotPlan &slot : process.liveSlots) { + auto type = cppType(plan, slot.type); + if (!type) + return type.takeError(); + output << " " << *type << " committed_" << slot.name << "_{};\n" + << " " << *type << " proposed_" << slot.name << "_{};\n"; + } + output << "};\n\n} // namespace acsim_generated\n"; + return makeFile("include/generated/processes/" + process.className + ".h", + output.str()); +} + +llvm::Expected +generateProcessSource(const ModelPlan &plan, const ProcessPlan &process) { + if (auto error = validateProcess(plan, process)) + return std::move(error); + const PcStatePlan *entry = findState(process, process.entryPc); + + std::ostringstream output; + output << "#include \"generated/processes/" << process.className + << ".h\"\n\n#include \n\nnamespace acsim_generated {\n\n" + << process.className << "::" << process.className + << "(std::string name, gfsim::ObjectId id, gfsim::SimObject *parent"; + for (const CapturePlan &capture : process.captures) { + auto type = cppType(plan, capture.type); + if (!type) + return type.takeError(); + output << ", " << *type << " &" << capture.name; + } + output << ")\n : ProcessRuntime(std::move(name), id, parent, " + << "static_cast(Pc::" << entry->name << "), kFairnessWork)"; + for (const CapturePlan &capture : process.captures) + output << ",\n capture_" << capture.name << "_(&" << capture.name + << ')'; + output << " {}\n\ngfsim::ProcessStep " << process.className + << "::executeProcessStep(uint32_t pc, gfsim::Epoch epoch) {\n" + " (void)epoch;\n switch (static_cast(pc)) {\n"; + for (const PcStatePlan &state : process.states) { + output << " case Pc::" << state.name << ": {\n"; + for (auto [index, capture] : llvm::enumerate(process.captures)) + output << " auto &arg" << index << " = *capture_" << capture.name + << "_;\n"; + if (state.blocks.size() > 1) { + output << " enum class Block_" << state.name << " {"; + for (auto [index, block] : llvm::enumerate(state.blocks)) + output << (index == 0 ? " " : ", ") << "b" << block.ordinal; + output << " };\n"; + for (const PcBlockPlan &block : state.blocks) { + for (auto [index, argument] : llvm::enumerate(block.arguments)) { + auto type = cppType(plan, argument.type); + if (!type) + return type.takeError(); + output << " std::optional<" << *type << "> b" << block.ordinal + << "_arg" << index << ";\n"; + } + } + output << " auto block_" << state.name << " = Block_" << state.name + << "::b0;\n for (;;) {\n switch (block_" << state.name + << ") {\n"; + for (const PcBlockPlan &block : state.blocks) { + output << " case Block_" << state.name << "::b" << block.ordinal + << ": {\n"; + if (block.ordinal != 0) + for (auto [index, argument] : llvm::enumerate(block.arguments)) + output << " auto " << argument.name << " = *b" + << block.ordinal << "_arg" << index << ";\n"; + for (const ProcessOperationPlan &operation : block.operations) + if (auto error = emitOperation(plan, process, output, operation)) + return std::move(error); + llvm::Error terminatorError = std::visit( + Overloaded{ + [&](const BranchPlan &branch) -> llvm::Error { + const PcBlockPlan &target = + state.blocks.at(branch.targetBlock); + for (auto [index, argument] : + llvm::enumerate(branch.arguments)) + output << " b" << target.ordinal << "_arg" << index + << " = " << argument << ";\n"; + output << " block_" << state.name << " = Block_" + << state.name << "::b" << target.ordinal + << ";\n continue;\n"; + return llvm::Error::success(); + }, + [&](const ConditionalBranchPlan &branch) -> llvm::Error { + output << " if (" << branch.condition << ") {\n"; + const PcBlockPlan &trueTarget = + state.blocks.at(branch.trueBlock); + for (auto [index, argument] : + llvm::enumerate(branch.trueArguments)) + output << " b" << trueTarget.ordinal << "_arg" + << index << " = " << argument << ";\n"; + output << " block_" << state.name << " = Block_" + << state.name << "::b" << trueTarget.ordinal + << ";\n } else {\n"; + const PcBlockPlan &falseTarget = + state.blocks.at(branch.falseBlock); + for (auto [index, argument] : + llvm::enumerate(branch.falseArguments)) + output << " b" << falseTarget.ordinal << "_arg" + << index << " = " << argument << ";\n"; + output << " block_" << state.name << " = Block_" + << state.name << "::b" << falseTarget.ordinal + << ";\n }\n continue;\n"; + return llvm::Error::success(); + }, + [&](const ContinuePlan &next) -> llvm::Error { + output << " return gfsim::ProcessStep::continueAt(" + << "static_cast(Pc::" << next.targetPc + << "));\n"; + return llvm::Error::success(); + }, + [&](const SuspendPlan &suspend) -> llvm::Error { + const PcStatePlan *target = + findState(process, suspend.targetPc); + output << " return gfsim::ProcessStep::suspendAt(" + << "static_cast(Pc::" << suspend.targetPc + << "), " << suspend.wakeValue << ", " + << static_cast(target->ordinal) + 1 + << ");\n"; + return llvm::Error::success(); + }, + [&](const TerminatePlan &terminate) -> llvm::Error { + if (terminate.status == "success") + output + << " return gfsim::ProcessStep::terminate();\n"; + else + output << " return gfsim::ProcessStep::fail(" + "\"process_terminated_failure\");\n"; + return llvm::Error::success(); + }}, + block.terminator); + if (terminatorError) + return std::move(terminatorError); + output << " }\n"; + } + output << " }\n }\n"; + } else { + for (const ProcessOperationPlan &operation : state.operations) + if (auto error = emitOperation(plan, process, output, operation)) + return std::move(error); + } + if (state.blocks.size() > 1) { + output << " }\n"; + continue; + } + std::visit( + Overloaded{ + [&](const ContinuePlan &next) { + output << " return gfsim::ProcessStep::continueAt(" + << "static_cast(Pc::" << next.targetPc + << "));\n"; + }, + [&](const SuspendPlan &suspend) { + const PcStatePlan *target = findState(process, suspend.targetPc); + output << " return gfsim::ProcessStep::suspendAt(" + << "static_cast(Pc::" << suspend.targetPc + << "), " << suspend.wakeValue << ", " + << static_cast(target->ordinal) + 1 << ");\n"; + }, + [&](const TerminatePlan &terminate) { + if (terminate.status == "success") + output << " return gfsim::ProcessStep::terminate();\n"; + else + output << " return gfsim::ProcessStep::fail(" + "\"process_terminated_failure\");\n"; + }}, + state.terminator); + output << " }\n"; + } + output + << " default:\n return gfsim::ProcessStep::fail(" + "\"invalid_process_pc\");\n }\n}\n\nvoid " + << process.className + << "::doXfer(gfsim::Epoch epoch) {\n ProcessRuntime::doXfer(epoch);\n"; + for (const LiveSlotPlan &slot : process.liveSlots) + output << " committed_" << slot.name << "_ = proposed_" << slot.name + << "_;\n"; + output << "}\n\nvoid " << process.className + << "::reset() {\n ProcessRuntime::reset();\n"; + for (const LiveSlotPlan &slot : process.liveSlots) + output << " committed_" << slot.name << "_ = {};\n proposed_" << slot.name + << "_ = {};\n"; + output << "}\n\n} // namespace acsim_generated\n"; + return makeFile("src/generated/processes/" + process.className + ".cpp", + output.str()); +} + +} // namespace acir::codegen::detail diff --git a/components/agentic-circuit/lib/CodeGen/ProcessGenerator.h b/components/agentic-circuit/lib/CodeGen/ProcessGenerator.h new file mode 100644 index 00000000..b7f369bd --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/ProcessGenerator.h @@ -0,0 +1,16 @@ +#ifndef ACIR_CODEGEN_PROCESSGENERATOR_H +#define ACIR_CODEGEN_PROCESSGENERATOR_H + +#include "acir/CodeGen/Generator.h" + +namespace acir::codegen::detail { + +llvm::Expected generateProcessHeader(const ModelPlan &plan, + const ProcessPlan &process); + +llvm::Expected generateProcessSource(const ModelPlan &plan, + const ProcessPlan &process); + +} // namespace acir::codegen::detail + +#endif // ACIR_CODEGEN_PROCESSGENERATOR_H diff --git a/components/agentic-circuit/lib/CodeGen/QueueBlockContract.cpp b/components/agentic-circuit/lib/CodeGen/QueueBlockContract.cpp new file mode 100644 index 00000000..d24ca46e --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/QueueBlockContract.cpp @@ -0,0 +1,370 @@ +#include "acir/CodeGen/QueueBlockContract.h" + +#include "acir/Bindings/Binding.h" + +#include "llvm/Support/JSON.h" + +#include + +namespace acir::codegen { +namespace { + +const std::vector Contracts = { + {"barrier", + "ac.barrier", + "scheduling", + "design", + "atomic", + 2, + -1, + 2, + -1, + "positionally_equal_input_output_payloads", + {"output_depths", "output_latencies"}, + true, + "gfsim::QueueBarrier>", + true, + "all_input_valid_and_all_output_ready_atomic_handshake", + {"input_transactions", "output_transactions", "barrier_firing"}}, + {"credit", + "ac.credit", + "scheduling", + "design", + "stateful", + 1, + 1, + 1, + 1, + "input_output_payload_equal", + {"credits", "depth", "latency"}, + true, + "gfsim::QueueCredit", + true, + "fixed_credit_register_window_and_countdown", + {"accepted_transaction", "completed_transaction", "active_credits"}}, + {"dependency", + "ac.dependency", + "scheduling", + "design", + "stateful", + 1, + 1, + 1, + 1, + "input_output_payload_equal", + {"capacity", "resources", "no_dependency", "depth", "latency"}, + true, + "gfsim::QueueDependency", + true, + "packed_dependency_window_resource_reservation_and_countdown", + {"accepted_transaction", "issued_transaction", "completed_transaction"}}, + {"expect", + "ac.expect", + "boundary", + "verification", + "verification", + 1, + 1, + 0, + 0, + "observe_input_payload", + {"message"}, + true, + "gfsim::QueueExpect", + false, + "pyc_testbench_boundary_only", + {"predicate_input", "assertion_outcome"}}, + {"memory_instance", + "ac.memory.instance", + "state", + "design", + "stateful", + 0, + 0, + 0, + 0, + "declared_storage_owner", + {"data_type", "entries", "init", "latency", "stable_id", "owner"}, + true, + "gfsim::QueueMemoryArbiter storage owner", + true, + "one_pyc.sync_mem_per_instance", + {"memory_write", "busy", "selected_endpoint"}}, + {"memory_request", + "ac.memory.request", + "state", + "design", + "stateful", + 1, + 1, + 1, + 1, + "input_output_payload_equal", + {"instance", "ordinal", "result_field", "depth"}, + true, + "gfsim::QueueMemoryArbiter", + true, + "pyc.sync_mem_with_aligned_request_state", + {"request_transaction", "response_transaction", "memory_write"}}, + {"source", + "ac.source", + "boundary", + "design", + "stateful", + 0, + 0, + 1, + 1, + "declared_output_payload", + {"depth", "latency"}, + true, + "typed_source_boundary", + true, + "ready_valid_input_boundary", + {"input_transaction"}}, + {"select", + "ac.select", + "transport", + "design", + "atomic", + 3, + -1, + 1, + 1, + "selected_data_payload_matches_output", + {"depth", "latency"}, + true, + "gfsim::QueueSelect", + true, + "selector_mux_and_selected_ready_handshake", + {"control_transaction", "selected_input_transaction", + "output_transaction"}}, + {"sink", + "ac.sink", + "boundary", + "design", + "stateful", + 1, + 1, + 0, + 0, + "consume_input_payload", + {}, + true, + "gfsim::QueueSink", + true, + "ready_valid_output_boundary", + {"output_transaction"}}, + {"observe", + "ac.observe", + "boundary", + "observation", + "observation", + 1, + 1, + 0, + 0, + "observe_input_payload", + {"name"}, + true, + "gfsim::QueueObserve", + true, + "non_backpressuring_alias_probe", + {"probe_value"}}, + {"transform", + "ac.transform", + "combinational", + "design", + "atomic", + 1, + -1, + 1, + -1, + "region_yields_match_output_payloads", + {"output_depths", "output_latencies"}, + true, + "gfsim::QueueTransform/QueueAtomicTransform", + true, + "pure_logic_plus_ready_valid_fifos", + {"accepted_transaction", "output_transaction"}}, + {"broadcast", + "ac.broadcast", + "transport", + "design", + "atomic", + 1, + 1, + 2, + -1, + "all_payloads_equal", + {"output_depths", "output_latencies"}, + true, + "gfsim::QueueBroadcast", + true, + "all_output_ready_conjunction", + {"input_transaction", "output_transactions"}}, + {"fork", + "ac.fork", + "transport", + "design", + "stateful", + 1, + 1, + 2, + -1, + "all_payloads_equal", + {"output_depths", "output_latencies"}, + true, + "gfsim::QueueFork", + true, + "per_output_delivered_registers", + {"input_transaction", "output_transactions"}}, + {"route", + "ac.route", + "transport", + "design", + "atomic", + 1, + 1, + 2, + -1, + "all_payloads_equal", + {"output_depths", "output_latencies"}, + true, + "gfsim::QueueRoute", + true, + "selector_decoder_and_ready_mux", + {"input_transaction", "selected_output_transaction"}}, + {"merge", + "ac.merge", + "transport", + "design", + "stateful", + 2, + -1, + 1, + 1, + "all_payloads_equal", + {"policy", "depth", "latency"}, + true, + "gfsim::QueueMerge", + true, + "priority_or_round_robin_arbiter", + {"selected_input_transaction", "output_transaction"}}, + {"reorder", + "ac.reorder", + "scheduling", + "design", + "stateful", + 1, + 1, + 1, + 1, + "input_output_payload_equal", + {"capacity", "start", "depth", "latency"}, + true, + "gfsim::QueueReorder", + true, + "tagged_register_bank_and_expected_key", + {"accepted_transaction", "retired_transaction", "sequence_order"}}, + {"feedback", + "ac.feedback", + "state", + "design", + "stateful", + 1, + 1, + 1, + 1, + "input_output_payload_equal", + {"depth", "latency", "max_iterations"}, + true, + "gfsim::QueueFeedback", + true, + "valid_data_iteration_registers", + {"input_transaction", "output_transaction", "iteration_limit"}}, + {"scope", + "ac.scope", + "structural", + "design", + "structural", + 0, + -1, + 0, + -1, + "positionally_equal_boundary_payloads", + {"sym_name"}, + true, + "gfsim::Module_hierarchy", + true, + "elaboration_time_scope_flattening", + {"boundary_transactions"}}, +}; + +llvm::json::Object arity(int64_t minimum, int64_t maximum) { + llvm::json::Object result{{"min", minimum}}; + if (maximum < 0) + result["max"] = nullptr; + else + result["max"] = maximum; + return result; +} + +llvm::json::Array strings(llvm::ArrayRef values) { + llvm::json::Array result; + for (const std::string &value : values) + result.push_back(value); + return result; +} + +} // namespace + +llvm::ArrayRef officialQueueBlockContracts() { + return Contracts; +} + +const QueueBlockContract *findQueueBlockContract(llvm::StringRef kind) { + auto found = std::find_if(Contracts.begin(), Contracts.end(), + [&](const QueueBlockContract &contract) { + return contract.kind == kind; + }); + return found == Contracts.end() ? nullptr : &*found; +} + +llvm::Expected canonicalQueueBlockCatalogJson() { + std::vector ordered; + ordered.reserve(Contracts.size()); + for (const QueueBlockContract &contract : Contracts) + ordered.push_back(&contract); + std::sort(ordered.begin(), ordered.end(), + [](const auto *left, const auto *right) { + return left->operation < right->operation; + }); + llvm::json::Array entries; + for (const QueueBlockContract *contract : ordered) { + entries.push_back(llvm::json::Object{ + {"category", contract->category}, + {"constants", strings(contract->constants)}, + {"effect", contract->effect}, + {"gfsim", + llvm::json::Object{{"available", contract->gfsimAvailable}, + {"realization", contract->gfsimRealization}}}, + {"inputs", arity(contract->minimumInputs, contract->maximumInputs)}, + {"kind", contract->kind}, + {"operation", contract->operation}, + {"outputs", arity(contract->minimumOutputs, contract->maximumOutputs)}, + {"payload_relation", contract->payloadRelation}, + {"pyc", llvm::json::Object{{"available", contract->pycAvailable}, + {"realization", contract->pycRealization}}}, + {"refinement_observations", strings(contract->refinementObservations)}, + {"role", contract->role}, + }); + } + llvm::json::Object root{{"contract_epoch", "0.3"}, + {"entries", std::move(entries)}, + {"schema", "agentic-circuit-opcode-catalog"}, + {"version", "0.3"}}; + return bindings::canonicalizeJson(llvm::json::Value(std::move(root))); +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/QueueGraphGenerator.cpp b/components/agentic-circuit/lib/CodeGen/QueueGraphGenerator.cpp new file mode 100644 index 00000000..615babda --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/QueueGraphGenerator.cpp @@ -0,0 +1,1111 @@ +#include "acir/CodeGen/QueueGraphGenerator.h" +#include "acir/CodeGen/QueueBlockContract.h" + +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +llvm::Error generatorError(const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "ACLOWER-QUEUE-CXX: " + message); +} + +template +void appendInitializer(std::vector &initializers, + const Values &...values) { + std::string initializer; + llvm::raw_string_ostream output(initializer); + (output << ... << values); + output.flush(); + initializers.push_back(std::move(initializer)); +} + +std::string identifier(llvm::StringRef value) { + std::string result; + for (char character : value) + result.push_back( + std::isalnum(static_cast(character)) ? character : '_'); + if (result.empty() || + std::isdigit(static_cast(result.front()))) + result.insert(result.begin(), '_'); + return result; +} + +std::string className(llvm::StringRef value) { + std::string result; + bool capitalize = true; + for (char character : value) { + if (!std::isalnum(static_cast(character))) { + capitalize = true; + continue; + } + result.push_back(capitalize ? static_cast(std::toupper( + static_cast(character))) + : character); + capitalize = false; + } + if (result.empty() || + std::isdigit(static_cast(result.front()))) + result.insert(result.begin(), '_'); + return result; +} + +std::string cppStringLiteral(llvm::StringRef value) { + std::string result = "\""; + for (char character : value) { + switch (character) { + case '\\': + result.append("\\\\"); + break; + case '"': + result.append("\\\""); + break; + case '\n': + result.append("\\n"); + break; + case '\r': + result.append("\\r"); + break; + case '\t': + result.append("\\t"); + break; + default: + result.push_back(character); + break; + } + } + result.push_back('"'); + return result; +} + +llvm::Expected cppType(llvm::StringRef type) { + if (type.starts_with('i')) { + unsigned width = 0; + if (!type.drop_front().getAsInteger(10, width) && width > 0) { + if (width == 1) + return std::string("bool"); + if (width <= 8) + return std::string("std::uint8_t"); + if (width <= 16) + return std::string("std::uint16_t"); + if (width <= 32) + return std::string("std::uint32_t"); + if (width <= 64) + return std::string("std::int64_t"); + } + } + constexpr llvm::StringLiteral prefix = "!ac.struct<@types::@"; + if (type.starts_with(prefix) && type.ends_with('>')) + return type.drop_front(prefix.size()).drop_back().str(); + return generatorError("no C++ storage realization for ACIR type '" + type + + "'"); +} + +std::vector pathParts(llvm::StringRef path) { + std::vector result; + while (!path.empty()) { + path = path.ltrim('/'); + if (path.empty()) + break; + auto split = path.split('/'); + result.push_back(split.first.str()); + path = split.second; + } + return result; +} + +std::string commonPath(llvm::StringRef left, llvm::StringRef right) { + std::vector lhs = pathParts(left); + std::vector rhs = pathParts(right); + std::string result; + for (size_t index = 0; index < std::min(lhs.size(), rhs.size()); ++index) { + if (lhs[index] != rhs[index]) + break; + result.push_back('/'); + result.append(lhs[index]); + } + return result.empty() ? "/" : result; +} + +llvm::Expected emitExpressionBody(const QueueBlockPlan &block, + llvm::StringRef yield, + unsigned indent) { + std::ostringstream output; + std::string padding(indent, ' '); + for (const QueueExpressionPlan &expression : block.expressions) { + auto operand = [&](size_t index) -> llvm::Expected { + if (index >= expression.operands.size()) + return generatorError("expression operand arity mismatch"); + return llvm::StringRef(expression.operands[index]); + }; + if (expression.kind == "constant") { + llvm::StringRef literal = expression.literal; + output << padding << "auto " << expression.result << " = " + << literal.split(" : ").first.str() << ";\n"; + continue; + } + auto first = operand(0); + if (!first) + return first.takeError(); + if (expression.kind == "get") { + output << padding << "auto " << expression.result << " = " << first->str() + << '.' << expression.field << ";\n"; + continue; + } + if (expression.kind == "popcount") { + output << padding << "auto " << expression.result + << " = __builtin_popcountll(static_cast(" + << first->str() << "));\n"; + continue; + } + auto second = operand(1); + if (!second) + return second.takeError(); + if (expression.kind == "with") { + output << padding << "auto " << expression.result << " = " << first->str() + << ";\n"; + output << padding << expression.result << '.' << expression.field << " = " + << second->str() << ";\n"; + continue; + } + llvm::StringRef operation; + if (expression.kind == "add") + operation = "+"; + else if (expression.kind == "sub") + operation = "-"; + else if (expression.kind == "mul") + operation = "*"; + else if (expression.kind == "cmp") { + operation = llvm::StringSwitch(expression.predicate) + .Case("eq", "==") + .Case("ne", "!=") + .Case("slt", "<") + .Case("sle", "<=") + .Case("sgt", ">") + .Case("sge", ">=") + .Default(""); + } + if (operation.empty()) + return generatorError("unsupported Var expression kind '" + + expression.kind + "'"); + output << padding << "auto " << expression.result << " = " << first->str() + << ' ' << operation.str() << ' ' << second->str() << ";\n"; + } + output << padding << "return " << yield.str() << ";\n"; + return output.str(); +} + +const QueuePlan *findQueue(const QueueGraphPlan &plan, llvm::StringRef name) { + auto found = + std::find_if(plan.queues.begin(), plan.queues.end(), + [&](const QueuePlan &queue) { return queue.name == name; }); + return found == plan.queues.end() ? nullptr : &*found; +} + +bool isRuntimeBlock(const QueueBlockPlan &block) { + return block.kind != "source"; +} + +} // namespace + +llvm::Expected generateQueueGraphCpp(const QueueGraphPlan &plan) { + if (plan.system.empty() || plan.queues.empty() || plan.blocks.empty()) + return generatorError("QueueGraph plan is incomplete"); + if (!plan.scopes.empty()) { + const QueueBlockContract *scope = findQueueBlockContract("scope"); + if (!scope || !scope->gfsimAvailable) + return generatorError("official opcode has no gfsim lowering: 'scope'"); + } + for (const QueueBlockPlan &block : plan.blocks) { + const QueueBlockContract *contract = findQueueBlockContract(block.kind); + if (!contract || !contract->gfsimAvailable) + return generatorError("official opcode has no gfsim lowering: '" + + block.kind + "'"); + if (block.kind == "reorder" && + (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 1 || block.capacity == 0)) + return generatorError("reorder contract is unsupported"); + if (block.kind == "dependency" && + (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 4 || block.capacity == 0 || + block.resources == 0)) + return generatorError("dependency contract is unsupported"); + if (block.kind == "credit" && + (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 1 || block.credits == 0)) + return generatorError("credit contract is unsupported"); + if (block.kind == "barrier" && + (block.inputs.size() < 2 || + block.outputs.size() != block.inputs.size() || + block.depths.size() != block.outputs.size() || + block.latencies.size() != block.outputs.size())) + return generatorError("barrier contract is unsupported"); + if (block.kind == "select" && + (block.inputs.size() < 3 || block.outputs.size() != 1 || + block.yields.size() != 1)) + return generatorError("select contract is unsupported"); + if (block.kind == "expect" && + (block.inputs.size() != 1 || !block.outputs.empty() || + block.yields.size() != 1 || block.message.empty())) + return generatorError("expect contract is unsupported"); + if (block.kind == "memory_request" && + (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 3 || block.memoryInstance.empty() || + block.resultField.empty())) + return generatorError("memory contract is unsupported"); + } + if (auto error = verifyQueueGraphPlan(plan)) + return std::move(error); + + llvm::StringMap queueMembers; + llvm::StringMap queueOwners; + for (const QueuePlan &queue : plan.queues) { + if (queueMembers.contains(queue.name)) + return generatorError("Queue names must be unique"); + queueMembers[queue.name] = identifier(queue.name) + "_"; + queueOwners[queue.name] = queue.scope; + } + for (const QueueBlockPlan &block : plan.blocks) + for (const std::string &input : block.inputs) { + auto owner = queueOwners.find(input); + if (owner == queueOwners.end()) + return generatorError("block input references unknown Queue '" + input + + "'"); + owner->getValue() = commonPath(owner->getValue(), block.scope); + } + + llvm::StringMap scopeMembers; + for (auto [index, scope] : llvm::enumerate(plan.scopes)) + scopeMembers[scope] = "scope_" + std::to_string(index) + "_"; + auto modulePointer = + [&](llvm::StringRef path) -> llvm::Expected { + if (path == "/") + return std::string("this"); + auto found = scopeMembers.find(path); + if (found == scopeMembers.end()) + return generatorError("unknown scope path '" + path + "'"); + return "&" + found->getValue(); + }; + auto attach = [&](llvm::StringRef path, + llvm::StringRef member) -> llvm::Expected { + if (path == "/") + return " attachChild(" + member.str() + ");"; + auto found = scopeMembers.find(path); + if (found == scopeMembers.end()) + return generatorError("unknown attachment scope '" + path + "'"); + return " " + found->getValue() + ".attachChild(" + member.str() + ");"; + }; + + std::vector runtimeBlocks; + for (const QueueBlockPlan &block : plan.blocks) + if (isRuntimeBlock(block) && block.kind != "memory_request") + runtimeBlocks.push_back(&block); + llvm::StringMap> memoryEndpoints; + for (const QueueBlockPlan &block : plan.blocks) + if (block.kind == "memory_request") + memoryEndpoints[block.memoryInstance].push_back(&block); + for (auto &entry : memoryEndpoints) + llvm::sort(entry.getValue(), + [](const QueueBlockPlan *left, const QueueBlockPlan *right) { + return left->endpointOrdinal < right->endpointOrdinal; + }); + llvm::StringMap queueIds; + for (auto [index, queue] : llvm::enumerate(plan.queues)) + queueIds[queue.name] = index; + uint64_t nextId = plan.queues.size(); + llvm::DenseMap feedbackStateIds; + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) + if (block->kind == "feedback") + feedbackStateIds[index] = nextId++; + llvm::StringMap blockIds; + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) + blockIds[block->name + "#" + std::to_string(index)] = nextId++; + llvm::StringMap memoryIds; + for (const MemoryInstancePlan &instance : plan.memoryInstances) + memoryIds[instance.name] = nextId++; + + std::ostringstream output; + output << "// Generated from frozen ACIR QueueGraph plan; do not edit.\n"; + if (!plan.specializationFingerprint.empty()) + output << "// Specialization: " << plan.specializationFingerprint << "\n"; + output << "#include \"gfsim/dispatch.h\"\n" + "#include \"gfsim/object.h\"\n" + "#include \"gfsim/queue.h\"\n" + "#include \"gfsim/queue_blocks.h\"\n\n" + "#include \n#include \n#include \n" + "#include \n\n" + "namespace ac_generated {\n\n"; + for (const QueuePayloadPlan &payload : plan.payloads) { + output << "struct " << payload.name << " {\n"; + for (const QueuePayloadFieldPlan &field : payload.fields) { + auto type = cppType(field.type); + if (!type) + return type.takeError(); + output << " " << *type << ' ' << field.name << "{};\n"; + } + output << " bool operator==(const " << payload.name + << " &) const = default;\n"; + output << "};\n\n"; + } + + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) { + if (block->kind != "transform" && block->kind != "route" && + block->kind != "select" && block->kind != "expect" && + block->kind != "dependency" && block->kind != "credit" && + block->kind != "reorder" && block->kind != "feedback") + continue; + if (block->kind == "dependency") { + const QueuePlan *input = findQueue(plan, block->inputs.front()); + if (!input) + return generatorError("dependency input Queue is missing"); + auto inputType = cppType(input->payloadType); + if (!inputType) + return inputType.takeError(); + constexpr llvm::StringLiteral policyNames[] = {"key", "dependency", + "resource", "cost"}; + for (auto [policyIndex, policyName] : llvm::enumerate(policyNames)) { + llvm::StringRef resultType = input->payloadType; + if (block->yields[policyIndex] != "item") { + auto expression = std::find_if( + block->expressions.begin(), block->expressions.end(), + [&](const QueueExpressionPlan &candidate) { + return candidate.result == block->yields[policyIndex]; + }); + if (expression == block->expressions.end()) + return generatorError("dependency policy yield type is missing"); + resultType = expression->type; + } + auto resultCppType = cppType(resultType); + if (!resultCppType) + return resultCppType.takeError(); + output << "struct block_" << index << '_' << policyName.str() + << "_policy {\n " << *resultCppType << " operator()(const " + << *inputType << " &item) const {\n"; + auto body = emitExpressionBody(*block, block->yields[policyIndex], 4); + if (!body) + return body.takeError(); + output << *body << " }\n};\n\n"; + } + continue; + } + if (block->kind == "transform" && + (block->inputs.size() != 1 || block->outputs.size() != 1)) { + if (block->inputs.empty() || block->outputs.empty() || + block->outputs.size() != block->yields.size()) + return generatorError("atomic transform arity is inconsistent"); + std::vector inputTypes; + std::vector outputTypes; + for (const std::string &inputName : block->inputs) { + const QueuePlan *input = findQueue(plan, inputName); + if (!input) + return generatorError("atomic transform input Queue is missing"); + auto type = cppType(input->payloadType); + if (!type) + return type.takeError(); + inputTypes.push_back(std::move(*type)); + } + for (const std::string &outputName : block->outputs) { + const QueuePlan *result = findQueue(plan, outputName); + if (!result) + return generatorError("atomic transform output Queue is missing"); + auto type = cppType(result->payloadType); + if (!type) + return type.takeError(); + outputTypes.push_back(std::move(*type)); + } + output << "struct block_" << index << "_policy {\n std::tuple<"; + for (auto [typeIndex, type] : llvm::enumerate(outputTypes)) { + if (typeIndex) + output << ", "; + output << type; + } + output << "> operator()("; + for (auto [typeIndex, type] : llvm::enumerate(inputTypes)) { + if (typeIndex) + output << ", "; + output << "const " << type << " &item"; + if (typeIndex) + output << typeIndex; + } + output << ") const {\n return {\n"; + for (auto [yieldIndex, yield] : llvm::enumerate(block->yields)) { + output << " [&]() -> " << outputTypes[yieldIndex] << " {\n"; + auto body = emitExpressionBody(*block, yield, 8); + if (!body) + return body.takeError(); + output << *body << " }()" + << (yieldIndex + 1 == block->yields.size() ? "\n" : ",\n"); + } + output << " };\n }\n};\n\n"; + continue; + } + const size_t expectedYields = block->kind == "feedback" ? 2 : 1; + if (block->yields.size() != expectedYields || + (block->kind != "select" && block->inputs.size() != 1)) + return generatorError("Queue policy arity is unsupported"); + const QueuePlan *input = findQueue(plan, block->inputs.front()); + if (!input) + return generatorError("policy input Queue is missing"); + auto inputType = cppType(input->payloadType); + if (!inputType) + return inputType.takeError(); + std::string policy = + "block_" + std::to_string(index) + + (block->kind == "feedback" ? "_update_policy" : "_policy"); + output << "struct " << policy << " {\n "; + if (block->kind == "route" || block->kind == "select") + output << "size_t"; + else if (block->kind == "expect") + output << "bool"; + else if (block->kind == "reorder" || block->kind == "credit") { + llvm::StringRef keyType = input->payloadType; + if (block->yields.front() != "item") { + auto expression = + std::find_if(block->expressions.begin(), block->expressions.end(), + [&](const QueueExpressionPlan &candidate) { + return candidate.result == block->yields.front(); + }); + if (expression == block->expressions.end()) + return generatorError(block->kind + " yield type is missing"); + keyType = expression->type; + } + auto keyCppType = cppType(keyType); + if (!keyCppType) + return keyCppType.takeError(); + output << *keyCppType; + } else { + const QueuePlan *result = findQueue(plan, block->outputs.front()); + if (!result) + return generatorError("transform output Queue is missing"); + auto resultType = cppType(result->payloadType); + if (!resultType) + return resultType.takeError(); + output << *resultType; + } + output << " operator()(const " << *inputType << " &item) const {\n"; + auto body = emitExpressionBody(*block, block->yields.front(), 4); + if (!body) + return body.takeError(); + if (block->kind == "route" || block->kind == "select") + output << " return static_cast([&]() {\n" + << *body << " }());\n"; + else + output << *body; + output << " }\n};\n\n"; + if (block->kind == "feedback") { + output << "struct block_" << index + << "_condition_policy {\n bool operator()(const " << *inputType + << " &item) const {\n"; + auto condition = emitExpressionBody(*block, block->yields[1], 4); + if (!condition) + return condition.takeError(); + output << *condition << " }\n};\n\n"; + } + } + + for (auto [memoryIndex, instance] : llvm::enumerate(plan.memoryInstances)) { + auto found = memoryEndpoints.find(instance.name); + if (found == memoryEndpoints.end() || found->getValue().empty()) + return generatorError("memory instance has no endpoints"); + const auto &endpoints = found->getValue(); + const QueuePlan *input = findQueue(plan, endpoints.front()->inputs.front()); + if (!input) + return generatorError("memory endpoint input Queue is missing"); + auto inputType = cppType(input->payloadType); + auto dataType = cppType(instance.dataType); + if (!inputType) + return inputType.takeError(); + if (!dataType) + return dataType.takeError(); + constexpr llvm::StringLiteral policyNames[] = {"address", "write", "data"}; + const std::array resultTypes = {"std::uint64_t", "bool", + *dataType}; + for (auto [policyIndex, policyName] : llvm::enumerate(policyNames)) { + output << "struct memory_" << memoryIndex << '_' << policyName.str() + << "_policy {\n " << resultTypes[policyIndex] + << " operator()(size_t endpoint, const " << *inputType + << " &item) const {\n switch (endpoint) {\n"; + for (const QueueBlockPlan *endpoint : endpoints) { + output << " case " << endpoint->endpointOrdinal << ": {\n"; + auto body = + emitExpressionBody(*endpoint, endpoint->yields[policyIndex], 6); + if (!body) + return body.takeError(); + output << *body << " }\n"; + } + output << " default: return {};\n }\n }\n};\n\n"; + } + output << "struct memory_" << memoryIndex << "_response_policy {\n " + << *inputType << " operator()(size_t endpoint, const " << *inputType + << " &item, const " << *dataType + << " &old_data) const {\n auto result = item;\n" + " switch (endpoint) {\n"; + for (const QueueBlockPlan *endpoint : endpoints) + output << " case " << endpoint->endpointOrdinal << ": result." + << endpoint->resultField << " = old_data; break;\n"; + output << " default: break;\n }\n return result;\n }\n};\n\n"; + } + + std::string modelClass = className(plan.system); + output << "class " << modelClass + << " final : public gfsim::Module {\npublic:\n " << modelClass + << "() : gfsim::Module(\"" << plan.system + << "\", gfsim::kInvalidObjectId, nullptr),\n"; + std::vector initializers; + for (const std::string &scope : plan.scopes) { + llvm::StringRef parent = llvm::StringRef(scope).rsplit('/').first; + if (parent.empty()) + parent = "/"; + auto parentPointer = modulePointer(parent); + if (!parentPointer) + return parentPointer.takeError(); + appendInitializer(initializers, scopeMembers[scope], "(\"", + pathParts(scope).back(), "\", gfsim::kInvalidObjectId, ", + *parentPointer, ")"); + } + for (const QueuePlan &queue : plan.queues) { + auto type = cppType(queue.payloadType); + auto parent = modulePointer(queueOwners[queue.name]); + if (!type) + return type.takeError(); + if (!parent) + return parent.takeError(); + appendInitializer(initializers, queueMembers[queue.name], "(\"", queue.name, + "\", ", queueIds[queue.name], ", ", *parent, ", ", + queue.depth, + ", std::numeric_limits::max(), nullptr, ", + queue.latency, ", ", queue.rate, ")"); + } + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) { + auto state = feedbackStateIds.find(index); + if (state == feedbackStateIds.end()) + continue; + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("feedback input Queue is missing")); + auto parent = modulePointer(block->scope); + if (!type) + return type.takeError(); + if (!parent) + return parent.takeError(); + appendInitializer(initializers, "block_", index, + "_state_(\"feedback_state_", block->name, "\", ", + state->second, ", ", *parent, + ", 1, std::numeric_limits::max(), nullptr, 1)"); + } + size_t sinkIndex = 0; + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) { + auto parent = modulePointer(block->scope); + if (!parent) + return parent.takeError(); + std::string member = "block_" + std::to_string(index) + "_"; + std::string key = block->name + "#" + std::to_string(index); + std::string instanceName = block->kind + "_" + block->name; + if (block->kind == "transform") { + if (block->inputs.size() == 1 && block->outputs.size() == 1) { + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", ", + queueMembers[block->inputs[0]], ", ", + queueMembers[block->outputs[0]], ")"); + } else { + std::string inputs; + std::string outputs; + for (size_t operand = 0; operand < block->inputs.size(); ++operand) { + if (operand) + inputs.append(", "); + inputs.append("&").append(queueMembers[block->inputs[operand]]); + } + for (size_t result = 0; result < block->outputs.size(); ++result) { + if (result) + outputs.append(", "); + outputs.append("&").append(queueMembers[block->outputs[result]]); + } + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", std::tuple{", inputs, + "}, std::tuple{", outputs, "})"); + } + } else if (block->kind == "broadcast" || block->kind == "fork" || + block->kind == "route") { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected(generatorError( + "topology input Queue is missing")); + if (!type) + return type.takeError(); + std::string outputs; + for (auto [outputIndex, name] : llvm::enumerate(block->outputs)) { + if (outputIndex) + outputs.append(", "); + outputs.append("&").append(queueMembers[name]); + } + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", ", + queueMembers[block->inputs[0]], + ", std::array *, ", + block->outputs.size(), ">{", outputs, "})"); + } else if (block->kind == "select") { + const QueuePlan *result = findQueue(plan, block->outputs[0]); + auto type = result ? cppType(result->payloadType) + : llvm::Expected(generatorError( + "select output Queue is missing")); + if (!type) + return type.takeError(); + std::string inputs; + for (size_t input = 1; input < block->inputs.size(); ++input) { + if (input > 1) + inputs.append(", "); + inputs.append("&").append(queueMembers[block->inputs[input]]); + } + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", ", + queueMembers[block->inputs[0]], + ", std::array *, ", + block->inputs.size() - 1, ">{", inputs, "}, ", + queueMembers[block->outputs[0]], ")"); + } else if (block->kind == "merge") { + const QueuePlan *result = findQueue(plan, block->outputs[0]); + auto type = result ? cppType(result->payloadType) + : llvm::Expected( + generatorError("merge output Queue is missing")); + if (!type) + return type.takeError(); + std::string inputs; + for (auto [inputIndex, name] : llvm::enumerate(block->inputs)) { + if (inputIndex) + inputs.append(", "); + inputs.append("&").append(queueMembers[name]); + } + std::string policy = block->policy == "priority" + ? "gfsim::QueueMergePolicy::Priority" + : "gfsim::QueueMergePolicy::RoundRobin"; + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, + ", std::array *, ", + block->inputs.size(), ">{", inputs, "}, ", + queueMembers[block->outputs[0]], ", ", policy, ")"); + } else if (block->kind == "barrier") { + std::string inputs; + std::string outputs; + for (size_t operand = 0; operand < block->inputs.size(); ++operand) { + if (operand) + inputs.append(", "); + inputs.append("&").append(queueMembers[block->inputs[operand]]); + if (operand) + outputs.append(", "); + outputs.append("&").append(queueMembers[block->outputs[operand]]); + } + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", std::tuple{", inputs, + "}, std::tuple{", outputs, "})"); + } else if (block->kind == "reorder") { + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", ", + queueMembers[block->inputs[0]], ", ", + queueMembers[block->outputs[0]], ", ", block->capacity, + ", ", block->start, ")"); + } else if (block->kind == "dependency") { + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", ", + queueMembers[block->inputs[0]], ", ", + queueMembers[block->outputs[0]], ", ", block->capacity, + ", ", block->resources, ", ", block->noDependency, ")"); + } else if (block->kind == "credit") { + appendInitializer( + initializers, member, "(\"", instanceName, "\", ", blockIds[key], + ", ", *parent, ", ", queueMembers[block->inputs[0]], ", ", + queueMembers[block->outputs[0]], ", ", block->credits, ")"); + } else if (block->kind == "feedback") { + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", ", + queueMembers[block->inputs[0]], ", block_", index, + "_state_, ", queueMembers[block->outputs[0]], ", ", + block->maxIterations, ")"); + } else if (block->kind == "expect") { + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", ", + queueMembers[block->inputs[0]], ", ", + cppStringLiteral(block->message), ")"); + } else if (block->kind == "sink" || block->kind == "observe") { + appendInitializer(initializers, member, "(\"", instanceName, "\", ", + blockIds[key], ", ", *parent, ", ", + queueMembers[block->inputs[0]], ")"); + ++sinkIndex; + } else { + return generatorError("unsupported native Queue block '" + block->kind + + "'"); + } + } + for (auto [memoryIndex, instance] : llvm::enumerate(plan.memoryInstances)) { + auto found = memoryEndpoints.find(instance.name); + if (found == memoryEndpoints.end() || found->getValue().empty()) + return generatorError("memory instance has no endpoints"); + const auto &endpoints = found->getValue(); + const QueuePlan *input = findQueue(plan, endpoints.front()->inputs.front()); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("memory input missing")); + auto parent = modulePointer(instance.ownerPath); + if (!type) + return type.takeError(); + if (!parent) + return parent.takeError(); + std::string inputs; + std::string outputs; + for (auto [index, endpoint] : llvm::enumerate(endpoints)) { + if (index) { + inputs.append(", "); + outputs.append(", "); + } + inputs.append("&").append(queueMembers[endpoint->inputs.front()]); + outputs.append("&").append(queueMembers[endpoint->outputs.front()]); + } + appendInitializer(initializers, "memory_", memoryIndex, "_(\"memory_", + instance.name, "\", ", memoryIds[instance.name], ", ", + *parent, ", std::array *, ", + endpoints.size(), ">{", inputs, + "}, std::array *, ", + endpoints.size(), ">{", outputs, "}, ", instance.entries, + ", ", instance.init, ", ", instance.latency, ")"); + } + for (auto [index, initializer] : llvm::enumerate(initializers)) + output << " " << initializer + << (index + 1 == initializers.size() ? "\n" : ",\n"); + output << " {\n setPath(\"/" << plan.system << "\");\n"; + for (const std::string &scope : plan.scopes) { + llvm::StringRef parent = llvm::StringRef(scope).rsplit('/').first; + if (parent.empty()) + parent = "/"; + auto line = attach(parent, scopeMembers[scope]); + if (!line) + return line.takeError(); + output << *line << '\n'; + } + for (const QueuePlan &queue : plan.queues) { + auto line = attach(queueOwners[queue.name], queueMembers[queue.name]); + if (!line) + return line.takeError(); + output << *line << '\n'; + } + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) { + if (!feedbackStateIds.contains(index)) + continue; + auto line = + attach(block->scope, "block_" + std::to_string(index) + "_state_"); + if (!line) + return line.takeError(); + output << *line << '\n'; + } + for (auto [memoryIndex, instance] : llvm::enumerate(plan.memoryInstances)) { + auto line = attach(instance.ownerPath, + "memory_" + std::to_string(memoryIndex) + "_"); + if (!line) + return line.takeError(); + output << *line << '\n'; + } + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) { + auto line = attach(block->scope, "block_" + std::to_string(index) + "_"); + if (!line) + return line.takeError(); + output << *line << '\n'; + } + output << " }\n\n"; + for (const QueueBlockPlan &block : plan.blocks) + if (block.kind == "source") { + const QueuePlan *queue = findQueue(plan, block.outputs.front()); + auto type = queue ? cppType(queue->payloadType) + : llvm::Expected( + generatorError("source Queue is missing")); + if (!type) + return type.takeError(); + output << " gfsim::SimQueue<" << *type << "> &" << block.outputs.front() + << "() { return " << queueMembers[block.outputs.front()] + << "; }\n"; + } + sinkIndex = 0; + size_t observationIndex = 0; + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) + if (block->kind == "sink") { + const QueuePlan *queue = findQueue(plan, block->inputs.front()); + auto type = queue ? cppType(queue->payloadType) + : llvm::Expected( + generatorError("sink Queue is missing")); + if (!type) + return type.takeError(); + output << " const std::vector<" << *type << "> &sink_" << sinkIndex + << "_values() const { return block_" << index + << "_.received(); }\n"; + ++sinkIndex; + } else if (block->kind == "observe") { + const QueuePlan *queue = findQueue(plan, block->inputs.front()); + auto type = queue ? cppType(queue->payloadType) + : llvm::Expected( + generatorError("observation Queue is missing")); + if (!type) + return type.takeError(); + output << " const std::vector<" << *type << "> &observation_" + << observationIndex << "_values() const { return block_" << index + << "_.observed(); }\n"; + ++observationIndex; + } + size_t dependencyIndex = 0; + size_t reorderIndex = 0; + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) { + if (block->kind == "dependency") { + output << " size_t dependency_" << dependencyIndex + << "_active() const { return block_" << index << "_.active(); }\n" + << " size_t dependency_" << dependencyIndex + << "_resource_active(size_t resource) const { return block_" + << index << "_.resourceActive(resource); }\n"; + ++dependencyIndex; + } else if (block->kind == "reorder") { + output << " size_t reorder_" << reorderIndex + << "_active() const { return block_" << index << "_.active(); }\n"; + ++reorderIndex; + } + } + output << "\n std::array dispatch_rows() {\n return {\n"; + for (const QueuePlan &queue : plan.queues) + output << " gfsim::makeDispatchRow(&" << queueMembers[queue.name] + << "),\n"; + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) + if (feedbackStateIds.contains(index)) + output << " gfsim::makeDispatchRow(&block_" << index + << "_state_),\n"; + for (size_t index = 0; index < runtimeBlocks.size(); ++index) { + output << " gfsim::makeDispatchRow(&block_" << index << "_),\n"; + } + for (size_t index = 0; index < plan.memoryInstances.size(); ++index) + output << " gfsim::makeDispatchRow(&memory_" << index << "_)" + << (index + 1 == plan.memoryInstances.size() ? "\n" : ",\n"); + output << " };\n }\n\nprivate:\n"; + for (const std::string &scope : plan.scopes) + output << " gfsim::Module " << scopeMembers[scope] << ";\n"; + for (const QueuePlan &queue : plan.queues) { + auto type = cppType(queue.payloadType); + if (!type) + return type.takeError(); + output << " gfsim::SimQueue<" << *type << "> " << queueMembers[queue.name] + << ";\n"; + } + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) { + if (!feedbackStateIds.contains(index)) + continue; + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("feedback state type is missing")); + if (!type) + return type.takeError(); + output << " gfsim::SimQueue> block_" + << index << "_state_;\n"; + } + sinkIndex = 0; + for (auto [index, block] : llvm::enumerate(runtimeBlocks)) { + if (block->kind == "transform") { + if (block->inputs.size() == 1 && block->outputs.size() == 1) { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + const QueuePlan *result = findQueue(plan, block->outputs[0]); + auto inputType = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("transform input missing")); + auto resultType = result ? cppType(result->payloadType) + : llvm::Expected(generatorError( + "transform output missing")); + if (!inputType) + return inputType.takeError(); + if (!resultType) + return resultType.takeError(); + output << " gfsim::QueueTransform<" << *inputType << ", " + << *resultType << ", block_" << index << "_policy, " + << result->rate << "> block_" << index << "_;\n"; + } else { + output << " gfsim::QueueAtomicTransforminputs)) { + const QueuePlan *input = findQueue(plan, inputName); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("atomic input missing")); + if (!type) + return type.takeError(); + if (inputIndex) + output << ", "; + output << *type; + } + output << ">, std::tuple<"; + for (auto [outputIndex, outputName] : llvm::enumerate(block->outputs)) { + const QueuePlan *result = findQueue(plan, outputName); + auto type = result ? cppType(result->payloadType) + : llvm::Expected(generatorError( + "atomic transform output missing")); + if (!type) + return type.takeError(); + if (outputIndex) + output << ", "; + output << *type; + } + output << ">> block_" << index << "_;\n"; + } + } else if (block->kind == "broadcast" || block->kind == "fork" || + block->kind == "route") { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("route input missing")); + if (!type) + return type.takeError(); + if (block->kind == "broadcast") + output << " gfsim::QueueBroadcast<" << *type << ", " + << block->outputs.size() << "> block_" << index << "_;\n"; + else if (block->kind == "fork") + output << " gfsim::QueueFork<" << *type << ", " + << block->outputs.size() << "> block_" << index << "_;\n"; + else + output << " gfsim::QueueRoute<" << *type << ", " + << block->outputs.size() << ", block_" << index + << "_policy> block_" << index << "_;\n"; + } else if (block->kind == "select") { + const QueuePlan *control = findQueue(plan, block->inputs[0]); + const QueuePlan *result = findQueue(plan, block->outputs[0]); + auto controlType = control ? cppType(control->payloadType) + : llvm::Expected(generatorError( + "select control input missing")); + auto dataType = result ? cppType(result->payloadType) + : llvm::Expected( + generatorError("select output missing")); + if (!controlType) + return controlType.takeError(); + if (!dataType) + return dataType.takeError(); + output << " gfsim::QueueSelect<" << *controlType << ", " << *dataType + << ", " << block->inputs.size() - 1 << ", block_" << index + << "_policy> block_" << index << "_;\n"; + } else if (block->kind == "merge") { + const QueuePlan *result = findQueue(plan, block->outputs[0]); + auto type = result ? cppType(result->payloadType) + : llvm::Expected( + generatorError("merge output missing")); + if (!type) + return type.takeError(); + output << " gfsim::QueueMerge<" << *type << ", " << block->inputs.size() + << "> block_" << index << "_;\n"; + } else if (block->kind == "barrier") { + output << " gfsim::QueueBarrierinputs)) { + const QueuePlan *input = findQueue(plan, inputName); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("barrier input missing")); + if (!type) + return type.takeError(); + if (inputIndex) + output << ", "; + output << *type; + } + output << ">> block_" << index << "_;\n"; + } else if (block->kind == "reorder") { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("reorder input missing")); + if (!type) + return type.takeError(); + output << " gfsim::QueueReorder<" << *type << ", block_" << index + << "_policy> block_" << index << "_;\n"; + } else if (block->kind == "dependency") { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("dependency input missing")); + if (!type) + return type.takeError(); + output << " gfsim::QueueDependency<" << *type << ", block_" << index + << "_key_policy, block_" << index << "_dependency_policy, block_" + << index << "_resource_policy, block_" << index + << "_cost_policy> block_" << index << "_;\n"; + } else if (block->kind == "credit") { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("credit input missing")); + if (!type) + return type.takeError(); + output << " gfsim::QueueCredit<" << *type << ", block_" << index + << "_policy> block_" << index << "_;\n"; + } else if (block->kind == "feedback") { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("feedback input missing")); + if (!type) + return type.takeError(); + output << " gfsim::QueueFeedback<" << *type << ", block_" << index + << "_update_policy, block_" << index << "_condition_policy> block_" + << index << "_;\n"; + } else if (block->kind == "expect") { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("expect input missing")); + if (!type) + return type.takeError(); + output << " gfsim::QueueExpect<" << *type << ", block_" << index + << "_policy> block_" << index << "_;\n"; + } else if (block->kind == "sink" || block->kind == "observe") { + const QueuePlan *input = findQueue(plan, block->inputs[0]); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("sink input missing")); + if (!type) + return type.takeError(); + if (block->kind == "sink") { + output << " gfsim::QueueSink<" << *type << "> block_" << index + << "_;\n"; + ++sinkIndex; + } else { + output << " gfsim::QueueObserve<" << *type << "> block_" << index + << "_;\n"; + } + } + } + for (auto [memoryIndex, instance] : llvm::enumerate(plan.memoryInstances)) { + auto found = memoryEndpoints.find(instance.name); + if (found == memoryEndpoints.end() || found->getValue().empty()) + return generatorError("memory instance has no endpoints"); + const auto &endpoints = found->getValue(); + const QueuePlan *input = findQueue(plan, endpoints.front()->inputs.front()); + auto type = input ? cppType(input->payloadType) + : llvm::Expected( + generatorError("memory input missing")); + auto dataType = cppType(instance.dataType); + if (!type) + return type.takeError(); + if (!dataType) + return dataType.takeError(); + output << " gfsim::QueueMemoryArbiter<" << *type << ", " << *dataType + << ", " << endpoints.size() << ", memory_" << memoryIndex + << "_address_policy, memory_" << memoryIndex + << "_write_policy, memory_" << memoryIndex << "_data_policy, memory_" + << memoryIndex << "_response_policy> memory_" << memoryIndex + << "_;\n"; + } + output << "};\n\n} // namespace ac_generated\n"; + return output.str(); +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/QueueGraphPlan.cpp b/components/agentic-circuit/lib/CodeGen/QueueGraphPlan.cpp new file mode 100644 index 00000000..814f91d3 --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/QueueGraphPlan.cpp @@ -0,0 +1,945 @@ +#include "acir/CodeGen/QueueGraphPlan.h" + +#include "acir/Bindings/Binding.h" +#include "acir/CodeGen/Manifest.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/ACIRTypes.h" + +#include "mlir/IR/Operation.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" + +#include + +namespace acir::codegen { +namespace { + +llvm::Error planError(const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "ACLOWER-QUEUE-PLAN: " + message); +} + +std::string printType(mlir::Type type) { + std::string result; + llvm::raw_string_ostream stream(result); + stream << type; + return result; +} + +std::string printAttribute(mlir::Attribute attribute) { + std::string result; + llvm::raw_string_ostream stream(result); + stream << attribute; + return result; +} + +std::string printRegion(mlir::Region ®ion) { + std::string result; + llvm::raw_string_ostream stream(result); + region.getParentOp()->print(stream); + return result; +} + +std::string scopePath(llvm::ArrayRef scope) { + std::string result; + for (llvm::StringRef part : scope) { + result.push_back('/'); + result.append(part); + } + return result.empty() ? "/" : result; +} + +llvm::Expected +queueName(mlir::Value value, + const llvm::DenseMap &names) { + auto found = names.find(value); + if (found == names.end()) + return planError("Queue operand has no frozen logical identity"); + return found->second; +} + +llvm::Expected> +queueNames(mlir::ValueRange values, + const llvm::DenseMap &names) { + std::vector result; + for (mlir::Value value : values) { + auto name = queueName(value, names); + if (!name) + return name.takeError(); + result.push_back(std::move(*name)); + } + return result; +} + +llvm::Expected> outputNames(mlir::Operation *op, + size_t count) { + std::vector result; + if (count == 1) + if (auto name = op->getAttrOfType("ac.name")) + result.push_back(name.getValue().str()); + if (result.empty()) + if (auto names = op->getAttrOfType("ac.output_names")) + for (mlir::Attribute value : names) { + auto name = mlir::dyn_cast(value); + if (!name) + return planError("ac.output_names must contain only strings"); + result.push_back(name.getValue().str()); + } + if (result.size() != count) + return planError("Queue-producing op requires exact frozen output names"); + return result; +} + +llvm::Error extractExpressions(mlir::Region ®ion, QueueBlockPlan &plan) { + mlir::Block &block = region.front(); + llvm::DenseMap values; + for (auto [index, argument] : llvm::enumerate(block.getArguments())) + values[argument] = index == 0 ? "item" : "item" + std::to_string(index); + auto operandNames = [&](mlir::ValueRange operands) + -> llvm::Expected> { + std::vector result; + for (mlir::Value operand : operands) { + auto found = values.find(operand); + if (found == values.end()) + return planError("Var expression operand has no local identity"); + result.push_back(found->second); + } + return result; + }; + auto append = [&](mlir::Operation &operation, llvm::StringRef kind, + llvm::StringRef field = {}, llvm::StringRef predicate = {}, + llvm::StringRef literal = {}) -> llvm::Error { + if (operation.getNumResults() != 1) + return planError("Var expression must produce exactly one result"); + auto resultType = + mlir::dyn_cast(operation.getResult(0).getType()); + if (!resultType) + return planError("Var expression result must be ac.var"); + auto operands = operandNames(operation.getOperands()); + if (!operands) + return operands.takeError(); + std::string result = "v" + std::to_string(plan.expressions.size()); + values[operation.getResult(0)] = result; + plan.expressions.push_back( + {std::move(result), kind.str(), printType(resultType.getElementType()), + std::move(*operands), field.str(), predicate.str(), literal.str()}); + return llvm::Error::success(); + }; + + for (mlir::Operation &operation : block) { + if (auto constant = mlir::dyn_cast(operation)) { + if (auto error = append(operation, "constant", {}, {}, + printAttribute(constant.getValueAttr()))) + return error; + continue; + } + if (mlir::isa(operation)) { + if (auto error = append(operation, "add")) + return error; + continue; + } + if (mlir::isa(operation)) { + if (auto error = append(operation, "sub")) + return error; + continue; + } + if (mlir::isa(operation)) { + if (auto error = append(operation, "mul")) + return error; + continue; + } + if (mlir::isa(operation)) { + if (auto error = append(operation, "popcount")) + return error; + continue; + } + if (auto compare = mlir::dyn_cast(operation)) { + if (auto error = append(operation, "cmp", {}, compare.getPredicate())) + return error; + continue; + } + if (auto get = mlir::dyn_cast(operation)) { + if (auto error = append(operation, "get", get.getField())) + return error; + continue; + } + if (auto with = mlir::dyn_cast(operation)) { + if (auto error = append(operation, "with", with.getField())) + return error; + continue; + } + llvm::SmallVector yielded; + if (auto yield = mlir::dyn_cast(operation)) + yielded.append(yield.getValues().begin(), yield.getValues().end()); + else if (auto yield = mlir::dyn_cast(operation)) + yielded.push_back(yield.getSelector()); + else if (auto yield = mlir::dyn_cast(operation)) + yielded.push_back(yield.getSelector()); + else if (auto yield = mlir::dyn_cast(operation)) + yielded.push_back(yield.getKey()); + else if (auto yield = mlir::dyn_cast(operation)) + yielded.push_back(yield.getValue()); + else if (auto yield = mlir::dyn_cast(operation)) + yielded.push_back(yield.getCost()); + else if (auto yield = mlir::dyn_cast(operation)) + yielded.push_back(yield.getValue()); + else if (auto yield = mlir::dyn_cast(operation)) + yielded.push_back(yield.getCondition()); + else if (auto yield = mlir::dyn_cast(operation)) { + yielded.push_back(yield.getValue()); + yielded.push_back(yield.getContinueValue()); + } else + return planError("unsupported operation in Queue Var region: " + + operation.getName().getStringRef()); + auto names = operandNames(yielded); + if (!names) + return names.takeError(); + plan.yields = std::move(*names); + } + if (plan.yields.empty()) + return planError("Queue Var region has no structured yield"); + return llvm::Error::success(); +} + +class Extractor { +public: + explicit Extractor(mlir::ModuleOp module) : module(module) {} + + llvm::Expected run() { + auto epoch = module->getAttrOfType("ac.contract_epoch"); + if (!epoch || epoch.getValue() != "0.3") + return planError("module requires ac.contract_epoch exactly '0.3'"); + auto system = module->getAttrOfType("ac.system"); + if (!system || system.getValue().empty()) + return planError("module requires non-empty ac.system"); + plan.system = system.getValue().str(); + if (auto specialization = + module->getAttrOfType("ac.specialization")) { + if (!isValidFingerprint(specialization.getValue())) + return planError("module ac.specialization fingerprint is invalid"); + plan.specializationFingerprint = specialization.getValue().str(); + } + if (auto error = extractBlock(*module.getBody(), {})) + return std::move(error); + if (auto error = validateGraph()) + return std::move(error); + return std::move(plan); + } + +private: + llvm::Error validateGraph() { return verifyQueueGraphPlan(plan); } + + llvm::Error addQueue(mlir::Value value, llvm::StringRef name, uint64_t depth, + uint64_t latency, uint64_t rate, + llvm::ArrayRef scope) { + if (name.empty() || !queueIdentities.insert(name).second) + return planError("Queue logical identities must be non-empty and unique"); + auto queue = mlir::dyn_cast(value.getType()); + if (!queue || depth == 0 || latency == 0 || rate == 0 || rate > depth) + return planError( + "Queue plan requires typed positive depth/latency and rate <= depth"); + names[value] = name.str(); + plan.queues.push_back({name.str(), printType(queue.getElementType()), + scopePath(scope), depth, latency, rate}); + return llvm::Error::success(); + } + + llvm::Error addOutputs(mlir::Operation *op, mlir::ValueRange outputs, + llvm::ArrayRef depths, + llvm::ArrayRef latencies, + llvm::ArrayRef scope, + std::vector &result) { + auto frozen = outputNames(op, outputs.size()); + if (!frozen) + return frozen.takeError(); + if (depths.size() != outputs.size() || latencies.size() != outputs.size()) + return planError("Queue output metadata count mismatch"); + llvm::SmallVector defaultRates(outputs.size(), 1); + llvm::ArrayRef rates = defaultRates; + if (auto attribute = + op->getAttrOfType("ac.output_rates")) + rates = attribute.asArrayRef(); + if (rates.size() != outputs.size()) + return planError("Queue output rate count must match result count"); + for (size_t index = 0; index < outputs.size(); ++index) { + if (depths[index] <= 0 || latencies[index] <= 0 || rates[index] <= 0 || + rates[index] > depths[index]) + return planError("Queue depth/latency must be positive and rate must " + "not exceed depth"); + auto error = addQueue(outputs[index], (*frozen)[index], depths[index], + latencies[index], rates[index], scope); + if (error) + return error; + } + result = std::move(*frozen); + return llvm::Error::success(); + } + + llvm::Error extractBlock(mlir::Block &block, std::vector scope) { + for (mlir::Operation &operation : block) { + if (auto typeScope = mlir::dyn_cast(operation)) { + for (mlir::Operation &declaration : typeScope.getBody().front()) { + auto structure = mlir::dyn_cast(declaration); + if (!structure) + continue; + if (!payloadIdentities.insert(structure.getSymName()).second) + return planError("payload identities must be unique"); + QueuePayloadPlan payload{structure.getSymName().str(), {}}; + for (mlir::Attribute rawField : structure.getFields()) { + auto field = mlir::dyn_cast(rawField); + auto name = field ? field.getAs("name") + : mlir::StringAttr(); + auto type = + field ? field.getAs("type") : mlir::TypeAttr(); + if (!name || !type) + return planError("struct field requires name and type"); + payload.fields.push_back( + {name.getValue().str(), printType(type.getValue())}); + } + plan.payloads.push_back(std::move(payload)); + } + continue; + } + if (auto instance = mlir::dyn_cast(operation)) { + plan.memoryInstances.push_back( + {instance.getSymName().str(), printType(instance.getDataType()), + uint64_t(instance.getEntries()), uint64_t(instance.getInit()), + uint64_t(instance.getLatency()), instance.getStableId().str(), + instance.getOwner().str()}); + continue; + } + if (auto source = mlir::dyn_cast(operation)) { + std::vector outputs; + if (auto error = addOutputs( + source, source->getResults(), {int64_t(source.getDepth())}, + {int64_t(source.getLatency())}, scope, outputs)) + return error; + plan.blocks.push_back({"source", + outputs.front(), + scopePath(scope), + {}, + outputs, + {uint64_t(source.getDepth())}, + {uint64_t(source.getLatency())}}); + continue; + } + if (auto transform = mlir::dyn_cast(operation)) { + auto inputs = queueNames(transform.getInputs(), names); + if (!inputs) + return inputs.takeError(); + std::vector outputs; + if (auto error = + addOutputs(transform, transform.getOutputs(), + transform.getOutputDepthsAttr().asArrayRef(), + transform.getOutputLatenciesAttr().asArrayRef(), + scope, outputs)) + return error; + QueueBlockPlan blockPlan{"transform", outputs.front(), scopePath(scope), + std::move(*inputs), outputs}; + for (int64_t value : transform.getOutputDepths()) + blockPlan.depths.push_back(value); + for (int64_t value : transform.getOutputLatencies()) + blockPlan.latencies.push_back(value); + blockPlan.region = printRegion(transform.getBody()); + if (auto error = extractExpressions(transform.getBody(), blockPlan)) + return error; + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto broadcast = mlir::dyn_cast(operation)) { + auto input = queueName(broadcast.getInput(), names); + if (!input) + return input.takeError(); + std::vector outputs; + if (auto error = + addOutputs(broadcast, broadcast.getOutputs(), + broadcast.getOutputDepthsAttr().asArrayRef(), + broadcast.getOutputLatenciesAttr().asArrayRef(), + scope, outputs)) + return error; + QueueBlockPlan blockPlan{"broadcast", + "broadcast_" + *input, + scopePath(scope), + {*input}, + outputs}; + for (int64_t value : broadcast.getOutputDepths()) + blockPlan.depths.push_back(value); + for (int64_t value : broadcast.getOutputLatencies()) + blockPlan.latencies.push_back(value); + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto fork = mlir::dyn_cast(operation)) { + auto input = queueName(fork.getInput(), names); + if (!input) + return input.takeError(); + std::vector outputs; + if (auto error = addOutputs(fork, fork.getOutputs(), + fork.getOutputDepthsAttr().asArrayRef(), + fork.getOutputLatenciesAttr().asArrayRef(), + scope, outputs)) + return error; + QueueBlockPlan blockPlan{ + "fork", "fork_" + *input, scopePath(scope), {*input}, outputs}; + for (int64_t value : fork.getOutputDepths()) + blockPlan.depths.push_back(value); + for (int64_t value : fork.getOutputLatencies()) + blockPlan.latencies.push_back(value); + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto route = mlir::dyn_cast(operation)) { + auto input = queueName(route.getInput(), names); + if (!input) + return input.takeError(); + std::vector outputs; + if (auto error = addOutputs(route, route.getOutputs(), + route.getOutputDepthsAttr().asArrayRef(), + route.getOutputLatenciesAttr().asArrayRef(), + scope, outputs)) + return error; + QueueBlockPlan blockPlan{"route", + "route_" + outputs.front(), + scopePath(scope), + {*input}, + outputs}; + for (int64_t value : route.getOutputDepths()) + blockPlan.depths.push_back(value); + for (int64_t value : route.getOutputLatencies()) + blockPlan.latencies.push_back(value); + blockPlan.region = printRegion(route.getSelector()); + if (auto error = extractExpressions(route.getSelector(), blockPlan)) + return error; + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto select = mlir::dyn_cast(operation)) { + auto inputs = queueNames(select.getInputs(), names); + if (!inputs) + return inputs.takeError(); + std::vector outputs; + if (auto error = addOutputs( + select, select->getResults(), {int64_t(select.getDepth())}, + {int64_t(select.getLatency())}, scope, outputs)) + return error; + QueueBlockPlan blockPlan{"select", + outputs.front(), + scopePath(scope), + std::move(*inputs), + outputs, + {uint64_t(select.getDepth())}, + {uint64_t(select.getLatency())}}; + blockPlan.region = printRegion(select.getKey()); + if (auto error = extractExpressions(select.getKey(), blockPlan)) + return error; + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto merge = mlir::dyn_cast(operation)) { + auto inputs = queueNames(merge.getInputs(), names); + if (!inputs) + return inputs.takeError(); + std::vector outputs; + if (auto error = addOutputs( + merge, merge->getResults(), {int64_t(merge.getDepth())}, + {int64_t(merge.getLatency())}, scope, outputs)) + return error; + plan.blocks.push_back({"merge", + outputs.front(), + scopePath(scope), + std::move(*inputs), + outputs, + {uint64_t(merge.getDepth())}, + {uint64_t(merge.getLatency())}, + merge.getPolicy().str()}); + continue; + } + if (auto barrier = mlir::dyn_cast(operation)) { + auto inputs = queueNames(barrier.getInputs(), names); + if (!inputs) + return inputs.takeError(); + std::vector outputs; + if (auto error = addOutputs( + barrier, barrier.getOutputs(), + barrier.getOutputDepthsAttr().asArrayRef(), + barrier.getOutputLatenciesAttr().asArrayRef(), scope, outputs)) + return error; + QueueBlockPlan blockPlan{"barrier", outputs.front(), scopePath(scope), + std::move(*inputs), outputs}; + for (int64_t value : barrier.getOutputDepths()) + blockPlan.depths.push_back(value); + for (int64_t value : barrier.getOutputLatencies()) + blockPlan.latencies.push_back(value); + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto reorder = mlir::dyn_cast(operation)) { + auto input = queueName(reorder.getInput(), names); + if (!input) + return input.takeError(); + std::vector outputs; + if (auto error = addOutputs( + reorder, reorder->getResults(), {int64_t(reorder.getDepth())}, + {int64_t(reorder.getLatency())}, scope, outputs)) + return error; + QueueBlockPlan blockPlan{"reorder", + outputs.front(), + scopePath(scope), + {*input}, + outputs, + {uint64_t(reorder.getDepth())}, + {uint64_t(reorder.getLatency())}}; + blockPlan.capacity = reorder.getCapacity(); + blockPlan.start = reorder.getStart(); + blockPlan.region = printRegion(reorder.getKey()); + if (auto error = extractExpressions(reorder.getKey(), blockPlan)) + return error; + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto dependency = mlir::dyn_cast(operation)) { + auto input = queueName(dependency.getInput(), names); + if (!input) + return input.takeError(); + std::vector outputs; + if (auto error = + addOutputs(dependency, dependency->getResults(), + {int64_t(dependency.getDepth())}, + {int64_t(dependency.getLatency())}, scope, outputs)) + return error; + QueueBlockPlan blockPlan{"dependency", + outputs.front(), + scopePath(scope), + {*input}, + outputs, + {uint64_t(dependency.getDepth())}, + {uint64_t(dependency.getLatency())}}; + blockPlan.capacity = dependency.getCapacity(); + blockPlan.noDependency = dependency.getNoDependency(); + blockPlan.resources = dependency.getResources(); + blockPlan.region = printRegion(dependency.getKey()); + std::vector policyYields; + for (mlir::Region *policy : + {&dependency.getKey(), &dependency.getWaitsFor(), + &dependency.getResource(), &dependency.getCost()}) { + if (auto error = extractExpressions(*policy, blockPlan)) + return error; + if (blockPlan.yields.size() != 1) + return planError("dependency policy must yield one value"); + policyYields.push_back(blockPlan.yields.front()); + } + blockPlan.yields = std::move(policyYields); + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto credit = mlir::dyn_cast(operation)) { + auto input = queueName(credit.getInput(), names); + if (!input) + return input.takeError(); + std::vector outputs; + if (auto error = addOutputs( + credit, credit->getResults(), {int64_t(credit.getDepth())}, + {int64_t(credit.getLatency())}, scope, outputs)) + return error; + QueueBlockPlan blockPlan{"credit", + outputs.front(), + scopePath(scope), + {*input}, + outputs, + {uint64_t(credit.getDepth())}, + {uint64_t(credit.getLatency())}}; + blockPlan.credits = credit.getCredits(); + blockPlan.region = printRegion(credit.getCost()); + if (auto error = extractExpressions(credit.getCost(), blockPlan)) + return error; + if (blockPlan.yields.size() != 1) + return planError("credit cost must yield one value"); + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto memory = mlir::dyn_cast(operation)) { + auto input = queueName(memory.getInput(), names); + if (!input) + return input.takeError(); + std::vector outputs; + if (auto error = + addOutputs(memory, memory->getResults(), + {int64_t(memory.getDepth())}, {1}, scope, outputs)) + return error; + QueueBlockPlan blockPlan{"memory_request", + outputs.front(), + scopePath(scope), + {*input}, + outputs, + {uint64_t(memory.getDepth())}, + {1}}; + blockPlan.resultField = memory.getResultField().str(); + blockPlan.memoryInstance = memory.getInstance().str(); + blockPlan.endpointOrdinal = memory.getOrdinal(); + blockPlan.region = printRegion(memory.getAddress()); + std::vector policyYields; + for (mlir::Region *policy : + {&memory.getAddress(), &memory.getWrite(), &memory.getData()}) { + if (auto error = extractExpressions(*policy, blockPlan)) + return error; + if (blockPlan.yields.size() != 1) + return planError("memory policy must yield one value"); + policyYields.push_back(blockPlan.yields.front()); + } + blockPlan.yields = std::move(policyYields); + plan.memoryRequests.push_back( + {blockPlan.memoryInstance, blockPlan.name, blockPlan.scope, + blockPlan.inputs.front(), blockPlan.outputs.front(), + blockPlan.endpointOrdinal, blockPlan.depths.front(), + blockPlan.resultField}); + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto feedback = mlir::dyn_cast(operation)) { + auto input = queueName(feedback.getInput(), names); + if (!input) + return input.takeError(); + std::vector outputs; + if (auto error = + addOutputs(feedback, feedback->getResults(), + {int64_t(feedback.getDepth())}, + {int64_t(feedback.getLatency())}, scope, outputs)) + return error; + QueueBlockPlan blockPlan{"feedback", + outputs.front(), + scopePath(scope), + {*input}, + outputs, + {uint64_t(feedback.getDepth())}, + {uint64_t(feedback.getLatency())}, + "", + uint64_t(feedback.getMaxIterations())}; + blockPlan.region = printRegion(feedback.getBody()); + if (auto error = extractExpressions(feedback.getBody(), blockPlan)) + return error; + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (auto nested = mlir::dyn_cast(operation)) { + std::vector nestedScope = scope; + nestedScope.push_back(nested.getSymName().str()); + plan.scopes.push_back(scopePath(nestedScope)); + mlir::Block &body = nested.getBody().front(); + if (body.getNumArguments() != nested.getInputs().size()) + return planError("scope input arity mismatch"); + for (size_t index = 0; index < nested.getInputs().size(); ++index) { + auto name = queueName(nested.getInputs()[index], names); + if (!name) + return name.takeError(); + names[body.getArgument(index)] = std::move(*name); + } + if (auto error = extractBlock(body, nestedScope)) + return error; + auto yield = mlir::dyn_cast(body.getTerminator()); + bool invalidYield = !yield; + if (yield) + invalidYield = yield.getQueues().size() != nested.getOutputs().size(); + if (invalidYield) + return planError("scope output arity mismatch"); + for (size_t index = 0; index < nested.getOutputs().size(); ++index) { + auto name = queueName(yield.getQueues()[index], names); + if (!name) + return name.takeError(); + names[nested.getOutputs()[index]] = std::move(*name); + } + continue; + } + auto sink = mlir::dyn_cast(operation); + if (sink) { + auto input = queueName(sink.getInput(), names); + if (!input) + return input.takeError(); + auto name = sink->getAttrOfType("ac.name"); + if (!name || name.getValue().empty()) + return planError("sink requires frozen ac.name"); + plan.blocks.push_back( + {"sink", name.getValue().str(), scopePath(scope), {*input}, {}}); + continue; + } + auto observe = mlir::dyn_cast(operation); + if (observe) { + auto input = queueName(observe.getInput(), names); + if (!input) + return input.takeError(); + plan.blocks.push_back({"observe", + observe.getName().str(), + scopePath(scope), + {*input}, + {}}); + continue; + } + auto expect = mlir::dyn_cast(operation); + if (expect) { + auto input = queueName(expect.getInput(), names); + if (!input) + return input.takeError(); + auto name = expect->getAttrOfType("ac.name"); + if (!name || name.getValue().empty()) + return planError("expect requires frozen ac.name"); + QueueBlockPlan blockPlan{ + "expect", name.getValue().str(), scopePath(scope), {*input}, {}}; + blockPlan.message = expect.getMessage().str(); + blockPlan.region = printRegion(expect.getPredicate()); + if (auto error = extractExpressions(expect.getPredicate(), blockPlan)) + return error; + plan.blocks.push_back(std::move(blockPlan)); + continue; + } + if (mlir::isa(operation) || + operation.hasTrait() || + mlir::isa(operation)) + continue; + if (operation.getName().getDialectNamespace() == "ac") + return planError("unsupported ACIR op in QueueGraph plan: " + + operation.getName().getStringRef()); + } + return llvm::Error::success(); + } + + mlir::ModuleOp module; + QueueGraphPlan plan; + llvm::DenseMap names; + llvm::StringSet<> queueIdentities; + llvm::StringSet<> payloadIdentities; +}; + +} // namespace + +llvm::Expected buildQueueGraphPlan(mlir::ModuleOp module) { + return Extractor(module).run(); +} + +llvm::Error verifyQueueGraphPlan(const QueueGraphPlan &plan) { + if (plan.system.empty() || plan.queues.empty() || plan.blocks.empty()) + return planError("QueueGraph plan is incomplete"); + if (!plan.specializationFingerprint.empty() && + !isValidFingerprint(plan.specializationFingerprint)) + return planError("QueueGraph specialization fingerprint is invalid"); + + llvm::StringSet<> queueNames; + llvm::StringMap memoryInstances; + for (const MemoryInstancePlan &instance : plan.memoryInstances) { + if (instance.name.empty() || + !memoryInstances.try_emplace(instance.name, &instance).second) + return planError( + "memory instance identities must be non-empty and unique"); + if (instance.dataType.empty() || instance.entries == 0 || + instance.init != 0 || instance.latency == 0 || + instance.stableId.empty() || instance.ownerPath.empty()) + return planError("memory instance metadata is incomplete"); + } + llvm::StringMap> endpointOrdinals; + for (const MemoryRequestPlan &request : plan.memoryRequests) { + if (!memoryInstances.contains(request.instance)) + return planError("memory request references unknown instance '" + + request.instance + "'"); + if (!endpointOrdinals[request.instance].insert(request.ordinal).second) + return planError("memory request endpoint ordinals must be unique"); + } + for (const auto &entry : memoryInstances) + if (!endpointOrdinals.contains(entry.getKey())) + return planError("memory instance '" + entry.getKey() + + "' has no request endpoints"); + for (const auto &entry : endpointOrdinals) + for (uint64_t ordinal = 0; ordinal < entry.getValue().size(); ++ordinal) + if (!entry.getValue().contains(ordinal)) + return planError("memory request endpoint ordinals must be contiguous " + "from zero"); + llvm::StringMap producers; + llvm::StringMap consumers; + llvm::StringMap indegree; + llvm::StringMap> successors; + for (const QueuePlan &queue : plan.queues) { + if (queue.name.empty() || !queueNames.insert(queue.name).second) + return planError("Queue logical identities must be non-empty and unique"); + if (queue.payloadType.empty() || queue.depth == 0 || queue.latency == 0 || + queue.rate == 0 || queue.rate > queue.depth) + return planError( + "Queue plan requires typed positive depth/latency and rate <= depth"); + indegree[queue.name] = 0; + } + + for (const QueueBlockPlan &block : plan.blocks) { + if (block.kind == "memory_request" && + !memoryInstances.contains(block.memoryInstance)) + return planError("memory request block references unknown instance"); + for (const std::string &input : block.inputs) + if (!queueNames.contains(input)) + return planError("block input references unknown Queue '" + input + + "'"); + for (const std::string &output : block.outputs) { + if (!queueNames.contains(output)) + return planError("block output references unknown Queue '" + output + + "'"); + ++producers[output]; + } + if (block.kind != "observe" && block.kind != "expect") + for (const std::string &input : block.inputs) + ++consumers[input]; + for (const std::string &input : block.inputs) + for (const std::string &output : block.outputs) { + successors[input].push_back(output); + ++indegree[output]; + } + } + + for (const QueuePlan &queue : plan.queues) { + if (producers[queue.name] != 1) + return planError("Queue '" + queue.name + + "' must have exactly one producer"); + if (consumers[queue.name] == 0) + return planError("Queue '" + queue.name + + "' has no consuming block; connect ac.sink"); + if (consumers[queue.name] > 1) + return planError("Queue '" + queue.name + + "' has multiple consuming blocks; insert ac.broadcast"); + } + + std::vector ready; + for (const QueuePlan &queue : plan.queues) + if (indegree[queue.name] == 0) + ready.push_back(queue.name); + size_t visited = 0; + for (size_t cursor = 0; cursor < ready.size(); ++cursor) { + ++visited; + auto found = successors.find(ready[cursor]); + if (found == successors.end()) + continue; + for (const std::string &successor : found->getValue()) + if (--indegree[successor] == 0) + ready.push_back(successor); + } + if (visited != plan.queues.size()) + return planError("QueueGraph contains a cycle; represent stateful loops " + "with ac.feedback"); + return llvm::Error::success(); +} + +llvm::Expected QueueGraphPlan::canonicalJson() const { + llvm::json::Array payloadValues; + for (const QueuePayloadPlan &payload : payloads) { + llvm::json::Array fields; + for (const QueuePayloadFieldPlan &field : payload.fields) + fields.push_back( + llvm::json::Object{{"name", field.name}, {"type", field.type}}); + payloadValues.push_back(llvm::json::Object{{"fields", std::move(fields)}, + {"name", payload.name}}); + } + llvm::json::Array scopeValues; + for (const std::string &scope : scopes) + scopeValues.push_back(scope); + llvm::json::Array queueValues; + for (const QueuePlan &queue : queues) + queueValues.push_back( + llvm::json::Object{{"depth", queue.depth}, + {"latency", queue.latency}, + {"name", queue.name}, + {"payload_type", queue.payloadType}, + {"rate", queue.rate}, + {"scope", queue.scope}}); + llvm::json::Array blockValues; + for (const QueueBlockPlan &block : blocks) { + llvm::json::Array inputs; + for (const std::string &input : block.inputs) + inputs.push_back(input); + llvm::json::Array outputs; + for (const std::string &output : block.outputs) + outputs.push_back(output); + llvm::json::Array depths; + for (uint64_t depth : block.depths) + depths.push_back(depth); + llvm::json::Array latencies; + for (uint64_t latency : block.latencies) + latencies.push_back(latency); + llvm::json::Array expressions; + for (const QueueExpressionPlan &expression : block.expressions) { + llvm::json::Array operands; + for (const std::string &operand : expression.operands) + operands.push_back(operand); + expressions.push_back( + llvm::json::Object{{"field", expression.field}, + {"kind", expression.kind}, + {"literal", expression.literal}, + {"operands", std::move(operands)}, + {"predicate", expression.predicate}, + {"result", expression.result}, + {"type", expression.type}}); + } + llvm::json::Array yields; + for (const std::string &yield : block.yields) + yields.push_back(yield); + blockValues.push_back( + llvm::json::Object{{"capacity", block.capacity}, + {"credits", block.credits}, + {"depths", std::move(depths)}, + {"entries", block.entries}, + {"expressions", std::move(expressions)}, + {"inputs", std::move(inputs)}, + {"kind", block.kind}, + {"latencies", std::move(latencies)}, + {"max_iterations", block.maxIterations}, + {"message", block.message}, + {"memory_instance", block.memoryInstance}, + {"name", block.name}, + {"no_dependency", block.noDependency}, + {"endpoint_ordinal", block.endpointOrdinal}, + {"outputs", std::move(outputs)}, + {"policy", block.policy}, + {"region", block.region}, + {"result_field", block.resultField}, + {"resources", block.resources}, + {"scope", block.scope}, + {"start", block.start}, + {"init", block.init}, + {"yields", std::move(yields)}}); + } + llvm::json::Array memoryInstanceValues; + for (const MemoryInstancePlan &instance : memoryInstances) + memoryInstanceValues.push_back( + llvm::json::Object{{"data_type", instance.dataType}, + {"entries", instance.entries}, + {"init", instance.init}, + {"latency", instance.latency}, + {"name", instance.name}, + {"owner_path", instance.ownerPath}, + {"stable_id", instance.stableId}}); + llvm::json::Array memoryRequestValues; + for (const MemoryRequestPlan &request : memoryRequests) + memoryRequestValues.push_back( + llvm::json::Object{{"depth", request.depth}, + {"input", request.input}, + {"instance", request.instance}, + {"name", request.name}, + {"ordinal", request.ordinal}, + {"output", request.output}, + {"result_field", request.resultField}, + {"scope", request.scope}}); + llvm::json::Object root{ + {"blocks", std::move(blockValues)}, + {"contract_epoch", "0.3"}, + {"memory_instances", std::move(memoryInstanceValues)}, + {"memory_requests", std::move(memoryRequestValues)}, + {"payloads", std::move(payloadValues)}, + {"queues", std::move(queueValues)}, + {"schema", "agentic-circuit-queue-graph-plan"}, + {"scopes", std::move(scopeValues)}, + {"specialization", specializationFingerprint.empty() + ? llvm::json::Value(nullptr) + : llvm::json::Value(specializationFingerprint)}, + {"system", system}, + {"version", "0.3"}}; + return bindings::canonicalizeJson(llvm::json::Value(std::move(root))); +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/QueueGraphPyc.cpp b/components/agentic-circuit/lib/CodeGen/QueueGraphPyc.cpp new file mode 100644 index 00000000..545182db --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/QueueGraphPyc.cpp @@ -0,0 +1,2418 @@ +#include "acir/CodeGen/QueueGraphPyc.h" +#include "acir/CodeGen/QueueBlockContract.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" + +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +llvm::Error pycError(const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "ACLOWER-PYC: " + message); +} + +const QueuePayloadPlan *findPayload(const QueueGraphPlan &plan, + llvm::StringRef type) { + constexpr llvm::StringLiteral prefix = "!ac.struct<@types::@"; + if (!type.starts_with(prefix) || !type.ends_with('>')) + return nullptr; + llvm::StringRef name = type.drop_front(prefix.size()).drop_back(); + auto found = + std::find_if(plan.payloads.begin(), plan.payloads.end(), + [&](const auto &payload) { return payload.name == name; }); + return found == plan.payloads.end() ? nullptr : &*found; +} + +llvm::Expected typeWidth(const QueueGraphPlan &plan, + llvm::StringRef type) { + if (type.starts_with('i')) { + unsigned width = 0; + if (!type.drop_front().getAsInteger(10, width) && width > 0) + return width; + } + const QueuePayloadPlan *payload = findPayload(plan, type); + if (!payload) + return pycError("unsupported PYC payload type '" + type + "'"); + unsigned total = 0; + for (const QueuePayloadFieldPlan &field : payload->fields) { + auto width = typeWidth(plan, field.type); + if (!width) + return width.takeError(); + total += *width; + } + if (total == 0) + return pycError("packed payload width must be positive"); + return total; +} + +llvm::Expected pycType(const QueueGraphPlan &plan, + llvm::StringRef type) { + auto width = typeWidth(plan, type); + if (!width) + return width.takeError(); + return "i" + std::to_string(*width); +} + +struct FieldLayout { + unsigned lsb = 0; + unsigned width = 0; + std::string type; +}; + +llvm::Expected fieldLayout(const QueueGraphPlan &plan, + llvm::StringRef recordType, + llvm::StringRef fieldName) { + const QueuePayloadPlan *payload = findPayload(plan, recordType); + if (!payload) + return pycError("field access requires a packed struct payload"); + auto total = typeWidth(plan, recordType); + if (!total) + return total.takeError(); + unsigned cursor = *total; + for (const QueuePayloadFieldPlan &field : payload->fields) { + auto width = typeWidth(plan, field.type); + if (!width) + return width.takeError(); + cursor -= *width; + if (field.name == fieldName) + return FieldLayout{cursor, *width, field.type}; + } + return pycError("unknown packed struct field '" + fieldName + "'"); +} + +const QueuePlan *findQueue(const QueueGraphPlan &plan, llvm::StringRef name) { + auto found = + std::find_if(plan.queues.begin(), plan.queues.end(), + [&](const QueuePlan &queue) { return queue.name == name; }); + return found == plan.queues.end() ? nullptr : &*found; +} + +llvm::Expected yieldedType(const QueueBlockPlan &block, + llvm::StringRef yield, + llvm::StringRef inputType) { + if (yield == "item") + return inputType.str(); + auto found = std::find_if(block.expressions.begin(), block.expressions.end(), + [&](const QueueExpressionPlan &expression) { + return expression.result == yield; + }); + if (found == block.expressions.end()) + return pycError("yield references unknown expression value"); + return found->type; +} + +llvm::Expected +emitTransform(const QueueGraphPlan &plan, const QueueBlockPlan &block, + llvm::ArrayRef inputData, + llvm::ArrayRef inputTypes, size_t yieldIndex, + unsigned &nextValue, std::ostringstream &body) { + if (inputData.size() != inputTypes.size() || inputData.empty()) + return pycError("transform input data/type arity mismatch"); + llvm::StringMap values; + llvm::StringMap types; + for (size_t index = 0; index < inputData.size(); ++index) { + std::string name = index == 0 ? "item" : "item" + std::to_string(index); + values[name] = inputData[index]; + types[name] = inputTypes[index]; + } + auto newValue = [&]() { return "%v" + std::to_string(nextValue++); }; + auto value = [&](llvm::StringRef name) -> llvm::Expected { + auto found = values.find(name); + if (found == values.end()) + return pycError("transform expression references unknown value '" + name + + "'"); + return found->getValue(); + }; + auto valueType = [&](llvm::StringRef name) -> llvm::Expected { + auto found = types.find(name); + if (found == types.end()) + return pycError("transform value has no type: '" + name + "'"); + return found->getValue(); + }; + for (const QueueExpressionPlan &expression : block.expressions) { + std::string result; + if (expression.kind == "constant") { + result = newValue(); + llvm::StringRef literal = expression.literal; + auto type = pycType(plan, expression.type); + if (!type) + return type.takeError(); + body << " " << result << " = pyc.constant " + << literal.split(" : ").first.str() << " : " << *type << "\n"; + } else { + if (expression.operands.empty()) + return pycError("transform expression operand is missing"); + auto first = value(expression.operands[0]); + if (!first) + return first.takeError(); + if (expression.kind == "popcount") { + if (expression.operands.size() != 1) + return pycError("popcount expression arity mismatch"); + auto inputType = valueType(expression.operands[0]); + if (!inputType) + return inputType.takeError(); + auto sourceType = pycType(plan, *inputType); + auto resultType = pycType(plan, expression.type); + if (!sourceType) + return sourceType.takeError(); + if (!resultType) + return resultType.takeError(); + auto inputWidth = typeWidth(plan, *inputType); + if (!inputWidth) + return inputWidth.takeError(); + std::vector terms; + terms.reserve(*inputWidth); + for (unsigned bit = 0; bit < *inputWidth; ++bit) { + std::string extracted = newValue(); + body << " " << extracted << " = pyc.extract " << *first + << " {lsb = " << bit << "} : " << *sourceType << " -> i1\n"; + if (*resultType == "i1") { + terms.push_back(std::move(extracted)); + } else { + std::string extended = newValue(); + body << " " << extended << " = pyc.zext " << extracted + << " : i1 -> " << *resultType << "\n"; + terms.push_back(std::move(extended)); + } + } + while (terms.size() > 1) { + std::vector next; + next.reserve((terms.size() + 1) / 2); + for (size_t index = 0; index < terms.size(); index += 2) { + if (index + 1 == terms.size()) { + next.push_back(std::move(terms[index])); + continue; + } + std::string added = newValue(); + body << " " << added << " = pyc.add " << terms[index] << ", " + << terms[index + 1] << " : " << *resultType << ", " + << *resultType << " -> " << *resultType << "\n"; + next.push_back(std::move(added)); + } + terms = std::move(next); + } + result = newValue(); + body << " " << result << " = pyc.alias " << terms.front() + << " {primitive_id = \"dataflow.popcount.v1\", " + "implementation_id = \"internal.reference.popcount.v1\", " + "qualification_report = " + "\"INT-11/smoke/comparison_report.json\"} : " + << *resultType << "\n"; + } else if (expression.kind == "add" || expression.kind == "sub" || + expression.kind == "mul") { + result = newValue(); + if (expression.operands.size() != 2) + return pycError("binary transform expression arity mismatch"); + auto second = value(expression.operands[1]); + if (!second) + return second.takeError(); + auto type = pycType(plan, expression.type); + if (!type) + return type.takeError(); + body << " " << result << " = pyc." << expression.kind << ' ' + << *first << ", " << *second << " : " << *type << ", " + << *type << " -> " << *type << "\n"; + } else if (expression.kind == "cmp") { + if (expression.operands.size() != 2) + return pycError("comparison expression arity mismatch"); + auto second = value(expression.operands[1]); + auto firstType = valueType(expression.operands[0]); + auto secondType = valueType(expression.operands[1]); + if (!second) + return second.takeError(); + if (!firstType) + return firstType.takeError(); + if (!secondType) + return secondType.takeError(); + if (*firstType != *secondType) + return pycError("comparison operand types must match"); + auto type = pycType(plan, *firstType); + if (!type) + return type.takeError(); + + llvm::StringRef opcode; + std::string lhs = *first; + std::string rhs = *second; + bool negate = false; + if (expression.predicate == "eq" || expression.predicate == "ne") { + opcode = "eq"; + negate = expression.predicate == "ne"; + } else if (expression.predicate == "slt" || + expression.predicate == "sge") { + opcode = "slt"; + negate = expression.predicate == "sge"; + } else if (expression.predicate == "sgt" || + expression.predicate == "sle") { + opcode = "slt"; + std::swap(lhs, rhs); + negate = expression.predicate == "sle"; + } else { + return pycError("unsupported comparison predicate"); + } + std::string compared = newValue(); + body << " " << compared << " = pyc." << opcode.str() << ' ' << lhs + << ", " << rhs << " : " << *type << ", " << *type + << " -> i1\n"; + if (negate) { + result = newValue(); + body << " " << result << " = pyc.not " << compared << " : i1\n"; + } else { + result = std::move(compared); + } + } else if (expression.kind == "get") { + auto recordType = valueType(expression.operands[0]); + if (!recordType) + return recordType.takeError(); + auto layout = fieldLayout(plan, *recordType, expression.field); + auto sourceType = pycType(plan, *recordType); + auto resultType = pycType(plan, expression.type); + if (!layout) + return layout.takeError(); + if (!sourceType) + return sourceType.takeError(); + if (!resultType) + return resultType.takeError(); + result = newValue(); + body << " " << result << " = pyc.extract " << *first + << " {lsb = " << layout->lsb << "} : " << *sourceType << " -> " + << *resultType << "\n"; + } else if (expression.kind == "with") { + if (expression.operands.size() != 2) + return pycError("packed field update arity mismatch"); + auto second = value(expression.operands[1]); + auto recordType = valueType(expression.operands[0]); + if (!second) + return second.takeError(); + if (!recordType) + return recordType.takeError(); + auto layout = fieldLayout(plan, *recordType, expression.field); + auto totalWidth = typeWidth(plan, *recordType); + if (!layout) + return layout.takeError(); + if (!totalWidth) + return totalWidth.takeError(); + std::vector> parts; + const unsigned highWidth = *totalWidth - layout->lsb - layout->width; + if (highWidth > 0) { + std::string high = newValue(); + body << " " << high << " = pyc.extract " << *first + << " {lsb = " << layout->lsb + layout->width << "} : i" + << *totalWidth << " -> i" << highWidth << "\n"; + parts.emplace_back(std::move(high), highWidth); + } + parts.emplace_back(*second, layout->width); + if (layout->lsb > 0) { + std::string low = newValue(); + body << " " << low << " = pyc.extract " << *first + << " {lsb = 0} : i" << *totalWidth << " -> i" << layout->lsb + << "\n"; + parts.emplace_back(std::move(low), layout->lsb); + } + if (parts.size() == 1) { + result = parts.front().first; + } else { + result = newValue(); + body << " " << result << " = pyc.concat("; + for (auto [index, part] : llvm::enumerate(parts)) { + if (index) + body << ", "; + body << part.first; + } + body << ") : ("; + for (auto [index, part] : llvm::enumerate(parts)) { + if (index) + body << ", "; + body << 'i' << part.second; + } + body << ") -> i" << *totalWidth << "\n"; + } + } else { + return pycError("unsupported PYC transform expression"); + } + } + values[expression.result] = result; + types[expression.result] = expression.type; + } + if (yieldIndex >= block.yields.size()) + return pycError("transform yield index is outside result arity"); + return value(block.yields[yieldIndex]); +} + +constexpr llvm::StringLiteral kStructMetrics = + "{\\\"ast_node_count\\\":0,\\\"collection_count\\\":0," + "\\\"collection_instance_count\\\":0," + "\\\"estimated_inline_cost\\\":0,\\\"hardware_call_count\\\":0," + "\\\"instance_count\\\":0,\\\"loop_count\\\":0," + "\\\"module_call_count\\\":0," + "\\\"module_family_collection_count\\\":0," + "\\\"repeat_pressure\\\":0,\\\"repeated_body_clusters\\\":[]," + "\\\"source_loc\\\":0,\\\"state_alloc_count\\\":0," + "\\\"state_call_count\\\":0}"; + +} // namespace + +llvm::Expected generateQueueGraphPyc(const QueueGraphPlan &plan) { + if (!plan.scopes.empty()) { + const QueueBlockContract *scope = findQueueBlockContract("scope"); + if (!scope || !scope->pycAvailable) + return pycError("official opcode has no PYC lowering: 'scope'"); + } + struct TransformProducer { + const QueueBlockPlan *block = nullptr; + size_t index = 0; + }; + struct RouteProducer { + const QueueBlockPlan *block = nullptr; + size_t index = 0; + }; + struct SelectState { + std::vector conditions; + std::string controlValid; + std::string selectedValid; + std::string selectorSafe; + }; + struct MergeState { + std::string nextWire; + std::string enableWire; + std::string cursor; + std::string valid; + std::string type; + }; + struct ForkState { + std::vector nextWires; + std::vector enableWires; + std::vector delivered; + }; + struct FeedbackState { + std::string validNext; + std::string validEnable; + std::string valid; + std::string dataNext; + std::string dataEnable; + std::string data; + std::string iterationNext; + std::string iterationEnable; + std::string iteration; + std::string selectedValid; + std::string selectedIteration; + std::string condition; + std::string updated; + std::string underLimit; + std::string dataType; + std::string iterationType; + }; + struct ReorderSlotState { + std::string next; + std::string enable; + std::string state; + std::string valid; + std::string key; + std::string data; + std::string free; + std::string match; + }; + struct ReorderState { + std::vector slots; + std::string expectedNext; + std::string expectedEnable; + std::string expected; + std::string inputKey; + std::string anyFree; + std::string freeIndex; + std::string freeIndexType; + std::string outputMatch; + std::string safeAdmission; + std::string keyType; + std::string dataType; + std::string slotType; + }; + struct DependencySlotState { + std::string next; + std::string enable; + std::string state; + std::string valid; + std::string phase; + std::string key; + std::string predecessor; + std::string resource; + std::string remaining; + std::string cost; + std::string data; + std::string free; + std::string done; + }; + struct DependencyState { + std::vector slots; + std::string inputKey; + std::string inputPredecessor; + std::string inputResource; + std::string inputCost; + std::string anyFree; + std::string freeIndex; + std::string freeIndexType; + std::string outputDone; + std::string doneIndex; + std::string safeAdmission; + std::string keyType; + std::string resourceType; + std::string costType; + std::string dataType; + std::string slotType; + uint64_t noDependency = 0; + uint64_t resources = 0; + }; + struct CreditSlotState { + std::string next; + std::string enable; + std::string state; + std::string valid; + std::string remaining; + std::string data; + std::string free; + std::string done; + }; + struct CreditState { + std::vector slots; + std::string inputCost; + std::string anyFree; + std::string freeIndex; + std::string freeIndexType; + std::string outputDone; + std::string doneIndex; + std::string safeAdmission; + std::string costType; + std::string dataType; + std::string slotType; + }; + std::vector sources; + std::vector sinks; + std::vector observations; + llvm::StringMap transformByOutput; + llvm::StringMap barrierByOutput; + llvm::StringMap broadcastByOutput; + llvm::StringMap forkByOutput; + llvm::StringMap routeByOutput; + llvm::StringMap selectByOutput; + llvm::StringMap mergeByOutput; + llvm::StringMap creditByOutput; + llvm::StringMap memoryByOutput; + llvm::StringMap dependencyByOutput; + llvm::StringMap reorderByOutput; + llvm::StringMap feedbackByOutput; + for (const QueueBlockPlan &block : plan.blocks) { + const QueueBlockContract *contract = findQueueBlockContract(block.kind); + if (contract && contract->role == "verification") + return pycError("verification-only opcode '" + contract->operation + + "' cannot appear in a design hierarchy; place it at " + "the PYC testbench boundary"); + if (!contract || !contract->pycAvailable) + return pycError("official opcode has no PYC lowering: '" + block.kind + + "'"); + if (block.kind == "source") + sources.push_back(&block); + else if (block.kind == "sink") + sinks.push_back(&block); + else if (block.kind == "observe") + observations.push_back(&block); + else if (block.kind == "transform") { + if (block.inputs.empty() || block.outputs.empty() || + block.yields.size() != block.outputs.size()) + return pycError("transform output arity is unsupported"); + for (auto [index, output] : llvm::enumerate(block.outputs)) + transformByOutput[output] = TransformProducer{&block, index}; + } else if (block.kind == "route") { + if (block.inputs.size() != 1 || block.outputs.size() < 2) + return pycError("route arity is unsupported"); + for (auto [index, output] : llvm::enumerate(block.outputs)) + routeByOutput[output] = RouteProducer{&block, index}; + } else if (block.kind == "select") { + if (block.inputs.size() < 3 || block.outputs.size() != 1 || + block.yields.size() != 1) + return pycError("select contract is unsupported"); + selectByOutput[block.outputs.front()] = █ + } else if (block.kind == "broadcast") { + if (block.inputs.size() != 1 || block.outputs.size() < 2) + return pycError("broadcast arity is unsupported"); + for (const std::string &output : block.outputs) + broadcastByOutput[output] = █ + } else if (block.kind == "fork") { + if (block.inputs.size() != 1 || block.outputs.size() < 2) + return pycError("fork arity is unsupported"); + for (const std::string &output : block.outputs) + forkByOutput[output] = █ + } else if (block.kind == "merge") { + if (block.outputs.size() != 1 || block.inputs.size() < 2) + return pycError("merge arity is unsupported"); + if (block.policy != "priority" && block.policy != "round_robin") + return pycError("PYC merge policy must be priority or round_robin"); + mergeByOutput[block.outputs.front()] = █ + } else if (block.kind == "barrier") { + if (block.inputs.size() < 2 || + block.outputs.size() != block.inputs.size()) + return pycError("barrier contract is unsupported"); + for (auto [index, output] : llvm::enumerate(block.outputs)) + barrierByOutput[output] = TransformProducer{&block, index}; + } else if (block.kind == "credit") { + if (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 1 || block.credits == 0) + return pycError("credit contract is unsupported"); + creditByOutput[block.outputs.front()] = █ + } else if (block.kind == "memory_request") { + if (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 3 || block.memoryInstance.empty() || + block.resultField.empty()) + return pycError("memory contract is unsupported"); + memoryByOutput[block.outputs.front()] = █ + } else if (block.kind == "reorder") { + if (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 1 || block.capacity == 0) + return pycError("reorder contract is unsupported"); + reorderByOutput[block.outputs.front()] = █ + } else if (block.kind == "dependency") { + if (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 4 || block.capacity == 0 || + block.resources == 0) + return pycError("dependency contract is unsupported"); + dependencyByOutput[block.outputs.front()] = █ + } else if (block.kind == "feedback") { + if (block.inputs.size() != 1 || block.outputs.size() != 1 || + block.yields.size() != 2 || block.maxIterations == 0) + return pycError("feedback contract is unsupported"); + feedbackByOutput[block.outputs.front()] = █ + } else { + return pycError( + "PYC QueueGraph supports " + "source/transform/broadcast/fork/route/select/" + "merge/barrier/credit/memory_request/dependency/reorder/feedback/" + "observe/sink"); + } + } + if (auto error = verifyQueueGraphPlan(plan)) + return std::move(error); + if (sources.empty() || sinks.empty()) + return pycError("PYC lowering requires at least one source and one sink"); + for (const QueuePlan &queue : plan.queues) { + if (auto width = typeWidth(plan, queue.payloadType); !width) + return width.takeError(); + if (queue.latency == 0) + return pycError("PYC Queue latency must be positive"); + if (queue.rate != 1) + return pycError( + "PYC Queue rate greater than one requires explicit lane lowering"); + } + llvm::StringMap sourceBoundary; + std::vector inputPortTypes; + for (auto [index, source] : llvm::enumerate(sources)) { + const QueuePlan *queue = findQueue(plan, source->outputs.front()); + if (!queue) + return pycError("source Queue is missing"); + auto type = pycType(plan, queue->payloadType); + if (!type) + return type.takeError(); + sourceBoundary[source->outputs.front()] = index; + inputPortTypes.push_back(std::move(*type)); + } + std::vector outputPortTypes; + for (const QueueBlockPlan *sink : sinks) { + const QueuePlan *queue = findQueue(plan, sink->inputs.front()); + if (!queue) + return pycError("sink Queue is missing"); + auto type = pycType(plan, queue->payloadType); + if (!type) + return type.takeError(); + outputPortTypes.push_back(std::move(*type)); + } + auto inputName = [&](size_t index, llvm::StringRef suffix) { + return sources.size() == 1 + ? ("%in_" + suffix).str() + : ("%in" + std::to_string(index) + "_" + suffix.str()); + }; + auto outputName = [&](size_t index, llvm::StringRef suffix) { + return sinks.size() == 1 + ? ("%out_" + suffix).str() + : ("%out" + std::to_string(index) + "_" + suffix.str()); + }; + + llvm::StringMap readyWires; + llvm::StringMap inputReady; + llvm::StringMap outputValid; + llvm::StringMap outputData; + llvm::StringMap routeSelector; + llvm::StringMap routeCondition; + llvm::StringMap selectStates; + llvm::StringMap atomicTransformValid; + llvm::StringMap> mergeGrants; + llvm::StringMap mergeStates; + llvm::StringMap forkStates; + llvm::StringMap feedbackStates; + llvm::StringMap creditStates; + llvm::StringMap memoryResponseValid; + llvm::StringMap memoryResponseData; + llvm::StringMap dependencyStates; + llvm::StringMap reorderStates; + llvm::StringMap forkOfferValid; + std::ostringstream body; + unsigned nextValue = 0; + auto newValue = [&]() { return "%v" + std::to_string(nextValue++); }; + auto emitConstant = [&](uint64_t value, llvm::StringRef type) { + std::string result = newValue(); + body << " " << result << " = pyc.constant " << value << " : " + << type.str() << "\n"; + return result; + }; + auto emitBinary = [&](llvm::StringRef operation, llvm::StringRef lhs, + llvm::StringRef rhs, llvm::StringRef type) { + std::string result = newValue(); + body << " " << result << " = pyc." << operation.str() << ' ' << lhs.str() + << ", " << rhs.str() << " : " << type.str() << ", " << type.str() + << " -> " + << (operation == "eq" || operation == "ult" || operation == "slt" + ? "i1" + : type.str()) + << "\n"; + return result; + }; + auto emitNot = [&](llvm::StringRef value) { + std::string result = newValue(); + body << " " << result << " = pyc.not " << value.str() << " : i1\n"; + return result; + }; + auto emitMux = [&](llvm::StringRef select, llvm::StringRef trueValue, + llvm::StringRef falseValue, llvm::StringRef type) { + std::string result = newValue(); + body << " " << result << " = pyc.mux " << select.str() << ", " + << trueValue.str() << ", " << falseValue.str() << " : i1, " + << type.str() << ", " << type.str() << " -> " << type.str() << "\n"; + return result; + }; + auto emitExtract = [&](llvm::StringRef value, uint64_t lsb, + llvm::StringRef inputType, + llvm::StringRef resultType) { + std::string result = newValue(); + body << " " << result << " = pyc.extract " << value.str() + << " {lsb = " << lsb << "} : " << inputType.str() << " -> " + << resultType.str() << "\n"; + return result; + }; + auto emitFieldReplace = + [&](llvm::StringRef record, llvm::StringRef recordType, + llvm::StringRef field, + llvm::StringRef replacement) -> llvm::Expected { + auto layout = fieldLayout(plan, recordType, field); + auto totalWidth = typeWidth(plan, recordType); + if (!layout) + return layout.takeError(); + if (!totalWidth) + return totalWidth.takeError(); + std::vector> parts; + const unsigned highWidth = *totalWidth - layout->lsb - layout->width; + if (highWidth > 0) + parts.emplace_back(emitExtract(record, layout->lsb + layout->width, + "i" + std::to_string(*totalWidth), + "i" + std::to_string(highWidth)), + highWidth); + parts.emplace_back(replacement.str(), layout->width); + if (layout->lsb > 0) + parts.emplace_back(emitExtract(record, 0, + "i" + std::to_string(*totalWidth), + "i" + std::to_string(layout->lsb)), + layout->lsb); + if (parts.size() == 1) + return parts.front().first; + std::string result = newValue(); + body << " " << result << " = pyc.concat("; + for (auto [index, part] : llvm::enumerate(parts)) { + if (index) + body << ", "; + body << part.first; + } + body << ") : ("; + for (auto [index, part] : llvm::enumerate(parts)) { + if (index) + body << ", "; + body << 'i' << part.second; + } + body << ") -> i" << *totalWidth << "\n"; + return result; + }; + auto reduceBalanced = [&](llvm::StringRef operation, + std::vector values, + llvm::StringRef type) { + while (values.size() > 1) { + std::vector next; + next.reserve((values.size() + 1) / 2); + for (size_t index = 0; index < values.size(); index += 2) { + if (index + 1 == values.size()) + next.push_back(values[index]); + else + next.push_back( + emitBinary(operation, values[index], values[index + 1], type)); + } + values = std::move(next); + } + return values.front(); + }; + auto selectBalanced = [&](std::vector valids, + std::vector values, + llvm::StringRef type) { + while (values.size() > 1) { + std::vector nextValids; + std::vector nextValues; + nextValids.reserve((values.size() + 1) / 2); + nextValues.reserve((values.size() + 1) / 2); + for (size_t index = 0; index < values.size(); index += 2) { + if (index + 1 == values.size()) { + nextValids.push_back(valids[index]); + nextValues.push_back(values[index]); + } else { + nextValues.push_back( + emitMux(valids[index], values[index], values[index + 1], type)); + nextValids.push_back( + emitBinary("or", valids[index], valids[index + 1], "i1")); + } + } + valids = std::move(nextValids); + values = std::move(nextValues); + } + return std::pair{valids.front(), values.front()}; + }; + for (const QueuePlan &queue : plan.queues) { + std::string ready = newValue(); + readyWires[queue.name] = ready; + body << " " << ready << " = pyc.wire : i1\n"; + } + for (const QueuePlan &queue : plan.queues) { + std::string producerValid; + std::string producerData; + auto source = sourceBoundary.find(queue.name); + if (source != sourceBoundary.end()) { + producerValid = inputName(source->getValue(), "valid"); + producerData = inputName(source->getValue(), "data"); + } else { + auto transformProducer = transformByOutput.find(queue.name); + auto barrierProducer = barrierByOutput.find(queue.name); + auto broadcastProducer = broadcastByOutput.find(queue.name); + auto forkProducer = forkByOutput.find(queue.name); + auto routeProducer = routeByOutput.find(queue.name); + auto selectProducer = selectByOutput.find(queue.name); + auto mergeProducer = mergeByOutput.find(queue.name); + auto creditProducer = creditByOutput.find(queue.name); + auto memoryProducer = memoryByOutput.find(queue.name); + auto dependencyProducer = dependencyByOutput.find(queue.name); + auto reorderProducer = reorderByOutput.find(queue.name); + auto feedbackProducer = feedbackByOutput.find(queue.name); + if (transformProducer != transformByOutput.end()) { + const TransformProducer &producer = transformProducer->getValue(); + const QueueBlockPlan &transform = *producer.block; + std::vector inputDataValues; + std::vector inputTypes; + std::string allValid; + for (const std::string &inputName : transform.inputs) { + auto valid = outputValid.find(inputName); + auto data = outputData.find(inputName); + const QueuePlan *inputQueue = findQueue(plan, inputName); + if (valid == outputValid.end() || data == outputData.end() || + !inputQueue) + return pycError("Queue transforms are not in topological order"); + allValid = allValid.empty() + ? valid->getValue() + : emitBinary("and", allValid, valid->getValue(), "i1"); + inputDataValues.push_back(data->getValue()); + inputTypes.push_back(inputQueue->payloadType); + } + if (transform.inputs.size() == 1 && transform.outputs.size() == 1) { + producerValid = allValid; + } else { + producerValid = newValue(); + body << " " << producerValid << " = pyc.wire : i1\n"; + atomicTransformValid[queue.name] = producerValid; + } + auto transformed = + emitTransform(plan, transform, inputDataValues, inputTypes, + producer.index, nextValue, body); + if (!transformed) + return transformed.takeError(); + producerData = std::move(*transformed); + } else if (barrierProducer != barrierByOutput.end()) { + const TransformProducer &producer = barrierProducer->getValue(); + const QueueBlockPlan &barrier = *producer.block; + for (const std::string &inputName : barrier.inputs) { + auto valid = outputValid.find(inputName); + if (valid == outputValid.end()) + return pycError( + "barrier input is not available in topological order"); + } + auto data = outputData.find(barrier.inputs[producer.index]); + if (data == outputData.end()) + return pycError("barrier input data is missing"); + producerValid = newValue(); + body << " " << producerValid << " = pyc.wire : i1\n"; + atomicTransformValid[queue.name] = producerValid; + producerData = data->getValue(); + } else if (broadcastProducer != broadcastByOutput.end()) { + const QueueBlockPlan &broadcast = *broadcastProducer->getValue(); + auto valid = outputValid.find(broadcast.inputs.front()); + auto data = outputData.find(broadcast.inputs.front()); + if (valid == outputValid.end() || data == outputData.end()) + return pycError( + "broadcast input is not available in topological order"); + producerValid = valid->getValue(); + producerData = data->getValue(); + } else if (forkProducer != forkByOutput.end()) { + const QueueBlockPlan &fork = *forkProducer->getValue(); + auto valid = outputValid.find(fork.inputs.front()); + auto data = outputData.find(fork.inputs.front()); + if (valid == outputValid.end() || data == outputData.end()) + return pycError("fork input is not available in topological order"); + auto state = forkStates.find(fork.name); + if (state == forkStates.end()) { + ForkState created; + std::string zero = emitConstant(0, "i1"); + for (size_t index = 0; index < fork.outputs.size(); ++index) { + std::string nextWire = newValue(); + std::string enableWire = newValue(); + std::string delivered = newValue(); + body << " " << nextWire << " = pyc.wire : i1\n"; + body << " " << enableWire << " = pyc.wire : i1\n"; + body << " " << delivered << " = pyc.reg %clk, %rst, " + << enableWire << ", " << nextWire << ", " << zero << " : i1\n"; + created.nextWires.push_back(std::move(nextWire)); + created.enableWires.push_back(std::move(enableWire)); + created.delivered.push_back(std::move(delivered)); + } + forkStates[fork.name] = std::move(created); + state = forkStates.find(fork.name); + } + auto output = + std::find(fork.outputs.begin(), fork.outputs.end(), queue.name); + if (output == fork.outputs.end()) + return pycError("fork output identity is missing"); + const size_t index = std::distance(fork.outputs.begin(), output); + std::string notDelivered = emitNot(state->getValue().delivered[index]); + producerValid = + emitBinary("and", valid->getValue(), notDelivered, "i1"); + forkOfferValid[queue.name] = producerValid; + producerData = data->getValue(); + } else if (routeProducer != routeByOutput.end()) { + const RouteProducer &producer = routeProducer->getValue(); + const QueueBlockPlan &route = *producer.block; + auto valid = outputValid.find(route.inputs.front()); + auto data = outputData.find(route.inputs.front()); + const QueuePlan *inputQueue = findQueue(plan, route.inputs.front()); + if (valid == outputValid.end() || data == outputData.end() || + !inputQueue) + return pycError("route input is not available in topological order"); + auto selector = routeSelector.find(route.name); + if (selector == routeSelector.end()) { + auto selected = + emitTransform(plan, route, {data->getValue()}, + {inputQueue->payloadType}, 0, nextValue, body); + if (!selected) + return selected.takeError(); + routeSelector[route.name] = *selected; + selector = routeSelector.find(route.name); + } + auto selectorType = + yieldedType(route, route.yields.front(), inputQueue->payloadType); + if (!selectorType) + return selectorType.takeError(); + auto selectorPycType = pycType(plan, *selectorType); + if (!selectorPycType) + return selectorPycType.takeError(); + std::string index = emitConstant(producer.index, *selectorPycType); + std::string condition = + emitBinary("eq", selector->getValue(), index, *selectorPycType); + routeCondition[queue.name] = condition; + producerValid = emitBinary("and", valid->getValue(), condition, "i1"); + producerData = data->getValue(); + } else if (selectProducer != selectByOutput.end()) { + const QueueBlockPlan &select = *selectProducer->getValue(); + auto controlValid = outputValid.find(select.inputs.front()); + auto controlData = outputData.find(select.inputs.front()); + const QueuePlan *controlQueue = findQueue(plan, select.inputs.front()); + if (controlValid == outputValid.end() || + controlData == outputData.end() || !controlQueue) + return pycError( + "select control is not available in topological order"); + auto selector = + emitTransform(plan, select, {controlData->getValue()}, + {controlQueue->payloadType}, 0, nextValue, body); + if (!selector) + return selector.takeError(); + auto selectorType = yieldedType(select, select.yields.front(), + controlQueue->payloadType); + if (!selectorType) + return selectorType.takeError(); + auto selectorPycType = pycType(plan, *selectorType); + if (!selectorPycType) + return selectorPycType.takeError(); + auto outputType = pycType(plan, queue.payloadType); + if (!outputType) + return outputType.takeError(); + + SelectState state; + state.controlValid = controlValid->getValue(); + std::vector selectedValidTerms; + std::vector dataValues; + for (size_t index = 1; index < select.inputs.size(); ++index) { + auto valid = outputValid.find(select.inputs[index]); + auto data = outputData.find(select.inputs[index]); + if (valid == outputValid.end() || data == outputData.end()) + return pycError( + "select data input is not available in topological order"); + std::string indexValue = emitConstant(index - 1, *selectorPycType); + std::string condition = + emitBinary("eq", *selector, indexValue, *selectorPycType); + state.conditions.push_back(condition); + selectedValidTerms.push_back( + emitBinary("and", valid->getValue(), condition, "i1")); + dataValues.push_back(data->getValue()); + } + state.selectedValid = reduceBalanced("or", selectedValidTerms, "i1"); + std::string selectedData = dataValues.back(); + for (size_t index = dataValues.size() - 1; index-- > 0;) + selectedData = emitMux(state.conditions[index], dataValues[index], + selectedData, *outputType); + std::string anyCondition = reduceBalanced("or", state.conditions, "i1"); + std::string invalidSelector = + emitBinary("and", state.controlValid, emitNot(anyCondition), "i1"); + state.selectorSafe = emitNot(invalidSelector); + producerValid = + emitBinary("and", state.controlValid, state.selectedValid, "i1"); + producerData = std::move(selectedData); + selectStates[select.name] = std::move(state); + } else if (mergeProducer != mergeByOutput.end()) { + const QueueBlockPlan &merge = *mergeProducer->getValue(); + std::vector valids; + std::vector dataValues; + for (const std::string &input : merge.inputs) { + auto valid = outputValid.find(input); + auto data = outputData.find(input); + if (valid == outputValid.end() || data == outputData.end()) + return pycError( + "merge input is not available in topological order"); + valids.push_back(valid->getValue()); + dataValues.push_back(data->getValue()); + } + std::string any = valids.front(); + for (size_t index = 1; index < valids.size(); ++index) { + any = emitBinary("or", any, valids[index], "i1"); + } + std::vector grants; + if (merge.policy == "priority") { + grants.push_back(valids.front()); + std::string prior = valids.front(); + for (size_t index = 1; index < valids.size(); ++index) { + std::string notPrior = emitNot(prior); + grants.push_back(emitBinary("and", valids[index], notPrior, "i1")); + prior = emitBinary("or", prior, valids[index], "i1"); + } + } else { + unsigned pointerWidth = 1; + while ((uint64_t{1} << pointerWidth) < valids.size()) + ++pointerWidth; + std::string pointerType = "i" + std::to_string(pointerWidth); + std::string nextWire = newValue(); + std::string enableWire = newValue(); + body << " " << nextWire << " = pyc.wire : " << pointerType << "\n"; + body << " " << enableWire << " = pyc.wire : i1\n"; + std::string zeroPointer = emitConstant(0, pointerType); + std::string cursor = newValue(); + body << " " << cursor << " = pyc.reg %clk, %rst, " << enableWire + << ", " << nextWire << ", " << zeroPointer << " : " + << pointerType << "\n"; + std::string zeroGrant = emitConstant(0, "i1"); + std::vector> grantsByCursor; + grantsByCursor.reserve(valids.size()); + for (size_t start = 0; start < valids.size(); ++start) { + std::vector cursorGrants(valids.size(), zeroGrant); + std::string prior = zeroGrant; + for (size_t offset = 0; offset < valids.size(); ++offset) { + const size_t input = (start + offset) % valids.size(); + cursorGrants[input] = + emitBinary("and", valids[input], emitNot(prior), "i1"); + prior = emitBinary("or", prior, valids[input], "i1"); + } + grantsByCursor.push_back(std::move(cursorGrants)); + } + grants.reserve(valids.size()); + for (size_t input = 0; input < valids.size(); ++input) { + std::string selected = zeroGrant; + for (size_t start = valids.size(); start-- > 0;) { + std::string startValue = emitConstant(start, pointerType); + std::string atStart = + emitBinary("eq", cursor, startValue, pointerType); + selected = emitMux(atStart, grantsByCursor[start][input], + selected, "i1"); + } + std::string qualified = newValue(); + body << " " << qualified << " = pyc.alias " << selected + << " {num_inputs = " << valids.size() << ", lane = " << input + << ", primitive_id = \"control.rr_arbiter.v1\", " + "implementation_id = \"internal.reference.rr_arbiter.v1\", " + "qualification_report = " + "\"DF-09/smoke_v072/arbiter_candidates\"} : i1\n"; + grants.push_back(std::move(qualified)); + } + mergeStates[merge.name] = + MergeState{nextWire, enableWire, cursor, any, pointerType}; + } + auto outputType = pycType(plan, queue.payloadType); + if (!outputType) + return outputType.takeError(); + std::string selectedData = dataValues.back(); + for (size_t index = dataValues.size() - 1; index-- > 0;) + selectedData = emitMux(grants[index], dataValues[index], selectedData, + *outputType); + mergeGrants[merge.name] = grants; + producerValid = any; + producerData = selectedData; + } else if (creditProducer != creditByOutput.end()) { + const QueueBlockPlan &credit = *creditProducer->getValue(); + auto inputValidValue = outputValid.find(credit.inputs.front()); + auto inputDataValue = outputData.find(credit.inputs.front()); + const QueuePlan *inputQueue = findQueue(plan, credit.inputs.front()); + if (inputValidValue == outputValid.end() || + inputDataValue == outputData.end() || !inputQueue) + return pycError("credit input is not available in topological order"); + auto dataType = pycType(plan, inputQueue->payloadType); + auto dataWidth = typeWidth(plan, inputQueue->payloadType); + if (!dataType) + return dataType.takeError(); + if (!dataWidth) + return dataWidth.takeError(); + auto costValue = + emitTransform(plan, credit, {inputDataValue->getValue()}, + {inputQueue->payloadType}, 0, nextValue, body); + if (!costValue) + return costValue.takeError(); + auto costType = + yieldedType(credit, credit.yields.front(), inputQueue->payloadType); + if (!costType) + return costType.takeError(); + auto costPycType = pycType(plan, *costType); + auto costWidth = typeWidth(plan, *costType); + if (!costPycType) + return costPycType.takeError(); + if (!costWidth) + return costWidth.takeError(); + if (*costWidth == 0 || *costWidth > 64) + return pycError("credit cost width is unsupported"); + + CreditState state; + state.inputCost = std::move(*costValue); + state.costType = std::move(*costPycType); + state.dataType = *dataType; + state.slotType = "i" + std::to_string(1 + *costWidth + *dataWidth); + std::string zeroSlot = emitConstant(0, state.slotType); + std::string zeroCost = emitConstant(0, state.costType); + std::vector freeValues; + std::vector doneValues; + std::vector dataValues; + for (uint64_t index = 0; index < credit.credits; ++index) { + CreditSlotState slot; + slot.next = newValue(); + slot.enable = newValue(); + slot.state = newValue(); + body << " " << slot.next << " = pyc.wire : " << state.slotType + << "\n"; + body << " " << slot.enable << " = pyc.wire : i1\n"; + body << " " << slot.state << " = pyc.reg %clk, %rst, " + << slot.enable << ", " << slot.next << ", " << zeroSlot << " : " + << state.slotType << "\n"; + slot.valid = emitExtract(slot.state, *costWidth + *dataWidth, + state.slotType, "i1"); + slot.remaining = emitExtract(slot.state, *dataWidth, state.slotType, + state.costType); + slot.data = + emitExtract(slot.state, 0, state.slotType, state.dataType); + slot.free = emitNot(slot.valid); + std::string atZero = + emitBinary("eq", slot.remaining, zeroCost, state.costType); + slot.done = emitBinary("and", slot.valid, atZero, "i1"); + freeValues.push_back(slot.free); + doneValues.push_back(slot.done); + dataValues.push_back(slot.data); + state.slots.push_back(std::move(slot)); + } + unsigned indexWidth = 1; + while ((uint64_t{1} << indexWidth) < credit.credits) + ++indexWidth; + state.freeIndexType = "i" + std::to_string(indexWidth); + std::vector indices; + indices.reserve(credit.credits); + for (uint64_t index = 0; index < credit.credits; ++index) + indices.push_back(emitConstant(index, state.freeIndexType)); + auto selectedFree = + selectBalanced(freeValues, indices, state.freeIndexType); + state.anyFree = std::move(selectedFree.first); + state.freeIndex = std::move(selectedFree.second); + auto selectedDoneIndex = + selectBalanced(doneValues, indices, state.freeIndexType); + state.doneIndex = std::move(selectedDoneIndex.second); + auto selectedDoneData = + selectBalanced(doneValues, dataValues, state.dataType); + state.outputDone = std::move(selectedDoneData.first); + std::string selectedData = std::move(selectedDoneData.second); + std::string invalidCost = + emitBinary("eq", state.inputCost, zeroCost, state.costType); + std::string invalidAdmission = + emitBinary("and", inputValidValue->getValue(), invalidCost, "i1"); + state.safeAdmission = emitNot(invalidAdmission); + producerValid = state.outputDone; + producerData = std::move(selectedData); + creditStates[credit.name] = std::move(state); + } else if (memoryProducer != memoryByOutput.end()) { + const QueueBlockPlan &memory = *memoryProducer->getValue(); + const QueuePlan *inputQueue = findQueue(plan, memory.inputs.front()); + if (!inputQueue) + return pycError("memory input Queue is missing"); + auto requestType = pycType(plan, inputQueue->payloadType); + if (!requestType) + return requestType.takeError(); + producerValid = newValue(); + producerData = newValue(); + body << " " << producerValid << " = pyc.wire : i1\n"; + body << " " << producerData << " = pyc.wire : " << *requestType + << "\n"; + memoryResponseValid[memory.outputs.front()] = producerValid; + memoryResponseData[memory.outputs.front()] = producerData; + } else if (dependencyProducer != dependencyByOutput.end()) { + const QueueBlockPlan &dependency = *dependencyProducer->getValue(); + auto inputValidValue = outputValid.find(dependency.inputs.front()); + auto inputDataValue = outputData.find(dependency.inputs.front()); + const QueuePlan *inputQueue = + findQueue(plan, dependency.inputs.front()); + if (inputValidValue == outputValid.end() || + inputDataValue == outputData.end() || !inputQueue) + return pycError( + "dependency input is not available in topological order"); + auto dataType = pycType(plan, inputQueue->payloadType); + auto dataWidth = typeWidth(plan, inputQueue->payloadType); + if (!dataType) + return dataType.takeError(); + if (!dataWidth) + return dataWidth.takeError(); + + std::vector policyValues; + std::vector policyTypes; + std::vector policyWidths; + for (size_t index = 0; index < dependency.yields.size(); ++index) { + auto value = + emitTransform(plan, dependency, {inputDataValue->getValue()}, + {inputQueue->payloadType}, index, nextValue, body); + if (!value) + return value.takeError(); + auto type = yieldedType(dependency, dependency.yields[index], + inputQueue->payloadType); + if (!type) + return type.takeError(); + auto pycType = ::acir::codegen::pycType(plan, *type); + auto width = typeWidth(plan, *type); + if (!pycType) + return pycType.takeError(); + if (!width) + return width.takeError(); + if (*width == 0 || *width > 64) + return pycError("dependency policy width is unsupported"); + policyValues.push_back(std::move(*value)); + policyTypes.push_back(std::move(*pycType)); + policyWidths.push_back(*width); + } + if (policyTypes[0] != policyTypes[1]) + return pycError("dependency key/predecessor types must match"); + if (policyWidths[1] < 64 && + dependency.noDependency >= (uint64_t{1} << policyWidths[1])) + return pycError("dependency sentinel does not fit predecessor type"); + if (policyWidths[2] < 64 && + dependency.resources > (uint64_t{1} << policyWidths[2])) + return pycError("dependency resources do not fit resource type"); + + DependencyState state; + state.inputKey = policyValues[0]; + state.inputPredecessor = policyValues[1]; + state.inputResource = policyValues[2]; + state.inputCost = policyValues[3]; + state.keyType = policyTypes[0]; + state.resourceType = policyTypes[2]; + state.costType = policyTypes[3]; + state.dataType = *dataType; + state.noDependency = dependency.noDependency; + state.resources = dependency.resources; + const uint64_t slotWidth = 1 + 2 + 2 * policyWidths[0] + + policyWidths[2] + 2 * policyWidths[3] + + *dataWidth; + state.slotType = "i" + std::to_string(slotWidth); + std::string zeroSlot = emitConstant(0, state.slotType); + std::string donePhase = emitConstant(2, "i2"); + + const uint64_t costLsb = *dataWidth; + const uint64_t remainingLsb = costLsb + policyWidths[3]; + const uint64_t resourceLsb = remainingLsb + policyWidths[3]; + const uint64_t predecessorLsb = resourceLsb + policyWidths[2]; + const uint64_t keyLsb = predecessorLsb + policyWidths[0]; + const uint64_t phaseLsb = keyLsb + policyWidths[0]; + const uint64_t validLsb = phaseLsb + 2; + std::vector freeValues; + std::vector doneValues; + std::vector dataValues; + std::vector duplicateValues; + for (uint64_t index = 0; index < dependency.capacity; ++index) { + DependencySlotState slot; + slot.next = newValue(); + slot.enable = newValue(); + slot.state = newValue(); + body << " " << slot.next << " = pyc.wire : " << state.slotType + << "\n"; + body << " " << slot.enable << " = pyc.wire : i1\n"; + body << " " << slot.state << " = pyc.reg %clk, %rst, " + << slot.enable << ", " << slot.next << ", " << zeroSlot << " : " + << state.slotType << "\n"; + slot.valid = emitExtract(slot.state, validLsb, state.slotType, "i1"); + slot.phase = emitExtract(slot.state, phaseLsb, state.slotType, "i2"); + slot.key = + emitExtract(slot.state, keyLsb, state.slotType, state.keyType); + slot.predecessor = emitExtract(slot.state, predecessorLsb, + state.slotType, state.keyType); + slot.resource = emitExtract(slot.state, resourceLsb, state.slotType, + state.resourceType); + slot.remaining = emitExtract(slot.state, remainingLsb, state.slotType, + state.costType); + slot.cost = + emitExtract(slot.state, costLsb, state.slotType, state.costType); + slot.data = + emitExtract(slot.state, 0, state.slotType, state.dataType); + slot.free = emitNot(slot.valid); + freeValues.push_back(slot.free); + std::string isDone = emitBinary("eq", slot.phase, donePhase, "i2"); + slot.done = emitBinary("and", slot.valid, isDone, "i1"); + doneValues.push_back(slot.done); + dataValues.push_back(slot.data); + std::string sameInput = + emitBinary("eq", slot.key, state.inputKey, state.keyType); + duplicateValues.push_back( + emitBinary("and", slot.valid, sameInput, "i1")); + state.slots.push_back(std::move(slot)); + } + + unsigned indexWidth = 1; + while ((uint64_t{1} << indexWidth) < dependency.capacity) + ++indexWidth; + state.freeIndexType = "i" + std::to_string(indexWidth); + std::vector indices; + indices.reserve(dependency.capacity); + for (uint64_t index = 0; index < dependency.capacity; ++index) + indices.push_back(emitConstant(index, state.freeIndexType)); + auto selectedFree = + selectBalanced(freeValues, indices, state.freeIndexType); + state.anyFree = std::move(selectedFree.first); + state.freeIndex = std::move(selectedFree.second); + auto selectedDoneIndex = + selectBalanced(doneValues, indices, state.freeIndexType); + state.doneIndex = std::move(selectedDoneIndex.second); + auto selectedDoneData = + selectBalanced(doneValues, dataValues, state.dataType); + state.outputDone = std::move(selectedDoneData.first); + std::string selectedData = std::move(selectedDoneData.second); + std::string duplicate = reduceBalanced("or", duplicateValues, "i1"); + std::string zeroCost = emitConstant(0, state.costType); + std::string costIsZero = + emitBinary("eq", state.inputCost, zeroCost, state.costType); + std::string resourceInvalid = emitConstant(0, "i1"); + if (policyWidths[2] == 64 || + dependency.resources < (uint64_t{1} << policyWidths[2])) { + std::string resourceLimit = + emitConstant(dependency.resources, state.resourceType); + std::string resourceValid = emitBinary( + "ult", state.inputResource, resourceLimit, state.resourceType); + resourceInvalid = emitNot(resourceValid); + } + std::string invalidInput = + emitBinary("or", duplicate, costIsZero, "i1"); + invalidInput = emitBinary("or", invalidInput, resourceInvalid, "i1"); + std::string invalidAdmission = + emitBinary("and", inputValidValue->getValue(), invalidInput, "i1"); + state.safeAdmission = emitNot(invalidAdmission); + producerValid = state.outputDone; + producerData = std::move(selectedData); + dependencyStates[dependency.name] = std::move(state); + } else if (reorderProducer != reorderByOutput.end()) { + const QueueBlockPlan &reorder = *reorderProducer->getValue(); + auto inputValidValue = outputValid.find(reorder.inputs.front()); + auto inputDataValue = outputData.find(reorder.inputs.front()); + const QueuePlan *inputQueue = findQueue(plan, reorder.inputs.front()); + if (inputValidValue == outputValid.end() || + inputDataValue == outputData.end() || !inputQueue) + return pycError( + "reorder input is not available in topological order"); + auto dataType = pycType(plan, inputQueue->payloadType); + if (!dataType) + return dataType.takeError(); + auto keyValue = + emitTransform(plan, reorder, {inputDataValue->getValue()}, + {inputQueue->payloadType}, 0, nextValue, body); + if (!keyValue) + return keyValue.takeError(); + auto keyType = yieldedType(reorder, reorder.yields.front(), + inputQueue->payloadType); + if (!keyType) + return keyType.takeError(); + auto keyPycType = pycType(plan, *keyType); + auto keyWidth = typeWidth(plan, *keyType); + auto dataWidth = typeWidth(plan, inputQueue->payloadType); + if (!keyPycType) + return keyPycType.takeError(); + if (!keyWidth) + return keyWidth.takeError(); + if (!dataWidth) + return dataWidth.takeError(); + if (*keyWidth == 0 || *keyWidth > 64 || + (*keyWidth < 64 && reorder.start >= (uint64_t{1} << *keyWidth))) + return pycError("reorder start does not fit key type"); + + ReorderState state; + state.inputKey = std::move(*keyValue); + state.keyType = *keyPycType; + state.dataType = *dataType; + state.slotType = "i" + std::to_string(1 + *keyWidth + *dataWidth); + std::string zeroSlot = emitConstant(0, state.slotType); + std::string initialExpected = + emitConstant(reorder.start, state.keyType); + state.expectedNext = newValue(); + state.expectedEnable = newValue(); + state.expected = newValue(); + body << " " << state.expectedNext + << " = pyc.wire : " << state.keyType << "\n"; + body << " " << state.expectedEnable << " = pyc.wire : i1\n"; + body << " " << state.expected << " = pyc.reg %clk, %rst, " + << state.expectedEnable << ", " << state.expectedNext << ", " + << initialExpected << " : " << state.keyType << "\n"; + + std::vector freeValues; + std::vector matchValues; + std::vector duplicateValues; + std::vector slotDataValues; + for (uint64_t index = 0; index < reorder.capacity; ++index) { + ReorderSlotState slot; + slot.next = newValue(); + slot.enable = newValue(); + slot.state = newValue(); + body << " " << slot.next << " = pyc.wire : " << state.slotType + << "\n"; + body << " " << slot.enable << " = pyc.wire : i1\n"; + body << " " << slot.state << " = pyc.reg %clk, %rst, " + << slot.enable << ", " << slot.next << ", " << zeroSlot << " : " + << state.slotType << "\n"; + slot.valid = newValue(); + body << " " << slot.valid << " = pyc.extract " << slot.state + << " {lsb = " << (*keyWidth + *dataWidth) + << "} : " << state.slotType << " -> i1\n"; + slot.key = newValue(); + body << " " << slot.key << " = pyc.extract " << slot.state + << " {lsb = " << *dataWidth << "} : " << state.slotType << " -> " + << state.keyType << "\n"; + slot.data = newValue(); + body << " " << slot.data << " = pyc.extract " << slot.state + << " {lsb = 0} : " << state.slotType << " -> " << state.dataType + << "\n"; + slot.free = emitNot(slot.valid); + freeValues.push_back(slot.free); + std::string expected = + emitBinary("eq", slot.key, state.expected, state.keyType); + slot.match = emitBinary("and", slot.valid, expected, "i1"); + matchValues.push_back(slot.match); + slotDataValues.push_back(slot.data); + std::string sameInput = + emitBinary("eq", slot.key, state.inputKey, state.keyType); + duplicateValues.push_back( + emitBinary("and", slot.valid, sameInput, "i1")); + state.slots.push_back(std::move(slot)); + } + unsigned freeIndexWidth = 1; + while ((uint64_t{1} << freeIndexWidth) < reorder.capacity) + ++freeIndexWidth; + state.freeIndexType = "i" + std::to_string(freeIndexWidth); + std::vector freeIndices; + freeIndices.reserve(reorder.capacity); + for (uint64_t index = 0; index < reorder.capacity; ++index) + freeIndices.push_back(emitConstant(index, state.freeIndexType)); + auto selectedFree = + selectBalanced(freeValues, freeIndices, state.freeIndexType); + state.anyFree = std::move(selectedFree.first); + state.freeIndex = std::move(selectedFree.second); + auto selected = + selectBalanced(matchValues, slotDataValues, state.dataType); + state.outputMatch = std::move(selected.first); + std::string selectedData = std::move(selected.second); + std::string duplicate = reduceBalanced("or", duplicateValues, "i1"); + std::string stale = + emitBinary("ult", state.inputKey, state.expected, state.keyType); + std::string invalidKey = emitBinary("or", duplicate, stale, "i1"); + std::string invalidAdmission = + emitBinary("and", inputValidValue->getValue(), invalidKey, "i1"); + state.safeAdmission = emitNot(invalidAdmission); + producerValid = state.outputMatch; + producerData = std::move(selectedData); + reorderStates[reorder.name] = std::move(state); + } else if (feedbackProducer != feedbackByOutput.end()) { + const QueueBlockPlan &feedback = *feedbackProducer->getValue(); + auto inputValidValue = outputValid.find(feedback.inputs.front()); + auto inputDataValue = outputData.find(feedback.inputs.front()); + const QueuePlan *inputQueue = findQueue(plan, feedback.inputs.front()); + if (inputValidValue == outputValid.end() || + inputDataValue == outputData.end() || !inputQueue) + return pycError( + "feedback input is not available in topological order"); + auto dataType = pycType(plan, inputQueue->payloadType); + if (!dataType) + return dataType.takeError(); + unsigned iterationWidth = 1; + while (iterationWidth < 64 && + (uint64_t{1} << iterationWidth) <= feedback.maxIterations) + ++iterationWidth; + std::string iterationType = "i" + std::to_string(iterationWidth); + std::string zeroValid = emitConstant(0, "i1"); + std::string zeroData = emitConstant(0, *dataType); + std::string zeroIteration = emitConstant(0, iterationType); + FeedbackState state; + state.validNext = newValue(); + state.validEnable = newValue(); + state.valid = newValue(); + body << " " << state.validNext << " = pyc.wire : i1\n"; + body << " " << state.validEnable << " = pyc.wire : i1\n"; + body << " " << state.valid << " = pyc.reg %clk, %rst, " + << state.validEnable << ", " << state.validNext << ", " + << zeroValid << " : i1\n"; + state.dataNext = newValue(); + state.dataEnable = newValue(); + state.data = newValue(); + body << " " << state.dataNext << " = pyc.wire : " << *dataType + << "\n"; + body << " " << state.dataEnable << " = pyc.wire : i1\n"; + body << " " << state.data << " = pyc.reg %clk, %rst, " + << state.dataEnable << ", " << state.dataNext << ", " << zeroData + << " : " << *dataType << "\n"; + state.iterationNext = newValue(); + state.iterationEnable = newValue(); + state.iteration = newValue(); + body << " " << state.iterationNext + << " = pyc.wire : " << iterationType << "\n"; + body << " " << state.iterationEnable << " = pyc.wire : i1\n"; + body << " " << state.iteration << " = pyc.reg %clk, %rst, " + << state.iterationEnable << ", " << state.iterationNext << ", " + << zeroIteration << " : " << iterationType << "\n"; + std::string selectedData = emitMux( + state.valid, state.data, inputDataValue->getValue(), *dataType); + state.selectedValid = + emitBinary("or", state.valid, inputValidValue->getValue(), "i1"); + state.selectedIteration = + emitMux(state.valid, state.iteration, zeroIteration, iterationType); + auto updated = + emitTransform(plan, feedback, {selectedData}, + {inputQueue->payloadType}, 0, nextValue, body); + if (!updated) + return updated.takeError(); + state.updated = std::move(*updated); + auto condition = + emitTransform(plan, feedback, {selectedData}, + {inputQueue->payloadType}, 1, nextValue, body); + if (!condition) + return condition.takeError(); + auto conditionType = + yieldedType(feedback, feedback.yields[1], inputQueue->payloadType); + if (!conditionType) + return conditionType.takeError(); + auto conditionPycType = pycType(plan, *conditionType); + if (!conditionPycType) + return conditionPycType.takeError(); + if (*conditionPycType != "i1") + return pycError("feedback condition must lower to i1"); + state.condition = std::move(*condition); + std::string limit = emitConstant(feedback.maxIterations, iterationType); + state.underLimit = + emitBinary("ult", state.selectedIteration, limit, iterationType); + std::string done = emitNot(state.condition); + producerValid = emitBinary("and", state.selectedValid, done, "i1"); + producerData = std::move(selectedData); + state.dataType = *dataType; + state.iterationType = std::move(iterationType); + feedbackStates[feedback.name] = std::move(state); + } else { + return pycError("Queue has no supported producer: '" + queue.name + + "'"); + } + } + auto dataType = pycType(plan, queue.payloadType); + if (!dataType) + return dataType.takeError(); + std::vector stageReady; + for (uint64_t stage = 0; stage + 1 < queue.latency; ++stage) { + std::string ready = newValue(); + body << " " << ready << " = pyc.wire : i1\n"; + stageReady.push_back(std::move(ready)); + } + stageReady.push_back(readyWires[queue.name]); + std::string currentValid = producerValid; + std::string currentData = producerData; + std::string firstReady; + for (uint64_t stage = 0; stage < queue.latency; ++stage) { + std::string inReady = newValue(); + std::string outValid = newValue(); + std::string outData = newValue(); + const uint64_t depth = stage == 0 ? queue.depth : 1; + body << " " << inReady << ", " << outValid << ", " << outData + << " = pyc.fifo %clk, %rst, " << currentValid << ", " << currentData + << ", " << stageReady[stage] << " {depth = " << depth + << "} : " << *dataType << "\n"; + if (stage == 0) + firstReady = inReady; + else + body << " pyc.assign " << stageReady[stage - 1] << ", " << inReady + << " : i1\n"; + currentValid = std::move(outValid); + currentData = std::move(outData); + } + inputReady[queue.name] = std::move(firstReady); + outputValid[queue.name] = std::move(currentValid); + outputData[queue.name] = std::move(currentData); + } + for (const MemoryInstancePlan &instance : plan.memoryInstances) { + std::vector endpoints; + for (const QueueBlockPlan &block : plan.blocks) + if (block.kind == "memory_request" && + block.memoryInstance == instance.name) + endpoints.push_back(&block); + llvm::sort(endpoints, + [](const QueueBlockPlan *left, const QueueBlockPlan *right) { + return left->endpointOrdinal < right->endpointOrdinal; + }); + if (endpoints.empty()) + return pycError("memory instance has no endpoints"); + const QueuePlan *firstInput = + findQueue(plan, endpoints.front()->inputs.front()); + if (!firstInput) + return pycError("memory endpoint input Queue is missing"); + auto requestType = pycType(plan, firstInput->payloadType); + auto dataType = pycType(plan, instance.dataType); + auto dataWidth = typeWidth(plan, instance.dataType); + if (!requestType) + return requestType.takeError(); + if (!dataType) + return dataType.takeError(); + if (!dataWidth) + return dataWidth.takeError(); + + std::vector endpointValids; + std::vector endpointData; + std::vector addresses; + std::vector addressWidths; + std::vector writes; + std::vector writeData; + unsigned addressWidth = 1; + for (const QueueBlockPlan *endpoint : endpoints) { + auto valid = outputValid.find(endpoint->inputs.front()); + auto data = outputData.find(endpoint->inputs.front()); + const QueuePlan *input = findQueue(plan, endpoint->inputs.front()); + if (valid == outputValid.end() || data == outputData.end() || !input) + return pycError("memory inputs are not available after Queue lowering"); + endpointValids.push_back(valid->getValue()); + endpointData.push_back(data->getValue()); + for (size_t policy = 0; policy < 3; ++policy) { + auto value = + emitTransform(plan, *endpoint, {data->getValue()}, + {input->payloadType}, policy, nextValue, body); + if (!value) + return value.takeError(); + auto yielded = yieldedType(*endpoint, endpoint->yields[policy], + input->payloadType); + if (!yielded) + return yielded.takeError(); + auto width = typeWidth(plan, *yielded); + if (!width) + return width.takeError(); + if (policy == 0) { + addresses.push_back(std::move(*value)); + addressWidths.push_back(*width); + addressWidth = std::max(addressWidth, *width); + } else if (policy == 1) { + auto type = pycType(plan, *yielded); + if (!type) + return type.takeError(); + if (*type != "i1") + return pycError("memory write policy must lower to i1"); + writes.push_back(std::move(*value)); + } else { + if (*width != *dataWidth) + return pycError( + "memory data policy must match instance data width"); + writeData.push_back(std::move(*value)); + } + } + } + const std::string addressType = "i" + std::to_string(addressWidth); + for (size_t index = 0; index < addresses.size(); ++index) { + if (addressWidths[index] == addressWidth) + continue; + std::string zero = emitConstant( + 0, "i" + std::to_string(addressWidth - addressWidths[index])); + std::string extended = newValue(); + body << " " << extended << " = pyc.concat(" << zero << ", " + << addresses[index] << ") : (i" + << addressWidth - addressWidths[index] << ", i" + << addressWidths[index] << ") -> " << addressType << "\n"; + addresses[index] = std::move(extended); + } + + std::string zeroI1 = emitConstant(0, "i1"); + std::string oneI1 = emitConstant(1, "i1"); + std::string busyNext = newValue(); + std::string busyEnable = newValue(); + body << " " << busyNext << " = pyc.wire : i1\n"; + body << " " << busyEnable << " = pyc.wire : i1\n"; + std::string busy = newValue(); + body << " " << busy << " = pyc.reg %clk, %rst, " << busyEnable << ", " + << busyNext << ", " << zeroI1 << " : i1\n"; + std::string idle = emitNot(busy); + std::vector grants; + std::string noHigher = oneI1; + for (size_t index = 0; index < endpoints.size(); ++index) { + std::string ready = emitBinary("and", idle, noHigher, "i1"); + body << " pyc.assign " << readyWires[endpoints[index]->inputs.front()] + << ", " << ready << " : i1\n"; + grants.push_back(emitBinary("and", endpointValids[index], ready, "i1")); + noHigher = + emitBinary("and", noHigher, emitNot(endpointValids[index]), "i1"); + } + std::string issue = reduceBalanced("or", grants, "i1"); + auto selectedAddress = + selectBalanced(grants, addresses, addressType).second; + auto selectedWrite = selectBalanced(grants, writes, "i1").second; + auto selectedWriteData = + selectBalanced(grants, writeData, *dataType).second; + auto selectedRequest = + selectBalanced(grants, endpointData, *requestType).second; + const bool fullAddressRange = + addressWidth < 64 && instance.entries == (uint64_t{1} << addressWidth); + if (!fullAddressRange) { + std::string addressLimit = emitConstant(instance.entries, addressType); + std::string addressSafe = + emitBinary("ult", selectedAddress, addressLimit, addressType); + std::string accessSafe = + emitBinary("or", emitNot(issue), addressSafe, "i1"); + body << " pyc.assert " << accessSafe + << " {msg = \"memory_address_out_of_range\"}\n"; + } + + unsigned ownerWidth = 1; + while ((uint64_t{1} << ownerWidth) < endpoints.size()) + ++ownerWidth; + std::string ownerType = "i" + std::to_string(ownerWidth); + std::vector ownerConstants; + for (size_t index = 0; index < endpoints.size(); ++index) + ownerConstants.push_back(emitConstant(index, ownerType)); + std::string selectedOwner = + selectBalanced(grants, ownerConstants, ownerType).second; + std::string ownerNext = newValue(); + std::string ownerEnable = newValue(); + body << " " << ownerNext << " = pyc.wire : " << ownerType << "\n"; + body << " " << ownerEnable << " = pyc.wire : i1\n"; + std::string owner = newValue(); + body << " " << owner << " = pyc.reg %clk, %rst, " << ownerEnable << ", " + << ownerNext << ", " << ownerConstants.front() << " : " << ownerType + << "\n"; + body << " pyc.assign " << ownerNext << ", " << selectedOwner << " : " + << ownerType << "\n"; + body << " pyc.assign " << ownerEnable << ", " << issue << " : i1\n"; + + std::string requestNext = newValue(); + std::string requestEnable = newValue(); + body << " " << requestNext << " = pyc.wire : " << *requestType << "\n"; + body << " " << requestEnable << " = pyc.wire : i1\n"; + std::string zeroRequest = emitConstant(0, *requestType); + std::string pendingRequest = newValue(); + body << " " << pendingRequest << " = pyc.reg %clk, %rst, " + << requestEnable << ", " << requestNext << ", " << zeroRequest << " : " + << *requestType << "\n"; + body << " pyc.assign " << requestNext << ", " << selectedRequest << " : " + << *requestType << "\n"; + body << " pyc.assign " << requestEnable << ", " << issue << " : i1\n"; + + std::string writeValid = emitBinary("and", issue, selectedWrite, "i1"); + const unsigned strobeWidth = (*dataWidth + 7) / 8; + const uint64_t strobeValue = (uint64_t{1} << strobeWidth) - 1; + std::string strobeType = "i" + std::to_string(strobeWidth); + std::string strobe = emitConstant(strobeValue, strobeType); + std::string readData = newValue(); + body << " " << readData << " = pyc.sync_mem %clk, %rst, " << issue + << ", " << selectedAddress << ", " << writeValid << ", " + << selectedAddress << ", " << selectedWriteData << ", " << strobe + << " {depth = " << instance.entries << ", name = \"" << instance.name + << "\"} : " << addressType << ", " << *dataType << ", " << strobeType + << "\n"; + + std::string responseMature = busy; + if (instance.latency > 1) { + const unsigned latencyWidth = std::max( + 1u, static_cast(std::bit_width(instance.latency - 1))); + const std::string latencyType = "i" + std::to_string(latencyWidth); + std::string zeroLatency = emitConstant(0, latencyType); + std::string oneLatency = emitConstant(1, latencyType); + std::string initialLatency = + emitConstant(instance.latency - 1, latencyType); + std::string latencyNext = newValue(); + std::string latencyEnable = newValue(); + body << " " << latencyNext << " = pyc.wire : " << latencyType << "\n"; + body << " " << latencyEnable << " = pyc.wire : i1\n"; + std::string remainingLatency = newValue(); + body << " " << remainingLatency << " = pyc.reg %clk, %rst, " + << latencyEnable << ", " << latencyNext << ", " << zeroLatency + << " : " << latencyType << "\n"; + std::string latencyIsZero = + emitBinary("eq", remainingLatency, zeroLatency, latencyType); + std::string latencyActive = + emitBinary("and", busy, emitNot(latencyIsZero), "i1"); + std::string decremented = + emitBinary("sub", remainingLatency, oneLatency, latencyType); + std::string nextLatency = + emitMux(issue, initialLatency, decremented, latencyType); + std::string updateLatency = emitBinary("or", issue, latencyActive, "i1"); + body << " pyc.assign " << latencyNext << ", " << nextLatency << " : " + << latencyType << "\n"; + body << " pyc.assign " << latencyEnable << ", " << updateLatency + << " : i1\n"; + responseMature = emitBinary("and", busy, latencyIsZero, "i1"); + } + + std::vector accepted; + for (size_t index = 0; index < endpoints.size(); ++index) { + std::string selected = + emitBinary("eq", owner, ownerConstants[index], ownerType); + std::string responseValid = + emitBinary("and", responseMature, selected, "i1"); + auto response = emitFieldReplace(pendingRequest, firstInput->payloadType, + endpoints[index]->resultField, readData); + if (!response) + return response.takeError(); + body << " pyc.assign " + << memoryResponseValid[endpoints[index]->outputs.front()] << ", " + << responseValid << " : i1\n"; + body << " pyc.assign " + << memoryResponseData[endpoints[index]->outputs.front()] << ", " + << *response << " : " << *requestType << "\n"; + accepted.push_back( + emitBinary("and", responseValid, + inputReady[endpoints[index]->outputs.front()], "i1")); + } + std::string responseAccepted = reduceBalanced("or", accepted, "i1"); + std::string retained = + emitBinary("and", busy, emitNot(responseAccepted), "i1"); + std::string nextBusy = emitBinary("or", retained, issue, "i1"); + std::string updateBusy = emitBinary("or", responseAccepted, issue, "i1"); + body << " pyc.assign " << busyNext << ", " << nextBusy << " : i1\n"; + body << " pyc.assign " << busyEnable << ", " << updateBusy << " : i1\n"; + } + for (const QueueBlockPlan &block : plan.blocks) { + if (block.kind == "transform") { + if (block.inputs.size() == 1 && block.outputs.size() == 1) { + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << inputReady[block.outputs.front()] << " : i1\n"; + } else { + std::string allReady = inputReady[block.outputs.front()]; + for (size_t index = 1; index < block.outputs.size(); ++index) + allReady = emitBinary("and", allReady, + inputReady[block.outputs[index]], "i1"); + std::string allValid = outputValid[block.inputs.front()]; + for (size_t index = 1; index < block.inputs.size(); ++index) + allValid = emitBinary("and", allValid, + outputValid[block.inputs[index]], "i1"); + for (auto [index, input] : llvm::enumerate(block.inputs)) { + std::string inputCanFire = allReady; + for (auto [otherIndex, other] : llvm::enumerate(block.inputs)) { + if (otherIndex == index) + continue; + inputCanFire = + emitBinary("and", inputCanFire, outputValid[other], "i1"); + } + body << " pyc.assign " << readyWires[input] << ", " << inputCanFire + << " : i1\n"; + } + for (auto [index, output] : llvm::enumerate(block.outputs)) { + auto validWire = atomicTransformValid.find(output); + if (validWire == atomicTransformValid.end()) + return pycError("atomic transform valid wire is missing"); + std::string outputCanFire = allValid; + for (auto [otherIndex, other] : llvm::enumerate(block.outputs)) { + if (otherIndex == index) + continue; + outputCanFire = + emitBinary("and", outputCanFire, inputReady[other], "i1"); + } + body << " pyc.assign " << validWire->getValue() << ", " + << outputCanFire << " : i1\n"; + } + } + } else if (block.kind == "barrier") { + std::string allReady = inputReady[block.outputs.front()]; + for (size_t index = 1; index < block.outputs.size(); ++index) + allReady = + emitBinary("and", allReady, inputReady[block.outputs[index]], "i1"); + std::string allValid = outputValid[block.inputs.front()]; + for (size_t index = 1; index < block.inputs.size(); ++index) + allValid = + emitBinary("and", allValid, outputValid[block.inputs[index]], "i1"); + for (auto [index, input] : llvm::enumerate(block.inputs)) { + std::string inputCanFire = allReady; + for (auto [otherIndex, other] : llvm::enumerate(block.inputs)) { + if (otherIndex == index) + continue; + inputCanFire = + emitBinary("and", inputCanFire, outputValid[other], "i1"); + } + body << " pyc.assign " << readyWires[input] << ", " << inputCanFire + << " : i1\n"; + } + for (auto [index, output] : llvm::enumerate(block.outputs)) { + auto validWire = atomicTransformValid.find(output); + if (validWire == atomicTransformValid.end()) + return pycError("barrier valid wire is missing"); + std::string outputCanFire = allValid; + for (auto [otherIndex, other] : llvm::enumerate(block.outputs)) { + if (otherIndex == index) + continue; + outputCanFire = + emitBinary("and", outputCanFire, inputReady[other], "i1"); + } + body << " pyc.assign " << validWire->getValue() << ", " + << outputCanFire << " : i1\n"; + } + } else if (block.kind == "broadcast") { + std::string allReady = inputReady[block.outputs.front()]; + for (size_t index = 1; index < block.outputs.size(); ++index) + allReady = + emitBinary("and", allReady, inputReady[block.outputs[index]], "i1"); + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << allReady << " : i1\n"; + } else if (block.kind == "fork") { + auto state = forkStates.find(block.name); + auto inputValidValue = outputValid.find(block.inputs.front()); + if (state == forkStates.end() || inputValidValue == outputValid.end()) + return pycError("fork state or input valid is missing"); + std::vector deliveredNow; + for (auto [index, output] : llvm::enumerate(block.outputs)) { + std::string accepted = + emitBinary("and", forkOfferValid[output], inputReady[output], "i1"); + deliveredNow.push_back(emitBinary( + "or", state->getValue().delivered[index], accepted, "i1")); + } + std::string deliveredAll = deliveredNow.front(); + for (size_t index = 1; index < deliveredNow.size(); ++index) + deliveredAll = + emitBinary("and", deliveredAll, deliveredNow[index], "i1"); + std::string complete = + emitBinary("and", inputValidValue->getValue(), deliveredAll, "i1"); + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << complete << " : i1\n"; + std::string zero = emitConstant(0, "i1"); + for (size_t index = 0; index < block.outputs.size(); ++index) { + std::string next = emitMux(complete, zero, deliveredNow[index], "i1"); + body << " pyc.assign " << state->getValue().nextWires[index] << ", " + << next << " : i1\n"; + body << " pyc.assign " << state->getValue().enableWires[index] + << ", " << inputValidValue->getValue() << " : i1\n"; + } + } else if (block.kind == "route") { + std::string selectedReady = emitConstant(0, "i1"); + for (size_t index = block.outputs.size(); index-- > 0;) { + auto condition = routeCondition.find(block.outputs[index]); + if (condition == routeCondition.end()) + return pycError("route output condition is missing"); + selectedReady = + emitMux(condition->getValue(), inputReady[block.outputs[index]], + selectedReady, "i1"); + } + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << selectedReady << " : i1\n"; + } else if (block.kind == "select") { + auto state = selectStates.find(block.name); + if (state == selectStates.end()) + return pycError("select state is missing"); + const std::string &outputReady = inputReady[block.outputs.front()]; + std::string controlReady = + emitBinary("and", outputReady, state->getValue().selectedValid, "i1"); + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << controlReady << " : i1\n"; + body << " pyc.assert " << state->getValue().selectorSafe + << " {msg = \"select_selector_out_of_range\"}\n"; + for (size_t index = 1; index < block.inputs.size(); ++index) { + std::string selected = + emitBinary("and", state->getValue().controlValid, + state->getValue().conditions[index - 1], "i1"); + std::string ready = emitBinary("and", outputReady, selected, "i1"); + body << " pyc.assign " << readyWires[block.inputs[index]] << ", " + << ready << " : i1\n"; + } + } else if (block.kind == "merge") { + auto grants = mergeGrants.find(block.name); + if (grants == mergeGrants.end() || + grants->getValue().size() != block.inputs.size()) + return pycError("merge grant plan is missing"); + const std::string &ready = inputReady[block.outputs.front()]; + for (auto [index, input] : llvm::enumerate(block.inputs)) { + std::string accepted = + emitBinary("and", ready, grants->getValue()[index], "i1"); + body << " pyc.assign " << readyWires[input] << ", " << accepted + << " : i1\n"; + } + auto state = mergeStates.find(block.name); + if (state != mergeStates.end()) { + std::string accepted = + emitBinary("and", ready, state->getValue().valid, "i1"); + body << " pyc.assign " << state->getValue().enableWire << ", " + << accepted << " : i1\n"; + std::string next = state->getValue().cursor; + for (size_t index = block.inputs.size(); index-- > 0;) { + std::string nextValue = emitConstant( + (index + 1) % block.inputs.size(), state->getValue().type); + next = emitMux(grants->getValue()[index], nextValue, next, + state->getValue().type); + } + body << " pyc.assign " << state->getValue().nextWire << ", " << next + << " : " << state->getValue().type << "\n"; + } + } else if (block.kind == "credit") { + auto state = creditStates.find(block.name); + auto inputValidValue = outputValid.find(block.inputs.front()); + auto inputDataValue = outputData.find(block.inputs.front()); + if (state == creditStates.end() || inputValidValue == outputValid.end() || + inputDataValue == outputData.end()) + return pycError("credit state or input is missing"); + const std::string &outputReady = inputReady[block.outputs.front()]; + std::string retire = + emitBinary("and", state->getValue().outputDone, outputReady, "i1"); + std::string inputReadyValue = + emitBinary("and", state->getValue().anyFree, + state->getValue().safeAdmission, "i1"); + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << inputReadyValue << " : i1\n"; + body << " pyc.assert " << state->getValue().safeAdmission + << " {msg = \"credit_nonpositive_cost\"}\n"; + std::string admit = + emitBinary("and", inputValidValue->getValue(), inputReadyValue, "i1"); + std::string zeroValid = emitConstant(0, "i1"); + std::string oneValid = emitConstant(1, "i1"); + std::string oneCost = emitConstant(1, state->getValue().costType); + std::vector freeGrants; + std::vector doneGrants; + for (size_t index = 0; index < state->getValue().slots.size(); ++index) { + std::string indexValue = + emitConstant(index, state->getValue().freeIndexType); + freeGrants.push_back(emitBinary("eq", state->getValue().freeIndex, + indexValue, + state->getValue().freeIndexType)); + doneGrants.push_back(emitBinary("eq", state->getValue().doneIndex, + indexValue, + state->getValue().freeIndexType)); + } + for (auto [index, slot] : llvm::enumerate(state->getValue().slots)) { + std::string admitSlot = + emitBinary("and", admit, freeGrants[index], "i1"); + std::string retireSlot = + emitBinary("and", retire, doneGrants[index], "i1"); + std::string active = + emitBinary("and", slot.valid, emitNot(slot.done), "i1"); + std::string decremented = emitBinary("sub", slot.remaining, oneCost, + state->getValue().costType); + std::string nextRemaining = emitMux(active, decremented, slot.remaining, + state->getValue().costType); + nextRemaining = emitMux(admitSlot, state->getValue().inputCost, + nextRemaining, state->getValue().costType); + std::string nextValid = + emitMux(retireSlot, zeroValid, slot.valid, "i1"); + nextValid = emitMux(admitSlot, oneValid, nextValid, "i1"); + std::string nextData = emitMux(admitSlot, inputDataValue->getValue(), + slot.data, state->getValue().dataType); + std::string nextState = newValue(); + body << " " << nextState << " = pyc.concat(" << nextValid << ", " + << nextRemaining << ", " << nextData << ") : (i1, " + << state->getValue().costType << ", " << state->getValue().dataType + << ") -> " << state->getValue().slotType << "\n"; + std::string changed = + reduceBalanced("or", {admitSlot, retireSlot, active}, "i1"); + body << " pyc.assign " << slot.next << ", " << nextState << " : " + << state->getValue().slotType << "\n"; + body << " pyc.assign " << slot.enable << ", " << changed + << " : i1\n"; + } + } else if (block.kind == "memory_request") { + // Shared memory-instance arbitration and endpoint ready/response demux + // are emitted once above, after every Queue signal is available. + } else if (block.kind == "dependency") { + auto state = dependencyStates.find(block.name); + auto inputValidValue = outputValid.find(block.inputs.front()); + auto inputDataValue = outputData.find(block.inputs.front()); + if (state == dependencyStates.end() || + inputValidValue == outputValid.end() || + inputDataValue == outputData.end()) + return pycError("dependency state or input is missing"); + const std::string &outputReady = inputReady[block.outputs.front()]; + std::string retire = + emitBinary("and", state->getValue().outputDone, outputReady, "i1"); + std::string inputReadyValue = + emitBinary("and", state->getValue().anyFree, + state->getValue().safeAdmission, "i1"); + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << inputReadyValue << " : i1\n"; + std::string admit = + emitBinary("and", inputValidValue->getValue(), inputReadyValue, "i1"); + std::string zeroValid = emitConstant(0, "i1"); + std::string oneValid = emitConstant(1, "i1"); + std::string waitingPhase = emitConstant(0, "i2"); + std::string executingPhase = emitConstant(1, "i2"); + std::string donePhase = emitConstant(2, "i2"); + std::string zeroCost = emitConstant(0, state->getValue().costType); + std::string oneCost = emitConstant(1, state->getValue().costType); + std::string noDependency = emitConstant(state->getValue().noDependency, + state->getValue().keyType); + + std::vector freeGrants; + std::vector doneGrants; + std::vector indexValues; + freeGrants.reserve(state->getValue().slots.size()); + doneGrants.reserve(state->getValue().slots.size()); + indexValues.reserve(state->getValue().slots.size()); + for (size_t index = 0; index < state->getValue().slots.size(); ++index) { + std::string indexValue = + emitConstant(index, state->getValue().freeIndexType); + indexValues.push_back(indexValue); + freeGrants.push_back(emitBinary("eq", state->getValue().freeIndex, + indexValue, + state->getValue().freeIndexType)); + doneGrants.push_back(emitBinary("eq", state->getValue().doneIndex, + indexValue, + state->getValue().freeIndexType)); + } + + std::vector waitingValues; + std::vector executingValues; + std::vector completeValues; + std::vector decrementValues; + std::vector decrementedValues; + std::vector dependencyReadyValues; + for (const DependencySlotState &slot : state->getValue().slots) { + std::string isWaiting = + emitBinary("eq", slot.phase, waitingPhase, "i2"); + waitingValues.push_back(emitBinary("and", slot.valid, isWaiting, "i1")); + std::string isExecuting = + emitBinary("eq", slot.phase, executingPhase, "i2"); + isExecuting = emitBinary("and", slot.valid, isExecuting, "i1"); + executingValues.push_back(isExecuting); + std::string sentinel = emitBinary("eq", slot.predecessor, noDependency, + state->getValue().keyType); + std::vector predecessorMatches; + predecessorMatches.reserve(state->getValue().slots.size()); + for (const DependencySlotState &candidate : state->getValue().slots) { + std::string same = emitBinary("eq", candidate.key, slot.predecessor, + state->getValue().keyType); + predecessorMatches.push_back( + emitBinary("and", candidate.done, same, "i1")); + } + std::string predecessorDone = + reduceBalanced("or", predecessorMatches, "i1"); + dependencyReadyValues.push_back( + emitBinary("or", sentinel, predecessorDone, "i1")); + std::string atOne = emitBinary("eq", slot.remaining, oneCost, + state->getValue().costType); + completeValues.push_back(emitBinary("and", isExecuting, atOne, "i1")); + std::string notAtOne = emitNot(atOne); + decrementValues.push_back( + emitBinary("and", isExecuting, notAtOne, "i1")); + decrementedValues.push_back(emitBinary("sub", slot.remaining, oneCost, + state->getValue().costType)); + } + + std::string noIssue = emitConstant(0, "i1"); + std::vector issueValues(state->getValue().slots.size(), + noIssue); + for (uint64_t resource = 0; resource < state->getValue().resources; + ++resource) { + std::string resourceValue = + emitConstant(resource, state->getValue().resourceType); + std::vector candidates; + std::vector blockers; + candidates.reserve(state->getValue().slots.size()); + blockers.reserve(state->getValue().slots.size()); + for (auto [index, slot] : llvm::enumerate(state->getValue().slots)) { + std::string sameResource = + emitBinary("eq", slot.resource, resourceValue, + state->getValue().resourceType); + std::string ready = emitBinary("and", waitingValues[index], + dependencyReadyValues[index], "i1"); + candidates.push_back(emitBinary("and", ready, sameResource, "i1")); + std::string stillExecuting = + emitBinary("and", executingValues[index], + emitNot(completeValues[index]), "i1"); + blockers.push_back( + emitBinary("and", stillExecuting, sameResource, "i1")); + } + auto selected = selectBalanced(candidates, indexValues, + state->getValue().freeIndexType); + std::string resourceBusy = reduceBalanced("or", blockers, "i1"); + std::string resourceCanIssue = + emitBinary("and", selected.first, emitNot(resourceBusy), "i1"); + for (size_t index = 0; index < state->getValue().slots.size(); + ++index) { + std::string selectedSlot = + emitBinary("eq", selected.second, indexValues[index], + state->getValue().freeIndexType); + std::string issue = + emitBinary("and", resourceCanIssue, selectedSlot, "i1"); + issueValues[index] = + emitBinary("or", issueValues[index], issue, "i1"); + } + } + + for (auto [index, slot] : llvm::enumerate(state->getValue().slots)) { + std::string admitSlot = + emitBinary("and", admit, freeGrants[index], "i1"); + std::string retireSlot = + emitBinary("and", retire, doneGrants[index], "i1"); + const std::string &issue = issueValues[index]; + const std::string &complete = completeValues[index]; + const std::string &decrement = decrementValues[index]; + const std::string &decremented = decrementedValues[index]; + + std::string nextValid = + emitMux(retireSlot, zeroValid, slot.valid, "i1"); + nextValid = emitMux(admitSlot, oneValid, nextValid, "i1"); + std::string nextPhase = emitMux(complete, donePhase, slot.phase, "i2"); + nextPhase = emitMux(issue, executingPhase, nextPhase, "i2"); + nextPhase = emitMux(admitSlot, waitingPhase, nextPhase, "i2"); + std::string nextRemaining = emitMux( + decrement, decremented, slot.remaining, state->getValue().costType); + nextRemaining = emitMux(complete, zeroCost, nextRemaining, + state->getValue().costType); + nextRemaining = emitMux(issue, slot.cost, nextRemaining, + state->getValue().costType); + nextRemaining = emitMux(admitSlot, zeroCost, nextRemaining, + state->getValue().costType); + std::string nextKey = emitMux(admitSlot, state->getValue().inputKey, + slot.key, state->getValue().keyType); + std::string nextPredecessor = + emitMux(admitSlot, state->getValue().inputPredecessor, + slot.predecessor, state->getValue().keyType); + std::string nextResource = + emitMux(admitSlot, state->getValue().inputResource, slot.resource, + state->getValue().resourceType); + std::string nextCost = emitMux(admitSlot, state->getValue().inputCost, + slot.cost, state->getValue().costType); + std::string nextData = emitMux(admitSlot, inputDataValue->getValue(), + slot.data, state->getValue().dataType); + std::string nextState = newValue(); + body << " " << nextState << " = pyc.concat(" << nextValid << ", " + << nextPhase << ", " << nextKey << ", " << nextPredecessor << ", " + << nextResource << ", " << nextRemaining << ", " << nextCost + << ", " << nextData << ") : (i1, i2, " << state->getValue().keyType + << ", " << state->getValue().keyType << ", " + << state->getValue().resourceType << ", " + << state->getValue().costType << ", " << state->getValue().costType + << ", " << state->getValue().dataType << ") -> " + << state->getValue().slotType << "\n"; + std::vector changes = {admitSlot, retireSlot, issue, + complete, decrement}; + std::string changed = reduceBalanced("or", changes, "i1"); + body << " pyc.assign " << slot.next << ", " << nextState << " : " + << state->getValue().slotType << "\n"; + body << " pyc.assign " << slot.enable << ", " << changed + << " : i1\n"; + } + } else if (block.kind == "reorder") { + auto state = reorderStates.find(block.name); + auto inputValidValue = outputValid.find(block.inputs.front()); + auto inputDataValue = outputData.find(block.inputs.front()); + if (state == reorderStates.end() || + inputValidValue == outputValid.end() || + inputDataValue == outputData.end()) + return pycError("reorder state or input is missing"); + const std::string &outputReady = inputReady[block.outputs.front()]; + std::string retire = + emitBinary("and", state->getValue().outputMatch, outputReady, "i1"); + std::string inputReadyValue = + emitBinary("and", state->getValue().anyFree, + state->getValue().safeAdmission, "i1"); + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << inputReadyValue << " : i1\n"; + std::string admit = + emitBinary("and", inputValidValue->getValue(), inputReadyValue, "i1"); + std::string zero = emitConstant(0, "i1"); + std::string one = emitConstant(1, "i1"); + std::string admittedState = newValue(); + body << " " << admittedState << " = pyc.concat(" << one << ", " + << state->getValue().inputKey << ", " << inputDataValue->getValue() + << ") : (i1, " << state->getValue().keyType << ", " + << state->getValue().dataType << ") -> " + << state->getValue().slotType << "\n"; + std::vector grants; + grants.reserve(state->getValue().slots.size()); + for (size_t index = 0; index < state->getValue().slots.size(); ++index) { + std::string indexValue = + emitConstant(index, state->getValue().freeIndexType); + grants.push_back(emitBinary("eq", state->getValue().freeIndex, + indexValue, + state->getValue().freeIndexType)); + } + for (auto [index, slot] : llvm::enumerate(state->getValue().slots)) { + std::string admitSlot = emitBinary("and", admit, grants[index], "i1"); + std::string retireSlot = emitBinary("and", retire, slot.match, "i1"); + std::string changed = emitBinary("or", admitSlot, retireSlot, "i1"); + std::string clearedState = newValue(); + body << " " << clearedState << " = pyc.concat(" << zero << ", " + << slot.key << ", " << slot.data << ") : (i1, " + << state->getValue().keyType << ", " << state->getValue().dataType + << ") -> " << state->getValue().slotType << "\n"; + std::string afterRetire = emitMux(retireSlot, clearedState, slot.state, + state->getValue().slotType); + std::string nextState = emitMux(admitSlot, admittedState, afterRetire, + state->getValue().slotType); + body << " pyc.assign " << slot.next << ", " << nextState << " : " + << state->getValue().slotType << "\n"; + body << " pyc.assign " << slot.enable << ", " << changed + << " : i1\n"; + } + std::string keyOne = emitConstant(1, state->getValue().keyType); + std::string nextExpected = emitBinary("add", state->getValue().expected, + keyOne, state->getValue().keyType); + body << " pyc.assign " << state->getValue().expectedNext << ", " + << nextExpected << " : " << state->getValue().keyType << "\n"; + body << " pyc.assign " << state->getValue().expectedEnable << ", " + << retire << " : i1\n"; + } else if (block.kind == "feedback") { + auto state = feedbackStates.find(block.name); + if (state == feedbackStates.end()) + return pycError("feedback state is missing"); + std::string notInternal = emitNot(state->getValue().valid); + std::string canProceed = + emitMux(state->getValue().condition, state->getValue().underLimit, + inputReady[block.outputs.front()], "i1"); + std::string accepted = + emitBinary("and", state->getValue().selectedValid, canProceed, "i1"); + std::string externalReady = + emitBinary("and", notInternal, canProceed, "i1"); + body << " pyc.assign " << readyWires[block.inputs.front()] << ", " + << externalReady << " : i1\n"; + std::string continueAccepted = + emitBinary("and", accepted, state->getValue().condition, "i1"); + body << " pyc.assign " << state->getValue().validNext << ", " + << state->getValue().condition << " : i1\n"; + body << " pyc.assign " << state->getValue().validEnable << ", " + << accepted << " : i1\n"; + body << " pyc.assign " << state->getValue().dataNext << ", " + << state->getValue().updated << " : " << state->getValue().dataType + << "\n"; + body << " pyc.assign " << state->getValue().dataEnable << ", " + << continueAccepted << " : i1\n"; + std::string one = emitConstant(1, state->getValue().iterationType); + std::string nextIteration = + emitBinary("add", state->getValue().selectedIteration, one, + state->getValue().iterationType); + body << " pyc.assign " << state->getValue().iterationNext << ", " + << nextIteration << " : " << state->getValue().iterationType << "\n"; + body << " pyc.assign " << state->getValue().iterationEnable << ", " + << continueAccepted << " : i1\n"; + std::string atLimit = emitNot(state->getValue().underLimit); + std::string limitCondition = + emitBinary("and", state->getValue().condition, atLimit, "i1"); + std::string limitViolation = emitBinary( + "and", state->getValue().selectedValid, limitCondition, "i1"); + std::string limitOk = emitNot(limitViolation); + body << " pyc.assert " << limitOk + << " {msg = \"feedback_iteration_limit\"}\n"; + } + } + for (auto [index, sink] : llvm::enumerate(sinks)) + body << " pyc.assign " << readyWires[sink->inputs.front()] << ", " + << outputName(index, "ready") << " : i1\n"; + for (const QueueBlockPlan *observation : observations) { + const QueuePlan *queue = findQueue(plan, observation->inputs.front()); + auto type = queue ? pycType(plan, queue->payloadType) + : llvm::Expected( + pycError("observation Queue is missing")); + if (!type) + return type.takeError(); + std::string alias = newValue(); + body << " " << alias << " = pyc.alias " + << outputData[observation->inputs.front()] << " {pyc.name = \"" + << observation->name << "\"} : " << *type << "\n"; + } + + llvm::StringRef top = plan.system; + std::vector arguments = {"%clk: !pyc.clock", "%rst: !pyc.reset"}; + std::vector argumentNames = {"clk", "rst"}; + for (size_t index = 0; index < sources.size(); ++index) { + arguments.push_back(inputName(index, "valid") + ": i1"); + arguments.push_back(inputName(index, "data") + ": " + + inputPortTypes[index]); + argumentNames.push_back(inputName(index, "valid").substr(1)); + argumentNames.push_back(inputName(index, "data").substr(1)); + } + for (size_t index = 0; index < sinks.size(); ++index) { + arguments.push_back(outputName(index, "ready") + ": i1"); + argumentNames.push_back(outputName(index, "ready").substr(1)); + } + std::vector resultTypes; + std::vector resultNames; + std::vector returnValues; + for (auto [index, sink] : llvm::enumerate(sinks)) { + resultTypes.push_back("i1"); + resultTypes.push_back(outputPortTypes[index]); + resultNames.push_back(outputName(index, "valid").substr(1)); + resultNames.push_back(outputName(index, "data").substr(1)); + returnValues.push_back(outputValid[sink->inputs.front()]); + returnValues.push_back(outputData[sink->inputs.front()]); + } + for (auto [index, source] : llvm::enumerate(sources)) { + resultTypes.push_back("i1"); + resultNames.push_back(inputName(index, "ready").substr(1)); + returnValues.push_back(inputReady[source->outputs.front()]); + } + auto writeList = + [](std::ostringstream &stream, const std::vector &values, + llvm::StringRef prefix = {}, llvm::StringRef suffix = {}) { + for (auto [index, value] : llvm::enumerate(values)) { + if (index) + stream << ", "; + stream << prefix.str() << value << suffix.str(); + } + }; + std::ostringstream output; + output << "module attributes {pyc.top = @" << top.str() + << ", pyc.frontend.contract = \"pycircuit\"} {\n func.func @" + << top.str() << '('; + writeList(output, arguments); + output << ") -> ("; + writeList(output, resultTypes); + output << ") attributes {arg_names = ["; + writeList(output, argumentNames, "\"", "\""); + output << "], result_names = ["; + writeList(output, resultNames, "\"", "\""); + output << "], pyc.value_params = [], pyc.value_param_types = [], " + "pyc.kind = \"module\", pyc.inline = \"false\", " + "pyc.params = \"{}\", pyc.base = \"" + << top.str() << "\", pyc.struct.metrics = \"" << kStructMetrics.str() + << "\", pyc.struct.collections = \"[]\"} {\n" + << body.str() << " func.return "; + writeList(output, returnValues); + output << " : "; + writeList(output, resultTypes); + output << "\n }\n}\n"; + return output.str(); +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/CodeGen/Staging.cpp b/components/agentic-circuit/lib/CodeGen/Staging.cpp new file mode 100644 index 00000000..4523ba8c --- /dev/null +++ b/components/agentic-circuit/lib/CodeGen/Staging.cpp @@ -0,0 +1,181 @@ +#include "BuildInternal.h" + +#include "acir/Bindings/Binding.h" + +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include + +namespace acir::codegen { +namespace { + +llvm::Error stageError(const llvm::Twine &message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "ACLOWER-FINGERPRINT: " + message); +} + +llvm::SmallString<256> joined(llvm::StringRef root, llvm::StringRef relative) { + llvm::SmallString<256> result(root); + llvm::sys::path::append(result, relative); + return result; +} + +llvm::Expected fileFingerprint(llvm::StringRef path) { + auto bytes = readFileBytes(path); + if (!bytes) + return bytes.takeError(); + return computeFingerprint(*bytes); +} + +} // namespace + +llvm::Expected normalizeArtifactPath(llvm::StringRef path) { + const llvm::StringRef original = path; + if (path.empty() || llvm::sys::path::is_absolute(path) || + llvm::sys::path::has_root_name(path) || path.ends_with('/') || + path.contains('\\') || path.contains('\0')) + return stageError("artifact path must be normalized and relative"); + llvm::SmallString<256> normalized; + while (!path.empty()) { + auto [component, remainder] = path.split('/'); + if (component.empty() || component == "." || component == "..") + return stageError("artifact path escapes its private stage"); + llvm::sys::path::append(normalized, component); + path = remainder; + } + if (normalized.empty() || normalized != original) + return stageError("artifact path is not canonical"); + return normalized.str().str(); +} + +llvm::Expected readFileBytes(llvm::StringRef path) { + auto buffer = llvm::MemoryBuffer::getFile(path, false, false); + if (!buffer) + return llvm::createStringError(buffer.getError(), "cannot read build file"); + return buffer.get()->getBuffer().str(); +} + +llvm::Error writeFileExclusive(llvm::StringRef stageRoot, + llvm::StringRef relativePath, + llvm::StringRef bytes) { + auto normalized = normalizeArtifactPath(relativePath); + if (!normalized) + return normalized.takeError(); + llvm::SmallString<256> destination = joined(stageRoot, *normalized); + llvm::SmallString<256> parent(destination); + llvm::sys::path::remove_filename(parent); + if (std::error_code error = llvm::sys::fs::create_directories(parent)) + return llvm::createStringError(error, "cannot create staged directory"); + + int descriptor = -1; + if (std::error_code error = llvm::sys::fs::openFileForWrite( + destination, descriptor, llvm::sys::fs::CD_CreateNew, + llvm::sys::fs::OF_None)) + return llvm::createStringError(error, "cannot create staged file"); + { + llvm::raw_fd_ostream output(descriptor, true); + output.write(bytes.data(), bytes.size()); + output.flush(); + if (output.has_error()) + return stageError("staged file write failed"); + } + auto stored = readFileBytes(destination); + if (!stored) + return stored.takeError(); + if (stored->size() != bytes.size() || + computeFingerprint(*stored) != computeFingerprint(bytes)) + return stageError("staged file verification failed"); + return llvm::Error::success(); +} + +llvm::Expected +publishImmutableStage(llvm::StringRef stageRoot, llvm::StringRef outputRoot, + llvm::StringRef buildFingerprint, + llvm::ArrayRef artifacts, + llvm::StringRef manifestBytes) { + if (!isValidFingerprint(buildFingerprint)) + return stageError("immutable build fingerprint is invalid"); + llvm::SmallString<256> builds(outputRoot); + llvm::sys::path::append(builds, "builds"); + if (std::error_code error = llvm::sys::fs::create_directories(builds)) + return llvm::createStringError(error, "cannot create immutable build root"); + llvm::SmallString<256> destination(builds); + llvm::sys::path::append(destination, buildFingerprint); + + if (llvm::sys::fs::exists(destination)) { + auto existingManifest = + readFileBytes(joined(destination, "build-manifest.json")); + if (!existingManifest || *existingManifest != manifestBytes) + return stageError("immutable build manifest differs for one fingerprint"); + for (const Artifact &artifact : artifacts) { + auto normalized = normalizeArtifactPath(artifact.path); + if (!normalized) + return normalized.takeError(); + auto existing = fileFingerprint(joined(destination, *normalized)); + if (!existing || *existing != artifact.sha256) + return stageError( + "immutable build artifact differs for one fingerprint"); + } + if (std::error_code error = llvm::sys::fs::remove_directories(stageRoot)) + return llvm::createStringError(error, + "cannot clean exact cache-hit stage"); + return PublishedStage{destination.str().str(), true}; + } + + std::error_code error; + std::filesystem::rename(std::filesystem::path(stageRoot.str()), + std::filesystem::path(destination.str().str()), + error); + if (error) + return llvm::createStringError(error, "cannot publish immutable build"); + return PublishedStage{destination.str().str(), false}; +} + +llvm::Error writeCurrentPointer(llvm::StringRef outputRoot, + llvm::StringRef buildFingerprint) { + llvm::json::Object pointer{ + {"build_fingerprint", buildFingerprint}, + {"path", (llvm::Twine("builds/") + buildFingerprint).str()}}; + auto bytes = + bindings::canonicalizeJson(llvm::json::Value(std::move(pointer))); + if (!bytes) + return bytes.takeError(); + + llvm::SmallString<256> temporary; + llvm::SmallString<256> prefix(outputRoot); + llvm::sys::path::append(prefix, ".current"); + int descriptor = -1; + if (std::error_code error = llvm::sys::fs::createUniqueFile( + llvm::Twine(prefix) + "-%%%%%%.json", descriptor, temporary)) + return llvm::createStringError(error, + "cannot create current pointer stage"); + { + llvm::raw_fd_ostream output(descriptor, true); + output << *bytes; + output.flush(); + if (output.has_error()) { + llvm::sys::fs::remove(temporary); + return stageError("cannot flush current pointer"); + } + } + llvm::SmallString<256> destination(outputRoot); + llvm::sys::path::append(destination, "current.json"); + std::error_code error; + std::filesystem::rename(std::filesystem::path(temporary.str().str()), + std::filesystem::path(destination.str().str()), + error); + if (error) { + llvm::sys::fs::remove(temporary); + return llvm::createStringError(error, "cannot atomically select build"); + } + return llvm::Error::success(); +} + +} // namespace acir::codegen diff --git a/components/agentic-circuit/lib/Compiler/CMakeLists.txt b/components/agentic-circuit/lib/Compiler/CMakeLists.txt new file mode 100644 index 00000000..42701d56 --- /dev/null +++ b/components/agentic-circuit/lib/Compiler/CMakeLists.txt @@ -0,0 +1,24 @@ +add_acir_library(ACIRCompiler SOURCES + Driver.cpp + Pipeline.cpp +) +target_link_libraries(ACIRCompiler PUBLIC + ACIRCodeGen + ACIRToACSim + ACIRTransforms + ACIRDialect + ACSimDialect + MLIRParser + MLIRPass + MLIRTransforms + LLVMSupport +) +target_include_directories(ACIRCompiler PUBLIC + "$" + "$" + "$" +) +target_include_directories(ACIRCompiler PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +add_library(AgenticCircuit::ACIRCompiler ALIAS ACIRCompiler) diff --git a/components/agentic-circuit/lib/Compiler/CompilerInternal.h b/components/agentic-circuit/lib/Compiler/CompilerInternal.h new file mode 100644 index 00000000..1df372ca --- /dev/null +++ b/components/agentic-circuit/lib/Compiler/CompilerInternal.h @@ -0,0 +1,17 @@ +#ifndef ACIR_COMPILER_INTERNAL_H +#define ACIR_COMPILER_INTERNAL_H + +#include "acir/Compiler/Driver.h" + +#include "llvm/Support/Error.h" + +#include + +namespace acir::compiler::detail { + +llvm::Expected> +selectPipeline(const CompilerRequest &request); + +} // namespace acir::compiler::detail + +#endif // ACIR_COMPILER_INTERNAL_H diff --git a/components/agentic-circuit/lib/Compiler/Driver.cpp b/components/agentic-circuit/lib/Compiler/Driver.cpp new file mode 100644 index 00000000..a79f6aaa --- /dev/null +++ b/components/agentic-circuit/lib/Compiler/Driver.cpp @@ -0,0 +1,531 @@ +#include "CompilerInternal.h" + +#include "acir/Bindings/Registry.h" +#include "acir/Dialect/ACIR/GraphRegion.h" +#include "acir/Transforms/ResolveBindings.h" + +#include "acir/CodeGen/Generator.h" +#include "acir/CodeGen/ModelPlan.h" +#include "acir/Conversion/ACIRToACSim/ACIRToACSim.h" +#include "acir/Dialect/ACSim/ACSimOps.h" +#include "acir/InitAllDialects.h" +#include "acir/InitAllPasses.h" +#include "acir/Transforms/Passes.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/Location.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Pass/PassRegistry.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include + +namespace acir::compiler { +namespace { + +struct DriverState { + mlir::MLIRContext context{mlir::MLIRContext::Threading::DISABLED}; + mlir::OwningOpRef module; + std::optional sources; + std::optional build; + std::string frozenAcir; + std::string canonicalAcsim; + std::string bindingLock = "[]"; + std::vector providerInputs; +}; + +std::string severityName(mlir::DiagnosticSeverity severity) { + switch (severity) { + case mlir::DiagnosticSeverity::Error: + return "error"; + case mlir::DiagnosticSeverity::Warning: + return "warning"; + case mlir::DiagnosticSeverity::Remark: + return "remark"; + case mlir::DiagnosticSeverity::Note: + return "note"; + } + return "error"; +} + +std::optional sourceLocation(mlir::Location location) { + if (auto file = mlir::dyn_cast(location)) + return SourceLocation{file.getFilename().str(), file.getLine(), + file.getColumn()}; + return std::nullopt; +} + +std::string diagnosticCode(llvm::StringRef message, llvm::StringRef fallback) { + for (size_t start = message.find("AC"); start != llvm::StringRef::npos; + start = message.find("AC", start + 2)) { + size_t end = start; + while (end < message.size() && + (std::isupper(static_cast(message[end])) || + std::isdigit(static_cast(message[end])) || + message[end] == '-')) + ++end; + llvm::StringRef candidate = message.slice(start, end); + if (candidate.contains('-') && candidate.size() >= 6) + return candidate.str(); + } + return fallback.str(); +} + +std::string defaultDiagnosticCode(CompilerStage stage) { + switch (stage) { + case CompilerStage::AcirParse: + return "ACIR-PARSE-001"; + case CompilerStage::AcirVerify: + return "ACIR-VERIFY-001"; + case CompilerStage::AcirNormalize: + return "ACIR-NORMALIZE-001"; + case CompilerStage::AcirFreeze: + return "ACIR-FREEZE-001"; + case CompilerStage::AcsimLower: + return "ACLOWER-001"; + case CompilerStage::AcsimVerify: + return "ACSIM-VERIFY-001"; + case CompilerStage::CxxEmit: + return "ACLOWER-CXX-001"; + case CompilerStage::CxxContract: + return "ACLOWER-CXX-CONTRACT-001"; + case CompilerStage::Compile: + return "ACLOWER-COMPILE-001"; + case CompilerStage::Link: + return "ACLOWER-LINK-001"; + case CompilerStage::Publish: + return "ACLOWER-PUBLISH-001"; + } + return "ACIR-COMPILER-001"; +} + +CompilerDiagnostic makeDiagnostic(CompilerStage stage, llvm::StringRef code, + llvm::StringRef message) { + return CompilerDiagnostic{.stage = compilerStageName(stage).str(), + .code = code.str(), + .severity = "error", + .message = message.str()}; +} + +llvm::Error compilerFailure(CompilerStage stage, llvm::StringRef code, + llvm::StringRef message) { + std::vector diagnostics; + diagnostics.push_back(makeDiagnostic(stage, code, message)); + return llvm::make_error(std::move(diagnostics)); +} + +llvm::Error compilerFailure(CompilerStage stage, llvm::Error error) { + return compilerFailure(stage, defaultDiagnosticCode(stage), + llvm::toString(std::move(error))); +} + +class DiagnosticCapture { +public: + explicit DiagnosticCapture(mlir::MLIRContext &context) + : handler_(&context, [&](mlir::Diagnostic &diagnostic) { + std::string message = diagnostic.str(); + diagnostics_.push_back(CompilerDiagnostic{ + .stage = compilerStageName(stage_).str(), + .code = diagnosticCode(message, defaultDiagnosticCode(stage_)), + .severity = severityName(diagnostic.getSeverity()), + .message = std::move(message), + .source = sourceLocation(diagnostic.getLocation())}); + return mlir::success(); + }) {} + + void setStage(CompilerStage stage) { stage_ = stage; } + + llvm::Error takeFailure(CompilerStage stage) { + if (diagnostics_.empty()) + diagnostics_.push_back(makeDiagnostic(stage, defaultDiagnosticCode(stage), + "compiler stage failed")); + if (stage == CompilerStage::AcirParse) + for (CompilerDiagnostic &diagnostic : diagnostics_) + diagnostic.code = "ACIR-PARSE-001"; + return llvm::make_error(std::move(diagnostics_)); + } + + std::vector takeDiagnostics() { + return std::move(diagnostics_); + } + +private: + CompilerStage stage_ = CompilerStage::AcirParse; + std::vector diagnostics_; + mlir::ScopedDiagnosticHandler handler_; +}; + +std::string printModule(mlir::ModuleOp module) { + std::string bytes; + llvm::raw_string_ostream output(bytes); + module.print(output); + output.flush(); + return bytes; +} + +bool requested(const CompilerRequest &request, codegen::ArtifactKind kind) { + return llvm::is_contained(request.emits, kind); +} + +bool namesStage(llvm::ArrayRef names, CompilerStage stage) { + return llvm::is_contained(names, compilerStageName(stage)); +} + +void addArtifact(CompilerResult &result, std::string path, + codegen::ArtifactKind kind, std::string bytes) { + result.artifacts.push_back({std::move(path), kind, std::move(bytes), {}}); + CompilerArtifact &artifact = result.artifacts.back(); + artifact.sha256 = codegen::computeFingerprint(artifact.bytes); +} + +unsigned stageOrdinal(CompilerStage stage) { + return static_cast(stage); +} + +llvm::Error validateRequest(const CompilerRequest &request, + llvm::ArrayRef pipeline) { + if (request.acirBytes.empty()) + return compilerFailure(CompilerStage::AcirParse, "ACIR-PARSE-001", + "ACIR input is empty"); + std::set unique; + for (codegen::ArtifactKind kind : request.emits) { + if (!unique.insert(kind).second) + return compilerFailure(CompilerStage::AcirParse, "ACIR-EMIT-001", + "artifact emission request is duplicated"); + CompilerStage required = CompilerStage::Publish; + switch (kind) { + case codegen::ArtifactKind::Acir: + required = CompilerStage::AcirFreeze; + break; + case codegen::ArtifactKind::Acsim: + required = CompilerStage::AcsimVerify; + break; + case codegen::ArtifactKind::CppHeader: + case codegen::ArtifactKind::CppSource: + required = CompilerStage::CxxEmit; + break; + case codegen::ArtifactKind::Executable: + required = CompilerStage::Publish; + break; + case codegen::ArtifactKind::Report: + required = CompilerStage::CxxContract; + break; + case codegen::ArtifactKind::Acpy: + return compilerFailure(CompilerStage::AcirParse, "ACIR-EMIT-001", + "the native compiler does not emit ACPy"); + } + if (pipeline.empty() || + stageOrdinal(required) > stageOrdinal(pipeline.back())) + return compilerFailure(required, "ACIR-EMIT-001", + "artifact requires a later compiler stage"); + } + auto validateDumps = [&](llvm::ArrayRef names) -> llvm::Error { + std::set uniqueNames; + for (const std::string &name : names) { + if (!uniqueNames.insert(name).second) + return compilerFailure(CompilerStage::AcirParse, "ACIR-DUMP-001", + "dump stage is duplicated"); + bool known = llvm::any_of(pipeline, [&](CompilerStage stage) { + return compilerStageName(stage) == name; + }); + if (!known) + return compilerFailure(CompilerStage::AcirParse, "ACIR-DUMP-001", + "dump stage is outside the selected pipeline"); + } + return llvm::Error::success(); + }; + if (auto error = validateDumps(request.dumpBefore)) + return error; + if (auto error = validateDumps(request.dumpAfter)) + return error; + return llvm::Error::success(); +} + +llvm::LogicalResult runPass(DriverState &state, + std::unique_ptr pass) { + mlir::PassManager manager(&state.context); + manager.addPass(std::move(pass)); + return manager.run(state.module.get()); +} + +llvm::LogicalResult runCustomPipeline(DriverState &state, + llvm::StringRef pipeline, + std::string &error) { + mlir::PassManager manager(&state.context); + llvm::raw_string_ostream stream(error); + if (mlir::failed(mlir::parsePassPipeline(pipeline, manager, stream))) + return mlir::failure(); + return manager.run(state.module.get()); +} + +llvm::Error runStage(CompilerStage stage, const CompilerRequest &request, + DriverState &state, CompilerResult &result, + DiagnosticCapture &capture) { + capture.setStage(stage); + switch (stage) { + case CompilerStage::AcirParse: + state.module = mlir::parseSourceString(request.acirBytes, + &state.context); + return state.module ? llvm::Error::success() : capture.takeFailure(stage); + case CompilerStage::AcirVerify: + if (mlir::failed(runPass(state, createVerifyACIRFilePass()))) + return capture.takeFailure(stage); + return llvm::Error::success(); + case CompilerStage::AcirNormalize: + if (request.profile == CompilerProfile::Custom) { + std::string error; + if (mlir::failed( + runCustomPipeline(state, *request.customPipeline, error))) { + if (!error.empty()) + return compilerFailure(stage, "ACIR-PIPELINE-001", error); + return capture.takeFailure(stage); + } + return llvm::Error::success(); + } + if (mlir::failed(runPass(state, createNormalizeACIRFilePass()))) + return capture.takeFailure(stage); + return llvm::Error::success(); + case CompilerStage::AcirFreeze: + if (mlir::failed(runPass(state, createFreezeTopologyPass()))) + return capture.takeFailure(stage); + state.frozenAcir = printModule(*state.module); + if (requested(request, codegen::ArtifactKind::Acir)) + addArtifact(result, "frozen.ac.mlir", codegen::ArtifactKind::Acir, + state.frozenAcir); + return llvm::Error::success(); + case CompilerStage::AcsimLower: { + bindings::BindingRegistryDocument registry; + if (!request.bindingRegistryBytes.empty()) { + auto parsed = + bindings::parseBindingRegistry(request.bindingRegistryBytes); + if (!parsed) + return compilerFailure(stage, parsed.takeError()); + registry = std::move(*parsed); + } + ACIRToACSimPassOptions options; + options.profile = request.profile == CompilerProfile::Validated + ? "validated" + : request.profile == CompilerProfile::Custom ? "custom" + : "fast"; + options.target = request.build.toolchain.targetTriple.empty() + ? "unknown-unknown" + : request.build.toolchain.targetTriple; + ResolveBindingsPassOptions resolutionOptions; + resolutionOptions.candidates = registry.candidates; + resolutionOptions.requests = registry.requests; + resolutionOptions.profile = options.profile; + resolutionOptions.target = options.target; + auto resolution = resolveModuleBindings(*state.module, resolutionOptions); + if (!resolution) + return compilerFailure(stage, resolution.takeError()); + if (!resolution->selections().empty() || request.bindingLockBytes.empty()) + state.bindingLock = resolution->canonicalLock().str(); + std::set providers; + for (const bindings::ResolvedBinding &selection : resolution->selections()) + providers.insert(selection.record().provider().str()); + state.providerInputs.assign(providers.begin(), providers.end()); + options.candidates = std::move(registry.candidates); + options.requests = std::move(registry.requests); + if (mlir::failed(runPass(state, createACIRToACSimPass(std::move(options))))) + return capture.takeFailure(stage); + return llvm::Error::success(); + } + case CompilerStage::AcsimVerify: + if (mlir::failed(mlir::verify(state.module.get())) || + !llvm::hasSingleElement(state.module->getOps())) + return capture.takeFailure(stage); + state.canonicalAcsim = printModule(*state.module); + if (requested(request, codegen::ArtifactKind::Acsim)) + addArtifact(result, "model.acsim.mlir", codegen::ArtifactKind::Acsim, + state.canonicalAcsim); + return llvm::Error::success(); + case CompilerStage::CxxEmit: { + auto plan = codegen::buildModelPlan(*state.module); + if (!plan) + return compilerFailure(stage, plan.takeError()); + auto sources = codegen::generateModelSources(*plan); + if (!sources) + return compilerFailure(stage, sources.takeError()); + state.sources = std::move(*sources); + for (const codegen::GeneratedFile &file : state.sources->files) { + codegen::ArtifactKind kind = + llvm::StringRef(file.relativePath).ends_with(".h") + ? codegen::ArtifactKind::CppHeader + : codegen::ArtifactKind::CppSource; + if (requested(request, kind)) + addArtifact(result, file.relativePath, kind, file.content); + } + return llvm::Error::success(); + } + case CompilerStage::CxxContract: { + auto plan = codegen::buildModelPlan(*state.module); + if (!plan) + return compilerFailure(stage, plan.takeError()); + if (auto error = codegen::validateSourceBundle(*plan, *state.sources)) + return compilerFailure(stage, std::move(error)); + return llvm::Error::success(); + } + case CompilerStage::Compile: { + codegen::BuildRequest build = request.build; + build.canonicalACSim = *state.module; + build.frozenAcirBytes = state.frozenAcir; + build.canonicalACSimBytes = state.canonicalAcsim; + build.bindingLockBytes = state.bindingLock; + build.providerInputs = state.providerInputs; + if (build.profile.empty()) + build.profile = request.profile == CompilerProfile::Validated + ? "validated" + : request.profile == CompilerProfile::Custom ? "custom" + : "fast"; + build.passPipeline = { + "acir-verify", + request.customPipeline.value_or("acir-normalize"), + "acir-freeze", + "acsim-lower", + "acsim-verify", + "acsim-emit-cxx", + "acsim-check-cxx-contract", + "compile", + "link", + }; + auto built = codegen::buildGeneratedModel(build); + if (!built) + return compilerFailure(stage, built.takeError()); + state.build = std::move(*built); + result.build = state.build; + return llvm::Error::success(); + } + case CompilerStage::Link: + return state.build ? llvm::Error::success() + : compilerFailure(stage, "ACLOWER-LINK-001", + "compile stage produced no build"); + case CompilerStage::Publish: + if (!state.build) + return compilerFailure(stage, "ACLOWER-PUBLISH-001", + "link stage produced no build"); + if (requested(request, codegen::ArtifactKind::Executable)) { + auto executable = llvm::MemoryBuffer::getFile(state.build->executable); + if (!executable) + return compilerFailure(stage, "ACLOWER-PUBLISH-001", + "published executable cannot be read"); + addArtifact(result, "bin/model", codegen::ArtifactKind::Executable, + (*executable)->getBuffer().str()); + } + return llvm::Error::success(); + } + llvm_unreachable("closed CompilerStage is exhaustive"); +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks): DriverState owns the + // parsed MLIR module across stage calls and destroys it after the pipeline. + +} // namespace + +char CompilerError::ID = 0; + +CompilerError::CompilerError(std::vector diagnostics) + : diagnostics_(std::move(diagnostics)) {} + +void CompilerError::log(llvm::raw_ostream &output) const { + if (diagnostics_.empty()) { + output << "compiler failed without a diagnostic"; + return; + } + output << diagnostics_.front().code << ": " << diagnostics_.front().message; +} + +std::error_code CompilerError::convertToErrorCode() const { + return llvm::inconvertibleErrorCode(); +} + +llvm::StringRef compilerStageName(CompilerStage stage) { + switch (stage) { + case CompilerStage::AcirParse: + return "acir-parse"; + case CompilerStage::AcirVerify: + return "acir-verify"; + case CompilerStage::AcirNormalize: + return "acir-normalize"; + case CompilerStage::AcirFreeze: + return "acir-freeze"; + case CompilerStage::AcsimLower: + return "acsim-lower"; + case CompilerStage::AcsimVerify: + return "acsim-verify"; + case CompilerStage::CxxEmit: + return "cxx-emit"; + case CompilerStage::CxxContract: + return "cxx-contract"; + case CompilerStage::Compile: + return "compile"; + case CompilerStage::Link: + return "link"; + case CompilerStage::Publish: + return "publish"; + } + llvm_unreachable("closed CompilerStage is exhaustive"); +} + +llvm::Expected runCompiler(const CompilerRequest &request) { + static std::once_flag passesRegistered; + std::call_once(passesRegistered, [] { registerAllPasses(); }); + + auto pipeline = detail::selectPipeline(request); + if (!pipeline) + return compilerFailure(CompilerStage::AcirParse, "ACIR-PIPELINE-001", + llvm::toString(pipeline.takeError())); + if (auto error = validateRequest(request, *pipeline)) + return std::move(error); + + mlir::DialectRegistry registry; + registerAllDialects(registry); + DriverState state; + state.bindingLock = + request.bindingLockBytes.empty() ? "[]" : request.bindingLockBytes; + state.context.appendDialectRegistry(registry); + state.context.loadAllAvailableDialects(); + if (!request.bindingRegistryBytes.empty()) { + auto bindingRegistry = + bindings::parseBindingRegistry(request.bindingRegistryBytes); + if (!bindingRegistry) + return compilerFailure(CompilerStage::AcirParse, + bindingRegistry.takeError()); + auto &providers = ac::getStructuralProviderRegistry(&state.context); + for (const bindings::BindingRequest &bindingRequest : + bindingRegistry->requests) + providers.registerExternal(bindingRequest.binding); + } + DiagnosticCapture capture(state.context); + CompilerResult result; + + for (CompilerStage stage : *pipeline) { + if (state.module && namesStage(request.dumpBefore, stage)) + addArtifact(result, + "dumps/" + compilerStageName(stage).str() + "-before.mlir", + codegen::ArtifactKind::Report, printModule(*state.module)); + if (auto error = runStage(stage, request, state, result, capture)) + return std::move(error); + if (request.verifyAfterEach && state.module && + mlir::failed(mlir::verify(state.module.get()))) + return capture.takeFailure(stage); + if (state.module && + (request.dumpAfterEach || namesStage(request.dumpAfter, stage))) + addArtifact(result, + "dumps/" + compilerStageName(stage).str() + "-after.mlir", + codegen::ArtifactKind::Report, printModule(*state.module)); + } + result.diagnostics = capture.takeDiagnostics(); + return result; +} + +} // namespace acir::compiler diff --git a/components/agentic-circuit/lib/Compiler/Pipeline.cpp b/components/agentic-circuit/lib/Compiler/Pipeline.cpp new file mode 100644 index 00000000..975c3603 --- /dev/null +++ b/components/agentic-circuit/lib/Compiler/Pipeline.cpp @@ -0,0 +1,39 @@ +#include "CompilerInternal.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Errc.h" + +#include + +namespace acir::compiler::detail { + +llvm::Expected> +selectPipeline(const CompilerRequest &request) { + if (request.profile == CompilerProfile::Custom && + (!request.customPipeline || request.customPipeline->empty())) + return llvm::createStringError(llvm::errc::invalid_argument, + "custom profile requires a pipeline"); + if (request.profile != CompilerProfile::Custom && request.customPipeline) + return llvm::createStringError( + llvm::errc::invalid_argument, + "custom pipeline is legal only for the custom profile"); + + std::vector stages{ + CompilerStage::AcirParse, CompilerStage::AcirVerify, + CompilerStage::AcirNormalize, CompilerStage::AcirFreeze, + CompilerStage::AcsimLower, CompilerStage::AcsimVerify, + CompilerStage::CxxEmit, CompilerStage::CxxContract, + CompilerStage::Compile, CompilerStage::Link, + CompilerStage::Publish, + }; + if (!request.stopAfter) + return stages; + auto found = llvm::find(stages, *request.stopAfter); + if (found == stages.end()) + return llvm::createStringError(llvm::errc::invalid_argument, + "stop stage is outside the pipeline"); + stages.erase(std::next(found), stages.end()); + return stages; +} + +} // namespace acir::compiler::detail diff --git a/components/agentic-circuit/lib/Conversion/ACIRToACSim/ACIRToACSim.cpp b/components/agentic-circuit/lib/Conversion/ACIRToACSim/ACIRToACSim.cpp new file mode 100644 index 00000000..9df2eba4 --- /dev/null +++ b/components/agentic-circuit/lib/Conversion/ACIRToACSim/ACIRToACSim.cpp @@ -0,0 +1,2319 @@ +// Atomic ACIR-to-ACSim whole-model lowering (ac-lower-to-acsim). +// +// Converts one frozen, verified ACIR file into one canonical acsim.model in a +// single transaction: +// ac.module (concrete, () -> ()) -> acsim.module with ownership placements +// ac.instance / ac.instances -> acsim.instance (one per named member) +// ac.array (homogeneous) -> acsim.array +// ac.module.extern -> acsim.binding from the in-memory exact +// binding resolution (no lock round-trip) +// ac.process (yield-only plan) -> acsim.process enum-PC state machine +// selected ac.system -> acsim.model with exact fingerprints, +// canonical construction/destruction +// order, dispatch rows, and self +// activation edges +// +// Every validation failure is diagnosed with an ACLOWER-* code before any IR +// mutation, so a rejected input never publishes a partial acsim.model. +#include "acir/Conversion/ACIRToACSim/ACIRToACSim.h" + +#include "acir/Analysis/ProcessStatePlan.h" +#include "acir/Bindings/Binding.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACSim/ACSimDialect.h" +#include "acir/Dialect/ACSim/ACSimOps.h" +#include "acir/Dialect/ACSim/ACSimTypes.h" +#include "acir/Transforms/ResolveBindings.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Index/IR/IndexDialect.h" +#include "mlir/Dialect/Index/IR/IndexOps.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Pass/Pass.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; + +namespace acir { +namespace { + +constexpr llvm::StringLiteral kEpoch = "0.3"; +constexpr llvm::StringLiteral kResultRoleIdentity = "acsim.result.role"; +constexpr uint64_t kMaxExpandedRows = 1U << 20; + +InFlightDiagnostic lowerError(Operation *op, llvm::StringRef code, + const llvm::Twine &message) { + return op->emitError() << code << ": " << message; +} + +// --------------------------------------------------------------------------- +// Canonical static values: MLIR attributes <-> RFC 8785 JSON +// --------------------------------------------------------------------------- + +/// Convert a frozen ACIR static attribute to its canonical JSON value. The +/// accepted domain mirrors the ac-resolve-gfsim-bindings normalizer. +llvm::Expected staticValueToJson(Attribute attribute) { + auto unsupported = [&]() { + return llvm::createStringError( + llvm::errc::invalid_argument, + "ACLOWER-PARAM-PHASE: unsupported static attribute kind"); + }; + if (auto boolean = dyn_cast(attribute)) + return llvm::json::Value(boolean.getValue()); + if (auto integer = dyn_cast(attribute)) { + const llvm::APInt &value = integer.getValue(); + if (!value.isSignedIntN(64)) + return unsupported(); + return llvm::json::Value(value.getSExtValue()); + } + if (auto floating = dyn_cast(attribute)) { + double value = floating.getValueAsDouble(); + if (!std::isfinite(value) || (std::signbit(value) && value == 0.0)) + return unsupported(); + return llvm::json::Value(value); + } + if (auto string = dyn_cast(attribute)) + return llvm::json::Value(string.getValue()); + if (auto array = dyn_cast(attribute)) { + llvm::json::Array values; + for (Attribute element : array) { + auto converted = staticValueToJson(element); + if (!converted) + return converted.takeError(); + values.push_back(std::move(*converted)); + } + return llvm::json::Value(std::move(values)); + } + if (auto dictionary = dyn_cast(attribute)) { + llvm::json::Object values; + for (NamedAttribute named : dictionary) { + auto converted = staticValueToJson(named.getValue()); + if (!converted) + return converted.takeError(); + values[named.getName().getValue()] = std::move(*converted); + } + return llvm::json::Value(std::move(values)); + } + if (isa(attribute)) { + std::string printed; + llvm::raw_string_ostream output(printed); + output << attribute; + return llvm::json::Value(output.str()); + } + return unsupported(); +} + +/// Convert a binding-lock JSON static value back to a canonical MLIR +/// attribute. Returns a null attribute for values outside the closed domain. +Attribute jsonToStaticAttribute(OpBuilder &builder, + const llvm::json::Value &value) { + switch (value.kind()) { + case llvm::json::Value::Boolean: + return builder.getBoolAttr(*value.getAsBoolean()); + case llvm::json::Value::Number: + if (auto integer = value.getAsInteger()) + return builder.getI64IntegerAttr(*integer); + if (auto number = value.getAsNumber()) + return builder.getF64FloatAttr(*number); + return Attribute(); + case llvm::json::Value::String: + return builder.getStringAttr(*value.getAsString()); + case llvm::json::Value::Array: { + llvm::SmallVector elements; + for (const llvm::json::Value &element : *value.getAsArray()) { + Attribute converted = jsonToStaticAttribute(builder, element); + if (!converted) + return Attribute(); + elements.push_back(converted); + } + return builder.getArrayAttr(elements); + } + case llvm::json::Value::Object: { + llvm::SmallVector members; + for (const auto &member : *value.getAsObject()) { + Attribute converted = jsonToStaticAttribute(builder, member.second); + if (!converted) + return Attribute(); + members.push_back(builder.getNamedAttr(member.first, converted)); + } + return builder.getDictionaryAttr(members); + } + case llvm::json::Value::Null: + return Attribute(); + } + return Attribute(); +} + +/// Fingerprint a canonical JSON descriptor with the shared RFC 8785 + SHA-256 +/// recipe used across the binding infrastructure. +std::string fingerprintJson(const llvm::json::Value &value) { + auto canonical = bindings::canonicalizeJson(value); + if (!canonical) { + llvm::consumeError(canonical.takeError()); + return {}; + } + return bindings::sha256Fingerprint(*canonical); +} + +// --------------------------------------------------------------------------- +// acsim.type symbol table +// --------------------------------------------------------------------------- + +struct TypeDeclaration { + std::string identity; + std::string symbol; + std::string cpp; + std::string kind; + std::string fingerprint; + std::optional period; + uint64_t phase = 0; + uint64_t tickScale = 1; + std::optional parent; + std::optional bridgeKind; + std::optional bridgeOwner; +}; + +/// Assigns deterministic canonical symbols and fingerprints to every C++ +/// realization identity referenced by binding records or generated process +/// helpers. Identities are interned in sorted order so symbol assignment is +/// independent of discovery order. +class TypeSymbolTable { +public: + /// Intern one identity. `fingerprint` may be empty, in which case the + /// fingerprint is the SHA-256 of the identity itself. + mlir::LogicalResult intern(Operation *reporter, llvm::StringRef identity, + llvm::StringRef kind, llvm::StringRef cpp, + llvm::StringRef fingerprint = llvm::StringRef()) { + auto found = entries.find(identity.str()); + if (found != entries.end()) { + TypeDeclaration &existing = found->second; + if (existing.kind != kind || existing.cpp != cpp) + return lowerError(reporter, "ACLOWER-TYPE-MISMATCH", + "realization identity '" + identity + + "' is used with conflicting acsim.type " + "kind or C++ spelling"); + if (!fingerprint.empty() && existing.fingerprint != fingerprint) + return lowerError( + reporter, "ACLOWER-FINGERPRINT", + "realization identity '" + identity + + "' carries conflicting fingerprints across binding records"); + return mlir::success(); + } + TypeDeclaration declaration; + declaration.identity = identity.str(); + declaration.kind = kind.str(); + declaration.cpp = cpp.str(); + declaration.fingerprint = fingerprint.empty() + ? bindings::sha256Fingerprint(identity) + : fingerprint.str(); + entries.emplace(declaration.identity, std::move(declaration)); + return mlir::success(); + } + + mlir::LogicalResult internTimeDomain(ac::TimeDomainOp domain) { + llvm::json::Object descriptor{ + {"name", domain.getSymName()}, + {"period", static_cast(domain.getPeriod())}, + {"phase", static_cast(domain.getPhase())}, + {"tick_scale", static_cast(domain.getTickScale())}}; + if (auto parent = domain.getParentAttr()) + descriptor["parent"] = parent.getValue(); + else + descriptor["parent"] = nullptr; + if (auto bridge = domain.getBridgeAttr()) { + descriptor["bridge"] = llvm::json::Object{ + {"kind", bridge.getAs("kind").getValue()}, + {"owner", bridge.getAs("owner").getValue()}}; + } else { + descriptor["bridge"] = nullptr; + } + std::string fingerprint = + fingerprintJson(llvm::json::Value(std::move(descriptor))); + if (failed(intern(domain, domain.getSymName(), "time_domain", + "gfsim::TimeDomainRuntime", fingerprint))) + return mlir::failure(); + TypeDeclaration &declaration = entries.at(domain.getSymName().str()); + declaration.period = static_cast(domain.getPeriod()); + declaration.phase = static_cast(domain.getPhase()); + declaration.tickScale = static_cast(domain.getTickScale()); + if (auto parent = domain.getParentAttr()) + declaration.parent = parent.getValue().str(); + if (auto bridge = domain.getBridgeAttr()) { + declaration.bridgeKind = + bridge.getAs("kind").getValue().str(); + declaration.bridgeOwner = + bridge.getAs("owner").getValue().str(); + } + return mlir::success(); + } + + /// Resolve symbols after all identities are interned. + mlir::LogicalResult finalize(Operation *reporter) { + llvm::StringMap ownerBySymbol; + for (auto &[identity, declaration] : entries) { + std::string base = sanitize(declaration.identity); + std::string symbol = base; + for (unsigned suffix = 2; ownerBySymbol.count(symbol); ++suffix) + symbol = base + "_" + std::to_string(suffix); + ownerBySymbol.try_emplace(symbol, declaration.identity); + declaration.symbol = symbol; + } + ordered.clear(); + for (auto &[identity, declaration] : entries) + ordered.push_back(&declaration); + llvm::sort(ordered, + [](const TypeDeclaration *left, const TypeDeclaration *right) { + return left->symbol < right->symbol; + }); + for (const TypeDeclaration *declaration : ordered) + if (declaration->symbol.empty()) + return lowerError(reporter, "ACLOWER-FINGERPRINT", + "realization identity '" + declaration->identity + + "' has no canonical symbol"); + return mlir::success(); + } + + llvm::StringRef symbolFor(llvm::StringRef identity) const { + auto found = entries.find(identity.str()); + return found == entries.end() ? llvm::StringRef() + : llvm::StringRef(found->second.symbol); + } + + llvm::ArrayRef declarations() const { + return ordered; + } + +private: + static std::string sanitize(llvm::StringRef identity) { + std::string symbol; + symbol.reserve(identity.size()); + for (char character : identity) + symbol.push_back(std::isalnum(static_cast(character)) || + character == '_' + ? character + : '_'); + if (symbol.empty() || + std::isdigit(static_cast(symbol.front()))) + symbol.insert(symbol.begin(), '_'); + return symbol; + } + + std::map entries; + llvm::SmallVector ordered; +}; + +// --------------------------------------------------------------------------- +// Binding record conversion +// --------------------------------------------------------------------------- + +/// Build the exact 20-field acsim.binding record dictionary from a typed +/// binding-lock record, mapping realization identities to canonical symbols. +mlir::Attribute convertBindingRecord(OpBuilder &builder, + const bindings::BindingRecord &record, + const TypeSymbolTable &types) { + MLIRContext *context = builder.getContext(); + auto string = [&](llvm::StringRef value) { + return builder.getStringAttr(value); + }; + auto reference = [&](llvm::StringRef identity) { + return FlatSymbolRefAttr::get(context, types.symbolFor(identity)); + }; + auto dictionary = + [&](llvm::ArrayRef members) -> DictionaryAttr { + return builder.getDictionaryAttr(members); + }; + auto named = [&](llvm::StringRef key, Attribute value) { + return builder.getNamedAttr(key, value); + }; + + llvm::SmallVector activationSources; + for (const bindings::ActivationSourceBinding &source : + record.activationSources()) + activationSources.push_back( + dictionary({named("kind", reference(source.kind)), + named("name", string(source.name))})); + + llvm::SmallVector constructionArguments; + for (const llvm::json::Value &argument : record.construction().arguments) + constructionArguments.push_back(jsonToStaticAttribute(builder, argument)); + + const bindings::CppBinding &cpp = record.cpp(); + DictionaryAttr entryPoints = + dictionary({named("pure", string(cpp.entryPoints.pure)), + named("reset", string(cpp.entryPoints.reset)), + named("validate", string(cpp.entryPoints.validate)), + named("work", string(cpp.entryPoints.work)), + named("xfer", string(cpp.entryPoints.xfer))}); + DictionaryAttr cppRecord = dictionary( + {named("concept", string(cpp.conceptName)), + named("entry_points", entryPoints), named("header", string(cpp.header)), + named("symbol", string(cpp.symbol)), + named("target", string(cpp.target))}); + + DictionaryAttr construction = dictionary( + {named("arguments", builder.getArrayAttr(constructionArguments)), + named("kind", string(record.construction().kind))}); + DictionaryAttr ownership = + dictionary({named("kind", string(record.ownership().kind)), + named("placement", string(record.ownership().placement))}); + + llvm::SmallVector parameters; + for (const bindings::ParameterBinding ¶meter : record.parameters()) + parameters.push_back(dictionary( + {named("acir_type", string(parameter.acirType)), + named("cpp_type", string(parameter.cppType)), + named("mapping", string(parameter.mapping)), + named("name", string(parameter.name)), + named("ordinal", builder.getI64IntegerAttr(parameter.ordinal)), + named("value", jsonToStaticAttribute(builder, parameter.value))})); + + llvm::SmallVector ports; + for (const bindings::PortBinding &port : record.ports()) + ports.push_back( + dictionary({named("accessor", reference(port.accessor)), + named("cardinality", string(port.cardinality)), + named("delegation", string(port.delegation)), + named("direction", string(port.direction)), + named("interface", reference(port.interface)), + named("ownership", string(port.ownership)), + named("payload", reference(port.payload)), + named("protocol", reference(port.protocol)), + named("role", reference(port.role)), + named("time_domain", reference(port.timeDomain))})); + + llvm::SmallVector resources; + for (const bindings::ResourceBinding &resource : record.resources()) + resources.push_back( + dictionary({named("accessor", reference(resource.accessor)), + named("delegation", string(resource.delegation)), + named("mode", string(resource.mode)), + named("ownership", string(resource.ownership)), + named("resource", reference(resource.resource)), + named("role", reference(resource.role)), + named("time_domain", reference(resource.timeDomain))})); + + llvm::SmallVector results; + for (const bindings::ResultBinding &result : record.results()) + results.push_back(dictionary({named("cpp_type", reference(result.cppType)), + named("name", string(result.name))})); + + return dictionary( + {named("activation_sources", builder.getArrayAttr(activationSources)), + named("availability", string(record.availability())), + named("binding", string(record.binding())), + named("binding_schema", string(record.bindingSchema())), + named("component_schema", reference(record.componentSchema())), + named("component_schema_fingerprint", + string(record.componentSchemaFingerprint())), + named("construction", construction), + named("contract_epoch", string(record.contractEpoch())), + named("cpp", cppRecord), + named("cpp_type", reference(record.cppType())), + named("effect", string(record.effect())), + named("fingerprint", string(record.fingerprint())), + named("implementation", reference(record.implementation())), + named("ownership", ownership), + named("parameters", builder.getArrayAttr(parameters)), + named("ports", builder.getArrayAttr(ports)), + named("provider", reference(record.provider())), + named("provider_implementation_fingerprint", + string(record.providerImplementationFingerprint())), + named("resources", builder.getArrayAttr(resources)), + named("results", builder.getArrayAttr(results))}); +} + +// --------------------------------------------------------------------------- +// Module and placement plans +// --------------------------------------------------------------------------- + +struct PortEndpointPlan { + Value value; + bindings::PortBinding metadata; +}; + +struct PlacementPlan { + enum class Kind { Instance, Array, Process }; + Kind kind = Kind::Instance; + std::string name; + // Instance/array realization. + std::string targetSymbol; + bool targetIsBinding = false; + bool targetIsPure = false; + std::string resultCppType; + ArrayAttr staticArgs; + std::string specialization; + llvm::SmallVector shape; + // Binding-target dispatch thunks. + std::string work; + std::string xfer; + std::string reset; + std::string validate; + llvm::SmallVector inputPorts; + llvm::SmallVector outputPorts; + // Process realization. + ac::ProcessOp process; + std::string processDefinitionKey; + uint64_t fairnessCap = 1; +}; + +struct BindingEdgePlan { + unsigned sourcePlacement = 0; + unsigned targetPlacement = 0; +}; + +struct PureCallPlan { + ac::InstanceOp source; + Value result; + std::string name; + std::string binding; + std::string cppType; +}; + +struct ModuleResultPlan { + Value source; + std::string name; + std::string cppType; +}; + +struct ModulePortPlan { + Value source; + std::string name; + bindings::PortBinding metadata; +}; + +struct ModulePlan { + ac::ModuleOp source; + std::string name; + ArrayAttr staticParams; + std::string specialization; + llvm::SmallVector placements; + llvm::SmallVector pureCalls; + llvm::SmallVector ports; + llvm::SmallVector results; + llvm::SmallVector bindingEdges; +}; + +// --------------------------------------------------------------------------- +// The pass +// --------------------------------------------------------------------------- + +class ACIRToACSimPass final + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(ACIRToACSimPass) + + explicit ACIRToACSimPass(ACIRToACSimPassOptions options) + : options(std::move(options)) {} + + llvm::StringRef getArgument() const override { return "ac-lower-to-acsim"; } + llvm::StringRef getDescription() const override { + return "Atomically lower one frozen ACIR model to canonical ACSim"; + } + + void getDependentDialects(DialectRegistry ®istry) const override { + registry.insert(); + } + + void runOnOperation() override { + if (failed(lower(getOperation()))) + signalPassFailure(); + } + +private: + mlir::LogicalResult lower(mlir::ModuleOp input); + + /// Validation and planning. No IR mutation happens in this phase. + mlir::LogicalResult plan(mlir::ModuleOp input); + + mlir::LogicalResult planModule(ac::ModuleOp module, ModulePlan &planned); + mlir::LogicalResult planInstanceTarget(Operation *placement, + llvm::StringRef definition, + DictionaryAttr staticArgs, + PlacementPlan &planned); + mlir::LogicalResult planInstancePorts(ac::InstanceOp instance, + PlacementPlan &planned); + mlir::LogicalResult planProcesses(mlir::ModuleOp input); + mlir::LogicalResult expand(mlir::ModuleOp input); + + void expandModule(unsigned moduleIndex, std::string pathPrefix, + llvm::SmallSet &active); + + /// Emission. Runs only after every check succeeded. + mlir::FailureOr> emit(mlir::ModuleOp input); + void publish(mlir::ModuleOp input, mlir::ModuleOp staged); + void emitModuleBody(OpBuilder &builder, const ModulePlan &planned); + void emitProcessBody(OpBuilder &builder, const PlacementPlan &placement, + const llvm::DenseMap &moduleValues); + + std::string moduleFingerprint(ac::ModuleOp module); + std::string processFingerprint(const ModulePlan &module, + const PlacementPlan &process); + std::string bindingInstanceFingerprint(const bindings::BindingRecord &record, + ArrayAttr values); + + ACIRToACSimPassOptions options; + + // Planning state. + ac::SystemOp selectedSystem; + llvm::StringMap moduleIndexByName; // concrete modules + llvm::StringMap externByName; + llvm::SmallVector modules; // sorted by name + llvm::SmallVector timeDomains; + std::optional resolution; + std::optional processPlans; + std::string processPlanBytes; + TypeSymbolTable typeSymbols; + std::vector generatedCalleeIdentities; + std::vector valueTypeIdentities; + llvm::StringMap wakeTypeIdentities; + + struct RuntimeRow { + unsigned moduleIndex; + unsigned placementIndex; + std::string contextPath; + std::string path; + llvm::SmallVector indices; + }; + llvm::SmallVector constructionOrder; + llvm::SmallVector runtimeRows; + llvm::StringSet<> frozenOwnerPaths; + + // Fingerprints. + std::string frozenAcirFingerprint; + std::string bindingLockFingerprint; + std::string providerFingerprint; + std::string schemaSetFingerprint; + std::string profileFingerprint; + std::string toolchainFingerprint; + + // Set when owner expansion detects an instantiation cycle. + bool expansionCycle = false; +}; + +std::string ACIRToACSimPass::moduleFingerprint(ac::ModuleOp module) { + llvm::json::Object descriptor; + descriptor["module"] = module.getSymName(); + auto typeSpelling = [](Type type) { + std::string storage; + llvm::raw_string_ostream stream(storage); + stream << type; + return storage; + }; + auto staticDictionary = [&](DictionaryAttr dictionary) { + llvm::json::Object values; + if (!dictionary) + return values; + for (NamedAttribute named : dictionary) { + auto value = staticValueToJson(named.getValue()); + if (!value) { + llvm::consumeError(value.takeError()); + continue; + } + values[named.getName().getValue()] = std::move(*value); + } + return values; + }; + auto targetFingerprint = [&](llvm::StringRef target) { + if (auto concrete = moduleIndexByName.find(target); + concrete != moduleIndexByName.end()) + return modules[concrete->second].specialization; + if (resolution) { + std::string key = ("@" + target).str(); + if (const bindings::ResolvedBinding *selection = + resolution->selectionForResolutionKey(key)) + return selection->record().fingerprint().str(); + } + return std::string(); + }; + + llvm::json::Object interface; + llvm::json::Array inputs; + llvm::json::Array results; + for (Type type : module.getFunctionType().getInputs()) + inputs.push_back(typeSpelling(type)); + for (Type type : module.getFunctionType().getResults()) + results.push_back(typeSpelling(type)); + interface["inputs"] = std::move(inputs); + interface["results"] = std::move(results); + descriptor["interface"] = std::move(interface); + + llvm::json::Object parameters; + for (NamedAttribute named : module.getStaticParams()) { + auto value = staticValueToJson(named.getValue()); + if (!value) { + llvm::consumeError(value.takeError()); + continue; + } + parameters[named.getName().getValue()] = std::move(*value); + } + descriptor["static"] = std::move(parameters); + + std::vector> definitions; + auto appendPlacement = [&](llvm::StringRef key, llvm::StringRef kind, + llvm::StringRef name, llvm::StringRef target, + DictionaryAttr staticArgs) { + llvm::json::Object entry; + entry["kind"] = kind; + entry["name"] = name; + entry["static"] = staticDictionary(staticArgs); + entry["target"] = target; + entry["target_specialization"] = targetFingerprint(target); + definitions.emplace_back(key.str(), std::move(entry)); + }; + for (Operation &operation : module.getBody().front()) { + if (auto instance = dyn_cast(operation)) { + appendPlacement(("instance:" + instance.getSymName()).str(), "instance", + instance.getSymName(), instance.getDefinition(), + instance.getStaticArgs()); + continue; + } + if (auto array = dyn_cast(operation)) { + llvm::json::Object entry; + entry["kind"] = "array"; + entry["name"] = array.getSymName(); + entry["target"] = array.getDefinition(); + entry["target_specialization"] = targetFingerprint(array.getDefinition()); + llvm::json::Array shape; + for (int64_t extent : array.getShape()) + shape.push_back(extent); + entry["shape"] = std::move(shape); + llvm::json::Array staticElements; + for (Attribute arguments : array.getStaticArgs()) + staticElements.push_back( + staticDictionary(cast(arguments))); + entry["static"] = std::move(staticElements); + definitions.emplace_back(("array:" + array.getSymName()).str(), + std::move(entry)); + continue; + } + if (auto collection = dyn_cast(operation)) { + for (auto [index, nameAttribute] : + llvm::enumerate(collection.getNames())) { + llvm::StringRef name = cast(nameAttribute).getValue(); + llvm::StringRef target = + cast(collection.getDefinitions()[index]) + .getValue(); + appendPlacement( + ("instance:" + name).str(), "instance", name, target, + cast(collection.getStaticArgs()[index])); + } + continue; + } + if (auto process = dyn_cast(operation)) { + llvm::json::Object entry; + entry["kind"] = "process"; + entry["name"] = process.getSymName(); + entry["process_kind"] = process.getKind(); + llvm::json::Array captureTypes; + for (Value capture : process.getCaptures()) + captureTypes.push_back(typeSpelling(capture.getType())); + entry["captures"] = std::move(captureTypes); + llvm::json::Array skeleton; + if (auto frozenSkeleton = + process->getAttrOfType("ac.frozen_process_skeleton")) + for (Attribute line : frozenSkeleton) + skeleton.push_back(cast(line).getValue()); + entry["skeleton"] = std::move(skeleton); + definitions.emplace_back(("process:" + process.getSymName()).str(), + std::move(entry)); + continue; + } + if (auto domain = dyn_cast(operation)) { + llvm::json::Object entry{ + {"kind", "time_domain"}, + {"name", domain.getSymName()}, + {"period", static_cast(domain.getPeriod())}, + {"phase", static_cast(domain.getPhase())}, + {"tick_scale", static_cast(domain.getTickScale())}}; + if (auto parent = domain.getParentAttr()) + entry["parent"] = parent.getValue(); + if (auto bridge = domain.getBridgeAttr()) + entry["bridge"] = llvm::json::Object{ + {"kind", bridge.getAs("kind").getValue()}, + {"owner", bridge.getAs("owner").getValue()}}; + definitions.emplace_back(("time_domain:" + domain.getSymName()).str(), + std::move(entry)); + continue; + } + if (auto returnOp = dyn_cast(operation)) { + llvm::json::Object entry; + entry["kind"] = "return"; + llvm::json::Array operandTypes; + for (Value operand : returnOp.getOperands()) + operandTypes.push_back(typeSpelling(operand.getType())); + entry["operands"] = std::move(operandTypes); + definitions.emplace_back("~return", std::move(entry)); + } + } + llvm::sort(definitions, [](const auto &left, const auto &right) { + return left.first < right.first; + }); + llvm::json::Array body; + for (auto &definition : definitions) + body.push_back(std::move(definition.second)); + descriptor["body"] = std::move(body); + return fingerprintJson(llvm::json::Value(std::move(descriptor))); +} + +std::string ACIRToACSimPass::processFingerprint(const ModulePlan &module, + const PlacementPlan &process) { + llvm::json::Object descriptor; + descriptor["module"] = module.name; + descriptor["module_specialization"] = module.specialization; + descriptor["process"] = process.name; + descriptor["process_plan"] = processPlanBytes; + return fingerprintJson(llvm::json::Value(std::move(descriptor))); +} + +std::string ACIRToACSimPass::bindingInstanceFingerprint( + const bindings::BindingRecord &record, ArrayAttr values) { + llvm::json::Object descriptor; + descriptor["binding"] = record.binding(); + descriptor["binding_fingerprint"] = record.fingerprint(); + descriptor["component_schema_fingerprint"] = + record.componentSchemaFingerprint(); + descriptor["profile"] = options.profile; + descriptor["provider_implementation_fingerprint"] = + record.providerImplementationFingerprint(); + llvm::json::Array staticValues; + for (Attribute value : values) { + auto converted = staticValueToJson(value); + if (!converted) { + llvm::consumeError(converted.takeError()); + continue; + } + staticValues.push_back(std::move(*converted)); + } + descriptor["static"] = std::move(staticValues); + descriptor["target"] = options.target; + return fingerprintJson(llvm::json::Value(std::move(descriptor))); +} + +// --------------------------------------------------------------------------- +// Planning +// --------------------------------------------------------------------------- + +mlir::LogicalResult ACIRToACSimPass::planInstanceTarget( + Operation *placement, llvm::StringRef definition, DictionaryAttr staticArgs, + PlacementPlan &planned) { + auto externIt = externByName.find(definition); + auto moduleIt = moduleIndexByName.find(definition); + if (externIt == externByName.end() && moduleIt == moduleIndexByName.end()) + return lowerError(placement, "ACLOWER-BINDING-MISSING", + "placement definition '@" + definition + + "' is not a module or external declaration"); + + DictionaryAttr declaredParams; + if (externIt != externByName.end()) + declaredParams = externIt->second.getStaticParams(); + else + declaredParams = modules[moduleIt->second].source.getStaticParams(); + // Zero-volume arrays carry no per-element dictionaries; the declared + // parameters are the single specialization. + if (staticArgs && staticArgs != declaredParams) + return lowerError(placement, "ACLOWER-PARAM-PHASE", + "placement static arguments must exactly equal the " + "frozen static parameters of '@" + + definition + + "' (per-instance specialization is outside the v0.1 " + "lowering stage)"); + + if (externIt != externByName.end()) { + // External declaration: realization comes from the exact binding lock. + std::string key = ("@" + definition).str(); + const bindings::ResolvedBinding *selection = + resolution->selectionForResolutionKey(key); + if (!selection) + return lowerError(placement, "ACLOWER-BINDING-MISSING", + "no exact binding selection exists for external " + "declaration '@" + + definition + "'"); + const bindings::BindingRecord &record = selection->record(); + if (record.effect() != "stateful" && record.effect() != "pure") + return lowerError(placement, "ACLOWER-TYPE-MISMATCH", + "external declaration '@" + definition + + "' resolved to binding '" + record.binding() + + "' with unknown effect '" + record.effect() + "'"); + // Registry validation has already proven the effect-specific entry-point + // set and exact result metadata. + const bindings::CppEntryPoints &entryPoints = record.cpp().entryPoints; + planned.targetSymbol = record.binding().str(); + planned.targetIsBinding = true; + planned.targetIsPure = record.effect() == "pure"; + if (planned.targetIsPure) { + if (record.results().size() != 1) + return lowerError( + placement, "ACLOWER-TYPE-MISMATCH", + "pure binding '" + record.binding() + + "' must have exactly one result for acsim.inline"); + planned.resultCppType = record.results().front().cppType; + } + OpBuilder builder(placement->getContext()); + llvm::SmallVector values; + for (const bindings::ParameterBinding ¶meter : record.parameters()) { + Attribute value = jsonToStaticAttribute(builder, parameter.value); + if (!value) + return lowerError(placement, "ACLOWER-PARAM-PHASE", + "binding '" + record.binding() + "' parameter '" + + parameter.name + + "' has a value outside the canonical static " + "domain"); + values.push_back(value); + } + planned.staticArgs = builder.getArrayAttr(values); + planned.specialization = + bindingInstanceFingerprint(record, planned.staticArgs); + if (!planned.targetIsPure) { + planned.work = entryPoints.work; + planned.xfer = entryPoints.xfer; + planned.reset = entryPoints.reset; + planned.validate = entryPoints.validate; + } + return mlir::success(); + } + + // Concrete generated module target. + ModulePlan &target = modules[moduleIt->second]; + planned.targetSymbol = target.name; + planned.targetIsBinding = false; + planned.staticArgs = target.staticParams; + planned.specialization = target.specialization; + return mlir::success(); +} + +mlir::LogicalResult ACIRToACSimPass::planInstancePorts(ac::InstanceOp instance, + PlacementPlan &planned) { + if (!planned.targetIsBinding || planned.targetIsPure) + return mlir::success(); + std::string key = ("@" + instance.getDefinition()).str(); + const bindings::ResolvedBinding *selection = + resolution->selectionForResolutionKey(key); + assert(selection && "validated external target must have a binding"); + const bindings::BindingRecord &record = selection->record(); + llvm::SmallVector used(record.ports().size(), false); + + auto planEndpoint = [&](Value value, llvm::StringRef direction, + llvm::SmallVectorImpl &endpoints) + -> mlir::LogicalResult { + auto endpoint = dyn_cast(value.getType()); + if (!endpoint) { + if (direction == "input" || !value.use_empty()) + return lowerError(instance, "ACLOWER-TYPE-MISMATCH", + "stateful binding scalar values cannot cross the " + "construction graph; use a typed endpoint/resource " + "or a pure binding result"); + return mlir::success(); + } + llvm::StringRef expectedRole = endpoint.getRole().getValue(); + if (direction == "input") { + ac::InterfaceOp interface; + for (ac::InterfaceOp candidate : + instance->getParentOfType() + .getOps()) + if (candidate.getSymName() == endpoint.getInterface().getValue()) { + interface = candidate; + break; + } + ac::RoleOp role; + if (interface) + for (ac::RoleOp candidate : interface.getOps()) + if (candidate.getSymName() == endpoint.getRole().getValue()) { + role = candidate; + break; + } + if (!role) + return lowerError(instance, "ACLOWER-TYPE-MISMATCH", + "endpoint role cannot be resolved for input port " + "lowering"); + expectedRole = role.getDual(); + } + std::optional match; + for (auto [index, port] : llvm::enumerate(record.ports())) { + if (!used[index] && port.direction == direction && + port.interface == endpoint.getInterface().getValue() && + port.role == expectedRole) { + if (match) + return lowerError(instance, "ACLOWER-BINDING-AMBIGUOUS", + "binding '" + record.binding() + + "' has multiple ports matching endpoint " + + endpoint.getInterface().getValue() + + "::" + expectedRole); + match = index; + } + } + if (!match) + return lowerError(instance, "ACLOWER-TYPE-MISMATCH", + "binding '" + record.binding() + "' has no exact " + + direction + " port for " + + endpoint.getInterface().getValue() + + "::" + expectedRole); + used[*match] = true; + endpoints.push_back({value, record.ports()[*match]}); + return mlir::success(); + }; + + for (Value input : instance.getInputs()) + if (failed(planEndpoint(input, "input", planned.inputPorts))) + return mlir::failure(); + for (Value output : instance.getOutputs()) + if (failed(planEndpoint(output, "output", planned.outputPorts))) + return mlir::failure(); + if (llvm::any_of(used, [](bool value) { return !value; })) + return lowerError(instance, "ACLOWER-TYPE-MISMATCH", + "binding '" + record.binding() + + "' exposes a port that is absent from the external " + "module signature"); + return mlir::success(); +} + +mlir::LogicalResult ACIRToACSimPass::planModule(ac::ModuleOp module, + ModulePlan &planned) { + FunctionType signature = module.getFunctionType(); + if (signature.getNumInputs() != 0) { + std::string printed; + llvm::raw_string_ostream stream(printed); + stream << signature; + return lowerError(module, "ACLOWER-TYPE-MISMATCH", + "generated ACSim modules do not carry dynamic block " + "arguments; module '@" + + module.getSymName() + "' has '" + stream.str() + "'"); + } + + OpBuilder builder(module->getContext()); + llvm::SmallVector staticValues; + for (NamedAttribute named : module.getStaticParams()) + staticValues.push_back(named.getValue()); + planned.staticParams = builder.getArrayAttr(staticValues); + planned.specialization = moduleFingerprint(module); + + llvm::SmallVector processes; + ac::ReturnOp moduleReturn; + for (Operation &operation : module.getBody().front()) { + if (auto instance = dyn_cast(operation)) { + PlacementPlan placement; + placement.kind = PlacementPlan::Kind::Instance; + placement.name = instance.getSymName().str(); + if (failed(planInstanceTarget(instance, instance.getDefinition(), + instance.getStaticArgs(), placement))) + return mlir::failure(); + if (placement.targetIsPure) { + if (instance.getNumResults() != 1) + return lowerError(instance, "ACLOWER-TYPE-MISMATCH", + "pure external placement must produce exactly one " + "SSA result"); + if (!instance.getInputs().empty()) + return lowerError(instance, "ACLOWER-UNSUPPORTED-CONSTRUCT", + "pure external SSA operands require typed graph " + "lowering"); + planned.pureCalls.push_back( + {instance, instance.getResult(0), instance.getSymName().str(), + placement.targetSymbol, placement.resultCppType}); + continue; + } + if (failed(planInstancePorts(instance, placement))) + return mlir::failure(); + planned.placements.push_back(std::move(placement)); + continue; + } + if (auto array = dyn_cast(operation)) { + PlacementPlan placement; + placement.kind = PlacementPlan::Kind::Array; + placement.name = array.getSymName().str(); + placement.shape.assign(array.getShape().begin(), array.getShape().end()); + // Homogeneous arrays require one exact specialization per element. + DictionaryAttr first; + for (Attribute element : array.getStaticArgs()) { + auto arguments = dyn_cast(element); + if (!arguments) + return lowerError(array, "ACLOWER-ARRAY", + "array static arguments must be concrete " + "dictionaries"); + if (!first) + first = arguments; + else if (arguments != first) + return lowerError(array, "ACLOWER-ARRAY", + "differently specialized array elements are " + "outside the lowering stage; lower them as " + "ordered named members instead"); + } + if (failed(planInstanceTarget(array, array.getDefinition(), first, + placement))) + return mlir::failure(); + if (placement.targetIsPure) + return lowerError(array, "ACLOWER-OWNERSHIP", + "pure bindings lower to acsim.inline and cannot own " + "an ac.array placement"); + planned.placements.push_back(std::move(placement)); + continue; + } + if (auto collection = dyn_cast(operation)) { + for (auto [index, definitionAttribute] : + llvm::enumerate(collection.getDefinitions())) { + auto definition = cast(definitionAttribute); + auto arguments = + cast(collection.getStaticArgs()[index]); + PlacementPlan placement; + placement.kind = PlacementPlan::Kind::Instance; + placement.name = + cast(collection.getNames()[index]).getValue().str(); + if (failed(planInstanceTarget(collection, definition.getValue(), + arguments, placement))) + return mlir::failure(); + if (placement.targetIsPure) + return lowerError(collection, "ACLOWER-OWNERSHIP", + "pure bindings lower to acsim.inline and cannot " + "own an ac.instances placement"); + planned.placements.push_back(std::move(placement)); + } + continue; + } + if (auto process = dyn_cast(operation)) { + PlacementPlan placement; + placement.kind = PlacementPlan::Kind::Process; + placement.name = process.getSymName().str(); + placement.process = process; + processes.push_back(std::move(placement)); + continue; + } + if (auto domain = dyn_cast(operation)) { + timeDomains.push_back(domain); + continue; + } + if (auto returnOp = dyn_cast(operation)) { + moduleReturn = returnOp; + continue; + } + return lowerError(&operation, "ACLOWER-UNSUPPORTED-CONSTRUCT", + "operation '" + operation.getName().getStringRef() + + "' has no ACSim realization in the lowering " + "stage (queues, resources, address maps, views, " + "and instrumentation are rejected, " + "never silently dropped)"); + } + + llvm::sort(planned.placements, + [](const PlacementPlan &left, const PlacementPlan &right) { + return left.name < right.name; + }); + for (auto [targetIndex, target] : llvm::enumerate(planned.placements)) { + for (const PortEndpointPlan &input : target.inputPorts) { + std::optional sourceIndex; + for (auto [candidateIndex, candidate] : + llvm::enumerate(planned.placements)) + if (llvm::any_of(candidate.outputPorts, + [&](const PortEndpointPlan &output) { + return output.value == input.value; + })) { + sourceIndex = candidateIndex; + break; + } + if (!sourceIndex) + return lowerError(target.inputPorts.front().value.getDefiningOp(), + "ACLOWER-TYPE-MISMATCH", + "typed endpoint input has no lowered producer"); + planned.bindingEdges.push_back( + {*sourceIndex, static_cast(targetIndex)}); + } + } + llvm::sort(processes, + [](const PlacementPlan &left, const PlacementPlan &right) { + return left.name < right.name; + }); + llvm::sort(planned.pureCalls, + [](const PureCallPlan &left, const PureCallPlan &right) { + return left.name < right.name; + }); + for (auto &process : processes) + planned.placements.push_back(std::move(process)); + for (auto [targetIndex, target] : llvm::enumerate(planned.placements)) { + if (target.kind != PlacementPlan::Kind::Process) + continue; + for (Value capture : target.process.getCaptures()) { + std::optional sourceIndex; + for (auto [candidateIndex, candidate] : + llvm::enumerate(planned.placements)) + if (llvm::any_of(candidate.outputPorts, + [&](const PortEndpointPlan &output) { + return output.value == capture; + })) { + sourceIndex = candidateIndex; + break; + } + if (!sourceIndex) + return lowerError(target.process, "ACLOWER-TYPE-MISMATCH", + "process capture has no lowered typed producer"); + planned.bindingEdges.push_back( + {*sourceIndex, static_cast(targetIndex)}); + } + } + + if (!moduleReturn || + moduleReturn.getNumOperands() != signature.getNumResults()) + return lowerError(module, "ACLOWER-TYPE-MISMATCH", + "module return arity must match the declared result " + "interface"); + for (auto [index, operand] : llvm::enumerate(moduleReturn.getOperands())) { + const PortEndpointPlan *exportedPort = nullptr; + for (const PlacementPlan &placement : planned.placements) + for (const PortEndpointPlan &port : placement.outputPorts) + if (port.value == operand) { + exportedPort = &port; + break; + } + if (exportedPort) { + planned.ports.push_back({operand, + llvm::formatv("port_{0:08}", index).str(), + exportedPort->metadata}); + continue; + } + const PureCallPlan *producer = nullptr; + for (const PureCallPlan &call : planned.pureCalls) + if (call.result == operand) { + producer = &call; + break; + } + if (!producer) + return lowerError( + moduleReturn, "ACLOWER-UNSUPPORTED-CONSTRUCT", + "module result " + llvm::Twine(index) + + " must be a typed endpoint export or be produced by an exact " + "pure external binding"); + ModuleResultPlan result; + result.source = operand; + result.name = llvm::formatv("result_{0:08}", index).str(); + result.cppType = producer->cppType; + planned.results.push_back(std::move(result)); + } + return mlir::success(); +} + +mlir::LogicalResult ACIRToACSimPass::planProcesses(mlir::ModuleOp input) { + bool hasProcess = false; + input.walk([&](ac::ProcessOp) { hasProcess = true; }); + if (!hasProcess) + return mlir::success(); + + auto plans = planProcessState(input); + if (failed(plans)) + return mlir::failure(); + if (failed(verifyProcessStatePlan(*plans))) + return mlir::failure(); + processPlans = std::move(*plans); + auto serializedPlans = serializeProcessStatePlan(*processPlans); + if (!serializedPlans) { + llvm::consumeError(serializedPlans.takeError()); + return lowerError(input, "ACLOWER-FINGERPRINT", + "failed to serialize the canonical process-state plan"); + } + processPlanBytes = std::move(*serializedPlans); + + generatedCalleeIdentities.resize(processPlans->callees().size()); + for (const ProcessGeneratedCalleePlan &callee : processPlans->callees()) { + llvm::StringRef symbol = callee.symbol(); + symbol.consume_front("@"); + generatedCalleeIdentities[callee.id().value()] = symbol.str(); + if (failed(typeSymbols.intern(input, symbol, "implementation", callee.cpp(), + callee.fingerprint()))) + return mlir::failure(); + } + for (const ProcessStatePlan &process : processPlans->processes()) { + for (const ProcessWakePlan &wake : process.wakes()) { + llvm::StringRef typeKey = wake.typeKey(); + typeKey.consume_front("@"); + wakeTypeIdentities[typeKey] = typeKey.str(); + } + } + for (const auto &[typeKey, identity] : wakeTypeIdentities) { + llvm::StringRef cppName = identity; + cppName.consume_front("acir_"); + std::string cpp = ("acir::generated::" + cppName).str(); + if (failed(typeSymbols.intern(input, identity, "wake", cpp))) + return mlir::failure(); + } + valueTypeIdentities.resize(processPlans->valueTypes().size()); + for (const ProcessValueTypePlan &valueType : processPlans->valueTypes()) { + llvm::StringRef symbol = valueType.symbol(); + symbol.consume_front("@"); + valueTypeIdentities[valueType.id().value()] = symbol.str(); + llvm::StringRef kind = + valueType.kind() == ProcessValueTypeKind::Packet ? "packet" : "value"; + if (failed(typeSymbols.intern(input, symbol, kind, valueType.cpp(), + valueType.fingerprint()))) + return mlir::failure(); + } + + // Attach plan-derived fairness caps to the module placements. + for (ModulePlan &module : modules) + for (PlacementPlan &placement : module.placements) { + if (placement.kind != PlacementPlan::Kind::Process) + continue; + std::string key = "@" + module.name + "::@" + placement.name; + const ProcessStatePlan *plan = processPlans->lookupByDefinitionKey(key); + if (!plan) + return lowerError(placement.process, "ACLOWER-PROCESS-STATE", + "process-state plan is missing process '@" + + placement.name + "'"); + placement.processDefinitionKey = key; + placement.fairnessCap = plan->fairnessWork(); + placement.specialization = processFingerprint(module, placement); + } + return mlir::success(); +} + +mlir::LogicalResult ACIRToACSimPass::plan(mlir::ModuleOp input) { + auto epoch = input->getAttrOfType("ac.contract_epoch"); + if (!epoch || epoch.getValue() != kEpoch) + return lowerError(input, "ACLOWER-EPOCH-MISMATCH", + "ac-lower-to-acsim requires ac.contract_epoch exactly " + "\"0.1\""); + auto frozen = input->getAttrOfType("ac.topology_frozen"); + auto freezeEpoch = input->getAttrOfType("ac.freeze_epoch"); + if (!frozen || !frozen.getValue() || !freezeEpoch || + freezeEpoch.getValue() != kEpoch) + return lowerError(input, "ACLOWER-EPOCH-MISMATCH", + "ac-lower-to-acsim requires a topology-frozen " + "model; run ac-freeze-topology first"); + auto frozenOwners = input->getAttrOfType("ac.frozen_owners"); + if (!frozenOwners) + return lowerError(input, "ACLOWER-OWNERSHIP", + "frozen model is missing its canonical owner manifest"); + for (Attribute owner : frozenOwners) { + auto record = dyn_cast(owner); + auto path = record ? record.getAs("path") : StringAttr(); + if (!path || !frozenOwnerPaths.insert(path.getValue()).second) + return lowerError(input, "ACLOWER-OWNERSHIP", + "frozen owner manifest has a missing or duplicate " + "canonical path"); + } + if (options.profile.empty() || options.target.empty()) + return lowerError(input, "ACLOWER-PROFILE", + "ac-lower-to-acsim requires an exact static build " + "profile and toolchain target"); + + unsigned selectedCount = 0; + for (auto system : input.getOps()) { + if (!system.getSelected()) + continue; + ++selectedCount; + selectedSystem = system; + } + if (selectedCount != 1) + return lowerError(input, "ACLOWER-OWNERSHIP", + "ac-lower-to-acsim requires exactly one selected " + "ac.system"); + + // Inventory concrete modules, externals, and top-level declarations. + for (Operation &operation : *input.getBody()) { + if (auto module = dyn_cast(operation)) { + moduleIndexByName[module.getSymName()] = modules.size(); + ModulePlan planned; + planned.source = module; + planned.name = module.getSymName().str(); + modules.push_back(std::move(planned)); + continue; + } + if (auto external = dyn_cast(operation)) { + externByName[external.getSymName()] = external; + continue; + } + if (isa(operation)) + return lowerError(&operation, "ACLOWER-UNSUPPORTED-CONSTRUCT", + "generator-based module declarations are outside the " + "lowering stage"); + if (isa(operation)) + continue; // Pure declarations are fully resolved before lowering. + return lowerError(&operation, "ACLOWER-UNSUPPORTED-CONSTRUCT", + "top-level operation '" + + operation.getName().getStringRef() + + "' has no ACSim realization in the lowering " + "stage"); + } + + llvm::sort(modules, [](const ModulePlan &left, const ModulePlan &right) { + return left.name < right.name; + }); + moduleIndexByName.clear(); + for (auto [index, module] : llvm::enumerate(modules)) + moduleIndexByName[module.name] = index; + + llvm::SmallVector dependencyCount(modules.size()); + llvm::SmallVector> parentsByChild( + modules.size()); + for (auto [ownerIndex, module] : llvm::enumerate(modules)) { + llvm::SmallSet dependencies; + auto addDependency = [&](llvm::StringRef definition) { + auto target = moduleIndexByName.find(definition); + if (target != moduleIndexByName.end()) + dependencies.insert(target->second); + }; + for (Operation &operation : module.source.getBody().front()) { + if (auto instance = dyn_cast(operation)) + addDependency(instance.getDefinition()); + else if (auto array = dyn_cast(operation)) + addDependency(array.getDefinition()); + else if (auto collection = dyn_cast(operation)) + for (Attribute definition : collection.getDefinitions()) + addDependency(cast(definition).getValue()); + } + dependencyCount[ownerIndex] = dependencies.size(); + for (unsigned childIndex : dependencies) + parentsByChild[childIndex].push_back(ownerIndex); + } + std::set> readyModules; + for (auto [index, module] : llvm::enumerate(modules)) + if (dependencyCount[index] == 0) + readyModules.emplace(module.name, index); + llvm::SmallVector orderedModules; + orderedModules.reserve(modules.size()); + while (!readyModules.empty()) { + unsigned childIndex = readyModules.begin()->second; + readyModules.erase(readyModules.begin()); + orderedModules.push_back(std::move(modules[childIndex])); + for (unsigned parentIndex : parentsByChild[childIndex]) + if (--dependencyCount[parentIndex] == 0) + readyModules.emplace(modules[parentIndex].name, parentIndex); + } + if (orderedModules.size() != modules.size()) + return lowerError(input, "ACLOWER-OWNERSHIP", + "module instantiation cycle cannot produce canonical " + "ACSim module order"); + modules = std::move(orderedModules); + moduleIndexByName.clear(); + for (auto [index, module] : llvm::enumerate(modules)) + moduleIndexByName[module.name] = index; + + // The selected root must be a concrete generated module. + llvm::StringRef rootName = selectedSystem.getRoot(); + if (!moduleIndexByName.count(rootName)) + return lowerError(selectedSystem, "ACLOWER-OWNERSHIP", + "selected system root '@" + rootName + + "' must be a concrete ac.module"); + + // Resolve exact bindings in memory (shared contract with + // ac-resolve-gfsim-bindings; no lock file round-trip). + ResolveBindingsPassOptions resolveOptions; + resolveOptions.candidates = options.candidates; + resolveOptions.requests = options.requests; + resolveOptions.profile = options.profile; + resolveOptions.target = options.target; + auto resolved = resolveModuleBindings(input, resolveOptions); + if (!resolved) { + input.emitError() << llvm::toString(resolved.takeError()); + return mlir::failure(); + } + resolution = std::move(*resolved); + + // Module references may point forward in canonical symbol order. Freeze + // every target identity before any body consults it so spelling never + // constrains the legal instantiation graph. + OpBuilder identityBuilder(input.getContext()); + for (ModulePlan &module : modules) { + llvm::SmallVector staticValues; + for (NamedAttribute named : module.source.getStaticParams()) + staticValues.push_back(named.getValue()); + module.staticParams = identityBuilder.getArrayAttr(staticValues); + module.specialization = moduleFingerprint(module.source); + } + + // Plan every concrete module body. + for (auto [index, module] : llvm::enumerate(modules)) + if (failed(planModule(module.source, modules[index]))) + return mlir::failure(); + + if (failed(planProcesses(input))) + return mlir::failure(); + + // Intern every binding-record realization identity. + for (const bindings::ResolvedBinding &selection : resolution->selections()) { + const bindings::BindingRecord &record = selection.record(); + if (failed(typeSymbols.intern(input, record.componentSchema(), "schema", + record.componentSchema(), + record.componentSchemaFingerprint())) || + failed( + typeSymbols.intern(input, record.implementation(), "implementation", + record.implementation(), + record.providerImplementationFingerprint())) || + failed(typeSymbols.intern(input, record.provider(), "provider", + record.provider())) || + failed(typeSymbols.intern(input, record.cppType(), "value", + record.cppType()))) + return mlir::failure(); + for (const bindings::PortBinding &port : record.ports()) + if (failed(typeSymbols.intern(input, port.accessor, "accessor", + port.accessor)) || + failed(typeSymbols.intern(input, port.interface, "interface", + port.interface)) || + failed(typeSymbols.intern(input, port.payload, "packet", + port.payload)) || + failed(typeSymbols.intern(input, port.protocol, "protocol", + port.protocol)) || + failed(typeSymbols.intern(input, port.role, "role", port.role)) || + failed(typeSymbols.intern(input, port.timeDomain, "time_domain", + port.timeDomain))) + return mlir::failure(); + for (const bindings::ResourceBinding &resource : record.resources()) + if (failed(typeSymbols.intern(input, resource.accessor, "accessor", + resource.accessor)) || + failed(typeSymbols.intern(input, resource.resource, "resource", + resource.resource)) || + failed(typeSymbols.intern(input, resource.role, "role", + resource.role)) || + failed(typeSymbols.intern(input, resource.timeDomain, "time_domain", + resource.timeDomain))) + return mlir::failure(); + for (const bindings::ResultBinding &result : record.results()) + if (failed(typeSymbols.intern(input, result.cppType, "value", + result.cppType))) + return mlir::failure(); + for (const bindings::ActivationSourceBinding &source : + record.activationSources()) + if (failed(typeSymbols.intern(input, source.kind, "wake", source.kind))) + return mlir::failure(); + } + llvm::sort(timeDomains, [](ac::TimeDomainOp left, ac::TimeDomainOp right) { + return left.getSymName() < right.getSymName(); + }); + for (ac::TimeDomainOp domain : timeDomains) + if (failed(typeSymbols.internTimeDomain(domain))) + return mlir::failure(); + if (llvm::any_of( + modules, + [](const ModulePlan &module) { return !module.results.empty(); }) && + failed(typeSymbols.intern(input, kResultRoleIdentity, "role", + "acsim::ResultRole"))) + return mlir::failure(); + if (failed(typeSymbols.finalize(input))) + return mlir::failure(); + + // Binding symbols must not collide with type or module symbols. + for (const bindings::ResolvedBinding &selection : resolution->selections()) { + llvm::StringRef binding = selection.record().binding(); + if (typeSymbols.symbolFor(binding).data() != nullptr || + moduleIndexByName.count(binding)) + return lowerError(input, "ACLOWER-BINDING-AMBIGUOUS", + "binding identity '" + binding + + "' collides with a type or module symbol"); + } + + // Fingerprints over exact inputs, computed before any mutation. + std::string frozenText; + { + llvm::raw_string_ostream output(frozenText); + input.print(output); + } + frozenAcirFingerprint = bindings::sha256Fingerprint(frozenText); + bindingLockFingerprint = resolution->lockFingerprint().str(); + + llvm::json::Array providers; + llvm::json::Array schemas; + { + std::map uniqueProviders; + std::map uniqueSchemas; + for (const bindings::ResolvedBinding &selection : + resolution->selections()) { + uniqueProviders[selection.record().provider().str()] = true; + uniqueSchemas[selection.record().componentSchema().str()] = true; + } + for (auto &[identity, unused] : uniqueProviders) + providers.push_back(identity); + for (auto &[identity, unused] : uniqueSchemas) + schemas.push_back(identity); + } + providerFingerprint = + fingerprintJson(llvm::json::Value(std::move(providers))); + schemaSetFingerprint = fingerprintJson(llvm::json::Value(std::move(schemas))); + profileFingerprint = fingerprintJson(llvm::json::Value(options.profile)); + toolchainFingerprint = fingerprintJson(llvm::json::Value(options.target)); + if (providerFingerprint.empty() || schemaSetFingerprint.empty() || + profileFingerprint.empty() || toolchainFingerprint.empty()) + return lowerError(input, "ACLOWER-FINGERPRINT", + "failed to derive canonical model fingerprints"); + + // Deterministic owner/runtime expansion over the planned structure. + llvm::SmallSet active; + expandModule(moduleIndexByName.lookup(rootName), + selectedSystem.getRootName().str(), active); + if (expansionCycle) + return lowerError(input, "ACLOWER-OWNERSHIP", + "module instantiation cycle cannot produce canonical " + "ACSim ownership order"); + const uint64_t maxExpandedRows = + options.maxExpandedRows != 0 ? options.maxExpandedRows : kMaxExpandedRows; + if (constructionOrder.size() > maxExpandedRows || + runtimeRows.size() > maxExpandedRows) + return lowerError(input, "ACLOWER-DISPATCH", + "expanded hierarchy exceeds the capability bound"); + if (llvm::any_of(constructionOrder, + [&](const std::string &path) { + return !frozenOwnerPaths.contains(path); + }) || + !frozenOwnerPaths.contains(selectedSystem.getRootName())) + return lowerError(input, "ACLOWER-OWNERSHIP", + "planned ACSim hierarchy paths do not exactly match " + "the frozen owner manifest"); + return mlir::success(); +} + +void ACIRToACSimPass::expandModule(unsigned moduleIndex, std::string pathPrefix, + llvm::SmallSet &active) { + ModulePlan &module = modules[moduleIndex]; + active.insert(moduleIndex); + for (size_t placementIndex = 0; placementIndex < module.placements.size(); + ++placementIndex) { + const PlacementPlan &placement = module.placements[placementIndex]; + auto elementPath = [&](llvm::ArrayRef indices) { + std::string path = pathPrefix; + path.push_back('.'); + path.append(placement.name); + llvm::raw_string_ostream stream(path); + for (int64_t index : indices) + stream << '[' << index << ']'; + return path; + }; + auto expandOne = [&](llvm::ArrayRef indices) { + std::string path = elementPath(indices); + constructionOrder.push_back(path); + if (placement.kind == PlacementPlan::Kind::Process || + placement.targetIsBinding) { + RuntimeRow row; + row.moduleIndex = moduleIndex; + row.placementIndex = static_cast(placementIndex); + row.contextPath = pathPrefix; + row.path = path; + row.indices.assign(indices.begin(), indices.end()); + runtimeRows.push_back(std::move(row)); + return; + } + unsigned targetIndex = moduleIndexByName.lookup(placement.targetSymbol); + if (active.contains(targetIndex)) { + // An instantiation cycle can never produce canonical ACSim. + expansionCycle = true; + constructionOrder.pop_back(); + return; + } + expandModule(targetIndex, std::move(path), active); + }; + + if (placement.kind == PlacementPlan::Kind::Array) { + uint64_t volume = 1; + for (int64_t extent : placement.shape) { + if (extent == 0) { + volume = 0; + break; + } + volume *= static_cast(extent); + } + for (uint64_t ordinal = 0; ordinal < volume; ++ordinal) { + llvm::SmallVector indices(placement.shape.size(), 0); + uint64_t remainder = ordinal; + for (size_t dimension = placement.shape.size(); dimension > 0; + --dimension) { + uint64_t extent = + static_cast(placement.shape[dimension - 1]); + indices[dimension - 1] = static_cast(remainder % extent); + remainder /= extent; + } + expandOne(indices); + } + continue; + } + expandOne({}); + } + active.erase(moduleIndex); +} + +// --------------------------------------------------------------------------- +// Emission +// --------------------------------------------------------------------------- + +void appendOccurrenceKey(llvm::raw_ostream &stream, + const ProcessOccurrenceId &occurrence) { + stream << static_cast(occurrence.kind()) << ':'; + switch (occurrence.kind()) { + case ProcessOccurrenceKind::Original: { + const ProcessOriginalOccurrence &original = occurrence.original(); + stream << original.operationPath() << '['; + for (const ProcessCallSitePlan &callSite : original.callSites()) { + stream << callSite.operationPath() << '('; + llvm::interleaveComma(callSite.iterationVector(), stream); + stream << ")"; + } + stream << "]("; + llvm::interleaveComma(original.iterationVector(), stream); + stream << ')'; + break; + } + case ProcessOccurrenceKind::SyntheticLoop: + appendOccurrenceKey(stream, occurrence.syntheticLoop().anchor()); + stream << ":loop:" + << static_cast(occurrence.syntheticLoop().phase()); + break; + case ProcessOccurrenceKind::SyntheticWrapper: + appendOccurrenceKey(stream, occurrence.syntheticWrapper().anchor()); + stream << ":wrapper:" << occurrence.syntheticWrapper().transition().value() + << ':' << occurrence.syntheticWrapper().slot().value() << ':' + << static_cast(occurrence.syntheticWrapper().direction()); + break; + case ProcessOccurrenceKind::SyntheticConstant: + appendOccurrenceKey(stream, occurrence.syntheticConstant().anchor()); + stream << ":constant:" << occurrence.syntheticConstant().constant(); + break; + } +} + +std::string plannedValueKey(const ProcessPlannedValue &value) { + std::string key; + llvm::raw_string_ostream stream(key); + stream << static_cast(value.kind()) << ':'; + switch (value.kind()) { + case ProcessPlannedValueKind::Original: + appendOccurrenceKey(stream, value.original().occurrence()); + stream << ':' << value.original().coordinate().ownerPath() << ':' + << value.original().coordinate().index(); + break; + case ProcessPlannedValueKind::Capture: + stream << value.capture().capture().value(); + break; + case ProcessPlannedValueKind::LiveSlot: + stream << value.liveSlot().slot().value(); + break; + case ProcessPlannedValueKind::Synthetic: + appendOccurrenceKey(stream, value.synthetic().occurrence()); + stream << ':' << value.synthetic().coordinate().ownerPath() << ':' + << value.synthetic().coordinate().index(); + break; + case ProcessPlannedValueKind::Constant: + stream << value.constant().value(); + break; + } + return key; +} + +void ACIRToACSimPass::emitProcessBody( + OpBuilder &builder, const PlacementPlan &placement, + const llvm::DenseMap &moduleValues) { + MLIRContext *context = builder.getContext(); + const ProcessStatePlan *plan = + processPlans->lookupByDefinitionKey(placement.processDefinitionKey); + assert(plan && "process placement must reference its validated public plan"); + + llvm::SmallVector pcs; + for (const ProcessPcPlan &pc : plan->pcs()) + pcs.push_back(FlatSymbolRefAttr::get(context, pc.name())); + llvm::SmallVector liveSlots; + for (const ProcessLiveSlotPlan &slot : plan->liveSlots()) { + llvm::StringRef identity = valueTypeIdentities[slot.storageType().value()]; + auto type = acsim::ValueType::get( + context, + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(identity))); + liveSlots.push_back(builder.getDictionaryAttr( + {builder.getNamedAttr("name", builder.getStringAttr(slot.name())), + builder.getNamedAttr("type", TypeAttr::get(type))})); + } + llvm::SmallVector captures; + llvm::SmallVector captureNames; + for (const ProcessCapturePlan &capture : plan->captures()) { + Value lowered = moduleValues.lookup(capture.operand()); + assert(lowered && "validated process capture must be projected"); + captures.push_back(lowered); + captureNames.push_back(builder.getStringAttr(capture.name())); + } + auto process = acsim::ProcessOp::create( + builder, placement.process->getLoc(), captures, placement.name, + builder.getArrayAttr(captureNames), plan->pcs().front().name(), + builder.getArrayAttr(pcs), builder.getArrayAttr(liveSlots), + placement.fairnessCap, placement.specialization, plan->pcs().size()); + + llvm::SmallVector blocks(plan->blocks().size()); + for (const ProcessPcPlan &pc : plan->pcs()) { + Region &state = process.getStates()[pc.id().value()]; + for (ProcessBlockId id : pc.blocks()) { + Block *block = new Block(); + state.push_back(block); + blocks[id.value()] = block; + } + Block *entry = blocks[pc.blocks().front().value()]; + for (Value capture : captures) + entry->addArgument(capture.getType(), placement.process->getLoc()); + } + + OpBuilder::InsertionGuard guard(builder); + llvm::SmallVector> valuesByPc(plan->pcs().size()); + for (const ProcessPcPlan &pc : plan->pcs()) { + Block *entry = blocks[pc.blocks().front().value()]; + auto &values = valuesByPc[pc.id().value()]; + for (auto [capture, argument] : + llvm::zip_equal(plan->captures(), entry->getArguments())) { + std::string key = std::to_string(static_cast( + ProcessPlannedValueKind::Capture)) + + ":" + std::to_string(capture.id().value()); + values[key] = argument; + } + } + for (const ProcessBlockPlan &blockPlan : plan->blocks()) { + Block *block = blocks[blockPlan.id().value()]; + builder.setInsertionPointToStart(block); + auto &values = valuesByPc[blockPlan.pc().value()]; + + for (const ProcessTransitionLoadPlan &load : blockPlan.loads()) { + const ProcessLiveSlotPlan &slot = plan->liveSlots()[load.slot().value()]; + llvm::StringRef valueIdentity = + valueTypeIdentities[slot.storageType().value()]; + auto storedType = acsim::ValueType::get( + context, FlatSymbolRefAttr::get( + context, typeSymbols.symbolFor(valueIdentity))); + auto loaded = + acsim::LiveLoadOp::create(builder, placement.process->getLoc(), + storedType, placement.name, slot.name()); + for (const ProcessPlannedValue &planned : load.replacements()) + values[plannedValueKey(planned)] = loaded.getResult(); + } + + llvm::StringSet<> requiredValues; + const ProcessControlEdgePlan &edge = blockPlan.edge(); + if (edge.kind() == ProcessControlEdgeKind::Branch) + requiredValues.insert(plannedValueKey(edge.condition())); + if (edge.kind() == ProcessControlEdgeKind::Suspend) { + const ProcessTransitionPlan &transition = + plan->transitions()[edge.transition().value()]; + for (const ProcessTransitionStorePlan &store : transition.stores()) + requiredValues.insert(plannedValueKey(store.source())); + } + for (const ProcessActionPlan &action : blockPlan.actions()) + if (action.emission() != ProcessEmissionClass::ForwardOnly) + for (const ProcessPlannedValue &operand : action.operands()) + requiredValues.insert(plannedValueKey(operand)); + + for (const ProcessActionPlan &action : blockPlan.actions()) { + if (isa_and_nonnull(action.sourceOperation())) + continue; + bool resultRequired = + action.emission() != ProcessEmissionClass::ForwardOnly; + for (const ProcessPlannedValue &result : action.results()) + resultRequired |= requiredValues.contains(plannedValueKey(result)); + if (!resultRequired) { + if (action.kind() == ProcessActionKind::ForInitialize && + action.operands().size() == action.results().size()) + for (auto [result, operand] : + llvm::zip_equal(action.results(), action.operands())) + if (auto found = values.find(plannedValueKey(operand)); + found != values.end()) + values[plannedValueKey(result)] = found->second; + continue; + } + + llvm::SmallVector operands; + bool operandsComplete = true; + for (const ProcessPlannedValue &operand : action.operands()) { + auto found = values.find(plannedValueKey(operand)); + if (found == values.end()) { + operandsComplete = false; + break; + } + operands.push_back(found->second); + } + if (!operandsComplete) + continue; + + llvm::SmallVector results; + if (action.kind() == ProcessActionKind::Constant) { + int64_t value = static_cast( + action.occurrence().syntheticConstant().constant()); + results.push_back(index::ConstantOp::create( + builder, placement.process->getLoc(), value)); + } else if (action.emission() == ProcessEmissionClass::Inline || + action.emission() == ProcessEmissionClass::Wrap || + action.emission() == ProcessEmissionClass::Unwrap) { + llvm::StringRef identity = + generatedCalleeIdentities[action.callee()->value()]; + Type resultType = action.resultTypes().front(); + if (action.emission() == ProcessEmissionClass::Wrap) { + ProcessLiveSlotId slot = + action.occurrence().syntheticWrapper().slot(); + llvm::StringRef valueIdentity = valueTypeIdentities + [plan->liveSlots()[slot.value()].storageType().value()]; + resultType = acsim::ValueType::get( + context, FlatSymbolRefAttr::get( + context, typeSymbols.symbolFor(valueIdentity))); + } + results.push_back( + acsim::InlineOp::create( + builder, placement.process->getLoc(), resultType, operands, + FlatSymbolRefAttr::get(context, + typeSymbols.symbolFor(identity))) + .getResult()); + } else if (action.emission() == ProcessEmissionClass::Invoke) { + llvm::StringRef identity = + generatedCalleeIdentities[action.callee()->value()]; + auto invoke = acsim::InvokeOp::create( + builder, placement.process->getLoc(), action.resultTypes(), + operands, + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(identity))); + results.append(invoke.getResults().begin(), invoke.getResults().end()); + } else { + llvm::StringRef operationName = + action.scalarOp() + ? action.scalarOp()->name() + : action.sourceOperation()->getName().getStringRef(); + OperationState state(placement.process->getLoc(), operationName); + state.addOperands(operands); + state.addTypes(action.resultTypes()); + if (action.sourceOperation()) + state.addAttributes(action.sourceOperation()->getAttrs()); + else if (action.scalarOp()) + for (const ProcessScalarAttribute &attribute : + action.scalarOp()->attributes()) + if (attribute.name() == "predicate") + state.addAttribute("predicate", builder.getI64IntegerAttr(2)); + Operation *emitted = builder.create(state); + results.append(emitted->getResults().begin(), + emitted->getResults().end()); + } + for (auto [planned, result] : llvm::zip_equal(action.results(), results)) + values[plannedValueKey(planned)] = result; + } + + if (edge.kind() == ProcessControlEdgeKind::Branch) { + auto condition = values.find(plannedValueKey(edge.condition())); + assert(condition != values.end() && + "validated branch condition must be emitted"); + cf::CondBranchOp::create(builder, placement.process->getLoc(), + condition->second, + blocks[edge.trueBlock().value()], ValueRange{}, + blocks[edge.falseBlock().value()], ValueRange{}); + continue; + } + if (edge.kind() == ProcessControlEdgeKind::LocalContinue) { + cf::BranchOp::create(builder, placement.process->getLoc(), + blocks[edge.targetBlock().value()]); + continue; + } + if (edge.kind() == ProcessControlEdgeKind::Terminate) { + acsim::TerminateOp::create( + builder, placement.process->getLoc(), + edge.status() == ProcessTerminateStatus::Success ? "success" + : "failure"); + continue; + } + + const ProcessTransitionPlan &transition = + plan->transitions()[edge.transition().value()]; + for (const ProcessTransitionStorePlan &store : transition.stores()) { + const ProcessLiveSlotPlan &slot = plan->liveSlots()[store.slot().value()]; + auto source = values.find(plannedValueKey(store.source())); + assert(source != values.end() && + "validated live store source must be emitted"); + Value stored = source->second; + llvm::StringRef valueIdentity = + valueTypeIdentities[slot.storageType().value()]; + auto storedType = acsim::ValueType::get( + context, FlatSymbolRefAttr::get( + context, typeSymbols.symbolFor(valueIdentity))); + assert(stored.getType() == storedType && + "validated scalar wrapper must produce the live storage type"); + acsim::LiveStoreOp::create(builder, placement.process->getLoc(), stored, + placement.name, slot.name()); + } + const ProcessWakePlan &wakePlan = plan->wakes()[transition.wake().value()]; + llvm::StringRef typeIdentity = wakePlan.typeKey(); + typeIdentity.consume_front("@"); + auto wakeType = acsim::WakeType::get( + context, + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(typeIdentity))); + llvm::StringRef calleeIdentity = + generatedCalleeIdentities[wakePlan.callee().value()]; + auto wake = acsim::InvokeOp::create( + builder, placement.process->getLoc(), TypeRange{wakeType}, ValueRange{}, + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(calleeIdentity))); + acsim::SuspendOp::create( + builder, placement.process->getLoc(), wake.getResults().front(), + FlatSymbolRefAttr::get( + context, plan->pcs()[transition.targetPc().value()].name())); + } +} + +void ACIRToACSimPass::emitModuleBody(OpBuilder &builder, + const ModulePlan &planned) { + MLIRContext *context = builder.getContext(); + llvm::SmallVector portRecords; + llvm::SmallVector resultRecords; + llvm::SmallVector exports; + auto reference = [&](llvm::StringRef identity) { + return FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(identity)); + }; + for (const ModulePortPlan &port : planned.ports) { + const bindings::PortBinding &metadata = port.metadata; + portRecords.push_back(builder.getDictionaryAttr( + {builder.getNamedAttr("accessor", reference(metadata.accessor)), + builder.getNamedAttr("cardinality", + builder.getStringAttr(metadata.cardinality)), + builder.getNamedAttr("delegation", + builder.getStringAttr(metadata.delegation)), + builder.getNamedAttr("direction", + builder.getStringAttr(metadata.direction)), + builder.getNamedAttr("interface", reference(metadata.interface)), + builder.getNamedAttr("name", builder.getStringAttr(port.name)), + builder.getNamedAttr("ownership", + builder.getStringAttr(metadata.ownership)), + builder.getNamedAttr("payload", reference(metadata.payload)), + builder.getNamedAttr("protocol", reference(metadata.protocol)), + builder.getNamedAttr("role", reference(metadata.role)), + builder.getNamedAttr("time_domain", reference(metadata.timeDomain))})); + exports.push_back(FlatSymbolRefAttr::get(context, port.name)); + } + for (const ModuleResultPlan &result : planned.results) { + resultRecords.push_back(builder.getDictionaryAttr( + {builder.getNamedAttr( + "cpp_type", FlatSymbolRefAttr::get( + context, typeSymbols.symbolFor(result.cppType))), + builder.getNamedAttr("name", builder.getStringAttr(result.name))})); + exports.push_back(FlatSymbolRefAttr::get(context, result.name)); + } + DictionaryAttr interface = builder.getDictionaryAttr( + {builder.getNamedAttr("ports", builder.getArrayAttr(portRecords)), + builder.getNamedAttr("resources", builder.getArrayAttr({})), + builder.getNamedAttr("results", builder.getArrayAttr(resultRecords))}); + auto module = acsim::ModuleOp::create( + builder, planned.source->getLoc(), builder.getStringAttr(planned.name), + interface, planned.staticParams, + builder.getStringAttr(planned.specialization), + builder.getArrayAttr(exports)); + Block *body = new Block(); + module.getBody().push_back(body); + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart(body); + llvm::DenseMap emittedValues; + llvm::SmallVector owners(planned.placements.size()); + llvm::SmallVector> inputProjections( + planned.placements.size()); + + // Rank 0: owned placements. + for (auto [placementIndex, placement] : llvm::enumerate(planned.placements)) { + switch (placement.kind) { + case PlacementPlan::Kind::Instance: { + auto target = SymbolRefAttr::get(context, placement.targetSymbol); + auto ownerType = acsim::OwnerType::get(context, target); + owners[placementIndex] = + acsim::InstanceOp::create( + builder, planned.source->getLoc(), ownerType, + builder.getStringAttr(placement.name), target, + placement.staticArgs, + builder.getStringAttr(placement.specialization)) + .getResult(); + break; + } + case PlacementPlan::Kind::Array: { + auto target = SymbolRefAttr::get(context, placement.targetSymbol); + auto ownerType = acsim::OwnerType::get(context, target); + auto shape = builder.getDenseI64ArrayAttr(placement.shape); + auto arrayType = acsim::ArrayType::get(context, shape, ownerType); + owners[placementIndex] = + acsim::ArrayOp::create( + builder, planned.source->getLoc(), arrayType, + builder.getStringAttr(placement.name), target, + placement.staticArgs, + builder.getStringAttr(placement.specialization), shape) + .getResult(); + break; + } + case PlacementPlan::Kind::Process: + break; + } + } + + auto emitPort = [&](Value base, const PortEndpointPlan &endpoint) { + const bindings::PortBinding &port = endpoint.metadata; + auto type = acsim::PortType::get( + context, + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(port.interface)), + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(port.role)), + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(port.payload)), + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(port.protocol))); + return acsim::PortOp::create( + builder, planned.source->getLoc(), type, base, + FlatSymbolRefAttr::get(context, + typeSymbols.symbolFor(port.accessor))) + .getResult(); + }; + + // Rank 2: exact typed endpoint projections. + for (auto [placementIndex, placement] : llvm::enumerate(planned.placements)) { + if (!owners[placementIndex]) + continue; + for (const PortEndpointPlan &endpoint : placement.outputPorts) + emittedValues[endpoint.value] = + emitPort(owners[placementIndex], endpoint); + for (const PortEndpointPlan &endpoint : placement.inputPorts) + inputProjections[placementIndex].push_back( + emitPort(owners[placementIndex], endpoint)); + } + + // Rank 3: ACIR SSA endpoint uses become exact construction-time binds. + for (auto [placementIndex, placement] : llvm::enumerate(planned.placements)) + for (auto [inputIndex, endpoint] : llvm::enumerate(placement.inputPorts)) { + Value source = emittedValues.lookup(endpoint.value); + assert(source && "validated endpoint producer must be projected"); + acsim::BindOp::create(builder, planned.source->getLoc(), source, + inputProjections[placementIndex][inputIndex], + builder.getStringAttr("port")); + } + + // Rank 4: pure binding calls. Static constructor arguments specialize the + // binding and therefore do not become dynamic acsim.inline operands. + for (const PureCallPlan &call : planned.pureCalls) { + auto resultType = acsim::ExprType::get( + context, + FlatSymbolRefAttr::get(context, typeSymbols.symbolFor(call.cppType))); + auto inlineOp = acsim::InlineOp::create( + builder, call.source->getLoc(), resultType, ValueRange{}, + FlatSymbolRefAttr::get(context, call.binding)); + emittedValues[call.result] = inlineOp.getResult(); + } + + // Rank 6: ordered endpoint and scalar exports. + llvm::SmallVector returned; + for (const ModulePortPlan &port : planned.ports) { + Value value = emittedValues.lookup(port.source); + assert(value && "validated module port producer must be emitted"); + auto exportOp = acsim::ExportOp::create( + builder, planned.source->getLoc(), value.getType(), value, + builder.getStringAttr(port.name), reference(port.metadata.role)); + returned.push_back(exportOp.getResult()); + } + llvm::StringRef resultRole = typeSymbols.symbolFor(kResultRoleIdentity); + for (const ModuleResultPlan &result : planned.results) { + Value value = emittedValues.lookup(result.source); + assert(value && "validated module result producer must be emitted"); + auto exportOp = acsim::ExportOp::create( + builder, planned.source->getLoc(), value.getType(), value, + builder.getStringAttr(result.name), + FlatSymbolRefAttr::get(context, resultRole)); + returned.push_back(exportOp.getResult()); + } + + // Rank 8: stateful processes. + for (const PlacementPlan &placement : planned.placements) + if (placement.kind == PlacementPlan::Kind::Process) + emitProcessBody(builder, placement, emittedValues); + + acsim::ReturnOp::create(builder, planned.source->getLoc(), returned); +} + +mlir::FailureOr> +ACIRToACSimPass::emit(mlir::ModuleOp input) { + MLIRContext *context = input.getContext(); + mlir::OwningOpRef staged = + mlir::ModuleOp::create(input.getLoc()); + (*staged)->setAttr("ac.contract_epoch", input->getAttr("ac.contract_epoch")); + OpBuilder builder(context); + builder.setInsertionPointToEnd(staged->getBody()); + + llvm::SmallVector construction; + llvm::SmallVector destructionAttrs; + for (const std::string &path : constructionOrder) + construction.push_back(builder.getStringAttr(path)); + for (auto it = constructionOrder.rbegin(); it != constructionOrder.rend(); + ++it) + destructionAttrs.push_back(builder.getStringAttr(*it)); + + DictionaryAttr fingerprints = builder.getDictionaryAttr( + {builder.getNamedAttr("frozen_acir", + builder.getStringAttr(frozenAcirFingerprint)), + builder.getNamedAttr("binding_lock", + builder.getStringAttr(bindingLockFingerprint)), + builder.getNamedAttr("provider", + builder.getStringAttr(providerFingerprint)), + builder.getNamedAttr("profile", + builder.getStringAttr(profileFingerprint)), + builder.getNamedAttr("toolchain", + builder.getStringAttr(toolchainFingerprint)), + builder.getNamedAttr("schema_set", + builder.getStringAttr(schemaSetFingerprint))}); + + auto model = acsim::ModelOp::create( + builder, input.getLoc(), + builder.getStringAttr(selectedSystem.getSymName()), + builder.getStringAttr(kEpoch), + FlatSymbolRefAttr::get(context, selectedSystem.getRoot()), + builder.getArrayAttr(construction), + builder.getArrayAttr(destructionAttrs), fingerprints); + + Block *modelBody = new Block(); + model.getBody().push_back(modelBody); + builder.setInsertionPointToStart(modelBody); + + // Rank 0: acsim.type declarations, strictly symbol-sorted. + for (const TypeDeclaration *declaration : typeSymbols.declarations()) { + IntegerAttr period; + IntegerAttr phase; + IntegerAttr tickScale; + FlatSymbolRefAttr parent; + DictionaryAttr bridge; + if (declaration->period) { + period = builder.getI64IntegerAttr(*declaration->period); + phase = builder.getI64IntegerAttr(declaration->phase); + tickScale = builder.getI64IntegerAttr(declaration->tickScale); + } + if (declaration->parent) + parent = FlatSymbolRefAttr::get( + context, typeSymbols.symbolFor(*declaration->parent)); + if (declaration->bridgeKind) + bridge = builder.getDictionaryAttr( + {builder.getNamedAttr( + "kind", builder.getStringAttr(*declaration->bridgeKind)), + builder.getNamedAttr( + "owner", + FlatSymbolRefAttr::get(context, *declaration->bridgeOwner))}); + acsim::TypeOp::create(builder, input.getLoc(), + builder.getStringAttr(declaration->symbol), + builder.getStringAttr(declaration->cpp), + builder.getStringAttr(declaration->kind), + builder.getStringAttr(declaration->fingerprint), + period, phase, tickScale, parent, bridge); + } + + // Rank 1: acsim.binding records, strictly symbol-sorted. + { + llvm::SmallVector records; + for (const bindings::ResolvedBinding &selection : resolution->selections()) + records.push_back(&selection.record()); + llvm::sort(records, [](const bindings::BindingRecord *left, + const bindings::BindingRecord *right) { + return left->binding() < right->binding(); + }); + for (const bindings::BindingRecord *record : records) + acsim::BindingOp::create(builder, input.getLoc(), + builder.getStringAttr(record->binding()), + cast(convertBindingRecord( + builder, *record, typeSymbols))); + } + + // Rank 2: acsim.module declarations, child-before-parent with + // symbol-sorted ties between independent nodes. + for (const ModulePlan &planned : modules) + emitModuleBody(builder, planned); + + // Rank 3: one typed dispatch row per runtime object, dense IDs. + llvm::SmallVector dispatches; + for (auto [id, row] : llvm::enumerate(runtimeRows)) { + const ModulePlan &module = modules[row.moduleIndex]; + const PlacementPlan &placement = module.placements[row.placementIndex]; + auto target = + SymbolRefAttr::get(context, module.name, + {FlatSymbolRefAttr::get(context, placement.name)}); + std::string work = placement.work; + std::string xfer = placement.xfer; + std::string reset = placement.reset; + std::string validate = placement.validate; + if (placement.kind == PlacementPlan::Kind::Process) { + std::string base = + ("acsim_generated::" + module.name + "::s" + + module.specialization.substr(7) + "::" + placement.name + "::p" + + placement.specialization.substr(7) + "::"); + work = base + "work"; + xfer = base + "xfer"; + reset = base + "reset"; + validate = base + "validate"; + } + dispatches.push_back(acsim::DispatchOp::create( + builder, input.getLoc(), acsim::ObjectIdType::get(context), + acsim::ActivationIdType::get(context), target, + builder.getStringAttr(row.path), + builder.getDenseI64ArrayAttr(row.indices), + builder.getI64IntegerAttr(static_cast(id)), + builder.getI64IntegerAttr(static_cast(id)), + builder.getStringAttr(work), builder.getStringAttr(xfer), + builder.getStringAttr(reset), builder.getStringAttr(validate))); + } + + // Rank 4: static activation adjacency. Every runtime object has its self + // wake, and each typed construction bind adds source-object -> target-object + // within the same expanded module context. + std::set> activationEdges; + for (unsigned id = 0; id < dispatches.size(); ++id) + activationEdges.emplace(id, id); + for (auto [moduleIndex, module] : llvm::enumerate(modules)) + for (const BindingEdgePlan &edge : module.bindingEdges) + for (auto [sourceId, source] : llvm::enumerate(runtimeRows)) { + if (source.moduleIndex != moduleIndex || + source.placementIndex != edge.sourcePlacement) + continue; + for (auto [targetId, target] : llvm::enumerate(runtimeRows)) + if (target.moduleIndex == moduleIndex && + target.placementIndex == edge.targetPlacement && + target.contextPath == source.contextPath) + activationEdges.emplace(sourceId, targetId); + } + for (auto [source, target] : activationEdges) + acsim::ActivateOp::create(builder, input.getLoc(), + dispatches[source].getActivation(), + dispatches[target].getObject()); + + if (failed(mlir::verify(*staged)) || + failed(acsim::verifyCanonicalACSimFile(*staged))) + return mlir::failure(); + return staged; +} + +void ACIRToACSimPass::publish(mlir::ModuleOp input, mlir::ModuleOp staged) { + Operation *model = &staged.getBody()->front(); + model->remove(); + + llvm::SmallVector obsolete; + for (Operation &operation : *input.getBody()) + obsolete.push_back(&operation); + for (Operation *operation : obsolete) + operation->erase(); + input.getBody()->push_back(model); + + llvm::SmallVector retained; + for (NamedAttribute attribute : input->getAttrs()) + if (attribute.getName() == "ac.contract_epoch") + retained.push_back(attribute); + input->setAttrs(retained); +} + +mlir::LogicalResult ACIRToACSimPass::lower(mlir::ModuleOp input) { + if (failed(plan(input))) + return mlir::failure(); + auto staged = emit(input); + if (failed(staged)) + return mlir::failure(); + publish(input, **staged); + return mlir::success(); +} + +} // namespace + +std::unique_ptr +createACIRToACSimPass(ACIRToACSimPassOptions options) { + return std::make_unique(std::move(options)); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Conversion/ACIRToACSim/CMakeLists.txt b/components/agentic-circuit/lib/Conversion/ACIRToACSim/CMakeLists.txt new file mode 100644 index 00000000..4149d015 --- /dev/null +++ b/components/agentic-circuit/lib/Conversion/ACIRToACSim/CMakeLists.txt @@ -0,0 +1,23 @@ +add_acir_library(ACIRToACSim SOURCES + ACIRToACSim.cpp +) +add_dependencies(ACIRToACSim ACIRTransformsIncGen ACIRDialectIncGen) +target_link_libraries(ACIRToACSim PUBLIC + ACIRAnalysis + ACIRBindings + ACIRTransforms + ACIRDialect + ACSimDialect + MLIRIndexDialect + MLIRPass + MLIRTransformUtils +) +target_include_directories(ACIRToACSim PUBLIC + "$" + "$" + "$" +) +target_include_directories(ACIRToACSim PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +add_library(AgenticCircuit::ACIRToACSim ALIAS ACIRToACSim) diff --git a/components/agentic-circuit/lib/Conversion/CMakeLists.txt b/components/agentic-circuit/lib/Conversion/CMakeLists.txt new file mode 100644 index 00000000..3f7b9a1d --- /dev/null +++ b/components/agentic-circuit/lib/Conversion/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(ACIRToACSim) diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ACIRDialect.cpp b/components/agentic-circuit/lib/Dialect/ACIR/ACIRDialect.cpp new file mode 100644 index 00000000..dd0b510a --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ACIRDialect.cpp @@ -0,0 +1,3 @@ +#include "acir/Dialect/ACIR/ACIRDialect.h" + +#include "acir/Dialect/ACIR/ACIRDialect.cpp.inc" diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ACIRInterfaces.cpp b/components/agentic-circuit/lib/Dialect/ACIR/ACIRInterfaces.cpp new file mode 100644 index 00000000..e9e46d4c --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ACIRInterfaces.cpp @@ -0,0 +1,3 @@ +#include "acir/Dialect/ACIR/ACIROps.h" + +#include "acir/Dialect/ACIR/ACIROpInterfaces.cpp.inc" diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ACIROps.cpp b/components/agentic-circuit/lib/Dialect/ACIR/ACIROps.cpp new file mode 100644 index 00000000..24e6dd40 --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ACIROps.cpp @@ -0,0 +1,3995 @@ +#include "acir/Dialect/ACIR/ACIROps.h" +#include "ACIROpsTestHooks.h" +#include "ProcessLowerability.h" +#include "acir/Dialect/ACIR/ACIRResources.h" +#include "acir/Dialect/ACIR/GraphRegion.h" + +#include "mlir/Dialect/DLTI/DLTI.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/Interfaces/FunctionImplementation.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/ADT/TypeSwitch.h" + +#include + +using namespace mlir; + +namespace acir::ac { +namespace { + +thread_local detail::ProcessLivenessWork *processLivenessWorkCollector = + nullptr; + +} // namespace + +LogicalResult TransformOp::verify() { + if (getInputs().empty()) + return emitOpError("requires at least one input queue"); + if (getOutputs().empty()) + return emitOpError("requires at least one output queue"); + + ArrayRef depths = getOutputDepthsAttr().asArrayRef(); + ArrayRef latencies = getOutputLatenciesAttr().asArrayRef(); + if (depths.size() != getOutputs().size()) + return emitOpError("output depth count must match result count"); + if (latencies.size() != getOutputs().size()) + return emitOpError("output latency count must match result count"); + if (llvm::any_of(depths, [](int64_t value) { return value <= 0; })) + return emitOpError("output depths must be positive"); + if (llvm::any_of(latencies, [](int64_t value) { return value <= 0; })) + return emitOpError("output latencies must be positive"); + + Block &block = getBody().front(); + if (block.getNumArguments() != getInputs().size()) + return emitOpError("body argument count must match input queue count"); + for (size_t index = 0; index < getInputs().size(); ++index) { + Value input = getInputs()[index]; + BlockArgument argument = block.getArgument(index); + auto queue = cast(input.getType()); + Type expected = VarType::get(getContext(), queue.getElementType()); + if (argument.getType() != expected) + return emitOpError() << "body argument " << index << " must be " + << expected; + } + + for (Operation &operation : block.without_terminator()) { + if (!isMemoryEffectFree(&operation)) + return emitOpError() << "body operation '" << operation.getName() + << "' must be pure"; + } + + auto yield = dyn_cast(block.getTerminator()); + if (!yield) + return emitOpError("body must terminate with ac.transform.yield"); + if (yield.getValues().size() != getOutputs().size()) + return emitOpError("yielded value count must match output queue count"); + for (size_t index = 0; index < getOutputs().size(); ++index) { + Value output = getOutputs()[index]; + Value value = yield.getValues()[index]; + auto queue = cast(output.getType()); + Type expected = VarType::get(getContext(), queue.getElementType()); + if (value.getType() != expected) + return emitOpError() << "yielded value " << index << " must be " + << expected; + } + return success(); +} + +LogicalResult SourceOp::verify() { + if (getDepth() <= 0) + return emitOpError("depth must be positive"); + if (getLatency() <= 0) + return emitOpError("latency must be positive"); + return success(); +} + +LogicalResult ObserveOp::verify() { + if (getName().empty()) + return emitOpError("name must be non-empty"); + return success(); +} + +LogicalResult ExpectOp::verify() { + if (getMessage().empty()) + return emitOpError("message must be non-empty"); + Block &block = getPredicate().front(); + Type payload = cast(getInput().getType()).getElementType(); + Type expected = VarType::get(getContext(), payload); + if (block.getNumArguments() != 1 || + block.getArgument(0).getType() != expected) + return emitOpError("predicate argument must match queue payload Var"); + for (Operation &operation : block.without_terminator()) + if (!isMemoryEffectFree(&operation)) + return emitOpError() << "predicate operation '" << operation.getName() + << "' must be pure"; + auto yield = dyn_cast(block.getTerminator()); + if (!yield || !cast(yield.getCondition().getType()) + .getElementType() + .isInteger(1)) + return emitOpError("predicate must terminate with an i1 ac.expect.yield"); + return success(); +} + +LogicalResult BroadcastOp::verify() { + if (getOutputs().size() < 2) + return emitOpError("requires at least two output queues"); + for (auto [index, output] : llvm::enumerate(getOutputs())) + if (output.getType() != getInput().getType()) + return emitOpError() << "output queue " << index + << " must match input queue type"; + ArrayRef depths = getOutputDepthsAttr().asArrayRef(); + ArrayRef latencies = getOutputLatenciesAttr().asArrayRef(); + if (depths.size() != getOutputs().size()) + return emitOpError("output depth count must match result count"); + if (latencies.size() != getOutputs().size()) + return emitOpError("output latency count must match result count"); + if (llvm::any_of(depths, [](int64_t value) { return value <= 0; })) + return emitOpError("output depths must be positive"); + if (llvm::any_of(latencies, [](int64_t value) { return value <= 0; })) + return emitOpError("output latencies must be positive"); + return success(); +} + +LogicalResult ForkOp::verify() { + if (getOutputs().size() < 2) + return emitOpError("requires at least two output queues"); + for (auto [index, output] : llvm::enumerate(getOutputs())) + if (output.getType() != getInput().getType()) + return emitOpError() << "output queue " << index + << " must match input queue type"; + ArrayRef depths = getOutputDepthsAttr().asArrayRef(); + ArrayRef latencies = getOutputLatenciesAttr().asArrayRef(); + if (depths.size() != getOutputs().size() || + llvm::any_of(depths, [](int64_t value) { return value <= 0; })) + return emitOpError("output depths must match results and be positive"); + if (latencies.size() != getOutputs().size() || + llvm::any_of(latencies, [](int64_t value) { return value <= 0; })) + return emitOpError("output latencies must match results and be positive"); + return success(); +} + +LogicalResult RouteOp::verify() { + if (getOutputs().size() < 2) + return emitOpError("requires at least two output queues"); + for (auto [index, output] : llvm::enumerate(getOutputs())) + if (output.getType() != getInput().getType()) + return emitOpError() << "output queue " << index + << " must match input queue type"; + ArrayRef depths = getOutputDepthsAttr().asArrayRef(); + ArrayRef latencies = getOutputLatenciesAttr().asArrayRef(); + if (depths.size() != getOutputs().size() || + llvm::any_of(depths, [](int64_t value) { return value <= 0; })) + return emitOpError("output depths must match results and be positive"); + if (latencies.size() != getOutputs().size() || + llvm::any_of(latencies, [](int64_t value) { return value <= 0; })) + return emitOpError("output latencies must match results and be positive"); + Block &block = getSelector().front(); + if (block.getNumArguments() != 1 || + block.getArgument(0).getType() != + VarType::get(getContext(), + cast(getInput().getType()).getElementType())) + return emitOpError("selector argument must match input payload Var"); + auto yield = dyn_cast(block.getTerminator()); + if (!yield) + return emitOpError("selector must terminate with ac.route.yield"); + Type selector = cast(yield.getSelector().getType()).getElementType(); + if (!isa(selector)) + return emitOpError("selector must be an integer or enum Var"); + return success(); +} + +LogicalResult SelectOp::verify() { + if (getInputs().size() < 3) + return emitOpError("requires one control and at least two data queues"); + llvm::DenseSet uniqueInputs; + for (Value input : getInputs()) + if (!uniqueInputs.insert(input).second) + return emitOpError("input queue operands must be unique"); + for (auto [index, input] : llvm::enumerate(getInputs().drop_front())) + if (input.getType() != getOutput().getType()) + return emitOpError() << "data input queue " << index + << " must match output queue type"; + if (getDepth() <= 0 || getLatency() <= 0) + return emitOpError("depth and latency must be positive"); + + Block &block = getKey().front(); + Type controlPayload = + cast(getInputs().front().getType()).getElementType(); + Type expected = VarType::get(getContext(), controlPayload); + if (block.getNumArguments() != 1 || + block.getArgument(0).getType() != expected) + return emitOpError("key argument must match control queue payload Var"); + for (Operation &operation : block.without_terminator()) + if (!isMemoryEffectFree(&operation)) + return emitOpError() << "key operation '" << operation.getName() + << "' must be pure"; + auto yield = dyn_cast(block.getTerminator()); + if (!yield) + return emitOpError("key must terminate with ac.select.yield"); + Type selector = cast(yield.getSelector().getType()).getElementType(); + auto integer = dyn_cast(selector); + if (!integer || integer.getWidth() > 64) + return emitOpError("key must yield an integer Var with width at most 64"); + const uint64_t candidates = getInputs().size() - 1; + if (integer.getWidth() < 64 && + candidates > (uint64_t{1} << integer.getWidth())) + return emitOpError("data input count must fit selector width"); + return success(); +} + +LogicalResult MergeOp::verify() { + if (getInputs().size() < 2) + return emitOpError("requires at least two input queues"); + for (auto [index, input] : llvm::enumerate(getInputs())) + if (input.getType() != getOutput().getType()) + return emitOpError() << "input queue " << index + << " must match output queue type"; + if (getPolicy() != "round_robin" && getPolicy() != "priority") + return emitOpError("policy must be 'round_robin' or 'priority'"); + if (getDepth() <= 0 || getLatency() <= 0) + return emitOpError("depth and latency must be positive"); + return success(); +} + +LogicalResult BarrierOp::verify() { + if (getInputs().size() < 2) + return emitOpError("requires at least two input queues"); + if (getOutputs().size() != getInputs().size()) + return emitOpError("output count must match input count"); + llvm::DenseSet uniqueInputs; + for (auto [index, input] : llvm::enumerate(getInputs())) { + if (!uniqueInputs.insert(input).second) + return emitOpError("input queue operands must be unique"); + if (getOutputs()[index].getType() != input.getType()) + return emitOpError() << "output queue " << index + << " must match its input queue type"; + } + ArrayRef depths = getOutputDepthsAttr().asArrayRef(); + ArrayRef latencies = getOutputLatenciesAttr().asArrayRef(); + if (depths.size() != getOutputs().size() || + llvm::any_of(depths, [](int64_t value) { return value <= 0; })) + return emitOpError("output depths must match results and be positive"); + if (latencies.size() != getOutputs().size() || + llvm::any_of(latencies, [](int64_t value) { return value <= 0; })) + return emitOpError("output latencies must match results and be positive"); + return success(); +} + +LogicalResult ReorderOp::verify() { + if (getInput().getType() != getOutput().getType()) + return emitOpError("output queue must match input queue type"); + if (getCapacity() <= 0 || getDepth() <= 0 || getLatency() <= 0) + return emitOpError("capacity, depth, and latency must be positive"); + if (getStart() < 0) + return emitOpError("start must be non-negative"); + + Block &block = getKey().front(); + Type payload = cast(getInput().getType()).getElementType(); + Type expected = VarType::get(getContext(), payload); + if (block.getNumArguments() != 1 || + block.getArgument(0).getType() != expected) + return emitOpError("key argument must match queue payload Var"); + for (Operation &operation : block.without_terminator()) + if (!isMemoryEffectFree(&operation)) + return emitOpError() << "key operation '" << operation.getName() + << "' must be pure"; + auto yield = dyn_cast(block.getTerminator()); + if (!yield) + return emitOpError("key must terminate with ac.reorder.yield"); + auto key = cast(yield.getKey().getType()).getElementType(); + auto integer = dyn_cast(key); + if (!integer || integer.getWidth() > 64) + return emitOpError("key must be an integer Var with width at most 64"); + if (integer.getWidth() < 64 && + static_cast(getStart()) >= (uint64_t{1} << integer.getWidth())) + return emitOpError("start must fit key width"); + return success(); +} + +LogicalResult DependencyOp::verify() { + if (getInput().getType() != getOutput().getType()) + return emitOpError("output queue must match input queue type"); + if (getCapacity() <= 0 || getResources() <= 0 || getDepth() <= 0 || + getLatency() <= 0) + return emitOpError( + "capacity, resources, depth, and latency must be positive"); + if (getNoDependency() < 0) + return emitOpError("no_dependency must be non-negative"); + + Type payload = cast(getInput().getType()).getElementType(); + Type argumentType = VarType::get(getContext(), payload); + auto verifyPolicy = [&](Region ®ion, + StringRef name) -> FailureOr { + Block &block = region.front(); + if (block.getNumArguments() != 1 || + block.getArgument(0).getType() != argumentType) { + emitOpError() << name << " argument must match queue payload Var"; + return failure(); + } + for (Operation &operation : block.without_terminator()) + if (!isMemoryEffectFree(&operation)) { + emitOpError() << name << " operation '" << operation.getName() + << "' must be pure"; + return failure(); + } + auto yield = dyn_cast(block.getTerminator()); + if (!yield) { + emitOpError() << name << " must terminate with ac.dependency.yield"; + return failure(); + } + auto value = cast(yield.getValue().getType()).getElementType(); + auto integer = dyn_cast(value); + if (!integer || integer.getWidth() > 64) { + emitOpError() << name + << " must yield an integer Var with width at most 64"; + return failure(); + } + return integer; + }; + + FailureOr key = verifyPolicy(getKey(), "key"); + FailureOr dependency = verifyPolicy(getWaitsFor(), "waits_for"); + FailureOr resource = verifyPolicy(getResource(), "resource"); + FailureOr cost = verifyPolicy(getCost(), "cost"); + if (failed(key) || failed(dependency) || failed(resource) || failed(cost)) + return failure(); + if (*key != *dependency) + return emitOpError("key and waits_for must use the same integer Var type"); + if (dependency->getWidth() < 64 && + static_cast(getNoDependency()) >= + (uint64_t{1} << dependency->getWidth())) + return emitOpError("no_dependency must fit dependency width"); + if (resource->getWidth() < 64 && static_cast(getResources()) > + (uint64_t{1} << resource->getWidth())) + return emitOpError("resources must fit resource width"); + return success(); +} + +LogicalResult CreditOp::verify() { + if (getInput().getType() != getOutput().getType()) + return emitOpError("output queue must match input queue type"); + if (getCredits() <= 0 || getDepth() <= 0 || getLatency() <= 0) + return emitOpError("credits, depth, and latency must be positive"); + + Block &block = getCost().front(); + Type payload = cast(getInput().getType()).getElementType(); + Type expected = VarType::get(getContext(), payload); + if (block.getNumArguments() != 1 || + block.getArgument(0).getType() != expected) + return emitOpError("cost argument must match queue payload Var"); + for (Operation &operation : block.without_terminator()) + if (!isMemoryEffectFree(&operation)) + return emitOpError() << "cost operation '" << operation.getName() + << "' must be pure"; + auto yield = dyn_cast(block.getTerminator()); + if (!yield) + return emitOpError("cost must terminate with ac.credit.yield"); + Type cost = cast(yield.getCost().getType()).getElementType(); + auto integer = dyn_cast(cost); + if (!integer || integer.getWidth() > 64) + return emitOpError("cost must yield an integer Var with width at most 64"); + return success(); +} + +LogicalResult FeedbackOp::verify() { + if (getInput().getType() != getOutput().getType()) + return emitOpError("output queue must match input queue type"); + if (getDepth() <= 0 || getLatency() <= 0 || getMaxIterations() <= 0) + return emitOpError("depth, latency, and max_iterations must be positive"); + + Block &block = getBody().front(); + Type payload = cast(getInput().getType()).getElementType(); + Type expectedValue = VarType::get(getContext(), payload); + if (block.getNumArguments() != 1 || + block.getArgument(0).getType() != expectedValue) + return emitOpError("body argument must match queue payload Var"); + for (Operation &operation : block.without_terminator()) + if (!isMemoryEffectFree(&operation)) + return emitOpError() << "body operation '" << operation.getName() + << "' must be pure"; + auto yield = dyn_cast(block.getTerminator()); + if (!yield) + return emitOpError("body must terminate with ac.feedback.yield"); + if (yield.getValue().getType() != expectedValue) + return emitOpError("yielded value must match queue payload Var"); + if (yield.getContinueValue().getType() != + VarType::get(getContext(), IntegerType::get(getContext(), 1))) + return emitOpError("continue value must be !ac.var"); + return success(); +} + +LogicalResult ScopeOp::verify() { + Block &block = getBody().front(); + if (block.getNumArguments() != getInputs().size()) + return emitOpError("body argument count must match input queue count"); + for (size_t index = 0; index < getInputs().size(); ++index) + if (block.getArgument(index).getType() != getInputs()[index].getType()) + return emitOpError() << "body argument " << index + << " must match input queue type"; + auto yield = dyn_cast(block.getTerminator()); + if (!yield) + return emitOpError("body must terminate with ac.scope.yield"); + if (yield.getQueues().size() != getOutputs().size()) + return emitOpError("yielded queue count must match result count"); + for (size_t index = 0; index < getOutputs().size(); ++index) + if (yield.getQueues()[index].getType() != getOutputs()[index].getType()) + return emitOpError() << "yielded queue " << index + << " must match result type"; + return success(); +} + +LogicalResult QueuePeekOp::verify() { + auto queue = cast(getQueue().getType()); + Type expected = VarType::get(getContext(), queue.getElementType()); + if (getValue().getType() != expected) + return emitOpError() << "result must be " << expected; + return success(); +} + +LogicalResult QueuePopOp::verify() { + auto queue = cast(getQueue().getType()); + Type expected = VarType::get(getContext(), queue.getElementType()); + if (getValue().getType() != expected) + return emitOpError() << "result must be " << expected; + return success(); +} + +LogicalResult QueuePushOp::verify() { + auto queue = cast(getQueue().getType()); + Type expected = VarType::get(getContext(), queue.getElementType()); + if (getValue().getType() != expected) + return emitOpError() << "value must be " << expected; + return success(); +} + +LogicalResult FiringOp::verify() { + llvm::DenseSet listed; + for (Value queue : getQueues()) + if (!listed.insert(queue).second) + return emitOpError("queue operands must be unique"); + + llvm::DenseSet popped; + llvm::DenseSet pushed; + size_t stateEffects = 0; + for (Operation &operation : getBody().front()) { + Value queue; + bool isPop = false; + bool isPush = false; + if (auto peek = dyn_cast(operation)) { + queue = peek.getQueue(); + } else if (auto pop = dyn_cast(operation)) { + queue = pop.getQueue(); + isPop = true; + } else if (auto push = dyn_cast(operation)) { + queue = push.getQueue(); + isPush = true; + } else if (isa(operation)) { + continue; + } else if (isMemoryEffectFree(&operation)) { + continue; + } else { + return emitOpError() << "body operation '" << operation.getName() + << "' is not a queue effect or pure computation"; + } + + if (!listed.contains(queue)) + return emitOpError("queue effect references an unlisted firing operand"); + if (isPop) { + if (!popped.insert(queue).second) + return emitOpError("queue may be popped at most once per firing"); + ++stateEffects; + } + if (isPush) { + if (!pushed.insert(queue).second) + return emitOpError("queue may be pushed at most once per firing"); + ++stateEffects; + } + } + if (stateEffects == 0) + return emitOpError("requires at least one queue state effect"); + if (!isa(getBody().front().getTerminator())) + return emitOpError("body must terminate with ac.firing.yield"); + return success(); +} + +namespace detail { + +ScopedProcessLivenessWorkCollector::ScopedProcessLivenessWorkCollector( + ProcessLivenessWork &work) + : previous(processLivenessWorkCollector) { + work = {}; + processLivenessWorkCollector = &work; +} + +ScopedProcessLivenessWorkCollector::~ScopedProcessLivenessWorkCollector() { + processLivenessWorkCollector = previous; +} + +} // namespace detail + +namespace { + +struct NamedRef { + SymbolRefAttr name; + StringRef opName; +}; + +std::optional namedRef(Type type) { + return TypeSwitch>(type) + .Case([](auto type) { + return NamedRef{type.getName(), StructOp::getOperationName()}; + }) + .Case([](auto type) { + return NamedRef{type.getName(), PacketOp::getOperationName()}; + }) + .Case([](auto type) { + return NamedRef{type.getName(), TransactionOp::getOperationName()}; + }) + .Case([](auto type) { + return NamedRef{type.getName(), EnumOp::getOperationName()}; + }) + .Case([](auto type) { + return NamedRef{type.getName(), UnionOp::getOperationName()}; + }) + .Default([](Type) { return std::nullopt; }); +} + +Operation *lookup(Operation *from, SymbolRefAttr name) { + if (name.getNestedReferences().size() != 1) + return SymbolTable::lookupNearestSymbolFrom(from, name); + Operation *scope = nullptr; + if (auto enclosing = from->getParentOfType(); + enclosing && enclosing.getSymNameAttr() == name.getRootReference()) + scope = enclosing; + if (!scope) { + auto root = FlatSymbolRefAttr::get(name.getRootReference()); + scope = SymbolTable::lookupNearestSymbolFrom(from, root); + if (!scope) + if (auto module = from->getParentOfType()) + scope = SymbolTable::lookupSymbolIn(module, root); + } + if (!isa_and_nonnull(scope)) + return nullptr; + return SymbolTable::lookupSymbolIn(scope, name.getLeafReference()); +} + +LogicalResult requireQualified(Operation *from, SymbolRefAttr name) { + if (name.getNestedReferences().size() == 1) + return success(); + return from->emitOpError( + "named data references require a qualified symbol such as " + "'@types::@S'"); +} + +LogicalResult verifyNamedTypes(Operation *from, Type type) { + LogicalResult result = success(); + type.walk([&](Type nested) { + auto ref = namedRef(nested); + if (!ref) + return WalkResult::advance(); + if (failed(requireQualified(from, ref->name))) { + result = failure(); + return WalkResult::interrupt(); + } + Operation *decl = lookup(from, ref->name); + if (!decl) { + from->emitOpError() << "unresolved named data type '" << ref->name << "'"; + result = failure(); + return WalkResult::interrupt(); + } + if (decl->getName().getStringRef() != ref->opName) { + from->emitOpError() << "named type '" << ref->name << "' requires " + << ref->opName << " but resolves to " + << decl->getName(); + result = failure(); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return result; +} + +LogicalResult verifyPlacement(Operation *op) { + if (isa_and_nonnull(op->getParentOp())) + return success(); + return op->emitOpError( + "named data declarations must be direct children of ac.type_scope"); +} + +FailureOr fieldDictionary(Operation *op, Attribute field) { + auto dictionary = dyn_cast(field); + if (!dictionary || !dictionary.getAs("name") || + !dictionary.getAs("type")) { + op->emitOpError("field metadata requires string 'name' and type 'type'"); + return failure(); + } + return dictionary; +} + +StringRef fieldName(DictionaryAttr field) { + return field.getAs("name").getValue(); +} + +Type fieldType(DictionaryAttr field) { + return field.getAs("type").getValue(); +} + +bool containsList(Type type) { + return type.walk([](ListType) { return WalkResult::interrupt(); }) + .wasInterrupted(); +} + +bool isNormativeValueType(Type type) { + if (isa(type)) + return true; + if (auto optional = dyn_cast(type)) + return isNormativeValueType(optional.getElementType()); + if (auto list = dyn_cast(type)) + return isNormativeValueType(list.getElementType()); + if (auto vector = dyn_cast(type)) + return isNormativeValueType(vector.getElementType()); + if (auto vector = dyn_cast(type)) + return isNormativeValueType(vector.getElementType()); + return false; +} + +bool isProtocolPayloadType(Type type) { + return isNormativeValueType(type) && !containsChannelType(type); +} + +bool isTopologyLeaf(Type type) { + return isa(type); +} + +Type findNestedTopologyLeaf(Type type) { + if (isTopologyLeaf(type)) + return {}; + Type found; + type.walk([&](Type nested) { + if (!isTopologyLeaf(nested)) + return WalkResult::advance(); + found = nested; + return WalkResult::interrupt(); + }); + return found; +} + +template +OpTy lookupChild(Operation *container, FlatSymbolRefAttr name) { + return dyn_cast_or_null(SymbolTable::lookupSymbolIn(container, name)); +} + +ProtocolOp lookupProtocol(Operation *from, FlatSymbolRefAttr name) { + auto module = from->getParentOfType(); + return module ? dyn_cast_or_null( + SymbolTable::lookupSymbolIn(module, name)) + : ProtocolOp(); +} + +bool isCarrierAction(StringRef action) { + return action == "offer" || action == "response" || action == "notify"; +} + +bool matchesCarrierEvent(ProtocolOp protocol, Type payload, + FlatSymbolRefAttr from = {}, + FlatSymbolRefAttr to = {}) { + return llvm::any_of(protocol.getBody().getOps(), [&](EventOp event) { + return isCarrierAction(event.getAction()) && + event.getPayload() == payload && + (!from || event.getFromAttr() == from) && + (!to || event.getToAttr() == to); + }); +} + +LogicalResult verifyRoleReference(Operation *from, Operation *container, + FlatSymbolRefAttr name, StringRef subject) { + if (lookupChild(container, name)) + return success(); + return from->emitOpError() + << "unresolved " << subject << " role '@" << name.getValue() << "'"; +} + +LogicalResult verifyRoleContainer(Operation *container) { + llvm::SmallDenseSet names; + for (RoleOp role : container->getRegion(0).getOps()) + if (!names.insert(role.getSymName()).second) + return role.emitOpError() + << "redefinition of symbol named '" << role.getSymName() << "'"; + for (RoleOp role : container->getRegion(0).getOps()) { + if (role.getCardinality() != "exclusive" && + role.getCardinality() != "shared") + return role.emitOpError() << "unsupported role cardinality '" + << role.getCardinality() << "'"; + RoleOp dual = lookupChild(container, role.getDualAttr()); + if (!dual) + return role.emitOpError() << "unresolved dual role '@" + << role.getDualAttr().getValue() << "'"; + if (dual == role) + return role.emitOpError("role cannot be its own dual"); + if (dual.getDualAttr() != + FlatSymbolRefAttr::get(role.getContext(), role.getSymName())) + return role.emitOpError("role duality must be symmetric"); + if (dual.getCardinality() != role.getCardinality()) + return role.emitOpError("dual roles must have matching cardinality"); + } + return success(); +} + +bool hasStringValue(StringRef value, ArrayRef accepted) { + return llvm::is_contained(accepted, value); +} + +GuaranteeOp findGuarantee(ProtocolOp protocol, StringRef kind) { + for (GuaranteeOp guarantee : protocol.getBody().getOps()) + if (guarantee.getKind() == kind) + return guarantee; + return {}; +} + +LogicalResult verifyStringGuarantee(GuaranteeOp op, + ArrayRef accepted) { + auto value = dyn_cast(op.getValue()); + if (!value || !hasStringValue(value.getValue(), accepted)) + return op.emitOpError() + << "unsupported " << op.getKind() << " value '" + << (value ? value.getValue() : StringRef("")) << "'"; + return success(); +} + +bool isAllowedGuardExpression(Operation *operation) { + return llvm::StringSwitch(operation->getName().getStringRef()) + .Cases({"arith.constant", + "arith.cmpi", + "arith.cmpf", + "arith.addi", + "arith.subi", + "arith.muli", + "arith.divui", + "arith.divsi", + "arith.remui", + "arith.remsi", + "arith.andi", + "arith.ori", + "arith.xori", + "arith.shli", + "arith.shrui", + "arith.shrsi", + "arith.select", + "arith.index_cast", + "arith.extui", + "arith.extsi", + "arith.trunci", + "arith.addf", + "arith.subf", + "arith.mulf", + "arith.divf", + "arith.negf", + "index.constant", + "index.add", + "index.sub", + "index.mul", + "index.divs", + "index.divu", + "index.rems", + "index.remu", + "index.cmp", + "index.casts", + "index.castu", + RecordCreateOp::getOperationName(), + RecordGetOp::getOperationName(), + RecordWithOp::getOperationName()}, + true) + .Default(false); +} + +LogicalResult verifyFields(Operation *op, ArrayAttr fields) { + llvm::SmallDenseSet seen; + for (Attribute attribute : fields) { + FailureOr field = fieldDictionary(op, attribute); + if (failed(field)) + return failure(); + StringRef name = fieldName(*field); + Type type = fieldType(*field); + if (!seen.insert(name).second) + return op->emitOpError() << "duplicate field '" << name << "'"; + if (!isNormativeValueType(type)) + return op->emitOpError() + << "field '" << name << "' has non-value type " << type; + if (failed(verifyNamedTypes(op, type))) + return failure(); + Attribute boundAttribute = field->get("max_length"); + auto bound = dyn_cast_or_null(boundAttribute); + if (containsList(type)) { + if (!bound || !bound.getType().isSignlessInteger(64) || + bound.getInt() <= 0) + return op->emitOpError() << "list field '" << name + << "' requires a finite positive max_length"; + } else if (boundAttribute) { + return op->emitOpError() + << "non-list field '" << name << "' cannot declare max_length"; + } + } + return success(); +} + +ArrayAttr declarationFields(Operation *op) { + return op->getAttrOfType("fields"); +} + +Operation *recordDecl(Operation *from, Type type) { + auto ref = namedRef(type); + if (!ref || (ref->opName != StructOp::getOperationName() && + ref->opName != PacketOp::getOperationName() && + ref->opName != TransactionOp::getOperationName())) + return nullptr; + if (failed(requireQualified(from, ref->name))) + return nullptr; + Operation *decl = lookup(from, ref->name); + return decl && decl->getName().getStringRef() == ref->opName ? decl : nullptr; +} + +std::optional findField(Operation *decl, StringRef name) { + for (auto [index, attribute] : llvm::enumerate(declarationFields(decl))) { + auto field = cast(attribute); + if (fieldName(field) == name) + return index; + } + return std::nullopt; +} + +Type fieldType(Operation *decl, unsigned index) { + return fieldType(cast(declarationFields(decl)[index])); +} + +SmallVector directValueReferences(Type type) { + if (isa(type)) + return {}; + if (auto ref = namedRef(type)) + return {*ref}; + if (auto optional = dyn_cast(type)) + return directValueReferences(optional.getElementType()); + if (auto vector = dyn_cast(type)) + return directValueReferences(vector.getElementType()); + if (auto vector = dyn_cast(type)) + return directValueReferences(vector.getElementType()); + return {}; +} + +LogicalResult verifyNoRecursion(Operation *root) { + auto rootName = + root->getAttrOfType(SymbolTable::getSymbolAttrName()); + llvm::SmallDenseSet active; + std::function visit = + [&](Operation *current) -> LogicalResult { + if (!active.insert(current).second) { + root->emitOpError() << "unbounded value recursion through '@" + << rootName.getValue() << "'"; + return failure(); + } + if (ArrayAttr fields = declarationFields(current)) { + for (Attribute attribute : fields) { + Type type = fieldType(cast(attribute)); + for (NamedRef ref : directValueReferences(type)) { + Operation *next = lookup(root, ref.name); + if (next && declarationFields(next) && failed(visit(next))) + return failure(); + } + } + } + active.erase(current); + return success(); + }; + return visit(root); +} + +LogicalResult verifyRecordDeclaration(Operation *op, ArrayAttr fields) { + if (failed(verifyPlacement(op)) || failed(verifyFields(op, fields))) + return failure(); + return verifyNoRecursion(op); +} + +SymbolRefAttr declarationReference(Operation *declaration) { + auto scope = cast(declaration->getParentOp()); + auto leaf = FlatSymbolRefAttr::get( + declaration->getAttrOfType(SymbolTable::getSymbolAttrName())); + return SymbolRefAttr::get(declaration->getContext(), scope.getSymName(), + ArrayRef{leaf}); +} + +Type declarationType(Operation *declaration) { + SymbolRefAttr reference = declarationReference(declaration); + return TypeSwitch(declaration) + .Case([&](auto) { + return StructType::get(declaration->getContext(), reference); + }) + .Case([&](auto) { + return PacketType::get(declaration->getContext(), reference); + }) + .Case([&](auto) { + return EnumType::get(declaration->getContext(), reference); + }) + .Case([&](auto) { + return UnionType::get(declaration->getContext(), reference); + }) + .Default([](Operation *) { return Type(); }); +} + +FailureOr queryLayout(TypeScopeOp scope, Type type) { + DataLayoutSpecInterface spec = scope.getDataLayoutSpec(); + if (!spec) + return failure(); + FailureOr value = spec.query(DataLayoutEntryKey(type)); + if (failed(value)) + return failure(); + auto dictionary = dyn_cast(*value); + if (!dictionary) + return failure(); + return dictionary; +} + +LogicalResult verifyDeclarationLayout(Operation *declaration) { + Type type = declarationType(declaration); + auto scope = cast(declaration->getParentOp()); + if (succeeded(queryLayout(scope, type))) + return success(); + return declaration->emitOpError() << "missing DLTI layout entry for " << type; +} + +FailureOr packetSerializationWidth(Operation *from, + SymbolRefAttr name) { + auto packet = dyn_cast_or_null(lookup(from, name)); + if (!packet) + return failure(); + FailureOr layout = + queryLayout(cast(packet->getParentOp()), + PacketType::get(from->getContext(), name)); + if (failed(layout)) + return failure(); + auto width = layout->getAs("serialization_width"); + if (!width || width.getInt() <= 0) + return failure(); + return width.getInt(); +} + +LogicalResult verifyUniqueEnumerants(EnumOp op) { + llvm::SmallDenseSet seen; + for (Attribute value : op.getEnumerants()) { + StringRef text = cast(value).getValue(); + if (!seen.insert(text).second) + return op.emitOpError() << "duplicate enumerant '" << text << "'"; + } + return success(); +} + +} // namespace + +DataLayoutSpecInterface TypeScopeOp::getDataLayoutSpec() { + return getOperation()->getAttrOfType( + DLTIDialect::kDataLayoutAttrName); +} + +TargetSystemSpecInterface TypeScopeOp::getTargetSystemSpec() { + return getOperation()->getAttrOfType( + DLTIDialect::kTargetSystemDescAttrName); +} + +LogicalResult TypeAliasOp::verify() { + if (failed(verifyPlacement(*this))) + return failure(); + return verifyNamedTypes(*this, getTarget()); +} + +LogicalResult StructOp::verify() { + if (failed(verifyRecordDeclaration(*this, getFields()))) + return failure(); + return verifyDeclarationLayout(*this); +} + +LogicalResult TransactionOp::verify() { + return verifyRecordDeclaration(*this, getFields()); +} + +LogicalResult PacketOp::verify() { + if (failed(verifyRecordDeclaration(*this, getFields()))) + return failure(); + return verifyDeclarationLayout(*this); +} + +LogicalResult EnumOp::verify() { + if (failed(verifyPlacement(*this)) || failed(verifyUniqueEnumerants(*this))) + return failure(); + return verifyDeclarationLayout(*this); +} + +LogicalResult UnionOp::verify() { + if (failed(verifyRecordDeclaration(*this, getFields()))) + return failure(); + auto index = findField(*this, getDiscriminator()); + if (!index) + return emitOpError() << "union discriminator '" << getDiscriminator() + << "' does not name a field"; + if (!isa(fieldType(*this, *index))) + return emitOpError() << "union discriminator '" << getDiscriminator() + << "' must name an integer or enum field"; + return verifyDeclarationLayout(*this); +} + +LogicalResult RecordCreateOp::verify() { + Operation *decl = recordDecl(*this, getResult().getType()); + if (!decl) + return emitOpError( + "record.create result must resolve to a record declaration"); + ArrayAttr fields = declarationFields(decl); + if (getFieldNames().size() != fields.size() || + getValues().size() != fields.size()) + return emitOpError("record.create fields must exactly match declaration"); + for (auto [index, value] : llvm::enumerate(getValues())) { + auto field = cast(fields[index]); + if (cast(getFieldNames()[index]).getValue() != fieldName(field)) + return emitOpError("record.create fields must exactly match declaration"); + Type expected = fieldType(field); + if (expected != value.getType()) + return emitOpError() << "field '" << fieldName(field) << "' expects " + << expected << " but received " << value.getType(); + } + return success(); +} + +LogicalResult RecordGetOp::verify() { + Operation *decl = recordDecl(*this, getRecord().getType()); + if (!decl) + return emitOpError("record.get requires a record-like operand"); + auto index = findField(decl, getField()); + if (!index) + return emitOpError() << "unknown record field '" << getField() << "'"; + Type type = fieldType(decl, *index); + if (type != getResult().getType()) + return emitOpError() << "field '" << getField() << "' has type " << type + << " but operation returns " << getResult().getType(); + return success(); +} + +LogicalResult RecordWithOp::verify() { + if (getRecord().getType() != getResult().getType()) + return emitOpError("record.with must preserve record identity"); + Operation *decl = recordDecl(*this, getRecord().getType()); + if (!decl) + return emitOpError("record.with requires a record-like operand"); + auto index = findField(decl, getField()); + if (!index) + return emitOpError() << "unknown record field '" << getField() << "'"; + Type type = fieldType(decl, *index); + if (type != getValue().getType()) + return emitOpError() << "field '" << getField() << "' expects " << type + << " but received " << getValue().getType(); + return success(); +} + +LogicalResult VarConstantOp::verify() { + auto result = cast(getResult().getType()); + auto value = dyn_cast(getValue()); + if (!value || value.getType() != result.getElementType()) + return emitOpError("attribute type must match Var element type"); + return success(); +} + +static LogicalResult verifyVarBinary(Operation *operation, Value lhs, Value rhs, + Value result) { + if (lhs.getType() != rhs.getType() || lhs.getType() != result.getType()) + return operation->emitOpError( + "operands and result must have one identical Var type"); + Type element = cast(result.getType()).getElementType(); + if (!isa(element)) + return operation->emitOpError( + "arithmetic Var element must be an integer or float"); + return success(); +} + +LogicalResult VarAddOp::verify() { + return verifyVarBinary(*this, getLhs(), getRhs(), getResult()); +} + +LogicalResult VarSubOp::verify() { + return verifyVarBinary(*this, getLhs(), getRhs(), getResult()); +} + +LogicalResult VarMulOp::verify() { + return verifyVarBinary(*this, getLhs(), getRhs(), getResult()); +} + +LogicalResult VarPopcountOp::verify() { + auto input = cast(getIn().getType()); + auto result = cast(getResult().getType()); + auto inputInt = dyn_cast(input.getElementType()); + auto resultInt = dyn_cast(result.getElementType()); + if (!inputInt || !resultInt) + return emitOpError("input and result payloads must be integer types"); + if (inputInt.getWidth() == 0 || inputInt.getWidth() > 64) + return emitOpError("input width must be in [1, 64]"); + + unsigned required = 0; + for (unsigned width = inputInt.getWidth(); width != 0; width >>= 1) + ++required; + if (resultInt.getWidth() != required) + return emitOpError() + << "result width must be ceil(log2(input_width + 1)) = " << required; + return success(); +} + +LogicalResult VarCmpOp::verify() { + if (getLhs().getType() != getRhs().getType()) + return emitOpError("operands must have the same Var type"); + Type payload = cast(getLhs().getType()).getElementType(); + if (!isa(payload)) + return emitOpError("operands must carry integer or enum payloads"); + if (getResult().getType() != + VarType::get(getContext(), IntegerType::get(getContext(), 1))) + return emitOpError("result must be !ac.var"); + if (!llvm::is_contained( + ArrayRef{"eq", "ne", "slt", "sle", "sgt", "sge"}, + getPredicate())) + return emitOpError("predicate must be eq, ne, slt, sle, sgt, or sge"); + return success(); +} + +LogicalResult VarGetOp::verify() { + auto record = cast(getRecord().getType()); + Operation *decl = recordDecl(*this, record.getElementType()); + if (!decl) + return emitOpError("requires a record-like Var operand"); + auto index = findField(decl, getField()); + if (!index) + return emitOpError() << "unknown field '" << getField() << "'"; + Type expected = VarType::get(getContext(), fieldType(decl, *index)); + if (getResult().getType() != expected) + return emitOpError() << "field '" << getField() << "' result must be " + << expected; + return success(); +} + +LogicalResult VarWithOp::verify() { + if (getRecord().getType() != getResult().getType()) + return emitOpError("must preserve record Var identity"); + auto record = cast(getRecord().getType()); + Operation *decl = recordDecl(*this, record.getElementType()); + if (!decl) + return emitOpError("requires a record-like Var operand"); + auto index = findField(decl, getField()); + if (!index) + return emitOpError() << "unknown field '" << getField() << "'"; + Type expected = VarType::get(getContext(), fieldType(decl, *index)); + if (getValue().getType() != expected) + return emitOpError() << "field '" << getField() << "' expects " << expected; + return success(); +} + +static std::string queueScopePath(Operation *operation) { + SmallVector parts; + for (Operation *parent = operation->getParentOp(); parent; + parent = parent->getParentOp()) + if (auto scope = dyn_cast(parent)) + parts.push_back(scope.getSymName()); + std::string path; + for (StringRef part : llvm::reverse(parts)) { + path.push_back('/'); + path.append(part); + } + return path.empty() ? "/" : path; +} + +LogicalResult MemoryInstanceOp::verify() { + auto data = dyn_cast(getDataType()); + if (!data || data.getWidth() == 0 || data.getWidth() > 64) + return emitOpError("data type must be an integer no wider than 64 bits"); + if (getEntries() <= 0 || getLatency() <= 0) + return emitOpError("entries and latency must be positive"); + if (getInit() != 0) + return emitOpError("memory init must be zero"); + if (getOwner().empty() || !getOwner().starts_with('/') || + (getOwner().size() > 1 && getOwner().ends_with('/'))) + return emitOpError("owner must be a canonical absolute scope path"); + if (getStableId().empty()) + return emitOpError("stable_id must be non-empty"); + + Operation *root = getOperation(); + while (root->getParentOp()) + root = root->getParentOp(); + std::string expectedStableId = "memory/"; + if (getOwner() != "/") { + expectedStableId.append(getOwner().drop_front()); + expectedStableId.push_back('/'); + } + expectedStableId.append(getSymName()); + if (getStableId() != expectedStableId) + return emitOpError("stable_id must match canonical owner/symbol identity"); + bool ownerExists = getOwner() == "/"; + bool duplicateStableId = false; + root->walk([&](Operation *operation) { + if (auto scope = dyn_cast(operation)) { + std::string path = queueScopePath(scope); + if (path != "/") + path.push_back('/'); + path.append(scope.getSymName()); + ownerExists |= path == getOwner(); + } + if (auto other = dyn_cast(operation)) + duplicateStableId |= + other != *this && other.getStableId() == getStableId(); + }); + if (!ownerExists) + return emitOpError("owner does not name a declared scope path"); + if (duplicateStableId) + return emitOpError("stable_id must be unique"); + unsigned requests = 0; + root->walk([&](MemoryRequestOp request) { + auto resolved = + dyn_cast_or_null(SymbolTable::lookupNearestSymbolFrom( + request, request.getInstanceAttr())); + if (resolved == *this) + ++requests; + }); + if (requests == 0) + return emitOpError("must have at least one memory.request endpoint"); + return success(); +} + +LogicalResult MemoryRequestOp::verify() { + if (getInput().getType() != getOutput().getType()) + return emitOpError("output queue must match input queue type"); + if (getOrdinal() < 0 || getDepth() <= 0) + return emitOpError("ordinal must be non-negative and depth positive"); + + auto instance = dyn_cast_or_null( + SymbolTable::lookupNearestSymbolFrom(*this, getInstanceAttr())); + if (!instance) + return emitOpError() << "unresolved memory instance " << getInstance(); + const std::string requestScope = queueScopePath(*this); + StringRef owner = instance.getOwner(); + StringRef requestPath(requestScope); + const bool visible = + owner == "/" || requestPath == owner || + (requestPath.size() > owner.size() && requestPath.starts_with(owner) && + requestPath[owner.size()] == '/'); + if (!visible) + return emitOpError("memory instance is outside the request scope ancestry"); + auto endpointPath = (*this)->getAttrOfType("ac.endpoint_path"); + auto endpointName = (*this)->getAttrOfType("ac.name"); + if (!endpointPath || endpointPath.getValue().empty() || !endpointName || + endpointName.getValue().empty()) + return emitOpError("requires stable ac.endpoint_path and ac.name"); + std::string expectedEndpointPath = requestScope; + if (expectedEndpointPath != "/") + expectedEndpointPath.push_back('/'); + expectedEndpointPath.append(endpointName.getValue()); + if (endpointPath.getValue() != expectedEndpointPath) + return emitOpError("ac.endpoint_path must match canonical scope/name path"); + + Type payload = cast(getInput().getType()).getElementType(); + Operation *declaration = recordDecl(*this, payload); + if (!declaration) + return emitOpError("requires a record-like Queue payload"); + auto fieldIndex = findField(declaration, getResultField()); + if (!fieldIndex) + return emitOpError() << "unknown result_field '" << getResultField() << "'"; + Type dataType = fieldType(declaration, *fieldIndex); + if (dataType != instance.getDataType()) + return emitOpError( + "result_field type must match memory instance data type"); + auto dataInteger = dyn_cast(dataType); + if (!dataInteger || dataInteger.getWidth() > 64) + return emitOpError( + "result_field must carry an integer no wider than 64 bits"); + + Type argumentType = VarType::get(getContext(), payload); + auto verifyPolicy = [&](Region ®ion, StringRef name) -> FailureOr { + Block &block = region.front(); + if (block.getNumArguments() != 1 || + block.getArgument(0).getType() != argumentType) { + emitOpError() << name << " argument must match queue payload Var"; + return failure(); + } + for (Operation &operation : block.without_terminator()) + if (!isMemoryEffectFree(&operation)) { + emitOpError() << name << " operation '" << operation.getName() + << "' must be pure"; + return failure(); + } + auto yield = dyn_cast(block.getTerminator()); + if (!yield) { + emitOpError() << name << " must terminate with ac.memory.yield"; + return failure(); + } + return cast(yield.getValue().getType()).getElementType(); + }; + + FailureOr address = verifyPolicy(getAddress(), "address"); + FailureOr write = verifyPolicy(getWrite(), "write"); + FailureOr data = verifyPolicy(getData(), "data"); + if (failed(address) || failed(write) || failed(data)) + return failure(); + auto addressInteger = dyn_cast(*address); + if (!addressInteger || addressInteger.getWidth() > 64) + return emitOpError( + "address must yield an integer Var no wider than 64 bits"); + if (addressInteger.getWidth() < 64 && + static_cast(instance.getEntries()) > + (uint64_t{1} << addressInteger.getWidth())) + return emitOpError("entries must fit address width"); + if (!write->isInteger(1)) + return emitOpError("write must yield !ac.var"); + if (*data != dataType) + return emitOpError("data must match result_field type"); + + Operation *root = getOperation(); + while (root->getParentOp()) + root = root->getParentOp(); + DenseSet ordinals; + StringSet<> endpointPaths; + Type payloadType; + uint64_t maximumOrdinal = 0; + unsigned endpointCount = 0; + WalkResult endpointResult = root->walk([&](MemoryRequestOp request) { + auto resolved = + dyn_cast_or_null(SymbolTable::lookupNearestSymbolFrom( + request, request.getInstanceAttr())); + if (resolved != instance) + return WalkResult::advance(); + ++endpointCount; + maximumOrdinal = std::max(maximumOrdinal, request.getOrdinal()); + if (!ordinals.insert(request.getOrdinal()).second) { + request.emitOpError("duplicate endpoint ordinal for memory instance"); + return WalkResult::interrupt(); + } + auto path = request->getAttrOfType("ac.endpoint_path"); + if (!path || !endpointPaths.insert(path.getValue()).second) { + request.emitOpError( + "duplicate or missing endpoint path for memory instance"); + return WalkResult::interrupt(); + } + Type candidate = + cast(request.getInput().getType()).getElementType(); + if (!payloadType) + payloadType = candidate; + else if (payloadType != candidate) { + request.emitOpError( + "all endpoints of one memory must use one payload type"); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (endpointResult.wasInterrupted()) + return failure(); + if (maximumOrdinal + 1 != endpointCount) + return emitOpError("memory endpoint ordinals must be contiguous from zero"); + return success(); +} + +LogicalResult PacketSerializeOp::verify() { + auto packetType = dyn_cast(getPacketValue().getType()); + if (!packetType) + return emitOpError("packet.serialize requires a packet operand"); + if (failed(requireQualified(*this, getPacketAttr()))) + return failure(); + if (packetType.getName() != getPacketAttr()) + return emitOpError( + "packet.serialize identity does not match packet operand"); + FailureOr width = packetSerializationWidth(*this, getPacketAttr()); + if (failed(width)) + return emitOpError("packet.serialize packet declaration is unresolved"); + auto bytes = dyn_cast(getBytes().getType()); + if (!bytes || !bytes.getElementType().isInteger(8)) + return emitOpError("packet.serialize result must be an i8 byte vector"); + if (bytes.getLength() != *width) + return emitOpError() + << "serialized byte vector width must equal packet serialization " + "width " + << *width; + return success(); +} + +LogicalResult PacketDeserializeOp::verify() { + if (failed(requireQualified(*this, getPacketAttr()))) + return failure(); + auto packetType = dyn_cast(getPacketValue().getType()); + if (!packetType || packetType.getName() != getPacketAttr()) + return emitOpError("packet.deserialize result identity does not match " + "serialization contract"); + FailureOr width = packetSerializationWidth(*this, getPacketAttr()); + if (failed(width)) + return emitOpError("packet.deserialize packet declaration is unresolved"); + auto bytes = dyn_cast(getBytes().getType()); + if (!bytes || !bytes.getElementType().isInteger(8)) + return emitOpError("packet.deserialize operand must be an i8 byte vector"); + if (bytes.getLength() != *width) + return emitOpError() + << "serialized byte vector width must equal packet serialization " + "width " + << *width; + return success(); +} + +LogicalResult InterfaceOp::verify() { + if (getBody().empty()) + return emitOpError("interface declaration requires a body block"); + for (Operation &child : getBody().front()) + if (!isa(child)) + return emitOpError() + << "interface body only permits ac.role and ac.port, " + << "found " << child.getName(); + return verifyRoleContainer(*this); +} + +LogicalResult ProtocolOp::verify() { + if (getBody().empty()) + return emitOpError("protocol declaration requires a body block"); + for (Operation &child : getBody().front()) + if (!isa(child)) + return emitOpError() << "protocol body contains unsupported operation " + << child.getName(); + + if (failed(verifyRoleContainer(*this))) + return failure(); + + unsigned initialStates = 0; + for (StateOp state : getBody().getOps()) + initialStates += state.getInitial() ? 1 : 0; + if (initialStates != 1) + return emitOpError() + << "protocol requires exactly one initial state, found " + << initialStates; + + llvm::SmallDenseSet guaranteeKinds; + for (GuaranteeOp guarantee : getBody().getOps()) + if (!guaranteeKinds.insert(guarantee.getKind()).second) + return guarantee.emitOpError() + << "duplicate protocol guarantee '" << guarantee.getKind() << "'"; + + SmallVector transitions; + for (TransitionOp transition : getBody().getOps()) { + if (!lookupChild(*this, transition.getSourceAttr())) + return transition.emitOpError() + << "unresolved transition source state '@" + << transition.getSourceAttr().getValue() << "'"; + if (!lookupChild(*this, transition.getTargetAttr())) + return transition.emitOpError() + << "unresolved transition target state '@" + << transition.getTargetAttr().getValue() << "'"; + if (!lookupChild(*this, transition.getEventAttr())) + return transition.emitOpError() + << "unresolved transition event '@" + << transition.getEventAttr().getValue() << "'"; + transitions.push_back(transition); + } + + for (unsigned i = 0; i < transitions.size(); ++i) { + SmallVector overlapping{transitions[i]}; + for (unsigned j = i + 1; j < transitions.size(); ++j) + if (transitions[i].getSourceAttr() == transitions[j].getSourceAttr() && + transitions[i].getEventAttr() == transitions[j].getEventAttr()) + overlapping.push_back(transitions[j]); + if (overlapping.size() < 2) + continue; + llvm::SmallSet priorities; + for (TransitionOp transition : overlapping) { + if (!transition.getPriority()) + return transition.emitOpError( + "overlapping transitions require explicit priority"); + if (!priorities.insert(static_cast(*transition.getPriority())) + .second) + return transition.emitOpError( + "overlapping transitions require unique priority"); + } + } + + GuaranteeOp stablePending = findGuarantee(*this, "stable_pending"); + bool stable = stablePending && dyn_cast(stablePending.getValue()) && + cast(stablePending.getValue()).getValue(); + for (TransitionOp transition : transitions) { + EventOp event = lookupChild(*this, transition.getEventAttr()); + if (transition.getTransfer() && transition.getRetain()) + return transition.emitOpError( + "transition cannot both transfer and retain ownership"); + if (event.getAction() == "offer" && !transition.getTransfer() && + !transition.getRetain()) + return transition.emitOpError( + "offer transition must transfer or retain ownership"); + if (event.getAction() == "offer" && transition.getRetain() && !stable) + return transition.emitOpError( + "retained pending offer requires stable_pending = true"); + if (event.getAction() == "retry" && !transition.getRetain()) + return transition.emitOpError( + "retry transition must retain the pending offer"); + if (event.getAction() == "retry" && transition.getTransfer()) + return transition.emitOpError( + "retry transition cannot transfer the pending offer"); + if (transition.getRetain() && event.getAction() != "offer" && + event.getAction() != "retry") + return transition.emitOpError( + "retain is only valid for offer and retry transitions"); + } + + enum class Ownership : uint8_t { NoPending = 1, Pending = 2 }; + SmallVector states(getBody().getOps()); + llvm::StringMap stateIndices; + for (auto [index, state] : llvm::enumerate(states)) + stateIndices.try_emplace(state.getSymName(), index); + auto stateIndex = [&](FlatSymbolRefAttr name) -> unsigned { + auto found = stateIndices.find(name.getValue()); + assert(found != stateIndices.end() && + "transition state references were verified"); + return found->second; + }; + + SmallVector> outgoing(states.size()); + SmallVector transitionTargets; + SmallVector transitionEvents; + transitionTargets.reserve(transitions.size()); + transitionEvents.reserve(transitions.size()); + for (auto [index, transition] : llvm::enumerate(transitions)) { + outgoing[stateIndex(transition.getSourceAttr())].push_back(index); + transitionTargets.push_back(stateIndex(transition.getTargetAttr())); + transitionEvents.push_back( + lookupChild(*this, transition.getEventAttr())); + } + + SmallVector ownership(states.size(), 0); + SmallVector> worklist; + auto ownershipBit = [](Ownership value) { + return static_cast(value); + }; + auto hasOwnership = [&](unsigned state, Ownership value) { + return (ownership[state] & ownershipBit(value)) != 0; + }; + auto addOwnership = [&](unsigned state, Ownership value) { + uint8_t bit = ownershipBit(value); + if (ownership[state] & bit) + return; + ownership[state] |= bit; + worklist.emplace_back(state, value); + }; + for (auto [index, state] : llvm::enumerate(states)) + if (state.getInitial()) + addOwnership(index, Ownership::NoPending); + + auto transferOwnership = [&](unsigned transitionIndex, + Ownership input) -> std::optional { + TransitionOp transition = transitions[transitionIndex]; + StringRef action = transitionEvents[transitionIndex].getAction(); + if (action == "offer") { + if (input == Ownership::Pending) + return std::nullopt; + return transition.getTransfer() ? Ownership::NoPending + : Ownership::Pending; + } + if (action == "retry") + return input == Ownership::Pending + ? std::optional(Ownership::Pending) + : std::nullopt; + if (action == "cancel" || action == "reject") + return input == Ownership::Pending + ? std::optional(Ownership::NoPending) + : std::nullopt; + if (transition.getTransfer()) + return input == Ownership::Pending + ? std::optional(Ownership::NoPending) + : std::nullopt; + return input; + }; + + for (unsigned cursor = 0; cursor < worklist.size(); ++cursor) { + auto [source, input] = worklist[cursor]; + for (unsigned transitionIndex : outgoing[source]) + if (std::optional output = + transferOwnership(transitionIndex, input)) + addOwnership(transitionTargets[transitionIndex], *output); + } + + uint8_t conflicting = + ownershipBit(Ownership::NoPending) | ownershipBit(Ownership::Pending); + for (auto [index, state] : llvm::enumerate(states)) + if (ownership[index] == conflicting) + return state.emitOpError() << "ownership state conflict at join state '@" + << state.getSymName() << "'"; + + auto firstTransition = [&](auto predicate) -> TransitionOp { + for (auto [index, transition] : llvm::enumerate(transitions)) + if (predicate(index, transition)) + return transition; + return {}; + }; + if (TransitionOp transition = firstTransition([&](unsigned index, auto op) { + return transitionEvents[index].getAction() == "offer" && + hasOwnership(stateIndex(op.getSourceAttr()), Ownership::Pending); + })) + return transition.emitOpError( + "offer cannot begin while another offer is pending"); + if (TransitionOp transition = firstTransition([&](unsigned index, auto op) { + return transitionEvents[index].getAction() == "retry" && + hasOwnership(stateIndex(op.getSourceAttr()), + Ownership::NoPending); + })) + return transition.emitOpError("retry requires a pending offer"); + if (TransitionOp transition = firstTransition([&](unsigned index, auto op) { + StringRef action = transitionEvents[index].getAction(); + return (action == "cancel" || action == "reject") && + hasOwnership(stateIndex(op.getSourceAttr()), + Ownership::NoPending); + })) + return transition.emitOpError( + "ownership resolution requires a pending offer"); + if (TransitionOp transition = firstTransition([&](unsigned index, auto op) { + StringRef action = transitionEvents[index].getAction(); + return op.getTransfer() && action != "offer" && action != "retry" && + action != "cancel" && action != "reject" && + hasOwnership(stateIndex(op.getSourceAttr()), + Ownership::NoPending); + })) + return transition.emitOpError( + "ownership transfer requires a pending offer"); + if (TransitionOp transition = firstTransition([&](unsigned index, auto op) { + if (ownership[stateIndex(op.getSourceAttr())] != 0) + return false; + StringRef action = transitionEvents[index].getAction(); + return op.getTransfer() || action == "retry" || action == "cancel" || + action == "reject"; + })) + return transition.emitOpError( + "ownership resolution is unreachable from the initial state"); + + for (auto [index, state] : llvm::enumerate(states)) { + if (!hasOwnership(index, Ownership::Pending)) + continue; + if (state.getTerminal()) + return state.emitOpError() << "terminal state '@" << state.getSymName() + << "' is reachable with pending ownership"; + if (outgoing[index].empty()) + return state.emitOpError() + << "pending ownership reaches state '@" << state.getSymName() + << "' with no outgoing transition"; + } + + GuaranteeOp maxInflight = findGuarantee(*this, "max_inflight"); + if (maxInflight) { + auto value = dyn_cast(maxInflight.getValue()); + if (!value || !value.getType().isSignlessInteger(64) || value.getInt() <= 0) + return maxInflight.emitOpError( + "max_inflight requires a positive i64 value"); + if (value.getInt() > 1 && !findGuarantee(*this, "correlation")) + return maxInflight.emitOpError( + "max_inflight greater than one requires correlation"); + } + GuaranteeOp backpressure = findGuarantee(*this, "backpressure"); + if (backpressure) + if (auto value = dyn_cast(backpressure.getValue()); + value && value.getValue() == "custom" && + !findGuarantee(*this, "custom_backpressure")) + return backpressure.emitOpError( + "custom backpressure requires a custom_backpressure declaration"); + GuaranteeOp ordering = findGuarantee(*this, "ordering"); + if (ordering) + if (auto value = dyn_cast(ordering.getValue()); + value && value.getValue() == "per_key" && + !findGuarantee(*this, "correlation")) + return ordering.emitOpError("per_key ordering requires correlation"); + GuaranteeOp completion = findGuarantee(*this, "completion"); + if (completion) { + if (auto value = dyn_cast(completion.getValue())) { + if (value.getValue() == "on_response" && + !findGuarantee(*this, "correlation")) + return completion.emitOpError( + "on_response completion requires correlation"); + auto hasReachableAction = [&](StringRef action) { + return llvm::any_of(transitions, [&](TransitionOp transition) { + return ownership[stateIndex(transition.getSourceAttr())] && + lookupChild(*this, transition.getEventAttr()) + .getAction() == action; + }); + }; + if (value.getValue() == "on_response" && !hasReachableAction("response")) + return completion.emitOpError( + "on_response completion requires a reachable response event"); + if (value.getValue() == "on_accept" && !hasReachableAction("accept")) + return completion.emitOpError( + "on_accept completion requires a reachable accept event"); + if (value.getValue() == "on_terminal_phase" && + llvm::none_of(llvm::enumerate(states), [&](auto indexedState) { + return indexedState.value().getTerminal() && + ownership[indexedState.index()] != 0; + })) + return completion.emitOpError( + "on_terminal_phase completion requires a reachable terminal state"); + } + } + GuaranteeOp correlation = findGuarantee(*this, "correlation"); + if (correlation) { + auto field = dyn_cast(correlation.getValue()); + if (field && !field.getValue().empty()) { + Type correlationType; + for (TransitionOp transition : transitions) { + if (!ownership[stateIndex(transition.getSourceAttr())]) + continue; + EventOp event = lookupChild(*this, transition.getEventAttr()); + if (event.getAction() != "offer") + continue; + Operation *declaration = recordDecl(event, event.getPayload()); + std::optional index = + declaration ? findField(declaration, field.getValue()) + : std::nullopt; + if (!index) + return correlation.emitOpError() + << "correlation field '" << field.getValue() + << "' is missing from reachable offer/response payload"; + Type type = fieldType(declaration, *index); + if (!correlationType) + correlationType = type; + else if (type != correlationType) + return correlation.emitOpError() + << "correlation field '" << field.getValue() << "' has type " + << type << " but expected " << correlationType; + } + if (!correlationType) + return correlation.emitOpError() + << "correlation field '" << field.getValue() + << "' requires a reachable offer event"; + for (TransitionOp transition : transitions) { + if (!ownership[stateIndex(transition.getSourceAttr())]) + continue; + EventOp event = lookupChild(*this, transition.getEventAttr()); + if (event.getAction() != "offer" && event.getAction() != "response") + continue; + Operation *declaration = recordDecl(event, event.getPayload()); + std::optional index = + declaration ? findField(declaration, field.getValue()) + : std::nullopt; + if (!index) + return correlation.emitOpError() + << "correlation field '" << field.getValue() + << "' is missing from reachable offer/response payload"; + Type type = fieldType(declaration, *index); + if (type != correlationType) + return correlation.emitOpError() + << "correlation field '" << field.getValue() << "' has type " + << type << " but expected " << correlationType; + } + } + } + return success(); +} + +LogicalResult RoleOp::verify() { + if (!isa_and_nonnull(getOperation()->getParentOp())) + return emitOpError( + "role must be a direct child of ac.interface or ac.protocol"); + if (getCardinality() != "exclusive" && getCardinality() != "shared") + return emitOpError() << "unsupported role cardinality '" << getCardinality() + << "'"; + return success(); +} + +LogicalResult StateOp::verify() { + if (!isa_and_nonnull(getOperation()->getParentOp())) + return emitOpError("state must be a direct child of ac.protocol"); + return success(); +} + +LogicalResult EventOp::verify() { + auto protocol = dyn_cast_or_null(getOperation()->getParentOp()); + if (!protocol) + return emitOpError("event must be a direct child of ac.protocol"); + if (failed(verifyRoleReference(*this, protocol, getFromAttr(), + "event source")) || + failed(verifyRoleReference(*this, protocol, getToAttr(), "event target"))) + return failure(); + if (getFromAttr() == getToAttr()) + return emitOpError("event source and target roles must differ"); + if (!isProtocolPayloadType(getPayload())) + return emitOpError( + "event payload type must be a normative ACIR value type"); + if (failed(verifyNamedTypes(*this, getPayload()))) + return failure(); + static constexpr StringRef actions[] = { + "offer", "accept", "cancel", "reject", "retry", "response", "notify"}; + if (!hasStringValue(getAction(), actions)) + return emitOpError() << "unsupported event action '" << getAction() << "'"; + return success(); +} + +LogicalResult TransitionOp::verify() { + if (!isa_and_nonnull(getOperation()->getParentOp())) + return emitOpError("transition must be a direct child of ac.protocol"); + if (auto priority = getPriority(); priority && *priority > INT64_MAX) + return emitOpError("transition priority must be a non-negative i64 value"); + WalkResult result = getGuard().walk([&](Operation *operation) { + if (!isAllowedGuardExpression(operation)) { + emitOpError() << "guard operation '" << operation->getName() + << "' is not in the pure expression allowlist"; + return WalkResult::interrupt(); + } + auto effects = dyn_cast(operation); + if (!effects) { + emitOpError() << "allowed guard operation '" << operation->getName() + << "' must implement MemoryEffectOpInterface"; + return WalkResult::interrupt(); + } + SmallVector instances; + effects.getEffects(instances); + if (!instances.empty()) { + emitOpError() << "allowed guard operation '" << operation->getName() + << "' must have no memory effects"; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (result.wasInterrupted()) + return failure(); + return success(); +} + +LogicalResult GuaranteeOp::verify() { + if (!isa_and_nonnull(getOperation()->getParentOp())) + return emitOpError("guarantee must be a direct child of ac.protocol"); + StringRef kind = getKind(); + if (kind == "backpressure") + return verifyStringGuarantee( + *this, {"none", "accept", "credit", "capacity", "custom"}); + if (kind == "ordering") + return verifyStringGuarantee(*this, {"fifo", "per_key", "unordered"}); + if (kind == "delivery") + return verifyStringGuarantee( + *this, {"exactly_once", "at_most_once", "best_effort"}); + if (kind == "completion") + return verifyStringGuarantee( + *this, {"on_accept", "on_response", "on_terminal_phase"}); + if (kind == "stable_pending") { + if (!isa(getValue())) + return emitOpError("stable_pending requires a boolean value"); + return success(); + } + if (kind == "max_inflight") + return success(); + if (kind == "correlation") { + auto value = dyn_cast(getValue()); + if (!value || value.getValue().empty()) + return emitOpError("correlation requires a non-empty field name"); + return success(); + } + if (kind == "custom_backpressure") { + auto value = dyn_cast(getValue()); + if (!value || value.getValue().empty()) + return emitOpError( + "custom_backpressure requires a non-empty declarative contract"); + return success(); + } + return emitOpError() << "unknown mandatory protocol guarantee '" << kind + << "'"; +} + +LogicalResult PortOp::verify() { + auto interface = dyn_cast_or_null(getOperation()->getParentOp()); + if (!interface) + return emitOpError("port must be a direct child of ac.interface"); + auto channel = dyn_cast(getType()); + if (!channel) + return emitOpError("port type must be !ac.channel"); + if (failed(verifyRoleReference(*this, interface, getFromAttr(), + "port source")) || + failed(verifyRoleReference(*this, interface, getToAttr(), "port target"))) + return failure(); + if (getFromAttr() == getToAttr()) + return emitOpError("port source and target roles must differ"); + RoleOp fromRole = lookupChild(interface, getFromAttr()); + if (fromRole.getDualAttr() != getToAttr()) + return emitOpError("port source and target roles must be dual"); + if (!isProtocolPayloadType(channel.getElementType())) + return emitOpError( + "channel payload type must be a normative ACIR value type"); + if (failed(verifyNamedTypes(*this, channel.getElementType()))) + return failure(); + ProtocolOp protocol = lookupProtocol(*this, channel.getProtocol()); + if (!protocol) + return emitOpError() << "unresolved channel protocol '@" + << channel.getProtocol().getValue() << "'"; + RoleOp protocolFrom = lookupChild(protocol, getProtocolFromAttr()); + if (!protocolFrom) + return emitOpError() << "unresolved mapped protocol source role '@" + << getProtocolFromAttr().getValue() << "'"; + RoleOp protocolTo = lookupChild(protocol, getProtocolToAttr()); + if (!protocolTo) + return emitOpError() << "unresolved mapped protocol target role '@" + << getProtocolToAttr().getValue() << "'"; + if (protocolFrom.getDualAttr() != getProtocolToAttr() || + protocolTo.getDualAttr() != getProtocolFromAttr()) + return emitOpError("mapped protocol roles must be dual"); + RoleOp toRole = lookupChild(interface, getToAttr()); + if (fromRole.getCardinality() != protocolFrom.getCardinality() || + toRole.getCardinality() != protocolTo.getCardinality()) + return emitOpError( + "interface and mapped protocol roles must have matching cardinality"); + if (!matchesCarrierEvent(protocol, channel.getElementType(), + getProtocolFromAttr(), getProtocolToAttr())) + return emitOpError() << "channel payload " << channel.getElementType() + << " from mapped protocol role '@" + << getProtocolFromAttr().getValue() << "' to '@" + << getProtocolToAttr().getValue() + << "' does not match any carrier event in protocol '@" + << channel.getProtocol().getValue() << "'"; + return success(); +} + +namespace { + +FunctionType graphSignature(Operation *op) { + if (!op) + return {}; + auto type = op->getAttrOfType("function_type"); + return type ? dyn_cast(type.getValue()) : FunctionType(); +} + +Operation *lookupGraphSymbol(Operation *from, FlatSymbolRefAttr name) { + auto file = from->getParentOfType(); + return file ? SymbolTable::lookupSymbolIn(file, name) : nullptr; +} + +LogicalResult verifyConcreteDictionary(Operation *op, DictionaryAttr values, + StringRef subject) { + for (NamedAttribute value : values) + if (!isConcreteStaticValue(value.getValue())) + return op->emitOpError() << subject + << " must contain only concrete builtin static " + "values"; + LogicalResult result = success(); + values.walk([&](SymbolRefAttr reference) { + if (SymbolTable::lookupNearestSymbolFrom(op, reference)) + return WalkResult::advance(); + op->emitOpError() << "unresolved static symbol reference '" << reference + << "'"; + result = failure(); + return WalkResult::interrupt(); + }); + if (failed(result)) + return failure(); + return success(); +} + +LogicalResult verifyOuterPlacement(Operation *op) { + auto outer = dyn_cast_or_null(op->getParentOp()); + if (outer && + (!outer->getParentOp() || (isa(outer->getParentOp()) && + !outer->getParentOp()->getParentOp()))) + return success(); + return op->emitOpError("must be a direct child of the outer builtin.module"); +} + +LogicalResult verifyStructuralPlacement(Operation *op) { + auto module = dyn_cast_or_null(op->getParentOp()); + if (module && !module.getBody().empty() && + op->getBlock() == &module.getBody().front()) + return success(); + return op->emitOpError( + "must be a direct child of the unique ac.module Graph block"); +} + +LogicalResult verifyExactBinding(Operation *op, DictionaryAttr binding, + StringRef subject, + StringRef requiredRegistry) { + auto registry = binding.getAs("registry"); + auto name = binding.getAs("name"); + if (binding.size() != 2 || !registry || registry.getValue().empty() || + !name || name.getValue().empty() || registry.getValue() == "generic") + return op->emitOpError() + << subject << " requires exact registered {registry, name} metadata"; + if (registry.getValue() != requiredRegistry) + return op->emitOpError() << subject << " requires registered registry '" + << requiredRegistry << "'"; + return success(); +} + +LogicalResult verifyCallShape(Operation *op, FunctionType signature, + TypeRange inputs, TypeRange outputs) { + if (!signature) + return op->emitOpError("definition has no canonical module signature"); + if (!llvm::equal(inputs, signature.getInputs())) + return op->emitOpError("operand types do not match module signature"); + if (!llvm::equal(outputs, signature.getResults())) + return op->emitOpError("result types do not match module signature"); + return success(); +} + +LogicalResult verifyStaticArgumentSet(Operation *op, DictionaryAttr arguments, + Operation *definition = nullptr) { + if (failed(verifyConcreteDictionary(op, arguments, "static arguments"))) + return failure(); + if (!definition) + return success(); + auto parameters = definition->getAttrOfType("static_params"); + if (!parameters || parameters.size() != arguments.size()) + return op->emitOpError( + "static argument names must exactly match definition parameters"); + for (NamedAttribute parameter : parameters) { + Attribute argument = arguments.get(parameter.getName()); + if (!argument) + return op->emitOpError( + "static argument names must exactly match definition parameters"); + if (argument.getTypeID() != parameter.getValue().getTypeID()) + return op->emitOpError() + << "static argument '" << parameter.getName().getValue() + << "' must match parameter attribute kind"; + auto parameterInteger = dyn_cast(parameter.getValue()); + auto argumentInteger = dyn_cast(argument); + if (parameterInteger && + parameterInteger.getType() != argumentInteger.getType()) + return op->emitOpError() + << "static argument '" << parameter.getName().getValue() + << "' must match parameter attribute type " + << parameterInteger.getType(); + auto parameterUnit = dyn_cast(parameter.getValue()); + if (parameterUnit) { + auto argumentUnit = cast(argument); + if (parameterUnit.getAs("unit") != + argumentUnit.getAs("unit")) + return op->emitOpError() + << "static argument '" << parameter.getName().getValue() + << "' must preserve unit '" + << parameterUnit.getAs("unit").getValue() << "'"; + } + } + return success(); +} + +bool isStructuralGraphChild(Operation &child) { + return isa(child) || + child.getName().getStringRef() == "arith.constant"; +} + +bool isStableHierarchySegment(StringRef segment) { + return !segment.empty() && llvm::all_of(segment, [](char c) { + return llvm::isAlnum(c) || c == '_' || c == '-'; + }); +} + +LogicalResult +verifyRuntimeReferences(ModuleOp module, + const llvm::StringMap &producerIndex) { + LogicalResult result = success(); + auto lookupExpected = [&](Operation *operation, StringRef reference, + StringRef expectedName) -> Operation * { + Operation *target = producerIndex.lookup(reference); + if (!target) { + operation->emitOpError() + << "unresolved runtime target '@" << reference << "'"; + result = failure(); + return nullptr; + } + if (target->getName().getStringRef() != expectedName) { + operation->emitOpError() << "runtime target '@" << reference + << "' must resolve to " << expectedName; + result = failure(); + return nullptr; + } + return target; + }; + for (ProcessOp process : module.getBody().front().getOps()) { + WalkResult walk = process.getBody().walk([&](Operation *operation) { + if (auto send = dyn_cast(operation)) { + auto queue = dyn_cast_or_null( + lookupExpected(send, send.getQueue(), QueueOp::getOperationName())); + if (queue && queue.getPayload() != send.getValue().getType()) { + send.emitOpError() + << "value type " << send.getValue().getType() + << " does not match queue payload type " << queue.getPayload(); + result = failure(); + } + } else if (auto recv = dyn_cast(operation)) { + auto queue = dyn_cast_or_null( + lookupExpected(recv, recv.getQueue(), QueueOp::getOperationName())); + if (queue && queue.getPayload() != recv.getValue().getType()) { + recv.emitOpError() + << "result type " << recv.getValue().getType() + << " does not match queue payload type " << queue.getPayload(); + result = failure(); + } + } else if (auto schedule = dyn_cast(operation)) { + auto target = dyn_cast_or_null(lookupExpected( + schedule, schedule.getTarget(), ProcessOp::getOperationName())); + if (target && (target.getCaptures().size() != 1 || + target.getCaptures().front().getType() != + schedule.getValue().getType())) { + schedule.emitOpError() + << "scheduled value type " << schedule.getValue().getType() + << " must match the target process's single capture type"; + result = failure(); + } + } else if (auto wait = dyn_cast(operation)) { + (void)lookupExpected(wait, wait.getResource(), + ResourceOp::getOperationName()); + } else if (auto await = dyn_cast(operation)) { + (void)lookupExpected(await, await.getEventQueue(), + EventQueueOp::getOperationName()); + } else if (auto probe = dyn_cast(operation)) { + StringRef expected = + llvm::StringSwitch(probe.getKind()) + .Case("queue", QueueOp::getOperationName()) + .Case("resource", ResourceOp::getOperationName()) + .Case("module", ProcessOp::getOperationName()) + .Case("storage", AddressSpaceOp::getOperationName()) + .Case("protocol", QueueOp::getOperationName()) + .Case("trace", ProcessOp::getOperationName()) + .Case("event_queue", EventQueueOp::getOperationName()) + .Case("external_io", ProcessOp::getOperationName()) + .Case("statistics", StatOp::getOperationName()) + .Default(StringRef()); + Operation *target = lookupExpected(probe, probe.getTarget(), expected); + if (auto queue = dyn_cast_or_null(target); + queue && probe.getKind() == "queue" && + queue.getPayload() != probe.getValue().getType()) { + probe.emitOpError() + << "result type " << probe.getValue().getType() + << " does not match queue payload type " << queue.getPayload(); + result = failure(); + } + if (auto eventQueue = dyn_cast_or_null(target); + eventQueue && + eventQueue.getPayload() != probe.getValue().getType()) { + probe.emitOpError() << "result type " << probe.getValue().getType() + << " does not match event queue payload type " + << eventQueue.getPayload(); + result = failure(); + } + } else if (auto stat = dyn_cast(operation)) { + (void)lookupExpected(stat, stat.getStat(), StatOp::getOperationName()); + } + return failed(result) ? WalkResult::interrupt() : WalkResult::advance(); + }); + if (walk.wasInterrupted()) + return failure(); + } + return result; +} + +LogicalResult verifyProcessOperations(ModuleOp module) { + for (ProcessOp process : module.getBody().front().getOps()) { + if (failed(process.verify())) + return failure(); + LogicalResult result = success(); + process.getBody().walk([&](Operation *operation) { + result = + TypeSwitch(operation) + .Case( + [](auto op) { return op.verify(); }) + .Default([](Operation *) { return success(); }); + return failed(result) ? WalkResult::interrupt() : WalkResult::advance(); + }); + if (failed(result)) + return failure(); + } + return success(); +} + +Operation *resolveSystemMember(SystemOp system, SymbolRefAttr reference, + bool instrumentation) { + if (reference.getRootReference() != system.getRootAttr().getValue()) + return nullptr; + ArrayRef nested = reference.getNestedReferences(); + if (nested.size() != (instrumentation ? 2u : 1u)) + return nullptr; + auto file = system->getParentOfType(); + if (!file) + return nullptr; + auto module = dyn_cast_or_null( + SymbolTable::lookupSymbolIn(file, system.getRootAttr())); + if (!module || module.getBody().empty()) + return nullptr; + ProcessOp process; + for (ProcessOp candidate : module.getBody().front().getOps()) + if (candidate.getSymName() == nested.front().getValue()) { + process = candidate; + break; + } + if (!process) + return nullptr; + if (!instrumentation) + return process; + Operation *found = nullptr; + process.getBody().walk([&](InstrumentationOp candidate) { + if (candidate.getSymName() == nested.back().getValue()) { + found = candidate; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return found; +} + +} // namespace + +LogicalResult SystemOp::verify() { + if (failed(verifyOuterPlacement(*this))) + return failure(); + if (!isStableHierarchySegment(getRootName())) + return emitOpError( + "root instance name must be one stable hierarchy segment"); + if (getTickEpoch() != 0) + return emitOpError("global tick epoch must be exactly 0"); + if (!hasStringValue(getTickUnit(), {"cycle", "ps", "ns", "us", "ms", "s"})) + return emitOpError() << "unsupported exact global tick unit '" + << getTickUnit() << "'"; + auto seedKind = getSeedPolicy().getAs("kind"); + auto seedValue = getSeedPolicy().getAs("value"); + if (getSeedPolicy().size() != 2 || !seedKind || + seedKind.getValue() != "fixed" || !seedValue || + !seedValue.getType().isSignlessInteger(64)) + return emitOpError( + "seed policy requires exact {kind = \"fixed\", value = signless i64} " + "schema"); + if (seedValue.getInt() < 0) + return emitOpError("fixed seed value must be a non-negative signless i64"); + auto resultId = getResultSchema().getAs("id"); + auto resultFormat = getResultSchema().getAs("format"); + if (getResultSchema().size() != 2 || !resultId || + resultId.getValue().empty() || !resultFormat || + resultFormat.getValue() != "json") + return emitOpError("result schema requires exact {id = non-empty string, " + "format = \"json\"}"); + if (SymbolRefAttr workload = getPrimaryWorkloadAttr()) { + auto process = dyn_cast_or_null( + resolveSystemMember(*this, workload, false)); + if (!process) + return emitOpError() << "primary workload '" << workload + << "' is unresolved"; + if (process.getKind() != "workload") + return emitOpError() << "primary workload '" << workload + << "' must reference a workload process"; + } + for (Attribute value : getInstrumentation()) { + auto reference = dyn_cast(value); + if (!reference) + return emitOpError("instrumentation entries must be symbol references"); + Operation *target = resolveSystemMember(*this, reference, true); + if (!target || target->getName().getStringRef() != "ac.instrumentation") + return emitOpError() << "instrumentation reference '" << reference + << "' does not resolve to ac.instrumentation"; + } + return success(); +} + +ParseResult ModuleOp::parse(OpAsmParser &parser, OperationState &result) { + StringAttr name; + SmallVector arguments; + SmallVector results; + SmallVector resultAttrs; + bool isVariadic = false; + if (parser.parseSymbolName(name, mlir::SymbolTable::getSymbolAttrName(), + result.attributes) || + function_interface_impl::parseFunctionSignatureWithArguments( + parser, /*allowVariadic=*/false, arguments, isVariadic, results, + resultAttrs)) + return failure(); + + SmallVector argumentAttrs; + argumentAttrs.reserve(arguments.size()); + bool hasArgumentAttrs = false; + for (const OpAsmParser::Argument &argument : arguments) { + DictionaryAttr attrs = argument.attrs; + hasArgumentAttrs |= static_cast(attrs) && !attrs.empty(); + argumentAttrs.push_back(attrs ? attrs + : parser.getBuilder().getDictionaryAttr({})); + } + if (hasArgumentAttrs) + result.addAttribute("arg_attrs", + parser.getBuilder().getArrayAttr(argumentAttrs)); + if (llvm::any_of(resultAttrs, [](DictionaryAttr attrs) { + return attrs && !attrs.empty(); + })) { + SmallVector normalizedResultAttrs; + normalizedResultAttrs.reserve(resultAttrs.size()); + for (DictionaryAttr attrs : resultAttrs) + normalizedResultAttrs.push_back( + attrs ? attrs : parser.getBuilder().getDictionaryAttr({})); + result.addAttribute( + "res_attrs", parser.getBuilder().getArrayAttr(normalizedResultAttrs)); + } + + DictionaryAttr staticParameters; + if (succeeded(parser.parseOptionalKeyword("parameters"))) { + if (parser.parseAttribute(staticParameters)) + return failure(); + } else { + staticParameters = parser.getBuilder().getDictionaryAttr({}); + } + result.addAttribute("static_params", staticParameters); + if (parser.parseOptionalAttrDictWithKeyword(result.attributes) || + parser.parseKeyword("graph")) + return failure(); + + SmallVector inputs; + inputs.reserve(arguments.size()); + for (const OpAsmParser::Argument &argument : arguments) + inputs.push_back(argument.type); + result.addAttribute( + "function_type", + TypeAttr::get(parser.getBuilder().getFunctionType(inputs, results))); + Region *body = result.addRegion(); + return parser.parseRegion(*body, arguments, /*enableNameShadowing=*/false); +} + +void ModuleOp::print(OpAsmPrinter &printer) { + printer << ' '; + printer.printSymbolName(getSymName()); + function_interface_impl::printFunctionSignature( + printer, *this, getArgumentTypes(), /*isVariadic=*/false, + getResultTypes()); + printer << " parameters " << getStaticParams(); + printer.printOptionalAttrDictWithKeyword( + (*this)->getAttrs(), + {mlir::SymbolTable::getSymbolAttrName(), "function_type", "static_params", + "arg_attrs", "res_attrs"}); + printer << " graph "; + printer.printRegion(getBody(), /*printEntryBlockArgs=*/false, + /*printBlockTerminators=*/true, + /*printEmptyBlock=*/true); +} + +LogicalResult ModuleOp::verify() { + if (failed(verifyOuterPlacement(*this))) + return failure(); + if (failed(verifyConcreteDictionary(*this, getStaticParams(), + "static parameters"))) + return failure(); + if (getBody().empty()) + return emitOpError("module requires one Graph body block"); + Block &entry = getBody().front(); + if (!llvm::equal(entry.getArgumentTypes(), getFunctionType().getInputs())) + return emitOpError("Graph region arguments must match module signature"); + llvm::StringSet<> localNames; + llvm::StringMap producerIndex; + llvm::StringSet<> stableIds; + llvm::StringSet<> paths; + for (Operation &child : entry) { + if (!isStructuralGraphChild(child)) + return child.emitOpError( + "operation is not legal in an ac.module structural Graph region"); + StringAttr localName; + StringAttr stableId; + StringAttr path; + if (auto instance = dyn_cast(child)) { + localName = instance.getSymNameAttr(); + stableId = instance.getStableIdAttr(); + path = instance.getPathAttr(); + } else if (auto array = dyn_cast(child)) { + localName = array.getSymNameAttr(); + stableId = array.getStableIdAttr(); + path = array.getPathAttr(); + } else if (auto instances = dyn_cast(child)) { + localName = instances.getSymNameAttr(); + stableId = instances.getStableIdAttr(); + path = instances.getPathAttr(); + } else if (auto view = dyn_cast(child)) { + localName = view.getSymNameAttr(); + } else if (auto queue = dyn_cast(child)) { + localName = queue.getSymNameAttr(); + stableId = queue.getStableIdAttr(); + path = queue.getPathAttr(); + } else if (auto eventQueue = dyn_cast(child)) { + localName = eventQueue.getSymNameAttr(); + stableId = eventQueue.getStableIdAttr(); + path = eventQueue.getPathAttr(); + } else if (auto resource = dyn_cast(child)) { + localName = resource.getSymNameAttr(); + stableId = resource.getStableIdAttr(); + path = resource.getPathAttr(); + } else if (auto addressSpace = dyn_cast(child)) { + localName = addressSpace.getSymNameAttr(); + stableId = addressSpace.getStableIdAttr(); + path = addressSpace.getPathAttr(); + } else if (auto addressMap = dyn_cast(child)) { + localName = addressMap.getSymNameAttr(); + } else if (auto timeDomain = dyn_cast(child)) { + localName = timeDomain.getSymNameAttr(); + } else if (auto process = dyn_cast(child)) { + localName = process.getSymNameAttr(); + } else if (auto stat = dyn_cast(child)) { + localName = stat.getSymNameAttr(); + } + if (localName && !localNames.insert(localName.getValue()).second) + return child.emitOpError() << "duplicate local structural name '" + << localName.getValue() << "'"; + if (localName) + producerIndex.try_emplace(localName.getValue(), &child); + if (stableId && !stableIds.insert(stableId.getValue()).second) + return child.emitOpError() << "duplicate local structural stable id '" + << stableId.getValue() << "'"; + if (path && !paths.insert(path.getValue()).second) + return child.emitOpError() + << "duplicate local structural path '" << path.getValue() << "'"; + } + if (entry.empty() || !isa(entry.back())) + return emitOpError("module Graph region must end with ac.return"); + llvm::StringMap traceSources; + for (ProcessOp process : entry.getOps()) { + WalkResult result = process.getBody().walk([&](TraceOpenOp trace) { + if (!traceSources.try_emplace(trace.getSource(), trace).second) { + trace.emitOpError() << "trace source '" << trace.getSource() + << "' must have exactly one cursor owner"; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (result.wasInterrupted()) + return failure(); + } + for (Operation &child : entry) { + LogicalResult local = TypeSwitch(&child) + .Case( + [](auto op) { return op.verify(); }) + .Default([](Operation *) { return success(); }); + if (failed(local)) + return failure(); + } + if (failed(verifyModuleResourceReferences(*this, producerIndex))) + return failure(); + if (failed(verifyProcessOperations(*this))) + return failure(); + if (failed(verifyRuntimeReferences(*this, producerIndex))) + return failure(); + for (ViewOp view : entry.getOps()) + if (failed(view.verify()) || + failed(view.verifyWithProducerIndex(producerIndex))) + return failure(); + return success(); +} + +LogicalResult ModuleExternOp::verify() { + if (failed(verifyOuterPlacement(*this))) + return failure(); + if (failed(verifyConcreteDictionary(*this, getStaticParams(), + "static parameters"))) + return failure(); + if (failed(verifyExactBinding(*this, getImplementation(), + "external module implementation", "cpp"))) + return failure(); + StringRef name = getImplementation().getAs("name").getValue(); + if (!getStructuralProviderRegistry(getContext()).hasExternal(name)) + return emitOpError() << "structural provider 'cpp:" << name + << "' is not registered"; + return success(); +} + +LogicalResult ModuleGeneratedOp::verify() { + if (failed(verifyOuterPlacement(*this))) + return failure(); + if (failed(verifyConcreteDictionary(*this, getStaticParams(), + "static parameters"))) + return failure(); + if (failed( + verifyExactBinding(*this, getGenerator(), "generated module", "ac"))) + return failure(); + StringRef name = getGenerator().getAs("name").getValue(); + if (!getStructuralProviderRegistry(getContext()).hasGenerator(name)) + return emitOpError() << "structural provider 'ac:" << name + << "' is not registered"; + return success(); +} + +LogicalResult InstanceOp::verify() { + if (failed(verifyStructuralPlacement(*this))) + return failure(); + Operation *definition = lookupGraphSymbol(*this, getDefinitionAttr()); + if (!isa_and_nonnull(definition)) + return emitOpError() << "unresolved module definition '" + << getDefinitionAttr() << "'"; + if (!isStableHierarchySegment(getSymName()) || + !isStableHierarchySegment(getStableId()) || + !isStableHierarchySegment(getPath())) + return emitOpError( + "instance name, stable id, and path must be stable local segments"); + if (failed(verifyStaticArgumentSet(*this, getStaticArgs(), definition))) + return failure(); + return verifyCallShape(*this, graphSignature(definition), + getInputs().getTypes(), getOutputs().getTypes()); +} + +LogicalResult ArrayOp::verify() { + if (failed(verifyStructuralPlacement(*this))) + return failure(); + Operation *definition = lookupGraphSymbol(*this, getDefinitionAttr()); + if (!isa_and_nonnull(definition)) + return emitOpError() << "unresolved array element definition '" + << getDefinitionAttr() << "'"; + if (!isStableHierarchySegment(getSymName()) || + !isStableHierarchySegment(getStableId()) || + !isStableHierarchySegment(getPath())) + return emitOpError( + "array name, stable id, and path must be stable local segments"); + if (getShape().empty()) + return emitOpError("array shape must have at least one dimension"); + uint64_t count = 1; + for (int64_t extent : getShape()) { + if (extent < 0) + return emitOpError("array shape dimensions must be non-negative"); + if (extent != 0 && count > std::numeric_limits::max() / + static_cast(extent)) + return emitOpError("array cardinality overflows 64 bits"); + count *= static_cast(extent); + } + constexpr uint64_t maxStaticElements = 1U << 20; + if (count > maxStaticElements) + return emitOpError( + "array cardinality exceeds static elaboration bound 1048576"); + FunctionType signature = graphSignature(definition); + if (!signature) + return emitOpError("array element definition has no signature"); + if (getStaticArgs().size() != count) + return emitOpError("array requires one concrete static argument set per " + "lexicographically ordered element"); + for (Attribute value : getStaticArgs()) { + auto arguments = dyn_cast(value); + if (!arguments || + failed(verifyStaticArgumentSet(*this, arguments, definition))) + return emitOpError( + "array static arguments must be concrete dictionaries"); + } + auto matchesRepeatedSignature = [count](TypeRange actual, + TypeRange elementTypes) { + if (elementTypes.empty()) + return actual.empty(); + if (count > std::numeric_limits::max() / elementTypes.size() || + actual.size() != count * elementTypes.size()) + return false; + for (auto [index, type] : llvm::enumerate(actual)) + if (type != elementTypes[index % elementTypes.size()]) + return false; + return true; + }; + if (!matchesRepeatedSignature(getInputs().getTypes(), + signature.getInputs()) || + !matchesRepeatedSignature(getOutputs().getTypes(), + signature.getResults())) + return emitOpError("array flattened interface shape does not match element " + "signature and static cardinality"); + return success(); +} + +LogicalResult InstancesOp::verify() { + if (failed(verifyStructuralPlacement(*this))) + return failure(); + if (!isStableHierarchySegment(getSymName()) || + !isStableHierarchySegment(getStableId()) || + !isStableHierarchySegment(getPath())) + return emitOpError("collection name, stable id, and path must be stable " + "local segments"); + size_t count = getDefinitions().size(); + if (count == 0 || getNames().size() != count || + getStableIds().size() != count || getPaths().size() != count || + getStaticArgs().size() != count) + return emitOpError("ordered instance metadata arrays must have identical " + "non-zero cardinality"); + llvm::SmallDenseSet names; + llvm::SmallDenseSet ids; + llvm::SmallDenseSet paths; + for (size_t index = 0; index < count; ++index) { + auto definition = dyn_cast(getDefinitions()[index]); + if (!definition) + return emitOpError("definitions must contain flat module symbols"); + Operation *target = lookupGraphSymbol(*this, definition); + if (!isa_and_nonnull(target)) + return emitOpError() << "unresolved collection definition '" << definition + << "'"; + if (graphSignature(target) != getInterface()) + return emitOpError("collection element definition does not implement " + "the exact declared common interface"); + auto arguments = dyn_cast(getStaticArgs()[index]); + if (!arguments || failed(verifyStaticArgumentSet(*this, arguments, target))) + return emitOpError("collection static arguments must be concrete " + "dictionaries"); + StringRef name = cast(getNames()[index]).getValue(); + StringRef id = cast(getStableIds()[index]).getValue(); + StringRef path = cast(getPaths()[index]).getValue(); + if (!isStableHierarchySegment(name) || !names.insert(name).second || + !isStableHierarchySegment(id) || !ids.insert(id).second) + return emitOpError("collection names and stable ids must be non-empty " + "and unique in declared order"); + if (!isStableHierarchySegment(path) || !paths.insert(path).second) + return emitOpError("collection paths must be stable, unique " + "parent-relative segments"); + } + auto matchesRepeatedInterface = [count](TypeRange actual, + TypeRange elementTypes) { + if (elementTypes.empty()) + return actual.empty(); + if (count > std::numeric_limits::max() / elementTypes.size() || + actual.size() != count * elementTypes.size()) + return false; + for (auto [index, type] : llvm::enumerate(actual)) + if (type != elementTypes[index % elementTypes.size()]) + return false; + return true; + }; + if (!matchesRepeatedInterface(getInputs().getTypes(), + getInterface().getInputs()) || + !matchesRepeatedInterface(getOutputs().getTypes(), + getInterface().getResults())) + return emitOpError("ordered collection IO does not match its common " + "interface shape"); + return success(); +} + +LogicalResult ViewOp::verify() { + if (failed(verifyStructuralPlacement(*this))) + return failure(); + if (!isStableHierarchySegment(getSymName())) + return emitOpError("view name must be a stable local segment"); + return success(); +} + +LogicalResult ViewOp::verifyWithProducerIndex( + const llvm::StringMap &producerIndex) { + ArrayRef indices = getIndices(); + ArrayRef shape = getShape(); + if (llvm::any_of(shape, [](int64_t value) { return value < 0; })) + return emitOpError("view shape must be fully static and non-negative"); + auto checkedProduct = [&](ArrayRef dimensions, + uint64_t &product) -> LogicalResult { + product = 1; + for (int64_t extent : dimensions) { + if (extent < 0) + return emitOpError( + "source shapes must be fully static and non-negative"); + if (extent != 0 && product > std::numeric_limits::max() / + static_cast(extent)) + return emitOpError("view cardinality overflows 64 bits"); + product *= static_cast(extent); + } + return success(); + }; + + SmallVector> sourceShapes; + SmallVector> sources; + llvm::SmallDenseSet sourceProducers; + auto getProducerShape = [&](Operation *producer) { + SmallVector producerShape; + if (auto instance = dyn_cast(producer)) + producerShape.push_back(instance.getNumResults()); + else if (auto array = dyn_cast(producer)) { + producerShape.append(array.getShape().begin(), array.getShape().end()); + producerShape.push_back( + graphSignature(lookupGraphSymbol(array, array.getDefinitionAttr())) + .getNumResults()); + } else if (auto instances = dyn_cast(producer)) { + producerShape.push_back(instances.getDefinitions().size()); + producerShape.push_back(instances.getInterface().getNumResults()); + } else if (auto view = dyn_cast(producer)) + producerShape.append(view.getShape().begin(), view.getShape().end()); + return producerShape; + }; + if (getSourceProducers().size() != getSourceShapes().size()) + return emitOpError( + "source_producers and source_shapes must have identical cardinality"); + size_t operandOffset = 0; + for (auto [producerReference, attribute] : + llvm::zip(getSourceProducers(), getSourceShapes())) { + auto sourceShape = dyn_cast(attribute); + if (!sourceShape) + return emitOpError("source_shapes entries must be dense i64 arrays"); + uint64_t cardinality = 0; + if (failed(checkedProduct(sourceShape.asArrayRef(), cardinality))) + return failure(); + if (cardinality > getInputs().size() - operandOffset) + return emitOpError("source_shapes do not partition the view operands"); + SmallVector source; + source.append(getInputs().begin() + operandOffset, + getInputs().begin() + operandOffset + cardinality); + operandOffset += cardinality; + auto producerSymbol = cast(producerReference); + if (!isStableHierarchySegment(producerSymbol.getValue())) + return emitOpError("source producer IDs must be stable local segments"); + Operation *producer = producerIndex.lookup(producerSymbol.getValue()); + if (!producer) + return emitOpError() << "source producer '" << producerReference + << "' is unresolved"; + if (producer == getOperation()) + return emitOpError("view cannot name itself as a source producer"); + if (!isa(producer) || + producer->getBlock() != getOperation()->getBlock()) + return emitOpError("source producer must resolve to a direct structural " + "producer in the same ac.module"); + if (!sourceProducers.insert(producer).second) + return emitOpError("source producers must not repeat"); + if (!source.empty()) { + if (source.front().getDefiningOp() != producer || + producer->getNumResults() != source.size()) + return emitOpError( + "each source must be the complete result group of its declared " + "structural producer"); + for (auto [index, value] : llvm::enumerate(source)) + if (value != producer->getResult(index)) + return emitOpError( + "source operands must preserve producer result order"); + } + SmallVector producerShape = getProducerShape(producer); + if (producerShape != sourceShape.asArrayRef()) + return emitOpError("source_shapes must exactly match producer shapes"); + sourceShapes.emplace_back(sourceShape.asArrayRef().begin(), + sourceShape.asArrayRef().end()); + sources.push_back(std::move(source)); + } + if (operandOffset != getInputs().size()) + return emitOpError("source_shapes do not partition the view operands"); + + SmallVector expected; + StringRef kind = getKind(); + if (kind == "select") { + if (sources.size() != 1 || indices.size() != sourceShapes[0].size() || + !shape.empty() || getAxisAttr()) + return emitOpError("select requires one source, one coordinate per " + "dimension, scalar shape, and no axis"); + uint64_t ordinal = 0; + for (auto [coordinate, extent] : llvm::zip(indices, sourceShapes[0])) { + if (coordinate < 0 || coordinate >= extent) + return emitOpError("select coordinate is out of bounds"); + ordinal = ordinal * static_cast(extent) + coordinate; + } + expected.push_back(sources[0][ordinal]); + } else if (kind == "slice") { + if (sources.size() != 1 || getAxisAttr() || + indices.size() != 2 * sourceShapes[0].size()) + return emitOpError("slice requires one source and [lower, upper] bounds " + "for every dimension"); + SmallVector derivedShape; + for (size_t dimension = 0; dimension < sourceShapes[0].size(); + ++dimension) { + int64_t lower = indices[2 * dimension]; + int64_t upper = indices[2 * dimension + 1]; + if (lower < 0 || upper < lower || upper > sourceShapes[0][dimension]) + return emitOpError("slice bounds are invalid"); + derivedShape.push_back(upper - lower); + } + if (derivedShape != shape) + return emitOpError("slice result shape must equal its bound extents"); + for (size_t ordinal = 0; ordinal < sources[0].size(); ++ordinal) { + size_t remainder = ordinal; + bool included = true; + for (size_t reverse = sourceShapes[0].size(); reverse-- > 0;) { + int64_t coordinate = remainder % sourceShapes[0][reverse]; + remainder /= sourceShapes[0][reverse]; + included &= coordinate >= indices[2 * reverse] && + coordinate < indices[2 * reverse + 1]; + } + if (included) + expected.push_back(sources[0][ordinal]); + } + } else if (kind == "concat") { + if (sources.size() < 2 || !indices.empty() || !getAxisAttr()) + return emitOpError("concat requires at least two sources, an axis, and " + "no index metadata"); + int64_t axis = getAxisAttr().getInt(); + size_t rank = sourceShapes.front().size(); + if (axis < 0 || static_cast(axis) >= rank) + return emitOpError("concat axis is out of bounds"); + SmallVector derivedShape = sourceShapes.front(); + derivedShape[axis] = 0; + for (ArrayRef sourceShape : sourceShapes) { + if (sourceShape.size() != rank) + return emitOpError("concat source ranks must match"); + for (size_t dimension = 0; dimension < rank; ++dimension) + if (dimension != static_cast(axis) && + sourceShape[dimension] != derivedShape[dimension]) + return emitOpError("concat non-axis dimensions must match"); + if (sourceShape[axis] > + std::numeric_limits::max() - derivedShape[axis]) + return emitOpError("concat axis extent overflows signed i64"); + derivedShape[axis] += sourceShape[axis]; + } + if (derivedShape != shape) + return emitOpError("concat result shape is not derived from its sources"); + uint64_t outer = 1, inner = 1; + for (int64_t extent : ArrayRef(shape).take_front(axis)) + outer *= extent; + for (int64_t extent : ArrayRef(shape).drop_front(axis + 1)) + inner *= extent; + for (uint64_t outerIndex = 0; outerIndex < outer; ++outerIndex) + for (auto [sourceIndex, source] : llvm::enumerate(sources)) { + uint64_t chunk = sourceShapes[sourceIndex][axis] * inner; + expected.append(source.begin() + outerIndex * chunk, + source.begin() + (outerIndex + 1) * chunk); + } + } else if (kind == "zip") { + if (sources.size() != 2 || !indices.empty() || getAxisAttr() || + sourceShapes[0] != sourceShapes[1]) + return emitOpError("zip requires two equal-shaped sources and no axis or " + "index metadata"); + SmallVector derivedShape = sourceShapes[0]; + derivedShape.push_back(2); + if (derivedShape != shape) + return emitOpError("zip result shape must append source count"); + for (size_t index = 0; index < sources[0].size(); ++index) { + expected.push_back(sources[0][index]); + expected.push_back(sources[1][index]); + } + } else if (kind == "permutation") { + if (sources.size() != 1 || getAxisAttr() || + !llvm::equal(shape, sourceShapes[0]) || + indices.size() != sources[0].size()) + return emitOpError("permutation requires one source, unchanged shape, " + "and one index per element"); + llvm::SmallDenseSet seen; + for (int64_t index : indices) { + if (index < 0 || static_cast(index) >= sources[0].size() || + !seen.insert(index).second) + return emitOpError( + "permutation indices must be an in-bounds bijection"); + expected.push_back(sources[0][index]); + } + } else if (kind == "elementwise") { + if (sources.size() < 2 || !indices.empty() || getAxisAttr()) + return emitOpError("elementwise requires at least two sources and no " + "axis or index metadata"); + if (llvm::any_of(sourceShapes, [&](const auto &sourceShape) { + return !llvm::equal(sourceShape, sourceShapes.front()); + })) + return emitOpError("elementwise source shapes must match"); + SmallVector derivedShape = sourceShapes.front(); + derivedShape.push_back(sources.size()); + if (derivedShape != shape) + return emitOpError("elementwise result shape must append source count"); + for (size_t index = 0; index < sources.front().size(); ++index) + for (ArrayRef source : sources) + expected.push_back(source[index]); + } else { + return emitOpError() << "unsupported static view kind '" << kind << "'"; + } + uint64_t cardinality = 0; + if (failed(checkedProduct(shape, cardinality))) + return failure(); + if (cardinality != expected.size() || + !llvm::equal(getOutputs().getTypes(), + llvm::map_range( + expected, [](Value value) { return value.getType(); }))) + return emitOpError("resolved view shape/order/types do not match outputs"); + return success(); +} + +LogicalResult ReturnOp::verify() { + ModuleOp module = getOperation()->getParentOfType(); + if (!module) + return emitOpError("must terminate an ac.module Graph region"); + if (!llvm::equal(getOperandTypes(), module.getFunctionType().getResults())) + return emitOpError("operand types and count must exactly match module " + "results"); + if (llvm::any_of(getOperandTypes(), + [](Type type) { return isa(type); })) + return emitOpError( + "private ownership handle cannot be exported from ac.module"); + return success(); +} + +LogicalResult verifyTopologyTypeUses(Operation *operation) { + if (failed(verifyGraphStructure(operation))) + return failure(); + std::function verifyType = + [&](Type type, Value value) -> LogicalResult { + if (auto function = dyn_cast(type)) { + for (Type input : function.getInputs()) + if (failed(verifyType(input, {}))) + return failure(); + for (Type result : function.getResults()) + if (failed(verifyType(result, {}))) + return failure(); + return success(); + } + if (Type nested = findNestedTopologyLeaf(type)) + return operation->emitOpError() << "topology type " << nested + << " cannot be nested inside " << type; + if (auto flow = dyn_cast(type)) { + if (!isProtocolPayloadType(flow.getElementType())) + return operation->emitOpError( + "flow payload type must be a normative ACIR value type"); + if (failed(verifyNamedTypes(operation, flow.getElementType()))) + return failure(); + ProtocolOp protocol = lookupProtocol(operation, flow.getProtocol()); + if (!protocol) + return operation->emitOpError() << "unresolved flow protocol '@" + << flow.getProtocol().getValue() << "'"; + if (!matchesCarrierEvent(protocol, flow.getElementType())) + return operation->emitOpError() + << "flow payload " << flow.getElementType() + << " does not match any carrier event in protocol '@" + << flow.getProtocol().getValue() << "'"; + if (value && !value.hasOneUse() && !value.use_empty()) + return operation->emitOpError( + "flow value has more than one functional use"); + } + if (auto endpoint = dyn_cast(type)) { + auto module = operation->getParentOfType(); + InterfaceOp interface = + module ? dyn_cast_or_null(SymbolTable::lookupSymbolIn( + module, endpoint.getInterface())) + : InterfaceOp(); + if (!interface) + return operation->emitOpError() + << "unresolved endpoint interface '@" + << endpoint.getInterface().getValue() << "'"; + RoleOp role = lookupChild(interface, endpoint.getRole()); + if (!role) + return operation->emitOpError() + << "endpoint role '@" << endpoint.getRole().getValue() + << "' is not a member of interface '@" + << endpoint.getInterface().getValue() << "'"; + if (role.getCardinality() == "exclusive" && value && !value.hasOneUse() && + !value.use_empty()) + return operation->emitOpError( + "exclusive endpoint value has more than one structural use"); + } + return success(); + }; + + auto verifyAttribute = [&](Attribute attribute) -> LogicalResult { + if (!attribute) + return success(); + LogicalResult result = success(); + attribute.walk([&](TypeAttr type) { + if (failed(verifyType(type.getValue(), {}))) { + result = failure(); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (failed(result)) + return failure(); + attribute.walk([&](Type type) { + if (failed(verifyType(type, {}))) { + result = failure(); + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return result; + }; + + for (Value result : operation->getResults()) + if (failed(verifyType(result.getType(), result))) + return failure(); + for (OpOperand &operand : operation->getOpOperands()) + if (failed(verifyType(operand.get().getType(), operand.get()))) + return failure(); + for (Region ®ion : operation->getRegions()) + for (Block &block : region) + for (BlockArgument argument : block.getArguments()) + if (failed(verifyType(argument.getType(), argument))) + return failure(); + for (NamedAttribute attribute : operation->getAttrs()) + if (failed(verifyAttribute(attribute.getValue()))) + return failure(); + if (failed(verifyAttribute(operation->getPropertiesAsAttribute())) || + failed(verifyAttribute(LocationAttr(operation->getLoc())))) + return failure(); + return success(); +} + +namespace { + +SymbolRefAttr qualifiedRuntimeOwner(Operation *operation, StringRef local) { + if (auto definition = operation->getParentOfType()) + return SymbolRefAttr::get( + operation->getContext(), definition.getSymName(), + {FlatSymbolRefAttr::get(operation->getContext(), local)}); + return SymbolRefAttr::get(operation->getContext(), local); +} + +Operation *resolvedRuntimeTarget(Operation *operation, StringRef local) { + ModuleOp module = operation->getParentOfType(); + if (!module || module.getBody().empty()) + return nullptr; + for (Operation &candidate : module.getBody().front()) { + auto name = + candidate.getAttrOfType(SymbolTable::getSymbolAttrName()); + if (name && name.getValue() == local) + return &candidate; + } + return nullptr; +} + +DictionaryAttr runtimeEffectParameters(Operation *operation, StringRef kind, + StringRef identity) { + Builder builder(operation->getContext()); + Operation *owner = resolvedRuntimeTarget(operation, identity); + if (!owner) + if (auto process = operation->getParentOfType()) + owner = process; + if (owner) + if (auto owners = owner->getAttrOfType("ac.frozen_owners")) + return builder.getDictionaryAttr({ + builder.getNamedAttr("identity_phase", + builder.getStringAttr("elaborated_absolute")), + builder.getNamedAttr("owner_kind", builder.getStringAttr(kind)), + builder.getNamedAttr("owners", owners), + }); + return builder.getDictionaryAttr({ + builder.getNamedAttr("identity_phase", + builder.getStringAttr("definition_pre_freeze")), + builder.getNamedAttr("owner_kind", builder.getStringAttr(kind)), + builder.getNamedAttr("identity", builder.getStringAttr(identity)), + }); +} + +void addEffect(SmallVectorImpl &effects, + Operation *operation, MemoryEffects::Effect *effect, + StringRef identity, StringRef kind, + SideEffects::Resource *resource) { + effects.emplace_back(effect, qualifiedRuntimeOwner(operation, identity), + runtimeEffectParameters(operation, kind, identity), + resource); +} + +DictionaryAttr contractEffectParameters(Operation *operation, StringRef phase, + StringRef identity) { + Builder builder(operation->getContext()); + if (auto process = operation->getParentOfType()) + if (auto owners = process->getAttrOfType("ac.frozen_owners")) + return builder.getDictionaryAttr({ + builder.getNamedAttr("identity_phase", + builder.getStringAttr("elaborated_absolute")), + builder.getNamedAttr("owner_kind", builder.getStringAttr("contract")), + builder.getNamedAttr("owners", owners), + builder.getNamedAttr("contract_phase", builder.getStringAttr(phase)), + }); + if (operation->getAttrOfType("ac.freeze_proven")) + return builder.getDictionaryAttr({ + builder.getNamedAttr("identity_phase", + builder.getStringAttr("elaborated_absolute")), + builder.getNamedAttr("owner_kind", builder.getStringAttr("contract")), + builder.getNamedAttr("identity", builder.getStringAttr(identity)), + builder.getNamedAttr("contract_phase", builder.getStringAttr(phase)), + builder.getNamedAttr("freeze_proven", builder.getBoolAttr(true)), + }); + return builder.getDictionaryAttr({ + builder.getNamedAttr("identity_phase", + builder.getStringAttr("definition_pre_freeze")), + builder.getNamedAttr("owner_kind", builder.getStringAttr("contract")), + builder.getNamedAttr("identity", builder.getStringAttr(identity)), + builder.getNamedAttr("contract_phase", builder.getStringAttr(phase)), + }); +} + +ProcessOp enclosingProcess(Operation *operation) { + return operation->getParentOfType(); +} + +LogicalResult requireProcess(Operation *operation) { + if (enclosingProcess(operation)) + return success(); + return operation->emitOpError("must be nested in ac.process"); +} + +StringRef processIdentity(Operation *operation) { + ProcessOp process = enclosingProcess(operation); + return process ? process.getSymName() : StringRef("invalid_process"); +} + +void addContractEffect(SmallVectorImpl &effects, + Operation *operation) { + if (!isa(operation) && + isa_and_nonnull(operation->getParentOp())) { + constexpr StringLiteral identity = "contracts"; + effects.emplace_back( + MemoryEffects::Read::get(), qualifiedRuntimeOwner(operation, identity), + contractEffectParameters(operation, "topology_freeze", identity), + ModuleStateResource::get()); + return; + } + StringRef identity = processIdentity(operation); + effects.emplace_back(MemoryEffects::Write::get(), + qualifiedRuntimeOwner(operation, identity), + contractEffectParameters(operation, "runtime", identity), + ExternalIOResource::get()); +} + +std::string traceOwnerIdentity(Operation *operation, StringRef trace) { + return (processIdentity(operation) + "/" + trace).str(); +} + +bool isSuspension(Operation *operation) { + return isa(operation); +} + +bool isLinearAcrossSuspension(Type type) { + return isa(type); +} + +bool isAllowedProcessOperation(Operation *operation) { + StringRef name = operation->getName().getStringRef(); + if (name.starts_with("arith.") || name.starts_with("index.") || + name == "func.call" || + isa( + operation)) + return true; + return isa(operation); +} + +std::optional constantBool(Value value) { + Operation *definition = value.getDefiningOp(); + if (!definition) + return std::nullopt; + Attribute attribute = definition->getAttr("value"); + if (auto boolean = dyn_cast_or_null(attribute)) + return boolean.getValue(); + if (auto integer = dyn_cast_or_null(attribute); + integer && integer.getType().isInteger(1)) + return integer.getValue().getBoolValue(); + return std::nullopt; +} + +LogicalResult verifySupportedSCFShape(ProcessOp process) { + LogicalResult result = success(); + process.getBody().walk([&](Operation *operation) { + auto requireTerminator = [&](Region ®ion, StringRef owner, + StringRef terminator) -> Operation * { + if (region.empty() || !llvm::hasSingleElement(region) || + region.front().empty() || + region.front().back().getName().getStringRef() != terminator) { + operation->emitOpError() + << "malformed " << owner << " region must terminate with " + << terminator; + result = failure(); + return nullptr; + } + return ®ion.front().back(); + }; + auto sameTypes = [](auto left, auto right) { + if (left.size() != right.size()) + return false; + return llvm::all_of(llvm::zip(left, right), [](auto pair) { + return std::get<0>(pair).getType() == std::get<1>(pair).getType(); + }); + }; + auto emitArityError = [&](StringRef owner) { + operation->emitOpError() + << "malformed " << owner + << " operand/result/block argument/yield arity or type mismatch"; + result = failure(); + return WalkResult::interrupt(); + }; + if (isa(operation)) { + if (operation->getNumRegions() != 2) { + operation->emitOpError( + "malformed scf.if region must terminate with scf.yield"); + result = failure(); + return WalkResult::interrupt(); + } + Region &thenRegion = operation->getRegion(0); + Region &elseRegion = operation->getRegion(1); + Operation *thenYield = + requireTerminator(thenRegion, "scf.if", "scf.yield"); + Operation *elseYield = + elseRegion.empty() + ? nullptr + : requireTerminator(elseRegion, "scf.if", "scf.yield"); + if (!thenYield || (!elseRegion.empty() && !elseYield)) + return WalkResult::interrupt(); + if (operation->getNumOperands() != 1 || + !operation->getOperand(0).getType().isInteger(1) || + !thenRegion.front().getArguments().empty() || + (!elseRegion.empty() && !elseRegion.front().getArguments().empty()) || + thenYield->getNumResults() != 0 || + (elseYield && elseYield->getNumResults() != 0) || + !sameTypes(thenYield->getOperands(), operation->getResults()) || + (elseRegion.empty() + ? operation->getNumResults() != 0 + : !sameTypes(elseYield->getOperands(), operation->getResults()))) + return emitArityError("scf.if"); + } else if (isa(operation)) { + if (operation->getNumRegions() != 1) + return emitArityError("scf.for"); + Region &body = operation->getRegion(0); + Operation *yield = requireTerminator(body, "scf.for", "scf.yield"); + if (!yield) + return WalkResult::interrupt(); + unsigned resultCount = operation->getNumResults(); + if (operation->getNumOperands() < 3 || + operation->getNumOperands() != resultCount + 3 || + body.front().getNumArguments() != resultCount + 1) + return emitArityError("scf.for"); + Type inductionType = operation->getOperand(0).getType(); + if (yield->getNumResults() != 0 || + (!inductionType.isIndex() && !isa(inductionType)) || + operation->getOperand(1).getType() != inductionType || + operation->getOperand(2).getType() != inductionType || + body.front().getArgument(0).getType() != inductionType || + !sameTypes(operation->getOperands().drop_front(3), + operation->getResults()) || + !sameTypes(body.front().getArguments().drop_front(), + operation->getResults()) || + !sameTypes(yield->getOperands(), operation->getResults())) + return emitArityError("scf.for"); + } else if (isa(operation)) { + if (operation->getNumRegions() != 2) + return emitArityError("scf.while"); + Region &before = operation->getRegion(0); + Region &after = operation->getRegion(1); + Operation *condition = + requireTerminator(before, "scf.while before", "scf.condition"); + Operation *yield = + requireTerminator(after, "scf.while after", "scf.yield"); + if (!condition || !yield) + return WalkResult::interrupt(); + if (condition->getNumResults() != 0 || yield->getNumResults() != 0 || + condition->getNumOperands() < 1 || + !condition->getOperand(0).getType().isInteger(1) || + !sameTypes(operation->getOperands(), before.front().getArguments()) || + !sameTypes(condition->getOperands().drop_front(), + operation->getResults()) || + !sameTypes(after.front().getArguments(), operation->getResults()) || + !sameTypes(yield->getOperands(), before.front().getArguments())) + return emitArityError("scf.while"); + } + return WalkResult::advance(); + }); + return result; +} + +class StructuredSuspensionAnalysis { +public: + explicit StructuredSuspensionAnalysis(ProcessOp process) + : work(processLivenessWorkCollector) { + buildSummaries(process.getBody()); + buildEpochs(process.getBody()); + } + + bool guaranteesSuspend(Region ®ion) const { + return regionGuarantees.lookup(®ion); + } + + LogicalResult verifyLinearLiveness(ProcessOp process) const { + LogicalResult result = success(); + process.getBody().walk([&](Block *block) { + if (failed(result) || !reachableBlocks.contains(block)) + return failed(result) ? WalkResult::interrupt() : WalkResult::advance(); + auto verifyValue = [&](Value value, + uint64_t definitionEpoch) -> LogicalResult { + if (work) + ++work->valueVisits; + if (!isLinearAcrossSuspension(value.getType())) + return success(); + for (OpOperand &use : value.getUses()) { + if (work) + ++work->useVisits; + if (!reachableBlocks.contains(use.getOwner()->getBlock())) + continue; + if (beforeEpoch.lookup(use.getOwner()) > definitionEpoch) + return process.emitOpError() + << "value of type " << value.getType() + << " cannot remain live across suspension"; + } + return success(); + }; + uint64_t entryEpoch = blockEntryEpoch.lookup(block); + for (BlockArgument argument : block->getArguments()) + if (failed(verifyValue(argument, entryEpoch))) { + result = failure(); + return WalkResult::interrupt(); + } + for (Operation &operation : *block) { + if (work) + ++work->livenessOperationVisits; + for (Value value : operation.getResults()) + if (failed(verifyValue(value, afterEpoch.lookup(&operation)))) { + result = failure(); + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + return result; + } + +private: + void buildSummaries(Region &root) { + SmallVector> worklist; + for (Block &block : llvm::reverse(root)) + for (Operation &operation : llvm::reverse(block)) + worklist.emplace_back(&operation, false); + while (!worklist.empty()) { + auto [operation, visited] = worklist.pop_back_val(); + if (!visited) { + worklist.emplace_back(operation, true); + for (Region ®ion : llvm::reverse(operation->getRegions())) + for (Block &block : llvm::reverse(region)) + for (Operation &nested : llvm::reverse(block)) + worklist.emplace_back(&nested, false); + continue; + } + if (work) + ++work->summaryOperationVisits; + for (Region ®ion : operation->getRegions()) { + bool may = false; + bool guarantees = false; + for (Block &block : region) + for (Operation &nested : block) { + may |= operationMaySuspend.lookup(&nested); + guarantees |= operationGuaranteesSuspend.lookup(&nested); + } + regionMaySuspend.try_emplace(®ion, may); + regionGuarantees.try_emplace(®ion, guarantees); + } + bool may = isSuspension(operation); + bool guarantees = isSuspension(operation); + if (auto ifOp = dyn_cast(operation)) { + if (std::optional condition = constantBool(ifOp.getCondition())) { + Region &taken = operation->getRegion(*condition ? 0 : 1); + may |= regionMaySuspend.lookup(&taken); + guarantees |= regionGuarantees.lookup(&taken); + } else { + Region &thenRegion = operation->getRegion(0); + Region &elseRegion = operation->getRegion(1); + may |= regionMaySuspend.lookup(&thenRegion) || + regionMaySuspend.lookup(&elseRegion); + guarantees |= !elseRegion.empty() && + regionGuarantees.lookup(&thenRegion) && + regionGuarantees.lookup(&elseRegion); + } + } else { + for (Region ®ion : operation->getRegions()) + may |= regionMaySuspend.lookup(®ion); + } + operationMaySuspend.try_emplace(operation, may); + operationGuaranteesSuspend.try_emplace(operation, guarantees); + } + bool may = false; + bool guarantees = false; + for (Block &block : root) + for (Operation &operation : block) { + may |= operationMaySuspend.lookup(&operation); + guarantees |= operationGuaranteesSuspend.lookup(&operation); + } + regionMaySuspend.try_emplace(&root, may); + regionGuarantees.try_emplace(&root, guarantees); + } + + void buildEpochs(Region &root) { + SmallVector> worklist; + for (Block &block : root) + worklist.emplace_back(&block, 0); + while (!worklist.empty()) { + auto [block, entryEpoch] = worklist.pop_back_val(); + if (!reachableBlocks.insert(block).second) + continue; + blockEntryEpoch.try_emplace(block, entryEpoch); + uint64_t epoch = entryEpoch; + for (Operation &operation : *block) { + if (work) + ++work->epochOperationVisits; + beforeEpoch.try_emplace(&operation, epoch); + SmallVector reachableRegions; + if (auto ifOp = dyn_cast(operation)) { + if (std::optional condition = constantBool(ifOp.getCondition())) + reachableRegions.push_back(*condition ? 0 : 1); + else + reachableRegions.append({0, 1}); + } else { + for (unsigned index = 0; index < operation.getNumRegions(); ++index) + reachableRegions.push_back(index); + } + for (unsigned index : llvm::reverse(reachableRegions)) + for (Block &nested : llvm::reverse(operation.getRegion(index))) + worklist.emplace_back(&nested, epoch); + epoch += operationMaySuspend.lookup(&operation) ? 1 : 0; + afterEpoch.try_emplace(&operation, epoch); + } + } + } + + llvm::DenseMap operationMaySuspend; + llvm::DenseMap operationGuaranteesSuspend; + llvm::DenseMap regionMaySuspend; + llvm::DenseMap regionGuarantees; + llvm::DenseMap blockEntryEpoch; + llvm::DenseMap beforeEpoch; + llvm::DenseMap afterEpoch; + llvm::DenseSet reachableBlocks; + detail::ProcessLivenessWork *work; +}; + +LogicalResult verifyTraceProvenance(ProcessOp process) { + llvm::DenseMap> forwarding; + llvm::DenseSet forwardingUses; + auto connect = [&](Value left, Value right, OpOperand *use = nullptr) { + if (!left.getType().isIndex() || !right.getType().isIndex()) + return; + forwarding[left].push_back(right); + forwarding[right].push_back(left); + if (use) + forwardingUses.insert(use); + }; + + process.getBody().walk([&](Operation *operation) { + if (auto ifOp = dyn_cast(operation)) { + for (Region *region : {&ifOp.getThenRegion(), &ifOp.getElseRegion()}) { + if (region->empty()) + continue; + auto yield = cast(region->front().getTerminator()); + for (auto [operand, result] : + llvm::zip(yield->getOpOperands(), ifOp.getResults())) + connect(operand.get(), result, &operand); + } + } else if (auto forOp = dyn_cast(operation)) { + auto yield = cast(forOp.getBody()->getTerminator()); + for (auto [index, init, iter, result, yielded] : + llvm::enumerate(forOp.getInitArgs(), forOp.getRegionIterArgs(), + forOp.getResults(), yield->getOpOperands())) { + connect(init, iter, &forOp->getOpOperand(index + 3)); + connect(iter, result); + connect(yielded.get(), iter, &yielded); + } + } else if (auto whileOp = dyn_cast(operation)) { + for (auto [index, init, argument] : + llvm::enumerate(whileOp.getInits(), whileOp.getBeforeArguments())) + connect(init, argument, &whileOp->getOpOperand(index)); + auto condition = whileOp.getConditionOp(); + for (auto [index, forwarded, afterArgument, result] : + llvm::enumerate(condition.getArgs(), whileOp.getAfterArguments(), + whileOp.getResults())) { + connect(forwarded, afterArgument, &condition->getOpOperand(index + 1)); + connect(afterArgument, result); + } + auto yield = whileOp.getYieldOp(); + for (auto [yielded, beforeArgument] : + llvm::zip(yield->getOpOperands(), whileOp.getBeforeArguments())) + connect(yielded.get(), beforeArgument, &yielded); + } + return WalkResult::advance(); + }); + + llvm::DenseMap component; + unsigned nextComponent = 0; + for (auto &entry : forwarding) { + Value seed = entry.first; + if (component.count(seed)) + continue; + SmallVector worklist{seed}; + component.try_emplace(seed, nextComponent); + while (!worklist.empty()) { + Value current = worklist.pop_back_val(); + for (Value adjacent : forwarding[current]) + if (component.try_emplace(adjacent, nextComponent).second) + worklist.push_back(adjacent); + } + ++nextComponent; + } + auto getComponent = [&](Value value) { + auto [it, inserted] = component.try_emplace(value, nextComponent); + if (inserted) + ++nextComponent; + return it->second; + }; + + SmallVector openOps; + SmallVector nextOps; + process.getBody().walk([&](Operation *operation) { + if (auto open = dyn_cast(operation)) { + openOps.push_back(open); + (void)getComponent(open.getCursor()); + } else if (auto next = dyn_cast(operation)) { + nextOps.push_back(next); + (void)getComponent(next.getInputCursor()); + (void)getComponent(next.getCursor()); + } else if (auto eof = dyn_cast(operation)) { + (void)getComponent(eof.getInputCursor()); + } else if (auto position = dyn_cast(operation)) { + (void)getComponent(position.getInputCursor()); + } + return WalkResult::advance(); + }); + + enum class CursorLattice { Unknown, NonCursor, SingleSource, Conflict }; + struct CursorState { + CursorLattice lattice = CursorLattice::Unknown; + StringAttr source; + }; + SmallVector states(nextComponent); + auto isConcreteNonCursor = [](Value value) { + if (isa_and_nonnull(value.getDefiningOp())) + return false; + if (auto next = dyn_cast_or_null(value.getDefiningOp())) + return value != next.getCursor(); + if (isa_and_nonnull( + value.getDefiningOp())) + return false; + if (auto argument = dyn_cast(value)) { + Operation *parent = argument.getOwner()->getParentOp(); + if (auto forOp = dyn_cast_or_null(parent)) + return argument == forOp.getInductionVar(); + return !isa_and_nonnull(parent); + } + return true; + }; + for (auto &[value, id] : component) + if (isConcreteNonCursor(value)) + states[id].lattice = CursorLattice::NonCursor; + + for (TraceOpenOp open : openOps) { + unsigned id = getComponent(open.getCursor()); + if (states[id].lattice == CursorLattice::NonCursor) { + states[id].lattice = CursorLattice::Conflict; + return open.emitOpError( + "trace cursor forwarding merges cursor and non-cursor values"); + } + if (states[id].lattice == CursorLattice::SingleSource && + states[id].source != open.getSourceAttr()) { + states[id].lattice = CursorLattice::Conflict; + return open.emitOpError( + "trace cursor forwarding merges distinct provenance"); + } + states[id] = {CursorLattice::SingleSource, open.getSourceAttr()}; + } + + bool changed = true; + while (changed) { + changed = false; + for (TraceNextOp next : nextOps) { + unsigned input = getComponent(next.getInputCursor()); + unsigned output = getComponent(next.getCursor()); + if (states[input].lattice != CursorLattice::SingleSource) + continue; + if (states[output].lattice == CursorLattice::NonCursor) { + states[output].lattice = CursorLattice::Conflict; + return next.emitOpError( + "trace cursor forwarding merges cursor and non-cursor values"); + } + if (states[output].lattice == CursorLattice::SingleSource && + states[output].source != states[input].source) { + states[output].lattice = CursorLattice::Conflict; + return next.emitOpError( + "trace cursor forwarding merges distinct provenance"); + } + if (states[output].lattice == CursorLattice::Unknown) { + states[output] = states[input]; + changed = true; + } + } + } + + SmallVector advancing(nextComponent); + LogicalResult result = success(); + auto verifyConsumer = [&](Operation *operation, Value cursor, + StringRef source, bool advances) -> LogicalResult { + unsigned id = getComponent(cursor); + if (id >= states.size() || + states[id].lattice != CursorLattice::SingleSource) + return operation->emitOpError( + "trace cursor must originate from ac.trace.open or ac.trace.next"); + if (states[id].source.getValue() != source) + return operation->emitOpError( + "trace cursor owner does not match 'from source'"); + if (advances && ++advancing[id] > 1) + return operation->emitOpError( + "trace cursor provenance has more than one advancing consumer"); + return success(); + }; + for (TraceNextOp next : nextOps) + if (failed(verifyConsumer(next, next.getInputCursor(), next.getSource(), + true))) + return failure(); + process.getBody().walk([&](Operation *operation) { + if (failed(result)) + return WalkResult::interrupt(); + if (auto eof = dyn_cast(operation)) + result = + verifyConsumer(eof, eof.getInputCursor(), eof.getSource(), false); + else if (auto position = dyn_cast(operation)) + result = verifyConsumer(position, position.getInputCursor(), + position.getSource(), false); + return failed(result) ? WalkResult::interrupt() : WalkResult::advance(); + }); + if (failed(result)) + return failure(); + + for (auto &[value, id] : component) { + if (id >= states.size() || + states[id].lattice != CursorLattice::SingleSource) + continue; + for (OpOperand &use : value.getUses()) { + if (forwardingUses.contains(&use) || + isa(use.getOwner())) + continue; + return use.getOwner()->emitOpError( + "trace cursor may only feed trace cursor operations"); + } + } + return success(); +} + +template +WalkResult walkOperationsIterative(Region ®ion, Callback callback) { + SmallVector worklist; + for (Block &block : llvm::reverse(region)) + for (Operation &operation : llvm::reverse(block)) + worklist.push_back(&operation); + while (!worklist.empty()) { + Operation *operation = worklist.pop_back_val(); + if (callback(operation).wasInterrupted()) + return WalkResult::interrupt(); + for (Region &nested : llvm::reverse(operation->getRegions())) + for (Block &block : llvm::reverse(nested)) + for (Operation &child : llvm::reverse(block)) + worklist.push_back(&child); + } + return WalkResult::advance(); +} + +bool isObservationConsumer(Operation *operation) { + return isa(operation) || + operation->getParentOfType(); +} + +SideEffects::Resource *probeResource(StringRef kind) { + return llvm::StringSwitch(kind) + .Case("queue", QueueStateResource::get()) + .Case("resource", ReservationStateResource::get()) + .Case("module", ModuleStateResource::get()) + .Case("storage", StorageStateResource::get()) + .Case("protocol", ProtocolStateResource::get()) + .Case("trace", TracePositionResource::get()) + .Case("event_queue", EventQueueStateResource::get()) + .Case("external_io", ExternalIOResource::get()) + .Case("statistics", StatisticsResource::get()) + .Default(ExternalIOResource::get()); +} + +} // namespace + +LogicalResult ProcessOp::verify() { + if (!isa_and_nonnull((*this)->getParentOp())) + return emitOpError("must be a direct child of ac.module"); + if (!isStableHierarchySegment(getSymName())) + return emitOpError( + "symbol name must be one stable hierarchy owner segment"); + if (getKind() != "control" && getKind() != "workload" && + getKind() != "monitor") + return emitOpError("kind must be 'control', 'workload', or 'monitor'"); + if (getBody().empty()) + return emitOpError("requires one non-empty body block"); + if (!llvm::equal(getBody().front().getArgumentTypes(), + getCaptures().getTypes())) + return emitOpError("body arguments must exactly match capture types"); + if (!isa(getBody().front().back())) + return emitOpError("body must terminate with ac.yield_sim"); + if (failed(verifyProcessLowerability(getOperation()))) + return failure(); + + StructuredSuspensionAnalysis suspensionAnalysis(*this); + + LogicalResult result = success(); + walkOperationsIterative(getBody(), [&](Operation *operation) { + if (getKind() == "monitor" && + isa(operation)) { + operation->emitOpError( + "monitor process cannot perform functional state effects"); + result = failure(); + return WalkResult::interrupt(); + } + if (auto whileOp = dyn_cast(operation)) { + std::optional condition = + constantBool(whileOp.getConditionOp().getCondition()); + if (condition != false && + !suspensionAnalysis.guaranteesSuspend(whileOp.getBefore()) && + !suspensionAnalysis.guaranteesSuspend(whileOp.getAfter())) { + operation->emitOpError( + "every scf.while backedge must suspend or prove bounded progress"); + result = failure(); + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + if (failed(result)) + return failure(); + + if (failed(suspensionAnalysis.verifyLinearLiveness(*this))) + return failure(); + llvm::StringSet<> instrumentationNames; + WalkResult instrumentationResult = + getBody().walk([&](InstrumentationOp instrumentation) { + if (!instrumentationNames.insert(instrumentation.getSymName()).second) { + instrumentation.emitOpError() + << "duplicate process-local instrumentation name '" + << instrumentation.getSymName() << "'"; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (instrumentationResult.wasInterrupted()) + return failure(); + return verifyTraceProvenance(*this); +} + +LogicalResult TrySendOp::verify() { return requireProcess(*this); } +LogicalResult TryRecvOp::verify() { return requireProcess(*this); } + +LogicalResult ScheduleOp::verify() { + if (Operation *definition = getDelay().getDefiningOp(); + definition && definition->getName().getStringRef() == "arith.constant") { + auto value = definition->getAttrOfType("value"); + if (value && value.getInt() < 0) + return emitOpError("schedule delay must be non-negative"); + } + return requireProcess(*this); +} + +LogicalResult WaitUntilOp::verify() { return requireProcess(*this); } +LogicalResult WaitForOp::verify() { return requireProcess(*this); } +LogicalResult AwaitEventOp::verify() { return requireProcess(*this); } + +LogicalResult YieldSimOp::verify() { + ProcessOp process = enclosingProcess(*this); + if (!process || (*this)->getParentOp() != process) + return emitOpError("must directly terminate an ac.process body"); + if (&(*this)->getBlock()->back() != getOperation()) + return emitOpError("must be the final operation in ac.process"); + return success(); +} + +LogicalResult TraceOpenOp::verify() { + if (!isStableHierarchySegment(getSource())) + return emitOpError( + "trace source must be one stable logical identifier segment"); + return requireProcess(*this); +} + +LogicalResult TraceNextOp::verify() { return requireProcess(*this); } + +LogicalResult TraceDecodeOp::verify() { + auto next = getEntry().getDefiningOp(); + if (!next || getEntry() != next.getEntry()) + return emitOpError("trace.decode input must be an ac.trace.next entry"); + return requireProcess(*this); +} + +LogicalResult TraceEofOp::verify() { return requireProcess(*this); } + +LogicalResult TracePositionOp::verify() { return requireProcess(*this); } + +LogicalResult RequireOp::verify() { + if (isa_and_nonnull((*this)->getParentOp())) + return success(); + return requireProcess(*this); +} + +LogicalResult EnsureOp::verify() { + if (isa_and_nonnull((*this)->getParentOp())) + return success(); + return requireProcess(*this); +} + +LogicalResult AssertOp::verify() { return requireProcess(*this); } + +LogicalResult ProbeOp::verify() { + if (!probeResource(getKind()) || + !hasStringValue(getKind(), + {"queue", "resource", "module", "storage", "protocol", + "trace", "event_queue", "external_io", "statistics"})) + return emitOpError("unsupported probe resource kind '") << getKind() << "'"; + if (failed(requireProcess(*this))) + return failure(); + for (Operation *user : getValue().getUsers()) + if (!isObservationConsumer(user)) + return emitOpError("probe result may only feed observation operations"); + return success(); +} + +LogicalResult StatOp::verify() { + if (!isa_and_nonnull((*this)->getParentOp())) + return emitOpError("must be a direct child of ac.module"); + if (!isStableHierarchySegment(getSymName())) + return emitOpError( + "symbol name must be one stable hierarchy owner segment"); + if (!hasStringValue(getKind(), + {"counter", "gauge", "histogram", "event_log"})) + return emitOpError( + "kind must be 'counter', 'gauge', 'histogram', or 'event_log'"); + return success(); +} + +LogicalResult StatAddOp::verify() { return requireProcess(*this); } + +LogicalResult InstrumentationOp::verify() { + if (!enclosingProcess(*this)) + return emitOpError("must be nested in ac.process"); + if (!isStableHierarchySegment(getSymName())) + return emitOpError( + "symbol name must be one stable hierarchy owner segment"); + LogicalResult result = success(); + walkOperationsIterative(getBody(), [&](Operation *operation) { + if (isa(operation) || isMemoryEffectFree(operation)) + if (!isa(operation)) + return WalkResult::advance(); + operation->emitOpError( + "instrumentation may contain only removable observation operations"); + result = failure(); + return WalkResult::interrupt(); + }); + return result; +} + +void TrySendOp::getEffects( + SmallVectorImpl &effects) { + if (!isa_and_nonnull(resolvedRuntimeTarget(*this, getQueue()))) + return; + addEffect(effects, *this, MemoryEffects::Read::get(), getQueue(), "queue", + QueueStateResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), getQueue(), "queue", + QueueStateResource::get()); + addEffect(effects, *this, MemoryEffects::Read::get(), getQueue(), "protocol", + ProtocolStateResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), getQueue(), "protocol", + ProtocolStateResource::get()); +} + +void TryRecvOp::getEffects( + SmallVectorImpl &effects) { + if (!isa_and_nonnull(resolvedRuntimeTarget(*this, getQueue()))) + return; + addEffect(effects, *this, MemoryEffects::Read::get(), getQueue(), "queue", + QueueStateResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), getQueue(), "queue", + QueueStateResource::get()); + addEffect(effects, *this, MemoryEffects::Read::get(), getQueue(), "protocol", + ProtocolStateResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), getQueue(), "protocol", + ProtocolStateResource::get()); +} + +void ScheduleOp::getEffects( + SmallVectorImpl &effects) { + if (!isa_and_nonnull(resolvedRuntimeTarget(*this, getTarget()))) + return; + addEffect(effects, *this, MemoryEffects::Write::get(), getTarget(), "module", + ModuleStateResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), getTarget(), + "event_queue", EventQueueStateResource::get()); +} + +void WaitUntilOp::getEffects( + SmallVectorImpl &effects) { + addEffect(effects, *this, MemoryEffects::Read::get(), processIdentity(*this), + "event_queue", EventQueueStateResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), processIdentity(*this), + "module", ModuleStateResource::get()); +} + +void WaitForOp::getEffects( + SmallVectorImpl &effects) { + if (!isa_and_nonnull(resolvedRuntimeTarget(*this, getResource()))) + return; + addEffect(effects, *this, MemoryEffects::Read::get(), getResource(), + "resource", ReservationStateResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), processIdentity(*this), + "module", ModuleStateResource::get()); +} + +void AwaitEventOp::getEffects( + SmallVectorImpl &effects) { + if (!isa_and_nonnull( + resolvedRuntimeTarget(*this, getEventQueue()))) + return; + addEffect(effects, *this, MemoryEffects::Read::get(), getEventQueue(), + "event_queue", EventQueueStateResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), processIdentity(*this), + "module", ModuleStateResource::get()); +} + +void YieldSimOp::getEffects( + SmallVectorImpl &effects) { + addEffect(effects, *this, MemoryEffects::Write::get(), processIdentity(*this), + "module", ModuleStateResource::get()); +} + +void TraceOpenOp::getEffects( + SmallVectorImpl &effects) { + std::string identity = traceOwnerIdentity(*this, getSource()); + addEffect(effects, *this, MemoryEffects::Read::get(), identity, "external_io", + ExternalIOResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), identity, "trace", + TracePositionResource::get()); +} + +void TraceNextOp::getEffects( + SmallVectorImpl &effects) { + std::string identity = traceOwnerIdentity(*this, getSource()); + addEffect(effects, *this, MemoryEffects::Read::get(), identity, "trace", + TracePositionResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), identity, "trace", + TracePositionResource::get()); + addEffect(effects, *this, MemoryEffects::Read::get(), identity, "external_io", + ExternalIOResource::get()); +} + +void TraceEofOp::getEffects( + SmallVectorImpl &effects) { + std::string identity = traceOwnerIdentity(*this, getSource()); + addEffect(effects, *this, MemoryEffects::Read::get(), identity, "trace", + TracePositionResource::get()); +} + +void TracePositionOp::getEffects( + SmallVectorImpl &effects) { + std::string identity = traceOwnerIdentity(*this, getSource()); + addEffect(effects, *this, MemoryEffects::Read::get(), identity, "trace", + TracePositionResource::get()); +} + +void RequireOp::getEffects( + SmallVectorImpl &effects) { + addContractEffect(effects, *this); +} + +void EnsureOp::getEffects( + SmallVectorImpl &effects) { + addContractEffect(effects, *this); +} + +void AssertOp::getEffects( + SmallVectorImpl &effects) { + addContractEffect(effects, *this); +} + +void ProbeOp::getEffects( + SmallVectorImpl &effects) { + Operation *target = resolvedRuntimeTarget(*this, getTarget()); + bool matches = llvm::StringSwitch(getKind()) + .Case("queue", isa_and_nonnull(target)) + .Case("resource", isa_and_nonnull(target)) + .Case("module", isa_and_nonnull(target)) + .Case("storage", isa_and_nonnull(target)) + .Case("protocol", isa_and_nonnull(target)) + .Case("trace", isa_and_nonnull(target)) + .Case("event_queue", isa_and_nonnull(target)) + .Case("external_io", isa_and_nonnull(target)) + .Case("statistics", isa_and_nonnull(target)) + .Default(false); + if (!matches) + return; + addEffect(effects, *this, MemoryEffects::Read::get(), getTarget(), getKind(), + probeResource(getKind())); +} + +void StatOp::getEffects( + SmallVectorImpl &effects) { + addEffect(effects, *this, MemoryEffects::Write::get(), getSymName(), + "statistics", StatisticsResource::get()); +} + +void StatAddOp::getEffects( + SmallVectorImpl &effects) { + if (!isa_and_nonnull(resolvedRuntimeTarget(*this, getStat()))) + return; + addEffect(effects, *this, MemoryEffects::Read::get(), getStat(), "statistics", + StatisticsResource::get()); + addEffect(effects, *this, MemoryEffects::Write::get(), getStat(), + "statistics", StatisticsResource::get()); +} + +} // namespace acir::ac + +#define GET_OP_CLASSES +#include "acir/Dialect/ACIR/ACIROps.cpp.inc" diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ACIROpsTestHooks.h b/components/agentic-circuit/lib/Dialect/ACIR/ACIROpsTestHooks.h new file mode 100644 index 00000000..f0321437 --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ACIROpsTestHooks.h @@ -0,0 +1,37 @@ +#ifndef ACIR_LIB_DIALECT_ACIR_ACIROPS_TEST_HOOKS_H +#define ACIR_LIB_DIALECT_ACIR_ACIROPS_TEST_HOOKS_H + +#include + +namespace acir::ac::detail { + +struct ProcessLivenessWork { + uint64_t summaryOperationVisits = 0; + uint64_t epochOperationVisits = 0; + uint64_t livenessOperationVisits = 0; + uint64_t valueVisits = 0; + uint64_t useVisits = 0; + + uint64_t total() const { + return summaryOperationVisits + epochOperationVisits + + livenessOperationVisits + valueVisits + useVisits; + } +}; + +class ScopedProcessLivenessWorkCollector { +public: + explicit ScopedProcessLivenessWorkCollector(ProcessLivenessWork &work); + ~ScopedProcessLivenessWorkCollector(); + + ScopedProcessLivenessWorkCollector( + const ScopedProcessLivenessWorkCollector &) = delete; + ScopedProcessLivenessWorkCollector & + operator=(const ScopedProcessLivenessWorkCollector &) = delete; + +private: + ProcessLivenessWork *previous; +}; + +} // namespace acir::ac::detail + +#endif // ACIR_LIB_DIALECT_ACIR_ACIROPS_TEST_HOOKS_H diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ACIRResources.cpp b/components/agentic-circuit/lib/Dialect/ACIR/ACIRResources.cpp new file mode 100644 index 00000000..5396a0bc --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ACIRResources.cpp @@ -0,0 +1,1413 @@ +#include "acir/Dialect/ACIR/ACIRResources.h" +#include "ACIRResourcesTestHooks.h" + +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/ACIRTypes.h" +#include "mlir/Analysis/Presburger/IntegerRelation.h" +#include "mlir/Dialect/DLTI/DLTI.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/SymbolTable.h" +#include "mlir/Interfaces/DataLayoutInterfaces.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/StringExtras.h" + +#include +#include +#include +#include + +using namespace mlir; + +namespace acir::ac { +namespace { + +thread_local detail::AddressMapVerificationWork + *addressMapVerificationWorkCollector = nullptr; + +void recordAddressMapWork(uint64_t detail::AddressMapVerificationWork::*field) { + if (addressMapVerificationWorkCollector) + ++(addressMapVerificationWorkCollector->*field); +} + +bool isStableSegment(StringRef value) { + return !value.empty() && llvm::all_of(value, [](char c) { + return llvm::isAlnum(c) || c == '_' || c == '-'; + }); +} + +LogicalResult verifyPlacement(Operation *op) { + auto module = dyn_cast_or_null(op->getParentOp()); + if (module && !module.getBody().empty() && + op->getBlock() == &module.getBody().front()) + return success(); + return op->emitOpError( + "must be a direct child of the unique ac.module Graph block"); +} + +Operation *lookupOuter(Operation *from, SymbolRefAttr reference) { + if (Operation *target = SymbolTable::lookupNearestSymbolFrom(from, reference)) + return target; + auto file = from->getParentOfType(); + return file ? SymbolTable::lookupSymbolIn(file, reference) : nullptr; +} + +LogicalResult verifyOwner(Operation *op, StringRef name, StringRef stableId, + StringRef path, int64_t delay) { + if (failed(verifyPlacement(op))) + return failure(); + if (!isStableSegment(name) || !isStableSegment(stableId) || + !isStableSegment(path)) + return op->emitOpError( + "owner name, stable id, and path must be stable local segments"); + if (delay != 1) + return op->emitOpError( + "stateful declaration delay_ticks must be exactly one positive tick"); + return success(); +} + +bool hasExactKeys(DictionaryAttr dictionary, ArrayRef keys) { + if (!dictionary || dictionary.size() != keys.size()) + return false; + return llvm::all_of( + keys, [&](StringRef key) { return dictionary.get(key) != nullptr; }); +} + +FailureOr positiveI64(Operation *op, DictionaryAttr dictionary, + StringRef key, StringRef diagnostic) { + auto value = dictionary.getAs(key); + if (!value || !value.getType().isSignlessInteger(64) || value.getInt() <= 0) { + op->emitOpError(diagnostic); + return failure(); + } + return value.getInt(); +} + +LogicalResult verifyLifecycle(ResourceOp op) { + DictionaryAttr lifecycle = op.getLifecycle(); + if (!hasExactKeys(lifecycle, {"reservation", "release", "cancellation"})) + return op.emitOpError( + "lifecycle requires exact reservation/release/cancellation schema"); + auto reservation = lifecycle.getAs("reservation"); + auto release = lifecycle.getAs("release"); + auto cancellation = lifecycle.getAs("cancellation"); + if (!reservation || reservation.getValue() != "propose_commit" || !release || + release.getValue() != "balanced" || !cancellation || + cancellation.getValue() != "explicit") + return op.emitOpError( + "lifecycle requires exact reservation/release/cancellation schema"); + return success(); +} + +LogicalResult +verifyParentCycles(ModuleOp module, bool timeDomains, + const llvm::StringMap &producerIndex) { + SmallVector nodes; + for (Operation &operation : module.getBody().front()) { + bool selected = timeDomains ? isa(operation) + : isa(operation); + if (!selected) + continue; + nodes.push_back(&operation); + } + + enum class State : uint8_t { Unvisited, Active, Complete }; + DenseMap states; + for (Operation *start : nodes) { + if (states.lookup(start) != State::Unvisited) + continue; + SmallVector path; + DenseMap pathIndex; + Operation *current = start; + while (current && states.lookup(current) == State::Unvisited) { + states[current] = State::Active; + pathIndex[current] = path.size(); + path.push_back(current); + FlatSymbolRefAttr parent = + timeDomains ? cast(current).getParentAttr() + : cast(current).getParentAttr(); + auto target = + parent ? producerIndex.find(parent.getValue()) : producerIndex.end(); + current = target == producerIndex.end() ? nullptr : target->second; + } + if (current && states.lookup(current) == State::Active) { + InFlightDiagnostic diagnostic = + current->emitOpError(timeDomains ? "time-domain parent cycle: " + : "address-space parent cycle: "); + unsigned begin = pathIndex.lookup(current); + for (unsigned index = begin; index < path.size(); ++index) + diagnostic << '@' + << path[index] + ->getAttrOfType( + SymbolTable::getSymbolAttrName()) + .getValue() + << " -> "; + diagnostic << '@' + << current + ->getAttrOfType( + SymbolTable::getSymbolAttrName()) + .getValue(); + return failure(); + } + for (Operation *node : path) + states[node] = State::Complete; + } + return success(); +} + +} // namespace + +namespace detail { + +ScopedAddressMapVerificationWorkCollector:: + ScopedAddressMapVerificationWorkCollector(AddressMapVerificationWork &work) + : previous(addressMapVerificationWorkCollector) { + work = {}; + addressMapVerificationWorkCollector = &work; +} + +ScopedAddressMapVerificationWorkCollector:: + ~ScopedAddressMapVerificationWorkCollector() { + addressMapVerificationWorkCollector = previous; +} + +} // namespace detail + +bool checkedAdd(uint64_t left, uint64_t right, uint64_t &result) { + if (left > std::numeric_limits::max() - right) + return false; + result = left + right; + return true; +} + +bool checkedMultiply(uint64_t left, uint64_t right, uint64_t &result) { + if (left != 0 && right > std::numeric_limits::max() / left) + return false; + result = left * right; + return true; +} + +bool intervalsOverlap(AddressInterval left, AddressInterval right) { + return left.begin < right.end && right.begin < left.end; +} + +int compareAddressMapOrder(AddressMapOrderKey left, AddressMapOrderKey right) { + if (left.base != right.base) + return left.base < right.base ? -1 : 1; + if (left.hasPriority != right.hasPriority) + return left.hasPriority ? -1 : 1; + if (left.hasPriority && left.priority != right.priority) + return left.priority > right.priority ? -1 : 1; + if (left.size != right.size) + return left.size < right.size ? -1 : 1; + return 0; +} + +bool checkedDomainTick(uint64_t phase, uint64_t period, uint64_t cycle, + uint64_t &tick) { + uint64_t elapsed = 0; + if (!checkedMultiply(period, cycle, elapsed) || + !checkedAdd(phase, elapsed, tick)) + return false; + return tick <= static_cast(std::numeric_limits::max()); +} + +bool normalizeRationalToTicks(uint64_t numerator, uint64_t denominator, + uint64_t quantumNumerator, + uint64_t quantumDenominator, uint64_t &ticks) { + if (!denominator || !quantumNumerator || !quantumDenominator) + return false; + uint64_t durationGcd = std::gcd(numerator, denominator); + numerator /= durationGcd; + denominator /= durationGcd; + uint64_t quantumGcd = std::gcd(quantumNumerator, quantumDenominator); + quantumNumerator /= quantumGcd; + quantumDenominator /= quantumGcd; + // The declared global quantum is an implementation capability boundary. + // Validate its independently reduced denominator before duration/quantum + // cross-cancellation can hide an unsupported declaration. + if (quantumDenominator > kMaxTickScale) + return false; + uint64_t crossGcd = std::gcd(numerator, quantumNumerator); + numerator /= crossGcd; + quantumNumerator /= crossGcd; + crossGcd = std::gcd(quantumDenominator, denominator); + quantumDenominator /= crossGcd; + denominator /= crossGcd; + if (quantumNumerator > kMaxTickScale || quantumDenominator > kMaxTickScale) + return false; + uint64_t divisor = 0; + if (!checkedMultiply(denominator, quantumNumerator, divisor) || !divisor) + return false; + uint64_t product = 0; + if (!checkedMultiply(numerator, quantumDenominator, product) || + product % divisor != 0) + return false; + ticks = product / divisor; + return ticks <= static_cast(std::numeric_limits::max()); +} + +DictionaryAttr ownerEffectParameters(Operation *operation, StringAttr stableId, + StringAttr path) { + Builder builder(operation->getContext()); + if (auto owners = operation->getAttrOfType("ac.frozen_owners")) + return builder.getDictionaryAttr({ + builder.getNamedAttr("identity_phase", + builder.getStringAttr("elaborated_absolute")), + builder.getNamedAttr("owners", owners), + }); + return builder.getDictionaryAttr({ + builder.getNamedAttr("identity_phase", + builder.getStringAttr("definition_pre_freeze")), + builder.getNamedAttr("stable_id", stableId), + builder.getNamedAttr("path", path), + }); +} + +SymbolRefAttr ownerEffectReference(Operation *operation, StringRef localName) { + ModuleOp definition = operation->getParentOfType(); + assert(definition && "Task 7 owner placement verifier requires ac.module"); + return SymbolRefAttr::get( + operation->getContext(), definition.getSymName(), + {FlatSymbolRefAttr::get(operation->getContext(), localName)}); +} + +void QueueOp::getEffects( + SmallVectorImpl &effects) { + effects.emplace_back( + MemoryEffects::Write::get(), ownerEffectReference(*this, getSymName()), + ownerEffectParameters(*this, getStableIdAttr(), getPathAttr()), + QueueStateResource::get()); +} + +void SourceOp::getEffects( + SmallVectorImpl &effects) { + effects.emplace_back(MemoryEffects::Write::get(), + getOperation()->getResult(0), QueueStateResource::get()); +} + +void SinkOp::getEffects( + SmallVectorImpl &effects) { + effects.emplace_back(MemoryEffects::Write::get(), + &getOperation()->getOpOperand(0), + QueueStateResource::get()); +} + +void QueuePeekOp::getEffects( + SmallVectorImpl &effects) { + effects.emplace_back(MemoryEffects::Read::get(), + &getOperation()->getOpOperand(0), + QueueStateResource::get()); +} + +void QueuePopOp::getEffects( + SmallVectorImpl &effects) { + effects.emplace_back(MemoryEffects::Write::get(), + &getOperation()->getOpOperand(0), + QueueStateResource::get()); +} + +void QueuePushOp::getEffects( + SmallVectorImpl &effects) { + effects.emplace_back(MemoryEffects::Write::get(), + &getOperation()->getOpOperand(0), + QueueStateResource::get()); +} + +void EventQueueOp::getEffects( + SmallVectorImpl &effects) { + effects.emplace_back( + MemoryEffects::Write::get(), ownerEffectReference(*this, getSymName()), + ownerEffectParameters(*this, getStableIdAttr(), getPathAttr()), + EventQueueStateResource::get()); +} + +void ResourceOp::getEffects( + SmallVectorImpl &effects) { + effects.emplace_back( + MemoryEffects::Write::get(), ownerEffectReference(*this, getSymName()), + ownerEffectParameters(*this, getStableIdAttr(), getPathAttr()), + ReservationStateResource::get()); +} + +LogicalResult QueueOp::verify() { + if (failed(verifyOwner(*this, getSymName(), getStableId(), getPath(), + getDelayTicksAttr().getInt()))) + return failure(); + int64_t entryCapacity = getEntryCapacityAttr().getInt(); + if (entryCapacity <= 0) + return emitOpError("entry capacity must be positive"); + if (auto bytes = getByteCapacityAttr(); bytes && bytes.getInt() <= 0) + return emitOpError("byte capacity must be positive when present"); + if (getOrdering() != "fifo" && getOrdering() != "per_key") + return emitOpError("ordering must be 'fifo' or 'per_key'"); + if (getOwnership() != "exclusive") + return emitOpError("queue ownership must be exactly 'exclusive'"); + if (!isNormativePayloadType(getPayload())) + return emitOpError("queue payload must be a normative ACIR value type"); + if (DictionaryAttr marks = getWatermarksAttr()) { + auto low = marks.getAs("low"); + auto high = marks.getAs("high"); + if (!hasExactKeys(marks, {"low", "high"}) || !low || !high || + !low.getType().isSignlessInteger(64) || + !high.getType().isSignlessInteger(64) || low.getInt() < 0 || + low.getInt() >= high.getInt() || high.getInt() > entryCapacity) + return emitOpError( + "watermarks require 0 <= low < high <= entry capacity"); + } + auto protocol = + dyn_cast_or_null(lookupOuter(*this, getProtocolAttr())); + if (!protocol) + return emitOpError() << "endpoint protocol '" << getProtocolAttr() + << "' is unresolved"; + StringRef protocolOrdering = "unordered"; + bool hasCorrelation = false; + for (Operation &operation : protocol.getBody().front()) { + auto guarantee = dyn_cast(operation); + if (!guarantee) + continue; + if (guarantee.getKind() == "ordering") { + auto ordering = dyn_cast(guarantee.getValue()); + if (!ordering) + return emitOpError("protocol ordering guarantee must be a string"); + protocolOrdering = ordering.getValue(); + } else if (guarantee.getKind() == "correlation") { + auto correlation = dyn_cast(guarantee.getValue()); + if (!correlation || correlation.getValue().empty()) + return emitOpError( + "protocol correlation guarantee must be a non-empty string"); + hasCorrelation = true; + } + } + auto orderingStrength = [](StringRef ordering) { + return ordering == "fifo" ? 2 : ordering == "per_key" ? 1 : 0; + }; + if (orderingStrength(getOrdering()) < orderingStrength(protocolOrdering)) + return emitOpError() << "queue ordering '" << getOrdering() + << "' weakens protocol ordering '" << protocolOrdering + << "'"; + if (getOrdering() == "per_key" && !hasCorrelation) + return emitOpError( + "per_key queue storage requires protocol correlation semantics"); + bool carrier = + llvm::any_of(protocol.getBody().getOps(), [&](EventOp event) { + return event.getPayload() == getPayload() && + (event.getAction() == "offer" || + event.getAction() == "response" || + event.getAction() == "notify"); + }); + if (!carrier) + return emitOpError("queue payload does not match endpoint protocol schema"); + return success(); +} + +LogicalResult EventQueueOp::verify() { + if (failed(verifyOwner(*this, getSymName(), getStableId(), getPath(), + getDelayTicksAttr().getInt()))) + return failure(); + if (getCapacityAttr().getInt() <= 0) + return emitOpError("event queue capacity must be positive"); + if (!isa(getPayload())) + return emitOpError("event queue payload must be an exact !ac.event type"); + if (getOrdering() != "time_then_sequence") + return emitOpError("ordering must be exactly 'time_then_sequence'"); + return success(); +} + +LogicalResult ResourceOp::verify() { + if (failed(verifyOwner(*this, getSymName(), getStableId(), getPath(), + getDelayTicksAttr().getInt()))) + return failure(); + int64_t capacity = getCapacityAttr().getInt(); + int64_t issueWidth = getIssueWidthAttr().getInt(); + if (capacity <= 0) + return emitOpError("resource capacity must be positive"); + if (issueWidth <= 0 || issueWidth > capacity) + return emitOpError("issue width must be in [1, capacity]"); + if (getInitiationIntervalAttr().getInt() < 1) + return emitOpError("initiation interval must be at least one global tick"); + DictionaryAttr latency = getLatencyModel(); + auto kind = latency.getAs("kind"); + if (!kind) + return emitOpError("latency model requires an exact kind"); + if (kind.getValue() == "fixed") { + if (!hasExactKeys(latency, {"kind", "ticks"})) + return emitOpError( + "fixed latency model requires exact kind/ticks schema"); + if (failed(positiveI64(*this, latency, "ticks", + "fixed latency ticks must be positive"))) + return failure(); + } else if (kind.getValue() == "symbol") { + auto reference = latency.getAs("ref"); + Operation *target = reference ? lookupOuter(*this, reference) : nullptr; + if (!hasExactKeys(latency, {"kind", "ref"}) || !reference || + !isa_and_nonnull(target)) + return emitOpError("symbol latency model reference is unresolved"); + } else { + return emitOpError("latency model kind must be 'fixed' or 'symbol'"); + } + if (failed(verifyLifecycle(*this))) + return failure(); + if (getOwnership() != "exclusive" && getOwnership() != "shared" && + getOwnership() != "contested") + return emitOpError( + "resource ownership must be exclusive, shared, or contested"); + FlatSymbolRefAttr arbiter = getArbitrationOwnerAttr(); + bool requiresArbiter = getOwnership() != "exclusive"; + if (requiresArbiter != static_cast(arbiter)) + return emitOpError( + requiresArbiter + ? "shared or contested resource requires one arbitration owner" + : "exclusive resource cannot declare an arbitration owner"); + DenseSet classes; + for (Attribute attribute : getTransactionClasses()) { + auto reference = dyn_cast(attribute); + if (!reference || !isa_and_nonnull( + reference ? lookupOuter(*this, reference) : nullptr)) + return emitOpError() << "transaction class '" << attribute + << "' is unresolved"; + if (!classes.insert(attribute).second) + return emitOpError() << "duplicate transaction class '" << attribute + << "'"; + } + return success(); +} + +LogicalResult AddressSpaceOp::verify() { + if (failed(verifyPlacement(*this))) + return failure(); + if (!isStableSegment(getSymName()) || !isStableSegment(getStableId()) || + !isStableSegment(getPath())) + return emitOpError("address-space name, stable id, and path must be stable " + "local segments"); + int64_t addressWidth = getAddressWidthAttr().getInt(); + if (addressWidth < 1 || addressWidth > 64) + return emitOpError("address width must be in [1, 64]"); + if (getAddressUnit() != "byte" && getAddressUnit() != "bit") + return emitOpError("address unit must be exactly 'byte' or 'bit'"); + if (Attribute layout = getDataLayoutAttr(); + layout && !isa(layout)) + return emitOpError( + "data layout hook must implement DataLayoutSpecInterface"); + FlatSymbolRefAttr parent = getParentAttr(); + DictionaryAttr translation = getTranslationAttr(); + if (static_cast(parent) != static_cast(translation)) + return emitOpError( + "parent address space and translation must appear together"); + if (!parent) + return success(); + auto numerator = translation.getAs("numerator"); + auto denominator = translation.getAs("denominator"); + auto offset = translation.getAs("offset"); + if (!numerator || !denominator || !offset || + !numerator.getType().isSignlessInteger(64) || + !denominator.getType().isSignlessInteger(64) || + !offset.getType().isSignlessInteger(64) || + numerator.getValue().isZero() || denominator.getValue().isZero()) + return emitOpError( + "translation values must be unsigned signless i64 with positive ratio"); + uint64_t n = numerator.getValue().getZExtValue(); + uint64_t d = denominator.getValue().getZExtValue(); + if (std::gcd(n, d) != 1) + return emitOpError( + "translation rational must be in canonical reduced form"); + if (d == 1) { + if (!hasExactKeys(translation, {"numerator", "denominator", "offset"})) + return emitOpError("integral translation requires exact " + "numerator/denominator/offset schema"); + } else { + auto alignment = translation.getAs("alignment"); + if (!hasExactKeys(translation, + {"numerator", "denominator", "offset", "alignment"}) || + !alignment || !alignment.getType().isSignlessInteger(64) || + alignment.getValue().isZero() || + (static_cast(alignment.getValue().getZExtValue()) * n) % + d != + 0) + return emitOpError("fractional translation requires exact positive " + "alignment proving divisibility"); + } + return success(); +} + +LogicalResult AddressMapOp::verify() { + if (failed(verifyPlacement(*this))) + return failure(); + if (!isStableSegment(getSymName())) + return emitOpError("address-map name must be one stable local segment"); + return success(); +} + +LogicalResult TimeDomainOp::verify() { + if (failed(verifyPlacement(*this))) + return failure(); + if (!isStableSegment(getSymName())) + return emitOpError("time-domain name must be one stable local segment"); + int64_t period = getPeriodAttr().getInt(); + int64_t phase = getPhaseAttr().getInt(); + int64_t scale = getTickScaleAttr().getInt(); + if (period <= 0) + return emitOpError("period must be positive global ticks"); + if (phase < 0) + return emitOpError("phase must be non-negative global ticks"); + if (scale <= 0 || static_cast(scale) > kMaxTickScale) + return emitOpError("tick scale exceeds implementation capability"); + FlatSymbolRefAttr parent = getParentAttr(); + DictionaryAttr bridge = getBridgeAttr(); + if (static_cast(parent) != static_cast(bridge)) + return emitOpError( + "cross-domain parent relation requires explicit bridge metadata"); + if (!parent) + return success(); + auto kind = bridge.getAs("kind"); + auto owner = bridge.getAs("owner"); + if (!hasExactKeys(bridge, {"kind", "owner"}) || !kind || + kind.getValue() != "explicit" || !owner) + return emitOpError("bridge requires exact {kind = \"explicit\", owner = " + "local symbol} schema"); + return success(); +} + +namespace { + +Operation *lookupIndexed(const llvm::StringMap &producerIndex, + FlatSymbolRefAttr reference) { + if (!reference) + return nullptr; + auto found = producerIndex.find(reference.getValue()); + return found == producerIndex.end() ? nullptr : found->second; +} + +FailureOr readUnsignedI64(Operation *op, IntegerAttr attribute, + StringRef diagnostic) { + if (!attribute || !attribute.getType().isSignlessInteger(64)) { + op->emitOpError(diagnostic); + return failure(); + } + return attribute.getValue().getZExtValue(); +} + +WideAddress addressLimit(unsigned width) { return WideAddress{1} << width; } + +LogicalResult +verifyAddressTranslation(AddressSpaceOp child, + const llvm::StringMap &producerIndex) { + FlatSymbolRefAttr parentRef = child.getParentAttr(); + if (!parentRef) + return success(); + auto parent = + dyn_cast_or_null(lookupIndexed(producerIndex, parentRef)); + if (!parent) + return child.emitOpError() + << "parent address space '" << parentRef << "' is unresolved"; + + DictionaryAttr translation = child.getTranslationAttr(); + uint64_t numerator = + translation.getAs("numerator").getValue().getZExtValue(); + uint64_t denominator = + translation.getAs("denominator").getValue().getZExtValue(); + uint64_t offset = + translation.getAs("offset").getValue().getZExtValue(); + uint64_t alignment = denominator == 1 + ? 1 + : translation.getAs("alignment") + .getValue() + .getZExtValue(); + + // Normative formula, in declared address units: + // parent = child * numerator / denominator + offset + // Fractional schemas constrain legal child addresses to multiples of + // alignment, which makes the division exact for every legal input. Unit + // conversion is encoded in the rational itself (byte -> bit is 8/1). + WideAddress childMaximum = + addressLimit(child.getAddressWidthAttr().getInt()) - 1; + WideAddress legalMaximum = childMaximum - childMaximum % alignment; + WideAddress translated = legalMaximum * numerator; + if (translated % denominator != 0) + return child.emitOpError( + "translation alignment does not make the full child domain exact"); + translated = translated / denominator + offset; + if (translated >= addressLimit(parent.getAddressWidthAttr().getInt())) + return child.emitOpError( + "translated child address range exceeds parent address width"); + return success(); +} + +std::string attributeToken(Attribute attribute) { + std::string storage; + llvm::raw_string_ostream stream(storage); + stream << attribute; + return storage; +} + +ArrayAttr sortedSetAttribute(MLIRContext *context, ArrayAttr values) { + SmallVector sorted(values.begin(), values.end()); + llvm::sort(sorted, [](Attribute left, Attribute right) { + return attributeToken(left) < attributeToken(right); + }); + return ArrayAttr::get(context, sorted); +} + +DictionaryAttr normalizedMapEntry(DictionaryAttr entry) { + MLIRContext *context = entry.getContext(); + NamedAttrList attributes(entry); + attributes.set( + "permissions", + sortedSetAttribute(context, entry.getAs("permissions"))); + attributes.set("classes", sortedSetAttribute( + context, entry.getAs("classes"))); + return DictionaryAttr::get(context, attributes); +} + +uint64_t unsignedEntryValue(DictionaryAttr entry, StringRef key) { + return entry.getAs(key).getValue().getZExtValue(); +} + +int compareAttributeToken(Attribute left, Attribute right) { + std::string leftToken = attributeToken(left); + std::string rightToken = attributeToken(right); + return leftToken < rightToken ? -1 : leftToken > rightToken ? 1 : 0; +} + +int compareNormalizedMapEntries(DictionaryAttr left, DictionaryAttr right) { + auto compareUnsigned = [](uint64_t leftValue, uint64_t rightValue) { + return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0; + }; + if (int order = compareUnsigned(unsignedEntryValue(left, "base"), + unsignedEntryValue(right, "base"))) + return order; + IntegerAttr leftPriority = left.getAs("priority"); + IntegerAttr rightPriority = right.getAs("priority"); + if (static_cast(leftPriority) != static_cast(rightPriority)) + return leftPriority ? -1 : 1; + if (leftPriority) { + int order = compareUnsigned(leftPriority.getValue().getZExtValue(), + rightPriority.getValue().getZExtValue()); + if (order) + return -order; + } + if (int order = compareUnsigned(unsignedEntryValue(left, "size"), + unsignedEntryValue(right, "size"))) + return order; + if (int order = compareAttributeToken(left.get("permissions"), + right.get("permissions"))) + return order; + if (int order = + compareAttributeToken(left.get("classes"), right.get("classes"))) + return order; + DictionaryAttr leftInterleave = left.getAs("interleave"); + DictionaryAttr rightInterleave = right.getAs("interleave"); + if (static_cast(leftInterleave) != static_cast(rightInterleave)) + return leftInterleave ? 1 : -1; + if (leftInterleave) { + for (StringRef key : {"granularity", "banks", "bank"}) + if (int order = compareUnsigned(unsignedEntryValue(leftInterleave, key), + unsignedEntryValue(rightInterleave, key))) + return order; + } + if (int order = + compareAttributeToken(left.get("target"), right.get("target"))) + return order; + return compareUnsigned(unsignedEntryValue(left, "offset"), + unsignedEntryValue(right, "offset")); +} + +struct InterleaveLane { + uint64_t granularity; + uint64_t banks; + uint64_t bank; +}; + +struct InterleaveGeometry { + uint64_t granularity; + uint64_t banks; + + auto operator<=>(const InterleaveGeometry &) const = default; +}; + +struct MapEntry { + AddressInterval interval; + AddressMapOrderKey order; + std::optional priority; + std::optional lane; + std::optional concreteSelection; + bool generalSelection = false; + SmallVector registrationKeys; + SmallVector queryKeys; +}; + +template struct PriorityPartitions { + Value all; + Value withoutPriority; + std::map byPriority; +}; + +using EntrySet = std::set; +using EndPartitions = PriorityPartitions>; + +struct GeneralCandidateIndex { + std::map>> + lanes; +}; + +void appendUnique(SmallVectorImpl &keys, std::string key) { + if (!llvm::is_contained(keys, key)) + keys.push_back(std::move(key)); +} + +void buildSelectorKeys(ArrayRef permissions, + ArrayRef classes, MapEntry &entry) { + bool wildcardClass = classes.empty(); + for (unsigned permission : permissions) { + std::string prefix = std::to_string(permission) + "|"; + appendUnique(entry.registrationKeys, "P|" + prefix); + if (wildcardClass) { + appendUnique(entry.registrationKeys, "CW|" + prefix); + appendUnique(entry.queryKeys, "P|" + prefix); + } else { + for (const std::string &className : classes) { + std::string key = "C|"; + key += prefix; + key += className; + appendUnique(entry.registrationKeys, key); + } + appendUnique(entry.queryKeys, "CW|" + prefix); + for (const std::string &className : classes) { + std::string key = "C|"; + key += prefix; + key += className; + appendUnique(entry.queryKeys, key); + } + } + } +} + +llvm::DynamicAPInt asDynamicInt(WideAddress value) { + uint64_t words[] = {static_cast(value), + static_cast(value >> 64)}; + return llvm::DynamicAPInt(llvm::APInt(128, words)); +} + +bool interleaveSetsIntersect(const InterleaveLane &left, + const InterleaveLane &right, WideAddress begin, + WideAddress end) { + if (begin >= end) + return false; + using namespace mlir::presburger; + // Variables are x, q_left, u_left, q_right, u_right. The fixed-dimensional + // Presburger query is the exact periodic block intersection: + // x = q * (granularity*banks) + granularity*bank + u, + // 0 <= u < granularity, begin <= x < end. + IntegerPolyhedron set(PresburgerSpace::getSetSpace(5)); + auto row = [] { return SmallVector(6); }; + auto addLane = [&](const InterleaveLane &lane, unsigned quotient, + unsigned within) { + WideAddress period = + static_cast(lane.granularity) * lane.banks; + WideAddress residue = + static_cast(lane.granularity) * lane.bank; + auto equality = row(); + equality[0] = llvm::DynamicAPInt(1); + equality[quotient] = -asDynamicInt(period); + equality[within] = llvm::DynamicAPInt(-1); + equality[5] = -asDynamicInt(residue); + set.addEquality(equality); + auto lower = row(); + lower[within] = llvm::DynamicAPInt(1); + set.addInequality(lower); + auto upper = row(); + upper[within] = llvm::DynamicAPInt(-1); + upper[5] = asDynamicInt(lane.granularity - 1); + set.addInequality(upper); + }; + addLane(left, 1, 2); + addLane(right, 3, 4); + auto lower = row(); + lower[0] = llvm::DynamicAPInt(1); + lower[5] = -asDynamicInt(begin); + set.addInequality(lower); + auto upper = row(); + upper[0] = llvm::DynamicAPInt(-1); + upper[5] = asDynamicInt(end - 1); + set.addInequality(upper); + return !set.isIntegerEmpty(); +} + +WideAddress selectedPrefix(const InterleaveLane &lane, WideAddress end) { + WideAddress cycle = static_cast(lane.granularity) * lane.banks; + WideAddress residue = static_cast(lane.granularity) * lane.bank; + WideAddress cycles = end / cycle; + WideAddress remainder = end % cycle; + WideAddress partial = 0; + if (remainder > residue) + partial = std::min(remainder - residue, lane.granularity); + return cycles * lane.granularity + partial; +} + +bool laneIntersectsInterval(const InterleaveLane &lane, WideAddress begin, + WideAddress end) { + return selectedPrefix(lane, end) != selectedPrefix(lane, begin); +} + +std::optional +materializeSingleSelection(const InterleaveLane &lane, AddressInterval range, + bool &isGeneral) { + if (lane.banks == 1) { + isGeneral = false; + return range; + } + WideAddress cycle = static_cast(lane.granularity) * lane.banks; + WideAddress residue = static_cast(lane.granularity) * lane.bank; + WideAddress blockStart = (range.begin / cycle) * cycle + residue; + if (blockStart + lane.granularity <= range.begin) + blockStart += cycle; + WideAddress selectedBegin = std::max(range.begin, blockStart); + WideAddress selectedEnd = std::min(range.end, blockStart + lane.granularity); + if (selectedBegin >= selectedEnd) { + isGeneral = false; + return std::nullopt; + } + isGeneral = blockStart + cycle < range.end; + if (isGeneral) + return std::nullopt; + return AddressInterval{selectedBegin, selectedEnd}; +} + +void updateEnds(std::multiset &ends, WideAddress end, bool add) { + if (add) { + ends.insert(end); + return; + } + auto found = ends.find(end); + assert(found != ends.end()); + ends.erase(found); +} + +template +void updatePartitions(PriorityPartitions &partitions, + const MapEntry &entry, Update update) { + update(partitions.all); + if (entry.priority) + update(partitions.byPriority[*entry.priority]); + else + update(partitions.withoutPriority); +} + +template +bool visitEligible(const PriorityPartitions &partitions, + const MapEntry &entry, Visit visit) { + if (!entry.priority) + return visit(partitions.all); + if (!visit(partitions.withoutPriority)) + return false; + auto samePriority = partitions.byPriority.find(*entry.priority); + if (samePriority != partitions.byPriority.end()) + return visit(samePriority->second); + return true; +} + +WideAddress maximumEnd(const std::multiset &ends) { + return ends.empty() ? 0 : *ends.rbegin(); +} + +LogicalResult +verifyAddressMap(AddressMapOp map, + const llvm::StringMap &producerIndex) { + auto source = dyn_cast_or_null( + lookupIndexed(producerIndex, map.getSourceAttr())); + if (!source) + return map.emitOpError() << "source address space '" << map.getSourceAttr() + << "' is unresolved"; + + DictionaryAttr fallback = map.getDefaultBehavior(); + auto fallbackKind = fallback.getAs("kind"); + if (fallbackKind && fallbackKind.getValue() == "unmapped") { + if (!hasExactKeys(fallback, {"kind"})) + return map.emitOpError( + "default behavior requires exact unmapped or target schema"); + } else if (fallbackKind && fallbackKind.getValue() == "target") { + auto target = fallback.getAs("target"); + if (!hasExactKeys(fallback, {"kind", "target"}) || !target || + !isa_and_nonnull( + lookupIndexed(producerIndex, target))) + return map.emitOpError("default target behavior is unresolved"); + } else { + return map.emitOpError( + "default behavior requires exact unmapped or target schema"); + } + + SmallVector entries; + entries.reserve(map.getEntries().size()); + WideAddress sourceLimit = addressLimit(source.getAddressWidthAttr().getInt()); + + for (Attribute attribute : map.getEntries()) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::entryNormalizationVisits); + auto dictionary = dyn_cast(attribute); + if (!dictionary) + return map.emitOpError("address-map entries must be dictionaries"); + static constexpr StringLiteral mandatory[] = { + "base", "size", "target", "offset", "permissions", "classes"}; + for (StringRef key : mandatory) + if (!dictionary.get(key)) + return map.emitOpError() + << "address-map entry is missing '" << key << "'"; + for (NamedAttribute value : dictionary) + if (!llvm::is_contained(ArrayRef{"base", "size", "target", + "offset", "permissions", + "classes", "priority", + "interleave"}, + value.getName().getValue())) + return map.emitOpError() << "unknown address-map entry key '" + << value.getName().getValue() << "'"; + + auto baseValue = readUnsignedI64( + map, dictionary.getAs("base"), + "address base/size/offset require unsigned signless i64 values"); + auto sizeValue = readUnsignedI64( + map, dictionary.getAs("size"), + "address base/size/offset require unsigned signless i64 values"); + auto offsetValue = readUnsignedI64( + map, dictionary.getAs("offset"), + "address base/size/offset require unsigned signless i64 values"); + if (failed(baseValue) || failed(sizeValue) || failed(offsetValue)) + return failure(); + uint64_t base = *baseValue; + uint64_t size = *sizeValue; + uint64_t offset = *offsetValue; + if (size == 0) + return map.emitOpError("address-map entry size must be positive"); + WideAddress end = static_cast(base) + size; + if (end > sourceLimit) + return map.emitOpError("address interval exceeds source address width"); + + auto targetRef = dictionary.getAs("target"); + Operation *target = lookupIndexed(producerIndex, targetRef); + if (!targetRef || + !isa_and_nonnull( + target)) + return map.emitOpError() + << "address-map target '" << targetRef << "' is unresolved"; + + auto permissions = dictionary.getAs("permissions"); + if (!permissions || permissions.empty()) + return map.emitOpError( + "address-map permissions must be a non-empty closed set"); + llvm::SmallSet permissionSet; + SmallVector permissionIds; + for (Attribute permissionAttr : permissions) { + auto permission = dyn_cast(permissionAttr); + if (!permission || + !llvm::is_contained(ArrayRef{"read", "write", "execute"}, + permission.getValue()) || + !permissionSet.insert(permission.getValue()).second) + return map.emitOpError( + "address-map permissions must be unique read/write/execute values"); + permissionIds.push_back(permission.getValue() == "read" ? 0 + : permission.getValue() == "write" ? 1 + : 2); + } + + auto classes = dictionary.getAs("classes"); + if (!classes) + return map.emitOpError("address-map classes must be an array"); + DenseSet classSet; + SmallVector classTokens; + for (Attribute classAttr : classes) { + auto reference = dyn_cast(classAttr); + if (!reference || !isa_and_nonnull( + reference ? lookupOuter(map, reference) : nullptr)) + return map.emitOpError() << "address-map transaction class '" + << classAttr << "' is unresolved"; + if (!classSet.insert(classAttr).second) + return map.emitOpError("duplicate address-map transaction class"); + classTokens.push_back(attributeToken(classAttr)); + } + + WideAddress targetSpan = size; + std::optional lane; + if (auto interleave = dictionary.getAs("interleave")) { + if (!hasExactKeys(interleave, {"granularity", "banks", "bank"})) + return map.emitOpError( + "interleave requires exact granularity/banks/bank schema"); + auto granularityValue = + readUnsignedI64(map, interleave.getAs("granularity"), + "interleave values must be unsigned signless i64"); + auto banksValue = + readUnsignedI64(map, interleave.getAs("banks"), + "interleave values must be unsigned signless i64"); + auto bankValue = + readUnsignedI64(map, interleave.getAs("bank"), + "interleave values must be unsigned signless i64"); + if (failed(granularityValue) || failed(banksValue) || failed(bankValue)) + return failure(); + uint64_t granularity = *granularityValue; + uint64_t banks = *banksValue; + uint64_t bank = *bankValue; + if (!granularity || !banks || bank >= banks) + return map.emitOpError("interleave bank must be in [0, banks)"); + WideAddress stripeWide = static_cast(granularity) * banks; + if (stripeWide > std::numeric_limits::max()) + return map.emitOpError("interleave geometry exceeds unsigned i64"); + lane = InterleaveLane{granularity, banks, bank}; + if (offset % granularity) + return map.emitOpError( + "interleave offset must be aligned to granularity"); + targetSpan = selectedPrefix(*lane, end) - selectedPrefix(*lane, base); + } + + WideAddress targetEnd = static_cast(offset) + targetSpan; + if (auto targetSpace = dyn_cast(target)) + if (targetEnd > addressLimit(targetSpace.getAddressWidthAttr().getInt())) + return map.emitOpError( + "address target range exceeds target address width"); + + std::optional priority; + if (IntegerAttr priorityAttr = dictionary.getAs("priority")) { + auto value = readUnsignedI64( + map, priorityAttr, + "address-map priority must be an unsigned signless i64"); + if (failed(value)) + return failure(); + priority.emplace(*value); + } + AddressMapOrderKey order{base, size, priority.has_value(), + priority.value_or(0)}; + entries.push_back({{base, end}, order, priority, lane}); + if (lane) + entries.back().concreteSelection = materializeSingleSelection( + *lane, entries.back().interval, entries.back().generalSelection); + else + entries.back().concreteSelection = entries.back().interval; + buildSelectorKeys(permissionIds, classTokens, entries.back()); + } + + llvm::stable_sort(entries, [](const MapEntry &left, const MapEntry &right) { + return compareAddressMapOrder(left.order, right.order) < 0; + }); + + auto emitConflict = [&](const MapEntry &left, + const MapEntry &right) -> LogicalResult { + if (!left.priority || !right.priority) + return map.emitOpError("overlapping selector intersections require " + "explicit distinct priorities"); + return map.emitOpError( + "overlapping selector intersections have equal priority"); + }; + + // Entries with an empty selection need no overlap analysis. All other + // non-general selections are exact intervals and use an ordinary sweep. + SmallVector concreteEntries; + for (const MapEntry &entry : entries) + if (entry.concreteSelection) + concreteEntries.push_back(&entry); + llvm::stable_sort(concreteEntries, [](const MapEntry *left, + const MapEntry *right) { + if (left->concreteSelection->begin != right->concreteSelection->begin) + return left->concreteSelection->begin < right->concreteSelection->begin; + return compareAddressMapOrder(left->order, right->order) < 0; + }); + std::map concreteIndex; + std::multimap activeConcrete; + auto updateConcrete = [&](const MapEntry &entry, bool add) { + for (const std::string &key : entry.registrationKeys) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::selectorUpdateVisits); + updatePartitions(concreteIndex[key], entry, [&](auto &ends) { + updateEnds(ends, entry.concreteSelection->end, add); + }); + } + }; + for (const MapEntry *entry : concreteEntries) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::concreteEntryVisits); + WideAddress begin = entry->concreteSelection->begin; + while (!activeConcrete.empty() && activeConcrete.begin()->first <= begin) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::concreteExpirationVisits); + updateConcrete(*activeConcrete.begin()->second, false); + activeConcrete.erase(activeConcrete.begin()); + } + for (const std::string &key : entry->queryKeys) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::selectorQueryVisits); + auto found = concreteIndex.find(key); + if (found == concreteIndex.end()) + continue; + if (!entry->priority) { + if (maximumEnd(found->second.all) > begin) + return map.emitOpError("overlapping selector intersections require " + "explicit distinct priorities"); + continue; + } + if (maximumEnd(found->second.withoutPriority) > begin) + return map.emitOpError("overlapping selector intersections require " + "explicit distinct priorities"); + auto samePriority = found->second.byPriority.find(*entry->priority); + if (samePriority != found->second.byPriority.end() && + maximumEnd(samePriority->second) > begin) + return map.emitOpError( + "overlapping selector intersections have equal priority"); + } + updateConcrete(*entry, true); + activeConcrete.emplace(entry->concreteSelection->end, entry); + } + + using GeneralSelectorMap = std::map; + auto updateGeneral = [&](GeneralSelectorMap &index, const MapEntry &entry, + bool add) { + assert(entry.generalSelection && entry.lane); + InterleaveGeometry geometry{entry.lane->granularity, entry.lane->banks}; + for (const std::string &key : entry.registrationKeys) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::selectorUpdateVisits); + auto &partitions = index[key].lanes[geometry][entry.lane->bank]; + updatePartitions(partitions, entry, [&](EntrySet &set) { + if (add) + set.insert(&entry); + else + set.erase(&entry); + }); + } + }; + auto visitGeneral = [&](const GeneralSelectorMap &index, + const MapEntry &entry, bool mixedOnly, auto visit) { + InterleaveGeometry current{entry.lane->granularity, entry.lane->banks}; + llvm::SmallPtrSet visited; + for (const std::string &key : entry.queryKeys) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::selectorQueryVisits); + auto found = index.find(key); + if (found == index.end()) + continue; + for (const auto &[geometry, banks] : found->second.lanes) { + if (mixedOnly && geometry == current) + continue; + if (!mixedOnly && geometry != current) + continue; + for (const auto &[bank, partitions] : banks) { + if (!mixedOnly && bank != entry.lane->bank) + continue; + if (!visitEligible(partitions, entry, [&](const EntrySet &set) { + for (const MapEntry *candidate : set) + if (visited.insert(candidate).second) + if (!visit(*candidate)) + return false; + return true; + })) + return false; + } + } + } + return true; + }; + + // Preflight every relation that could reach Presburger. The fixed diagnostic + // is independent of which normalized relation crosses the saturated bound. + GeneralSelectorMap preflightIndex; + std::multimap activeGeneral; + uint64_t generalRelationCount = 0; + for (const MapEntry &entry : entries) { + if (!entry.generalSelection) + continue; + while (!activeGeneral.empty() && + activeGeneral.begin()->first <= entry.interval.begin) { + updateGeneral(preflightIndex, *activeGeneral.begin()->second, false); + activeGeneral.erase(activeGeneral.begin()); + } + bool withinLimit = + visitGeneral(preflightIndex, entry, true, [&](const MapEntry &) { + if (generalRelationCount == kMaxGeneralSelectorIntersectionQueries) + return false; + ++generalRelationCount; + return true; + }); + if (!withinLimit) + return map.emitOpError() + << "general mixed interleave analysis exceeds ACIR limit " + << kMaxGeneralSelectorIntersectionQueries; + updateGeneral(preflightIndex, entry, true); + activeGeneral.emplace(entry.interval.end, &entry); + } + + auto selectionsIntersect = [&](const MapEntry &left, const MapEntry &right) { + WideAddress begin = std::max(left.interval.begin, right.interval.begin); + WideAddress end = std::min(left.interval.end, right.interval.end); + if (begin >= end) + return false; + if (left.generalSelection && right.generalSelection) { + InterleaveGeometry leftGeometry{left.lane->granularity, left.lane->banks}; + InterleaveGeometry rightGeometry{right.lane->granularity, + right.lane->banks}; + if (leftGeometry == rightGeometry) + return left.lane->bank == right.lane->bank && + laneIntersectsInterval(*left.lane, begin, end); + recordAddressMapWork( + &detail::AddressMapVerificationWork::generalRelationChecks); + return interleaveSetsIntersect(*left.lane, *right.lane, begin, end); + } + const MapEntry &general = left.generalSelection ? left : right; + const MapEntry &concrete = left.generalSelection ? right : left; + if (!concrete.concreteSelection) + return false; + begin = std::max(begin, concrete.concreteSelection->begin); + end = std::min(end, concrete.concreteSelection->end); + if (begin >= end) + return false; + return laneIntersectsInterval(*general.lane, begin, end); + }; + + GeneralSelectorMap generalIndex; + std::map> fastIndex; + std::multimap activeEntries; + auto updateActive = [&](const MapEntry &entry, bool add) { + if (entry.generalSelection) { + updateGeneral(generalIndex, entry, add); + return; + } + if (!entry.concreteSelection) + return; + for (const std::string &key : entry.registrationKeys) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::selectorUpdateVisits); + updatePartitions(fastIndex[key], entry, [&](EntrySet &set) { + if (add) + set.insert(&entry); + else + set.erase(&entry); + }); + } + }; + for (const MapEntry &entry : entries) { + while (!activeEntries.empty() && + activeEntries.begin()->first <= entry.interval.begin) { + updateActive(*activeEntries.begin()->second, false); + activeEntries.erase(activeEntries.begin()); + } + llvm::SmallPtrSet candidates; + auto check = [&](const MapEntry &candidate) -> LogicalResult { + if (!candidates.insert(&candidate).second) + return success(); + recordAddressMapWork( + &detail::AddressMapVerificationWork::candidateIntersectionChecks); + if (selectionsIntersect(entry, candidate)) + return emitConflict(entry, candidate); + return success(); + }; + if (entry.generalSelection) { + bool conflict = false; + visitGeneral(generalIndex, entry, false, [&](const MapEntry &candidate) { + if (!conflict && failed(check(candidate))) + conflict = true; + return !conflict; + }); + visitGeneral(generalIndex, entry, true, [&](const MapEntry &candidate) { + if (!conflict && failed(check(candidate))) + conflict = true; + return !conflict; + }); + for (const std::string &key : entry.queryKeys) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::selectorQueryVisits); + auto found = fastIndex.find(key); + if (found == fastIndex.end()) + continue; + visitEligible(found->second, entry, [&](const EntrySet &set) { + for (const MapEntry *candidate : set) + if (!conflict && failed(check(*candidate))) + conflict = true; + return !conflict; + }); + } + if (conflict) + return failure(); + } else if (entry.concreteSelection) { + bool conflict = false; + for (const std::string &key : entry.queryKeys) { + recordAddressMapWork( + &detail::AddressMapVerificationWork::selectorQueryVisits); + auto found = generalIndex.find(key); + if (found == generalIndex.end()) + continue; + for (const auto &[geometry, banks] : found->second.lanes) + for (const auto &[bank, partitions] : banks) + visitEligible(partitions, entry, [&](const EntrySet &set) { + for (const MapEntry *candidate : set) + if (!conflict && failed(check(*candidate))) + conflict = true; + return !conflict; + }); + } + if (conflict) + return failure(); + } + updateActive(entry, true); + activeEntries.emplace(entry.interval.end, &entry); + } + return success(); +} + +} // namespace + +void normalizeAddressMaps(Operation *topLevel) { + topLevel->walk([](AddressMapOp map) { + SmallVector entries; + entries.reserve(map.getEntries().size()); + for (Attribute attribute : map.getEntries()) + entries.push_back(normalizedMapEntry(cast(attribute))); + llvm::stable_sort(entries, [](Attribute left, Attribute right) { + return compareNormalizedMapEntries(cast(left), + cast(right)) < 0; + }); + map.setEntriesAttr(ArrayAttr::get(map.getContext(), entries)); + }); +} + +LogicalResult verifyModuleResourceReferences( + Operation *operation, const llvm::StringMap &producerIndex) { + auto module = dyn_cast(operation); + if (!module) + return success(); + for (Operation &child : module.getBody().front()) { + if (auto eventQueue = dyn_cast(child)) { + if (!isa_and_nonnull( + lookupIndexed(producerIndex, eventQueue.getTimeDomainAttr()))) + return eventQueue.emitOpError() + << "time domain '" << eventQueue.getTimeDomainAttr() + << "' is unresolved"; + } else if (auto resource = dyn_cast(child)) { + if (FlatSymbolRefAttr arbiter = resource.getArbitrationOwnerAttr(); + arbiter && !isa_and_nonnull( + lookupIndexed(producerIndex, arbiter))) + return resource.emitOpError() + << "arbitration owner '" << arbiter << "' is unresolved"; + } else if (auto addressSpace = dyn_cast(child)) { + if (failed(verifyAddressTranslation(addressSpace, producerIndex))) + return failure(); + } else if (auto addressMap = dyn_cast(child)) { + if (failed(verifyAddressMap(addressMap, producerIndex))) + return failure(); + } else if (auto domain = dyn_cast(child)) { + if (!domain.getParentAttr()) + continue; + if (!isa_and_nonnull( + lookupIndexed(producerIndex, domain.getParentAttr()))) + return domain.emitOpError() + << "parent time domain '" << domain.getParentAttr() + << "' is unresolved"; + FlatSymbolRefAttr owner = + domain.getBridgeAttr().getAs("owner"); + if (!isa_and_nonnull( + lookupIndexed(producerIndex, owner))) + return domain.emitOpError( + "bridge owner must resolve to a local structural owner"); + } + } + if (failed(verifyParentCycles(module, false, producerIndex)) || + failed(verifyParentCycles(module, true, producerIndex))) + return failure(); + return success(); +} + +} // namespace acir::ac diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ACIRResourcesTestHooks.h b/components/agentic-circuit/lib/Dialect/ACIR/ACIRResourcesTestHooks.h new file mode 100644 index 00000000..a0f1cd9c --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ACIRResourcesTestHooks.h @@ -0,0 +1,42 @@ +#ifndef ACIR_LIB_DIALECT_ACIR_ACIR_RESOURCES_TEST_HOOKS_H +#define ACIR_LIB_DIALECT_ACIR_ACIR_RESOURCES_TEST_HOOKS_H + +#include + +namespace acir::ac::detail { + +struct AddressMapVerificationWork { + uint64_t entryNormalizationVisits = 0; + uint64_t concreteEntryVisits = 0; + uint64_t concreteExpirationVisits = 0; + uint64_t selectorQueryVisits = 0; + uint64_t selectorUpdateVisits = 0; + uint64_t candidateIntersectionChecks = 0; + uint64_t generalRelationChecks = 0; + + uint64_t total() const { + return entryNormalizationVisits + concreteEntryVisits + + concreteExpirationVisits + selectorQueryVisits + + selectorUpdateVisits + candidateIntersectionChecks + + generalRelationChecks; + } +}; + +class ScopedAddressMapVerificationWorkCollector { +public: + explicit ScopedAddressMapVerificationWorkCollector( + AddressMapVerificationWork &work); + ~ScopedAddressMapVerificationWorkCollector(); + + ScopedAddressMapVerificationWorkCollector( + const ScopedAddressMapVerificationWorkCollector &) = delete; + ScopedAddressMapVerificationWorkCollector & + operator=(const ScopedAddressMapVerificationWorkCollector &) = delete; + +private: + AddressMapVerificationWork *previous; +}; + +} // namespace acir::ac::detail + +#endif // ACIR_LIB_DIALECT_ACIR_ACIR_RESOURCES_TEST_HOOKS_H diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ACIRTypes.cpp b/components/agentic-circuit/lib/Dialect/ACIR/ACIRTypes.cpp new file mode 100644 index 00000000..d1a46887 --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ACIRTypes.cpp @@ -0,0 +1,311 @@ +#include "acir/Dialect/ACIR/ACIRDialect.h" +#include "acir/Dialect/ACIR/GraphRegion.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/DialectImplementation.h" +#include "mlir/Interfaces/DataLayoutInterfaces.h" +#include "llvm/ADT/TypeSwitch.h" + +using namespace mlir; + +namespace acir::ac { +namespace { + +bool isTimeUnit(Unit unit) { + switch (unit) { + case Unit::Ticks: + case Unit::Cycles: + case Unit::Seconds: + case Unit::Milliseconds: + case Unit::Microseconds: + case Unit::Nanoseconds: + case Unit::Picoseconds: + return true; + case Unit::Bytes: + case Unit::Bits: + case Unit::Entries: + case Unit::Packets: + case Unit::Transactions: + return false; + } + llvm_unreachable("unknown ACIR unit"); +} + +LogicalResult verifyValueElement(function_ref emitError, + Type elementType) { + if (containsChannelType(elementType)) + return emitError() << "channel types cannot be nested inside value types"; + if (containsQueueOrVarType(elementType)) + return emitError() + << "queue and var types cannot be nested inside value types"; + return success(); +} + +DictionaryAttr findLayoutEntry(Type type, DataLayoutEntryListRef entries) { + for (DataLayoutEntryInterface entry : entries) + if (entry.getKey() == DataLayoutEntryKey(type)) + return dyn_cast(entry.getValue()); + return {}; +} + +int64_t getPositiveLayoutInteger(Type type, DataLayoutEntryListRef entries, + StringRef name) { + DictionaryAttr entry = findLayoutEntry(type, entries); + auto value = entry ? entry.getAs(name) : IntegerAttr(); + return value && value.getInt() > 0 ? value.getInt() : 0; +} + +llvm::TypeSize getNamedTypeSizeInBits(Type type, + DataLayoutEntryListRef entries) { + return llvm::TypeSize::getFixed( + static_cast(getPositiveLayoutInteger(type, entries, "size")) * + 8); +} + +uint64_t getNamedTypeAlignment(Type type, DataLayoutEntryListRef entries, + StringRef name) { + return static_cast(getPositiveLayoutInteger(type, entries, name)); +} + +LogicalResult verifyNamedLayoutEntries(DataLayoutEntryListRef entries, + Location location, bool packet) { + for (DataLayoutEntryInterface entry : entries) { + auto dictionary = dyn_cast(entry.getValue()); + auto size = + dictionary ? dictionary.getAs("size") : IntegerAttr(); + auto abi = dictionary ? dictionary.getAs("abi_alignment") + : IntegerAttr(); + auto preferred = dictionary + ? dictionary.getAs("preferred_alignment") + : IntegerAttr(); + auto endian = + dictionary ? dictionary.getAs("endianness") : StringAttr(); + if (!size || !abi || !preferred || !endian || size.getInt() <= 0 || + abi.getInt() <= 0 || preferred.getInt() <= 0 || + (endian.getValue() != "little" && endian.getValue() != "big")) + return emitError(location) + << "layout entry requires positive size/alignment and explicit " + "endianness"; + if (packet) { + auto width = dictionary.getAs("serialization_width"); + if (!width || width.getInt() <= 0) + return emitError(location) + << "packet layout entry requires positive serialization_width"; + } + } + return success(); +} + +bool isStaticCollectionElementType(Type type) { + return isa(type); +} + +LogicalResult +verifyStaticCollectionElement(function_ref emitError, + Type elementType, StringRef collection) { + if (isStaticCollectionElementType(elementType)) + return success(); + return emitError() << collection + << " element must be a queue, var, or static collection"; +} + +} // namespace + +bool containsChannelType(Type type) { + return type.walk([](ChannelType) { return WalkResult::interrupt(); }) + .wasInterrupted(); +} + +bool containsQueueOrVarType(Type type) { + return type + .walk([](Type nested) { + return isa(nested) ? WalkResult::interrupt() + : WalkResult::advance(); + }) + .wasInterrupted(); +} + +bool isImmutablePayloadType(Type type) { + if (isa(type)) + return true; + if (auto optional = dyn_cast(type)) + return isImmutablePayloadType(optional.getElementType()); + if (auto vector = dyn_cast(type)) + return isImmutablePayloadType(vector.getElementType()); + if (auto vector = dyn_cast(type)) + return isImmutablePayloadType(vector.getElementType()); + return false; +} + +bool isNormativePayloadType(Type type) { + if (isImmutablePayloadType(type)) + return true; + if (auto list = dyn_cast(type)) + return isNormativePayloadType(list.getElementType()); + return false; +} + +LogicalResult +verifyQualifiedDataName(function_ref emitError, + SymbolRefAttr name) { + if (name.getNestedReferences().size() == 1) + return success(); + return emitError() + << "named data references require a qualified symbol such as " + "'@types::@S'"; +} + +#define ACIR_DEFINE_DATA_NAME_VERIFY(TYPE) \ + LogicalResult TYPE::verify(function_ref emitError, \ + SymbolRefAttr name) { \ + return verifyQualifiedDataName(emitError, name); \ + } + +ACIR_DEFINE_DATA_NAME_VERIFY(StructType) +ACIR_DEFINE_DATA_NAME_VERIFY(PacketType) +ACIR_DEFINE_DATA_NAME_VERIFY(TransactionType) +ACIR_DEFINE_DATA_NAME_VERIFY(EnumType) +ACIR_DEFINE_DATA_NAME_VERIFY(UnionType) + +#undef ACIR_DEFINE_DATA_NAME_VERIFY + +LogicalResult OptionalType::verify(function_ref emitError, + Type elementType) { + return verifyValueElement(emitError, elementType); +} + +LogicalResult ListType::verify(function_ref emitError, + Type elementType) { + return verifyValueElement(emitError, elementType); +} + +LogicalResult VectorType::verify(function_ref emitError, + int64_t length, Type elementType) { + if (length <= 0) + return emitError() << "vector length must be positive"; + return verifyValueElement(emitError, elementType); +} + +LogicalResult VarType::verify(function_ref emitError, + Type elementType) { + if (isImmutablePayloadType(elementType)) + return success(); + return emitError() << "var payload must be an immutable ACIR value type"; +} + +LogicalResult QueueType::verify(function_ref emitError, + Type elementType) { + if (isImmutablePayloadType(elementType)) + return success(); + return emitError() << "queue payload must be an immutable ACIR value type"; +} + +LogicalResult ArrayType::verify(function_ref emitError, + int64_t length, Type elementType) { + if (length <= 0) + return emitError() << "array length must be positive"; + return verifyStaticCollectionElement(emitError, elementType, "array"); +} + +LogicalResult MapType::verify(function_ref emitError, + ArrayAttr keys, Type elementType) { + if (keys.empty()) + return emitError() + << "map keys must be non-empty and strictly lexicographic"; + StringRef previous; + for (Attribute rawKey : keys) { + auto key = dyn_cast(rawKey); + if (!key || key.getValue().empty() || + (!previous.empty() && previous >= key.getValue())) + return emitError() + << "map keys must be non-empty and strictly lexicographic"; + previous = key.getValue(); + } + return verifyStaticCollectionElement(emitError, elementType, "map"); +} + +LogicalResult SetType::verify(function_ref emitError, + int64_t length, Type elementType) { + if (length <= 0) + return emitError() << "set length must be positive"; + return verifyStaticCollectionElement(emitError, elementType, "set"); +} + +LogicalResult FlowType::verify(function_ref emitError, + Type elementType, FlatSymbolRefAttr) { + return verifyValueElement(emitError, elementType); +} + +LogicalResult ChannelType::verify(function_ref emitError, + Type elementType, FlatSymbolRefAttr) { + if (!containsChannelType(elementType)) + return success(); + return emitError() << "channel types cannot carry channel types"; +} + +LogicalResult DurationType::verify(function_ref emitError, + Unit unit) { + if (isTimeUnit(unit)) + return success(); + return emitError() << "duration requires a time unit"; +} + +LogicalResult RateType::verify(function_ref emitError, + Unit numerator, Unit denominator) { + if (isTimeUnit(numerator)) + return emitError() << "rate numerator must be a data unit"; + if (!isTimeUnit(denominator)) + return emitError() << "rate denominator must be a time unit"; + return success(); +} + +LogicalResult EventType::verify(function_ref emitError, + Type elementType) { + return verifyValueElement(emitError, elementType); +} + +#define ACIR_DEFINE_NAMED_LAYOUT(TYPE, IS_PACKET) \ + llvm::TypeSize TYPE::getTypeSizeInBits( \ + const DataLayout &, DataLayoutEntryListRef entries) const { \ + return getNamedTypeSizeInBits(*this, entries); \ + } \ + uint64_t TYPE::getABIAlignment(const DataLayout &, \ + DataLayoutEntryListRef entries) const { \ + return getNamedTypeAlignment(*this, entries, "abi_alignment"); \ + } \ + uint64_t TYPE::getPreferredAlignment(const DataLayout &, \ + DataLayoutEntryListRef entries) const { \ + return getNamedTypeAlignment(*this, entries, "preferred_alignment"); \ + } \ + LogicalResult TYPE::verifyEntries(DataLayoutEntryListRef entries, \ + Location location) const { \ + return verifyNamedLayoutEntries(entries, location, IS_PACKET); \ + } + +ACIR_DEFINE_NAMED_LAYOUT(StructType, false) +ACIR_DEFINE_NAMED_LAYOUT(PacketType, true) +ACIR_DEFINE_NAMED_LAYOUT(EnumType, false) +ACIR_DEFINE_NAMED_LAYOUT(UnionType, false) + +#undef ACIR_DEFINE_NAMED_LAYOUT + +} // namespace acir::ac + +#include "acir/Dialect/ACIR/ACIREnums.cpp.inc" + +#define GET_TYPEDEF_CLASSES +#include "acir/Dialect/ACIR/ACIRTypes.cpp.inc" + +void acir::ac::ACIRDialect::initialize() { + addInterfaces(); + addTypes< +#define GET_TYPEDEF_LIST +#include "acir/Dialect/ACIR/ACIRTypes.cpp.inc" + >(); + addOperations< +#define GET_OP_LIST +#include "acir/Dialect/ACIR/ACIROps.cpp.inc" + >(); +} diff --git a/components/agentic-circuit/lib/Dialect/ACIR/CMakeLists.txt b/components/agentic-circuit/lib/Dialect/ACIR/CMakeLists.txt new file mode 100644 index 00000000..5f3b2144 --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/CMakeLists.txt @@ -0,0 +1,32 @@ +add_acir_library(ACIRDialect SOURCES + ACIRDialect.cpp + ACIRInterfaces.cpp + ACIROps.cpp + ACIRResources.cpp + ACIRTypes.cpp + GraphRegion.cpp + ProcessLowerability.cpp +) +add_dependencies(ACIRDialect + ACIRDialectIncGen + ACIRAttributesIncGen + ACIRTypesIncGen + ACIROpsIncGen + ACIROpInterfacesIncGen +) +target_link_libraries(ACIRDialect PUBLIC + MLIRControlFlowInterfaces + MLIRDLTIDialect + MLIRDataLayoutInterfaces + MLIRFunctionInterfaces + MLIRFuncDialect + MLIRPresburger + MLIRSCFDialect + MLIRSideEffectInterfaces +) +target_include_directories(ACIRDialect PUBLIC + "$" + "$" + "$" +) +add_library(AgenticCircuit::ACIRDialect ALIAS ACIRDialect) diff --git a/components/agentic-circuit/lib/Dialect/ACIR/GraphRegion.cpp b/components/agentic-circuit/lib/Dialect/ACIR/GraphRegion.cpp new file mode 100644 index 00000000..16865bdb --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/GraphRegion.cpp @@ -0,0 +1,573 @@ +#include "acir/Dialect/ACIR/GraphRegion.h" + +#include "acir/Dialect/ACIR/ACIRDialect.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/SymbolTable.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringSet.h" + +#include + +using namespace mlir; + +namespace acir::ac { +namespace { + +bool validSegment(StringRef segment) { + return !segment.empty() && llvm::all_of(segment, [](char c) { + return llvm::isAlnum(c) || c == '_' || c == '-'; + }); +} + +bool isModuleDeclaration(Operation *op) { + return isa_and_nonnull(op); +} + +Operation *lookupDefinition(SymbolTable &symbols, FlatSymbolRefAttr reference) { + return symbols.lookup(reference.getValue()); +} + +SmallVector instantiatedDefinitions(ModuleOp module, + SymbolTable &symbols) { + SmallVector definitions; + for (Operation &child : module.getBody().front()) { + if (auto instance = dyn_cast(child)) { + definitions.push_back( + lookupDefinition(symbols, instance.getDefinitionAttr())); + } else if (auto array = dyn_cast(child)) { + definitions.push_back( + lookupDefinition(symbols, array.getDefinitionAttr())); + } else if (auto instances = dyn_cast(child)) { + for (Attribute definition : instances.getDefinitions()) + definitions.push_back( + lookupDefinition(symbols, cast(definition))); + } + } + return definitions; +} + +LogicalResult verifyFiniteInstantiationGraph(mlir::ModuleOp file, + SymbolTable &symbols) { + enum class State : uint8_t { Unvisited, Active, Complete }; + struct Frame { + ModuleOp module; + SmallVector definitions; + size_t nextDefinition = 0; + }; + llvm::DenseMap states; + SmallVector stack; + for (ModuleOp root : file.getOps()) { + if (states.lookup(root) != State::Unvisited) + continue; + states[root] = State::Active; + stack.push_back({root, instantiatedDefinitions(root, symbols)}); + while (!stack.empty()) { + Frame &frame = stack.back(); + if (frame.nextDefinition == frame.definitions.size()) { + states[frame.module] = State::Complete; + stack.pop_back(); + continue; + } + + auto child = + dyn_cast_or_null(frame.definitions[frame.nextDefinition++]); + if (!child) + continue; + if (states.lookup(child) == State::Active) { + auto start = llvm::find_if(stack, [child](const Frame &candidate) { + return candidate.module == child; + }); + InFlightDiagnostic diagnostic = + frame.module.emitOpError("recursive module instantiation cycle: "); + for (auto current = start; current != stack.end(); ++current) + diagnostic << '@' << current->module.getSymName() << " -> "; + diagnostic << '@' << child.getSymName(); + return failure(); + } + if (states.lookup(child) == State::Unvisited) { + states[child] = State::Active; + stack.push_back({child, instantiatedDefinitions(child, symbols)}); + } + } + } + return success(); +} + +} // namespace + +void StructuralProviderRegistry::registerExternal(StringRef name) { + externalProviders.insert(name); +} + +void StructuralProviderRegistry::registerGenerator(StringRef name) { + generatorProviders.insert(name); +} + +bool StructuralProviderRegistry::hasExternal(StringRef name) const { + return externalProviders.contains(name); +} + +bool StructuralProviderRegistry::hasGenerator(StringRef name) const { + return generatorProviders.contains(name); +} + +StructuralProviderRegistry & +getStructuralProviderRegistry(MLIRContext *context) { + auto *dialect = context->getOrLoadDialect(); + auto *interface = + dialect->getRegisteredInterface(); + assert(interface && "ACIR structural provider interface must be registered"); + return interface->getRegistry(); +} + +bool isConcreteStaticValue(Attribute value) { + if (!value) + return false; + if (isa(value)) + return true; + if (auto dictionary = dyn_cast(value)) { + if (dictionary.size() != 2) + return false; + auto amount = dictionary.getAs("value"); + auto unit = dictionary.getAs("unit"); + return amount && amount.getType().isSignlessInteger(64) && unit && + symbolizeUnit(unit.getValue()).has_value(); + } + return false; +} + +std::string buildArrayElementPath(StringRef base, ArrayRef indices) { + std::string path = base.str(); + for (int64_t index : indices) { + path.push_back('['); + path.append(std::to_string(index)); + path.push_back(']'); + } + return path; +} + +static LogicalResult verifyGraphStructureImpl( + Operation *topLevel, + SmallVectorImpl *elaboratedStateOwners, + SmallVectorImpl *elaboratedTopologyOwners) { + auto file = dyn_cast(topLevel); + if (!file) + return success(); + SymbolTable symbols(file); + + unsigned selected = 0; + SystemOp selectedSystem; + for (SystemOp system : file.getOps()) { + if (system.getSelected()) { + ++selected; + selectedSystem = system; + } + Operation *root = lookupDefinition(symbols, system.getRootAttr()); + if (!isa_and_nonnull(root)) { + if (system.getSelected()) + return system.emitOpError( + "selected root must resolve to a materialized ac.module"); + return system.emitOpError() << "root '" << system.getRootAttr() + << "' does not resolve to ac.module"; + } + if (SymbolRefAttr workload = system.getPrimaryWorkloadAttr()) { + Operation *target = nullptr; + if (workload.getRootReference() == system.getRootAttr().getValue() && + workload.getNestedReferences().size() == 1) { + auto module = cast(root); + StringRef processName = + workload.getNestedReferences().front().getValue(); + for (ProcessOp process : module.getBody().front().getOps()) + if (process.getSymName() == processName) { + target = process; + break; + } + } + if (!target) + return system.emitOpError() + << "primary workload '" << workload << "' is unresolved"; + if (!isa(target)) + return system.emitOpError() << "primary workload '" << workload + << "' does not resolve to ac.process"; + } + } + if (!file.getOps().empty() && selected != 1) + return file.emitError() << "ACIR file requires exactly one selected " + "ac.system, found " + << selected; + if (failed(verifyFiniteInstantiationGraph(file, symbols))) + return failure(); + if (!selectedSystem) + return success(); + + constexpr uint64_t maxHierarchyDepth = 1024; + constexpr uint64_t maxHierarchyOwners = 1048576; + struct ExpansionStats { + uint64_t owners = 0; + uint64_t depth = 0; + }; + auto saturatedAdd = [](uint64_t left, uint64_t right, uint64_t cap) { + return left >= cap || right >= cap || left > cap - right ? cap + : left + right; + }; + auto saturatedMultiply = [](uint64_t left, uint64_t right, uint64_t cap) { + return left && right > cap / left ? cap : std::min(left * right, cap); + }; + + auto selectedRoot = + cast(lookupDefinition(symbols, selectedSystem.getRootAttr())); + llvm::DenseMap greatestIncomingDepth; + SmallVector> depthWorklist; + greatestIncomingDepth[selectedRoot] = 0; + depthWorklist.push_back({selectedRoot, 0}); + while (!depthWorklist.empty()) { + auto [module, incomingDepth] = depthWorklist.pop_back_val(); + if (greatestIncomingDepth.lookup(module) != incomingDepth) + continue; + for (Operation &child : module.getBody().front()) { + auto enqueue = [&](Operation *definition, + uint64_t depth) -> LogicalResult { + if (depth > maxHierarchyDepth) + return failure(); + auto target = dyn_cast_or_null(definition); + if (target && greatestIncomingDepth.lookup(target) < depth) { + greatestIncomingDepth[target] = depth; + depthWorklist.push_back({target, depth}); + } + return success(); + }; + if (auto instance = dyn_cast(child)) { + if (failed( + enqueue(lookupDefinition(symbols, instance.getDefinitionAttr()), + incomingDepth + 1))) + return selectedSystem.emitOpError() + << "elaborated hierarchy depth exceeds bound " + << maxHierarchyDepth; + } else if (auto array = dyn_cast(child)) { + if (incomingDepth + 1 > maxHierarchyDepth) + return selectedSystem.emitOpError() + << "elaborated hierarchy depth exceeds bound " + << maxHierarchyDepth; + uint64_t count = 1; + for (int64_t extent : array.getShape()) + count = saturatedMultiply(count, static_cast(extent), 1); + if (count != 0 && + failed(enqueue(lookupDefinition(symbols, array.getDefinitionAttr()), + incomingDepth + 2))) + return selectedSystem.emitOpError() + << "elaborated hierarchy depth exceeds bound " + << maxHierarchyDepth; + } else if (auto instances = dyn_cast(child)) { + if (incomingDepth + 1 > maxHierarchyDepth) + return selectedSystem.emitOpError() + << "elaborated hierarchy depth exceeds bound " + << maxHierarchyDepth; + for (Attribute reference : instances.getDefinitions()) + if (failed(enqueue( + lookupDefinition(symbols, cast(reference)), + incomingDepth + 2))) + return selectedSystem.emitOpError() + << "elaborated hierarchy depth exceeds bound " + << maxHierarchyDepth; + } + } + } + + SmallVector postorder; + SmallVector> traversal; + llvm::DenseSet scheduled; + traversal.push_back({selectedRoot, false}); + while (!traversal.empty()) { + auto [module, expanded] = traversal.pop_back_val(); + if (expanded) { + postorder.push_back(module); + continue; + } + if (!scheduled.insert(module).second) + continue; + traversal.push_back({module, true}); + SmallVector definitions = + instantiatedDefinitions(module, symbols); + for (Operation *definition : llvm::reverse(definitions)) + if (auto child = dyn_cast_or_null(definition)) + traversal.push_back({child, false}); + } + + llvm::DenseMap expansionMemo; + llvm::DenseMap>> + traceSourceMemo; + for (ModuleOp module : postorder) { + ExpansionStats stats; + SmallVector> traceSources; + llvm::StringMap traceSourceIndex; + auto addTraceSource = [&](StringRef source, + Operation *owner) -> LogicalResult { + if (!traceSourceIndex.try_emplace(source, owner).second) + return owner->emitOpError() + << "trace source '" << source + << "' has multiple elaborated cursor owners"; + traceSources.push_back({source.str(), owner}); + return success(); + }; + auto mergeTraceSources = [&](Operation *definition, uint64_t multiplicity, + Operation *owner) -> LogicalResult { + auto found = traceSourceMemo.find(definition); + if (found == traceSourceMemo.end() || found->second.empty() || + multiplicity == 0) + return success(); + if (multiplicity > 1) + return owner->emitOpError() + << "trace source '" << found->second.front().first + << "' has multiple elaborated cursor owners"; + for (const auto &[source, declaration] : found->second) + if (failed(addTraceSource(source, declaration))) + return failure(); + return success(); + }; + for (Operation &child : module.getBody().front()) { + ExpansionStats childStats; + uint64_t localOwners = 0; + uint64_t localDepth = 0; + if (auto instance = dyn_cast(child)) { + Operation *definition = + lookupDefinition(symbols, instance.getDefinitionAttr()); + childStats = expansionMemo.lookup(definition); + if (failed(mergeTraceSources(definition, 1, &child))) + return failure(); + localOwners = + saturatedAdd(1, childStats.owners, maxHierarchyOwners + 1); + localDepth = saturatedAdd(1, childStats.depth, maxHierarchyDepth + 1); + } else if (auto array = dyn_cast(child)) { + uint64_t count = 1; + for (int64_t extent : array.getShape()) + count = saturatedMultiply(count, static_cast(extent), + maxHierarchyOwners + 1); + Operation *definition = + lookupDefinition(symbols, array.getDefinitionAttr()); + childStats = expansionMemo.lookup(definition); + if (failed(mergeTraceSources(definition, count, &child))) + return failure(); + uint64_t perElement = + saturatedAdd(1, childStats.owners, maxHierarchyOwners + 1); + localOwners = saturatedAdd( + 1, saturatedMultiply(count, perElement, maxHierarchyOwners + 1), + maxHierarchyOwners + 1); + localDepth = count == 0 ? 1 + : saturatedAdd(2, childStats.depth, + maxHierarchyDepth + 1); + } else if (auto instances = dyn_cast(child)) { + localOwners = 1; + localDepth = 1; + for (Attribute reference : instances.getDefinitions()) { + Operation *definition = + lookupDefinition(symbols, cast(reference)); + childStats = expansionMemo.lookup(definition); + if (failed(mergeTraceSources(definition, 1, &child))) + return failure(); + localOwners = saturatedAdd( + localOwners, + saturatedAdd(1, childStats.owners, maxHierarchyOwners + 1), + maxHierarchyOwners + 1); + localDepth = + std::max(localDepth, saturatedAdd(2, childStats.depth, + maxHierarchyDepth + 1)); + } + } else if (isa(child)) { + localOwners = 1; + localDepth = 1; + } + if (auto process = dyn_cast(child)) { + WalkResult result = process.getBody().walk([&](TraceOpenOp trace) { + if (failed(addTraceSource(trace.getSource(), trace))) + return WalkResult::interrupt(); + return WalkResult::advance(); + }); + if (result.wasInterrupted()) + return failure(); + } + stats.owners = + saturatedAdd(stats.owners, localOwners, maxHierarchyOwners + 1); + stats.depth = std::max(stats.depth, localDepth); + } + expansionMemo[module] = stats; + traceSourceMemo[module] = std::move(traceSources); + } + ExpansionStats selectedStats = expansionMemo.lookup(selectedRoot); + if (selectedStats.depth > maxHierarchyDepth) + return selectedSystem.emitOpError() + << "elaborated hierarchy depth exceeds bound " << maxHierarchyDepth; + if (saturatedAdd(1, selectedStats.owners, maxHierarchyOwners + 1) > + maxHierarchyOwners) + return selectedSystem.emitOpError() + << "elaborated hierarchy owner count exceeds bound " + << maxHierarchyOwners; + + llvm::StringSet<> paths; + llvm::StringSet<> stableIds; + auto registerOwner = [&](Operation *op, StringRef path, + StringRef stableId) -> LogicalResult { + if (!paths.insert(path).second) + return op->emitOpError() + << "duplicate elaborated hierarchy path '" << path << "'"; + if (!stableIds.insert(stableId).second) + return op->emitOpError() + << "duplicate elaborated stable ownership id '" << stableId << "'"; + if (elaboratedTopologyOwners) + elaboratedTopologyOwners->push_back({op, path.str(), stableId.str()}); + return success(); + }; + + std::function elaborate = + [&](Operation *definition, StringRef parentPath, + StringRef parentId) -> LogicalResult { + auto module = dyn_cast(definition); + if (!module) + return success(); + for (Operation &child : module.getBody().front()) { + if (auto instance = dyn_cast(child)) { + std::string path = (parentPath + "." + instance.getPath()).str(); + std::string id = (parentId + "/" + instance.getStableId()).str(); + if (failed(registerOwner(&child, path, id)) || + failed(elaborate( + lookupDefinition(symbols, instance.getDefinitionAttr()), path, + id))) + return failure(); + } else if (auto array = dyn_cast(child)) { + std::string basePath = (parentPath + "." + array.getPath()).str(); + std::string baseId = (parentId + "/" + array.getStableId()).str(); + if (failed(registerOwner(&child, basePath, baseId))) + return failure(); + uint64_t count = 1; + for (int64_t extent : array.getShape()) + count *= static_cast(extent); + SmallVector indices(array.getShape().size()); + for (uint64_t ordinal = 0; ordinal < count; ++ordinal) { + uint64_t remainder = ordinal; + for (int64_t dimension = array.getShape().size() - 1; dimension >= 0; + --dimension) { + int64_t extent = array.getShape()[dimension]; + indices[dimension] = remainder % static_cast(extent); + remainder /= static_cast(extent); + } + std::string path = buildArrayElementPath(basePath, indices); + std::string id = buildArrayElementPath(baseId, indices); + if (failed(registerOwner(&child, path, id)) || + failed(elaborate( + lookupDefinition(symbols, array.getDefinitionAttr()), path, + id))) + return failure(); + } + } else if (auto instances = dyn_cast(child)) { + std::string collectionPath = + (parentPath + "." + instances.getPath()).str(); + std::string collectionId = + (parentId + "/" + instances.getStableId()).str(); + if (failed(registerOwner(&child, collectionPath, collectionId))) + return failure(); + for (size_t index = 0; index < instances.getDefinitions().size(); + ++index) { + StringRef segment = + cast(instances.getPaths()[index]).getValue(); + StringRef localId = + cast(instances.getStableIds()[index]).getValue(); + std::string path = (collectionPath + "." + segment).str(); + std::string id = (collectionId + "/" + localId).str(); + auto target = + cast(instances.getDefinitions()[index]); + if (failed(registerOwner(&child, path, id)) || + failed(elaborate(lookupDefinition(symbols, target), path, id))) + return failure(); + } + } else if (auto queue = dyn_cast(child)) { + std::string path = (parentPath + "." + queue.getPath()).str(); + std::string id = (parentId + "/" + queue.getStableId()).str(); + if (failed(registerOwner(&child, path, id))) + return failure(); + if (elaboratedStateOwners) + elaboratedStateOwners->push_back({&child, path, id}); + } else if (auto eventQueue = dyn_cast(child)) { + std::string path = (parentPath + "." + eventQueue.getPath()).str(); + std::string id = (parentId + "/" + eventQueue.getStableId()).str(); + if (failed(registerOwner(&child, path, id))) + return failure(); + if (elaboratedStateOwners) + elaboratedStateOwners->push_back({&child, path, id}); + } else if (auto resource = dyn_cast(child)) { + std::string path = (parentPath + "." + resource.getPath()).str(); + std::string id = (parentId + "/" + resource.getStableId()).str(); + if (failed(registerOwner(&child, path, id))) + return failure(); + if (elaboratedStateOwners) + elaboratedStateOwners->push_back({&child, path, id}); + } else if (auto addressSpace = dyn_cast(child)) { + std::string path = (parentPath + "." + addressSpace.getPath()).str(); + std::string id = (parentId + "/" + addressSpace.getStableId()).str(); + if (failed(registerOwner(&child, path, id))) + return failure(); + if (elaboratedStateOwners) + elaboratedStateOwners->push_back({&child, path, id}); + } else if (auto process = dyn_cast(child)) { + std::string path = (parentPath + "." + process.getSymName()).str(); + std::string id = (parentId + "/" + process.getSymName()).str(); + if (failed(registerOwner(&child, path, id))) + return failure(); + if (elaboratedStateOwners) { + SmallVector sources; + process.getBody().walk([&](TraceOpenOp trace) { + sources.push_back(trace.getSource().str()); + }); + elaboratedStateOwners->push_back( + {&child, path, id, std::move(sources)}); + } + } else if (auto stat = dyn_cast(child)) { + std::string path = (parentPath + "." + stat.getSymName()).str(); + std::string id = (parentId + "/" + stat.getSymName()).str(); + if (failed(registerOwner(&child, path, id))) + return failure(); + if (elaboratedStateOwners) + elaboratedStateOwners->push_back({&child, path, id}); + } + } + return success(); + }; + + if (!validSegment(selectedSystem.getRootName())) + return selectedSystem.emitOpError( + "root instance name must be one stable hierarchy segment"); + std::string root = selectedSystem.getRootName().str(); + paths.insert(root); + stableIds.insert(root); + return elaborate(lookupDefinition(symbols, selectedSystem.getRootAttr()), + root, root); +} + +LogicalResult verifyGraphStructure(Operation *topLevel) { + return verifyGraphStructureImpl(topLevel, nullptr, nullptr); +} + +LogicalResult +collectElaboratedStateOwners(Operation *topLevel, + SmallVectorImpl &owners) { + owners.clear(); + if (failed(verifyGraphStructureImpl(topLevel, &owners, nullptr))) { + owners.clear(); + return failure(); + } + return success(); +} + +LogicalResult collectElaboratedTopologyOwners( + Operation *topLevel, SmallVectorImpl &owners) { + owners.clear(); + if (failed(verifyGraphStructureImpl(topLevel, nullptr, &owners))) { + owners.clear(); + return failure(); + } + return success(); +} + +} // namespace acir::ac diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ProcessLowerability.cpp b/components/agentic-circuit/lib/Dialect/ACIR/ProcessLowerability.cpp new file mode 100644 index 00000000..23a0929a --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ProcessLowerability.cpp @@ -0,0 +1,320 @@ +#include "ProcessLowerability.h" + +#include "acir/Dialect/ACIR/ACIROps.h" +#include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/IR/Matchers.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/SmallVector.h" + +#include + +using namespace mlir; + +namespace acir::ac { +namespace { + +constexpr uint64_t kMaxStaticForTrips = 1U << 20; + +bool isSuspension(Operation *operation) { + return isa(operation); +} + +std::optional constantBoolean(Value value) { + Attribute constant; + if (!matchPattern(value, m_Constant(&constant))) + return std::nullopt; + if (auto boolean = dyn_cast(constant)) + return boolean.getValue(); + if (auto integer = dyn_cast(constant); + integer && integer.getType().isInteger(1)) + return integer.getValue().getBoolValue(); + return std::nullopt; +} + +bool isAllowedProcessOperation(Operation *operation) { + StringRef name = operation->getName().getStringRef(); + if (name.starts_with("arith.") || name.starts_with("index.") || + isa(operation)) + return true; + return isa(operation); +} + +LogicalResult verifySCFShape(Operation *operation) { + auto requireSingleBlockTerminator = [&](Region ®ion, StringRef owner, + StringRef terminator) -> Operation * { + if (region.empty() || !llvm::hasSingleElement(region) || + region.front().empty() || + region.front().back().getName().getStringRef() != terminator) { + operation->emitOpError() << "malformed " << owner + << " region must terminate with " << terminator; + return nullptr; + } + return ®ion.front().back(); + }; + auto sameTypes = [](auto left, auto right) { + if (left.size() != right.size()) + return false; + return llvm::all_of(llvm::zip(left, right), [](auto pair) { + return std::get<0>(pair).getType() == std::get<1>(pair).getType(); + }); + }; + auto malformed = [&](StringRef owner) { + operation->emitOpError() + << "malformed " << owner + << " operand/result/block argument/yield arity or type mismatch"; + return failure(); + }; + + if (auto ifOp = dyn_cast(operation)) { + if (operation->getNumRegions() != 2) { + operation->emitOpError( + "malformed scf.if region must terminate with scf.yield"); + return failure(); + } + Region &thenRegion = ifOp.getThenRegion(); + Region &elseRegion = ifOp.getElseRegion(); + Operation *thenYield = + requireSingleBlockTerminator(thenRegion, "scf.if", "scf.yield"); + Operation *elseYield = + elseRegion.empty() + ? nullptr + : requireSingleBlockTerminator(elseRegion, "scf.if", "scf.yield"); + if (!thenYield || (!elseRegion.empty() && !elseYield)) + return failure(); + if (!ifOp.getCondition().getType().isInteger(1) || + !thenRegion.front().getArguments().empty() || + (!elseRegion.empty() && !elseRegion.front().getArguments().empty()) || + !sameTypes(thenYield->getOperands(), operation->getResults()) || + (elseRegion.empty() + ? operation->getNumResults() != 0 + : !sameTypes(elseYield->getOperands(), operation->getResults()))) + return malformed("scf.if"); + } else if (auto forOp = dyn_cast(operation)) { + if (operation->getNumRegions() != 1) + return malformed("scf.for"); + Region &body = forOp.getRegion(); + Operation *yield = + requireSingleBlockTerminator(body, "scf.for", "scf.yield"); + if (!yield || body.front().getNumArguments() != forOp.getNumResults() + 1 || + !sameTypes(forOp.getInitArgs(), forOp.getResults()) || + !sameTypes(forOp.getRegionIterArgs(), forOp.getResults()) || + !sameTypes(yield->getOperands(), forOp.getResults())) + return yield ? malformed("scf.for") : failure(); + } else if (auto whileOp = dyn_cast(operation)) { + if (operation->getNumRegions() != 2) + return malformed("scf.while"); + Region &before = whileOp.getBefore(); + Region &after = whileOp.getAfter(); + Operation *condition = requireSingleBlockTerminator( + before, "scf.while before", "scf.condition"); + Operation *yield = + requireSingleBlockTerminator(after, "scf.while after", "scf.yield"); + if (!condition || !yield || condition->getNumOperands() < 1 || + !condition->getOperand(0).getType().isInteger(1) || + !sameTypes(whileOp.getInits(), whileOp.getBeforeArguments()) || + !sameTypes(condition->getOperands().drop_front(), + whileOp.getResults()) || + !sameTypes(whileOp.getAfterArguments(), whileOp.getResults()) || + !sameTypes(yield->getOperands(), whileOp.getBeforeArguments())) + return (condition && yield) ? malformed("scf.while") : failure(); + } + return success(); +} + +std::optional constantInt(Value value) { + Attribute constant; + if (!matchPattern(value, m_Constant(&constant))) + return std::nullopt; + auto integer = dyn_cast(constant); + if (!integer || !integer.getValue().isSignedIntN(64)) + return std::nullopt; + return integer.getValue().getSExtValue(); +} + +bool isIntegerConstant(Value value) { + Attribute constant; + return matchPattern(value, m_Constant(&constant)) && + isa(constant); +} + +} // namespace + +LogicalResult walkStructuredOperationsIterative( + Operation *root, function_ref visitor, + const RawModelStructureLimits &limits) { + struct WorkItem { + Operation *operation; + uint64_t depth; + }; + SmallVector worklist{{root, 0}}; + uint64_t nodes = 0; + uint64_t edges = 0; + while (!worklist.empty()) { + WorkItem item = worklist.pop_back_val(); + if (item.depth > limits.maxNestedRegionDepth) + return emitError(root->getLoc()) + << "whole-model region nesting exceeds ACIR capability " + "limit " + << limits.maxNestedRegionDepth; + if (nodes == limits.maxNodes || + item.operation->getNumOperands() > limits.maxEdges - edges) + return emitError(root->getLoc()) + << "whole-model indexed analysis exceeds ACIR capability " + "limits (nodes " + << limits.maxNodes << ", edges " << limits.maxEdges << ')'; + ++nodes; + edges += item.operation->getNumOperands(); + if (failed(visitor(item.operation))) + return failure(); + + SmallVector children; + for (Region ®ion : item.operation->getRegions()) + for (Block &block : region) + for (Operation &child : block) + children.push_back(&child); + for (Operation *child : llvm::reverse(children)) + worklist.push_back({child, item.depth + 1}); + } + return success(); +} + +LogicalResult +preflightRawModelStructure(mlir::ModuleOp module, + const RawModelStructureLimits &limits) { + return walkStructuredOperationsIterative( + module.getOperation(), [](Operation *) { return success(); }, limits); +} + +FailureOr analyzeStaticFor(scf::ForOp op) { + std::optional lower = constantInt(op.getLowerBound()); + std::optional upper = constantInt(op.getUpperBound()); + std::optional step = constantInt(op.getStep()); + if (!lower || !upper || !step) { + if (isIntegerConstant(op.getLowerBound()) && + isIntegerConstant(op.getUpperBound()) && + isIntegerConstant(op.getStep())) + op.emitOpError() + << "static scf.for trip count exceeds ACIR capability limit " + << kMaxStaticForTrips; + return failure(); + } + if (*step <= 0) { + op.emitOpError("static scf.for step must be positive"); + return failure(); + } + unsigned __int128 distance = + *upper <= *lower + ? 0 + : static_cast(static_cast<__int128>(*upper) - + static_cast<__int128>(*lower)); + unsigned __int128 trips = (distance + static_cast(*step) - 1) / + static_cast(*step); + if (trips > kMaxStaticForTrips || + trips > std::numeric_limits::max()) { + op.emitOpError() + << "static scf.for trip count exceeds ACIR capability limit " + << kMaxStaticForTrips; + return failure(); + } + return StaticForTripCount{*lower, *upper, *step, + static_cast(trips)}; +} + +LogicalResult verifyProcessLowerability(Operation *processLikeOp, + const RawModelStructureLimits &limits) { + SmallVector dynamicLoops; + if (failed(walkStructuredOperationsIterative( + processLikeOp, + [&](Operation *operation) -> LogicalResult { + if (operation == processLikeOp) + return success(); + if (!isAllowedProcessOperation(operation)) + return operation->emitOpError() + << "ac.process contains unsupported operation " + << operation->getName(); + StringRef name = operation->getName().getStringRef(); + if ((name.starts_with("arith.") || name.starts_with("index.")) && + (!operation->getRegions().empty() || + !isMemoryEffectFree(operation))) + return operation->emitOpError( + "arith/index operation in ac.process must be regionless and " + "memory-effect free"); + if (failed(verifySCFShape(operation))) + return failure(); + if (auto forOp = dyn_cast(operation)) { + bool allConstant = isIntegerConstant(forOp.getLowerBound()) && + isIntegerConstant(forOp.getUpperBound()) && + isIntegerConstant(forOp.getStep()); + if (allConstant) { + if (failed(analyzeStaticFor(forOp))) + return failure(); + } else + dynamicLoops.push_back(forOp); + } + return success(); + }, + limits))) + return failure(); + + // Compute suspension guarantees bottom-up without consuming the native + // stack. The preceding bounded walk has already validated every node. + DenseMap guarantees; + struct SummaryTask { + Operation *operation; + bool expanded; + }; + SmallVector pending{{processLikeOp, false}}; + while (!pending.empty()) { + SummaryTask task = pending.pop_back_val(); + if (!task.expanded) { + pending.push_back({task.operation, true}); + SmallVector children; + for (Region ®ion : task.operation->getRegions()) + for (Block &block : region) + for (Operation &child : block) + children.push_back(&child); + for (Operation *child : children) + pending.push_back({child, false}); + continue; + } + for (Region ®ion : task.operation->getRegions()) { + bool regionGuarantee = false; + if (!region.empty()) { + for (Operation &operation : region.front()) { + if (isSuspension(&operation)) { + regionGuarantee = true; + break; + } + auto ifOp = dyn_cast(operation); + if (!ifOp) + continue; + if (std::optional condition = + constantBoolean(ifOp.getCondition())) { + Region &taken = + *condition ? ifOp.getThenRegion() : ifOp.getElseRegion(); + regionGuarantee = guarantees.lookup(&taken); + } else + regionGuarantee = !ifOp.getElseRegion().empty() && + guarantees.lookup(&ifOp.getThenRegion()) && + guarantees.lookup(&ifOp.getElseRegion()); + if (regionGuarantee) + break; + } + } + guarantees[®ion] = regionGuarantee; + } + } + for (scf::ForOp loop : dynamicLoops) + if (!guarantees.lookup(&loop.getRegion())) + return loop.emitOpError( + "dynamic scf.for requires every reachable backedge to suspend"); + return success(); +} + +} // namespace acir::ac diff --git a/components/agentic-circuit/lib/Dialect/ACIR/ProcessLowerability.h b/components/agentic-circuit/lib/Dialect/ACIR/ProcessLowerability.h new file mode 100644 index 00000000..68cf0b5c --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACIR/ProcessLowerability.h @@ -0,0 +1,43 @@ +#ifndef ACIR_DIALECT_ACIR_PROCESSLOWERABILITY_H +#define ACIR_DIALECT_ACIR_PROCESSLOWERABILITY_H + +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/Support/LogicalResult.h" +#include "llvm/ADT/STLFunctionalExtras.h" + +#include + +namespace acir::ac { + +struct RawModelStructureLimits { + uint64_t maxNodes = 1U << 20; + uint64_t maxEdges = 1U << 22; + uint64_t maxNestedRegionDepth = 512; +}; + +mlir::LogicalResult preflightRawModelStructure( + mlir::ModuleOp module, + const RawModelStructureLimits &limits = RawModelStructureLimits()); + +mlir::LogicalResult walkStructuredOperationsIterative( + mlir::Operation *root, + llvm::function_ref visitor, + const RawModelStructureLimits &limits = RawModelStructureLimits()); + +struct StaticForTripCount { + int64_t lowerBound = 0; + int64_t upperBound = 0; + int64_t step = 0; + uint64_t tripCount = 0; +}; + +mlir::FailureOr analyzeStaticFor(mlir::scf::ForOp op); + +mlir::LogicalResult verifyProcessLowerability( + mlir::Operation *processLikeOp, + const RawModelStructureLimits &limits = RawModelStructureLimits()); + +} // namespace acir::ac + +#endif // ACIR_DIALECT_ACIR_PROCESSLOWERABILITY_H diff --git a/components/agentic-circuit/lib/Dialect/ACSim/ACSimDialect.cpp b/components/agentic-circuit/lib/Dialect/ACSim/ACSimDialect.cpp new file mode 100644 index 00000000..91d68d6f --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACSim/ACSimDialect.cpp @@ -0,0 +1,3 @@ +#include "acir/Dialect/ACSim/ACSimDialect.h" + +#include "acir/Dialect/ACSim/ACSimDialect.cpp.inc" diff --git a/components/agentic-circuit/lib/Dialect/ACSim/ACSimOps.cpp b/components/agentic-circuit/lib/Dialect/ACSim/ACSimOps.cpp new file mode 100644 index 00000000..00eebbeb --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACSim/ACSimOps.cpp @@ -0,0 +1,3119 @@ +#include "acir/Dialect/ACSim/ACSimOps.h" +#include "ACSimOpsTestHooks.h" + +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinDialect.h" +#include "mlir/IR/OpImplementation.h" +#include "mlir/IR/Verifier.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallPtrSet.h" +#include "llvm/ADT/SmallSet.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; + +namespace acir::acsim { +namespace { +thread_local detail::ModelVerificationWork *modelVerificationWorkCollector = + nullptr; +thread_local const detail::ModelVerificationLimits *modelVerificationLimits = + nullptr; + +const detail::ModelVerificationLimits ¤tModelVerificationLimits() { + static constexpr detail::ModelVerificationLimits defaults; + return modelVerificationLimits ? *modelVerificationLimits : defaults; +} +} // namespace + +namespace detail { + +ScopedModelVerificationWorkCollector::ScopedModelVerificationWorkCollector( + ModelVerificationWork &work) + : previous(modelVerificationWorkCollector) { + modelVerificationWorkCollector = &work; +} + +ScopedModelVerificationWorkCollector::~ScopedModelVerificationWorkCollector() { + modelVerificationWorkCollector = previous; +} + +ScopedModelVerificationLimits::ScopedModelVerificationLimits( + const ModelVerificationLimits &limits) + : previous(modelVerificationLimits) { + modelVerificationLimits = &limits; +} + +ScopedModelVerificationLimits::~ScopedModelVerificationLimits() { + modelVerificationLimits = previous; +} + +} // namespace detail + +namespace { + +constexpr StringLiteral kSourceMapAttrName = "acsim.source_map"; + +bool isSha256(StringRef value) { + if (!value.consume_front("sha256:") || value.size() != 64) + return false; + return llvm::all_of(value, [](char c) { + return std::isdigit(static_cast(c)) || + (c >= 'a' && c <= 'f'); + }); +} + +LogicalResult verifyFingerprint(Operation *operation, StringAttr fingerprint, + StringRef label = "fingerprint") { + if (fingerprint && isSha256(fingerprint.getValue())) + return success(); + return operation->emitOpError() << label + << " must be sha256: followed by 64 " + "lowercase hexadecimal digits"; +} + +bool hasRawCppFragment(StringRef value) { + return value.contains(';') || value.contains('{') || value.contains('}') || + value.contains('\n') || value.contains('#'); +} + +bool isCppQualifiedSymbol(StringRef value) { + if (value.empty() || value.starts_with("::") || value.ends_with("::")) + return false; + while (!value.empty()) { + auto [segment, remainder] = value.split("::"); + if (segment.empty() || + !(std::isalpha(static_cast(segment.front())) || + segment.front() == '_') || + !llvm::all_of(segment.drop_front(), [](char character) { + return std::isalnum(static_cast(character)) || + character == '_'; + })) + return false; + value = remainder; + } + return true; +} + +bool isCanonicalIdentifier(StringRef value) { + if (value.empty() || + !(std::isalpha(static_cast(value.front())) || + value.front() == '_')) + return false; + return llvm::all_of(value.drop_front(), [](char character) { + return std::isalnum(static_cast(character)) || + character == '_'; + }); +} + +bool isCanonicalStaticData(Attribute root) { + SmallVector stack{root}; + while (!stack.empty()) { + Attribute attribute = stack.pop_back_val(); + if (isa( + attribute)) + continue; + if (auto string = dyn_cast(attribute)) { + if (hasRawCppFragment(string.getValue()) || + string.getValue().contains('(') || string.getValue().contains(')') || + string.getValue().contains('=')) + return false; + continue; + } + if (auto array = dyn_cast(attribute)) { + stack.append(array.begin(), array.end()); + continue; + } + if (auto dictionary = dyn_cast(attribute)) { + for (NamedAttribute named : dictionary) { + if (!isCanonicalIdentifier(named.getName().getValue())) + return false; + stack.push_back(named.getValue()); + } + continue; + } + return false; + } + return true; +} + +bool hasExactKeys(DictionaryAttr dictionary, ArrayRef keys) { + if (!dictionary || dictionary.size() != keys.size()) + return false; + return llvm::all_of(keys, + [&](StringLiteral key) { return dictionary.get(key); }); +} + +LogicalResult verifyBindingLockShape(BindingOp binding) { + constexpr std::array topKeys = { + "activation_sources", + "availability", + "binding", + "binding_schema", + "component_schema", + "component_schema_fingerprint", + "construction", + "contract_epoch", + "cpp", + "cpp_type", + "effect", + "fingerprint", + "implementation", + "ownership", + "parameters", + "ports", + "provider", + "provider_implementation_fingerprint", + "resources", + "results", + }; + DictionaryAttr record = binding.getRecord(); + if (!hasExactKeys(record, topKeys)) + return binding.emitOpError( + "binding lock must contain exactly the acsim-binding-0.1 fields"); + auto identity = record.getAs("binding"); + auto epoch = record.getAs("contract_epoch"); + auto availability = record.getAs("availability"); + if (!identity || identity.getValue() != binding.getSymName() || + binding.getBindingSchema() != "acsim-binding-0.1" || !epoch || + epoch.getValue() != "0.3" || !availability || + availability.getValue() != "available" || + (binding.getEffect() != "pure" && binding.getEffect() != "stateful") || + !binding.getCppTypeAttr() || !binding.getSchemaAttr() || + !binding.getProviderAttr() || !binding.getImplementationAttr()) + return binding.emitOpError( + "binding lock identity, epoch, availability, and effect are invalid"); + for (StringLiteral field : + {StringLiteral("component_schema_fingerprint"), + StringLiteral("provider_implementation_fingerprint"), + StringLiteral("fingerprint")}) { + auto fingerprint = record.getAs(field); + if (!fingerprint || !isSha256(fingerprint.getValue())) + return binding.emitOpError() << "binding lock field '" << field + << "' requires an exact fingerprint"; + } + + constexpr std::array cppKeys = { + "concept", "entry_points", "header", "symbol", "target"}; + constexpr std::array entryKeys = { + "pure", "reset", "validate", "work", "xfer"}; + DictionaryAttr cpp = binding.getCppRecord(); + auto entries = + cpp ? cpp.getAs("entry_points") : DictionaryAttr(); + if (!hasExactKeys(cpp, cppKeys) || !hasExactKeys(entries, entryKeys)) + return binding.emitOpError( + "binding lock C++ record and entry points must be exact"); + for (StringLiteral field : + {StringLiteral("concept"), StringLiteral("header"), + StringLiteral("symbol"), StringLiteral("target")}) { + auto value = cpp.getAs(field); + if (!value || value.getValue().empty() || + hasRawCppFragment(value.getValue())) + return binding.emitOpError( + "binding metadata cannot contain raw C++ or emitter behavior"); + } + StringRef header = cpp.getAs("header").getValue(); + if (header.starts_with('/') || header.contains('\\') || + header.starts_with("../") || header.contains("/../") || + header.ends_with("/..") || header == "..") + return binding.emitOpError( + "cpp.header must be a repository-relative header path"); + if (!isCppQualifiedSymbol(cpp.getAs("concept").getValue()) || + !isCppQualifiedSymbol(cpp.getAs("symbol").getValue()) || + !isCanonicalIdentifier(cpp.getAs("target").getValue())) + return binding.emitOpError( + "C++ concept, symbol, and target must be declarative qualified names"); + for (StringLiteral field : entryKeys) { + auto value = entries.getAs(field); + if (!value || + (!value.getValue().empty() && !isCppQualifiedSymbol(value.getValue()))) + return binding.emitOpError( + "binding entry points must be empty or qualified C++ symbols"); + } + if ((binding.getEffect() == "pure" && + (entries.getAs("pure").getValue().empty() || + !entries.getAs("reset").getValue().empty() || + !entries.getAs("validate").getValue().empty() || + !entries.getAs("work").getValue().empty() || + !entries.getAs("xfer").getValue().empty())) || + (binding.getEffect() == "stateful" && + (!entries.getAs("pure").getValue().empty() || + entries.getAs("work").getValue().empty() || + entries.getAs("xfer").getValue().empty()))) + return binding.emitOpError( + "binding effect requires its exact executable entry points"); + + constexpr std::array constructionKeys = {"arguments", + "kind"}; + constexpr std::array ownershipKeys = {"kind", "placement"}; + auto construction = record.getAs("construction"); + auto ownership = record.getAs("ownership"); + if (!hasExactKeys(construction, constructionKeys) || + !construction.getAs("arguments") || + !construction.getAs("kind") || + !hasExactKeys(ownership, ownershipKeys) || + !ownership.getAs("kind") || + !ownership.getAs("placement")) + return binding.emitOpError( + "binding construction and ownership records must be exact"); + auto constructionKind = construction.getAs("kind"); + if (constructionKind.getValue() != "constructor") + return binding.emitOpError( + "construction kind must be exactly 'constructor'"); + for (Attribute argument : construction.getAs("arguments")) + if (!isCanonicalStaticData(argument)) + return binding.emitOpError( + "construction arguments must be canonical static data"); + auto ownershipKind = ownership.getAs("kind"); + auto ownershipPlacement = ownership.getAs("placement"); + if (binding.getEffect() == "pure" && + (ownershipKind.getValue() != "none" || + ownershipPlacement.getValue() != "inline")) + return binding.emitOpError( + "pure binding ownership must be exactly none/inline"); + if (binding.getEffect() == "stateful" && + (ownershipKind.getValue() != "unique" || + !llvm::is_contained( + {StringRef("member_or_array"), StringRef("root_or_process")}, + ownershipPlacement.getValue()))) + return binding.emitOpError( + "stateful binding ownership must be exactly unique/member_or_array or " + "unique/root_or_process"); + + constexpr std::array parameterKeys = { + "acir_type", "cpp_type", "mapping", "name", "ordinal", "value"}; + auto parameters = record.getAs("parameters"); + if (!parameters) + return binding.emitOpError("binding parameters must be a static array"); + int64_t expectedOrdinal = 0; + llvm::StringSet<> parameterNames; + for (Attribute attribute : parameters) { + auto parameter = dyn_cast(attribute); + auto name = parameter ? parameter.getAs("name") : StringAttr(); + auto ordinal = + parameter ? parameter.getAs("ordinal") : IntegerAttr(); + auto mapping = + parameter ? parameter.getAs("mapping") : StringAttr(); + if (!hasExactKeys(parameter, parameterKeys) || !name || !ordinal || + ordinal.getInt() != expectedOrdinal++ || !mapping || + !parameter.getAs("acir_type") || + !parameter.getAs("cpp_type") || !parameter.get("value") || + !parameterNames.insert(name.getValue()).second) + return binding.emitOpError( + "binding lock parameter must contain exact static fields"); + if (!isCanonicalIdentifier(name.getValue())) + return binding.emitOpError( + "parameter name must be a canonical identifier"); + if (parameter.getAs("acir_type").getValue().empty() || + parameter.getAs("cpp_type").getValue().empty() || + !isCanonicalStaticData(parameter.get("value"))) + return binding.emitOpError( + "parameter types and value must be non-empty canonical static data"); + if (!llvm::is_contained({StringRef("template_argument"), + StringRef("constexpr_argument"), + StringRef("constructor_constant")}, + mapping.getValue())) + return binding.emitOpError("parameter mapping must be template_argument, " + "constexpr_argument, or constructor_constant"); + } + auto verifyRecordArray = [&](StringRef name, + ArrayRef keys) -> LogicalResult { + auto records = record.getAs(name); + if (!records) + return binding.emitOpError() + << "binding " << name << " must be a static record array"; + for (Attribute attribute : records) + if (!hasExactKeys(dyn_cast(attribute), keys)) + return binding.emitOpError() + << "binding " << name + << " records must have exact closed fields"; + return success(); + }; + constexpr std::array portKeys = { + "accessor", "cardinality", "delegation", "direction", "interface", + "ownership", "payload", "protocol", "role", "time_domain"}; + constexpr std::array resourceKeys = { + "accessor", "delegation", "mode", "ownership", + "resource", "role", "time_domain"}; + constexpr std::array resultKeys = {"cpp_type", "name"}; + constexpr std::array activationKeys = {"kind", "name"}; + if (failed(verifyRecordArray("ports", portKeys)) || + failed(verifyRecordArray("resources", resourceKeys)) || + failed(verifyRecordArray("results", resultKeys)) || + failed(verifyRecordArray("activation_sources", activationKeys))) + return failure(); + if (binding.getEffect() == "pure" && + !record.getAs("activation_sources").empty()) + return binding.emitOpError( + "pure binding cannot contain activation or wakeup metadata"); + llvm::StringSet<> portAccessors; + for (Attribute attribute : record.getAs("ports")) { + auto port = cast(attribute); + auto direction = port.getAs("direction"); + auto accessor = port.getAs("accessor"); + if (!accessor || !portAccessors.insert(accessor.getValue()).second || + !port.getAs("interface") || + !port.getAs("role") || + !port.getAs("payload") || + !port.getAs("protocol") || !direction || + !llvm::is_contained({StringRef("input"), StringRef("output")}, + direction.getValue()) || + !port.get("cardinality") || !port.getAs("delegation") || + !port.getAs("ownership") || !port.get("time_domain")) + return binding.emitOpError( + "binding port records require exact typed endpoint metadata"); + if (!port.getAs("time_domain")) + return binding.emitOpError("time_domain must be a flat symbol reference"); + auto cardinality = port.getAs("cardinality"); + if (!cardinality || + !llvm::is_contained({StringRef("exclusive"), StringRef("shared")}, + cardinality.getValue())) + return binding.emitOpError( + "cardinality must be exactly 'exclusive' or 'shared'"); + auto delegation = port.getAs("delegation"); + if (!llvm::is_contained({StringRef("forbidden"), StringRef("allowed"), + StringRef("required")}, + delegation.getValue())) + return binding.emitOpError( + "delegation must be forbidden, allowed, or required"); + auto endpointOwnership = port.getAs("ownership"); + if (!llvm::is_contained( + {StringRef("owned"), StringRef("borrowed"), StringRef("shared")}, + endpointOwnership.getValue())) + return binding.emitOpError( + "endpoint ownership must be owned, borrowed, or shared"); + } + llvm::StringSet<> resourceAccessors; + for (Attribute attribute : record.getAs("resources")) { + auto resource = cast(attribute); + auto mode = resource.getAs("mode"); + auto accessor = resource.getAs("accessor"); + if (!accessor || !resourceAccessors.insert(accessor.getValue()).second || + !resource.getAs("resource") || + !resource.getAs("role") || !mode || + !llvm::is_contained({StringRef("initiator"), StringRef("target")}, + mode.getValue()) || + !resource.getAs("delegation") || + !resource.getAs("ownership") || + !resource.get("time_domain")) + return binding.emitOpError( + "binding resource records require exact typed endpoint metadata"); + if (!resource.getAs("time_domain")) + return binding.emitOpError("time_domain must be a flat symbol reference"); + auto delegation = resource.getAs("delegation"); + if (!llvm::is_contained({StringRef("forbidden"), StringRef("allowed"), + StringRef("required")}, + delegation.getValue())) + return binding.emitOpError( + "delegation must be forbidden, allowed, or required"); + auto endpointOwnership = resource.getAs("ownership"); + if (!llvm::is_contained( + {StringRef("owned"), StringRef("borrowed"), StringRef("shared")}, + endpointOwnership.getValue())) + return binding.emitOpError( + "endpoint ownership must be owned, borrowed, or shared"); + } + llvm::StringSet<> resultNames; + for (Attribute attribute : record.getAs("results")) { + auto result = cast(attribute); + auto name = result.getAs("name"); + if (!result.getAs("cpp_type") || !name || + !isCanonicalIdentifier(name.getValue()) || + !resultNames.insert(name.getValue()).second) + return binding.emitOpError( + "result name must be a canonical identifier and result metadata " + "must be exact"); + } + llvm::StringSet<> activationNames; + for (Attribute attribute : record.getAs("activation_sources")) { + auto source = cast(attribute); + auto name = source.getAs("name"); + if (!source.getAs("kind") || !name || + !isCanonicalIdentifier(name.getValue()) || + !activationNames.insert(name.getValue()).second) + return binding.emitOpError( + "activation-source name must be a canonical identifier and source " + "metadata must be exact"); + } + return success(); +} + +LogicalResult verifyModuleInterfaceShape(ModuleOp module) { + constexpr std::array interfaceKeys = {"ports", "resources", + "results"}; + constexpr std::array portKeys = { + "accessor", "cardinality", "delegation", "direction", + "interface", "name", "ownership", "payload", + "protocol", "role", "time_domain"}; + constexpr std::array resourceKeys = { + "accessor", "delegation", "mode", "name", + "ownership", "resource", "role", "time_domain"}; + constexpr std::array resultKeys = {"cpp_type", "name"}; + DictionaryAttr interface = module.getInterface(); + if (!hasExactKeys(interface, interfaceKeys)) + return module.emitOpError( + "interface must contain exactly ports, resources, and results"); + auto ports = interface.getAs("ports"); + auto resources = interface.getAs("resources"); + auto results = interface.getAs("results"); + if (!ports || !resources || !results) + return module.emitOpError("interface fields must be ordered record arrays"); + + llvm::StringSet<> accessors; + auto verifyNames = [&](ArrayAttr records, ArrayRef keys, + StringRef kind) -> LogicalResult { + StringRef previous; + for (Attribute attribute : records) { + auto record = dyn_cast(attribute); + auto name = record ? record.getAs("name") : StringAttr(); + if (!hasExactKeys(record, keys) || !name || + !isCanonicalIdentifier(name.getValue())) + return module.emitOpError() + << kind << " interface records require exact closed fields and " + << "canonical names"; + if (!previous.empty() && name.getValue() <= previous) + return module.emitOpError() + << kind << " interface records must be strictly name-sorted"; + previous = name.getValue(); + } + return success(); + }; + if (failed(verifyNames(ports, portKeys, "port")) || + failed(verifyNames(resources, resourceKeys, "resource")) || + failed(verifyNames(results, resultKeys, "result"))) + return failure(); + + for (Attribute attribute : ports) { + auto record = cast(attribute); + auto accessor = record.getAs("accessor"); + auto cardinality = record.getAs("cardinality"); + auto delegation = record.getAs("delegation"); + auto direction = record.getAs("direction"); + auto ownership = record.getAs("ownership"); + if (!accessor || !accessors.insert(accessor.getValue()).second || + !record.getAs("interface") || + !record.getAs("payload") || + !record.getAs("protocol") || + !record.getAs("role") || + !record.getAs("time_domain") || !cardinality || + !delegation || !direction || !ownership || + !llvm::is_contained({StringRef("exclusive"), StringRef("shared")}, + cardinality.getValue()) || + !llvm::is_contained({StringRef("forbidden"), StringRef("allowed"), + StringRef("required")}, + delegation.getValue()) || + !llvm::is_contained({StringRef("input"), StringRef("output")}, + direction.getValue()) || + !llvm::is_contained( + {StringRef("owned"), StringRef("borrowed"), StringRef("shared")}, + ownership.getValue())) + return module.emitOpError( + "port interface records require exact typed endpoint metadata and " + "globally unique accessors"); + } + for (Attribute attribute : resources) { + auto record = cast(attribute); + auto accessor = record.getAs("accessor"); + auto delegation = record.getAs("delegation"); + auto mode = record.getAs("mode"); + auto ownership = record.getAs("ownership"); + if (!accessor || !accessors.insert(accessor.getValue()).second || + !record.getAs("resource") || + !record.getAs("role") || + !record.getAs("time_domain") || !delegation || + !mode || !ownership || + !llvm::is_contained({StringRef("forbidden"), StringRef("allowed"), + StringRef("required")}, + delegation.getValue()) || + !llvm::is_contained({StringRef("initiator"), StringRef("target")}, + mode.getValue()) || + !llvm::is_contained( + {StringRef("owned"), StringRef("borrowed"), StringRef("shared")}, + ownership.getValue())) + return module.emitOpError( + "resource interface records require exact typed endpoint metadata " + "and globally unique accessors"); + } + for (Attribute attribute : results) + if (!cast(attribute).getAs("cpp_type")) + return module.emitOpError( + "result interface records require an exact C++ type realization"); + return success(); +} + +std::string symbolKey(SymbolRefAttr reference) { + std::string result = reference.getRootReference().getValue().str(); + for (FlatSymbolRefAttr nested : reference.getNestedReferences()) { + result.append("::"); + result.append(nested.getValue()); + } + return result; +} + +StringAttr symbolName(Operation *operation) { + return operation->getAttrOfType(SymbolTable::getSymbolAttrName()); +} + +ModuleOp enclosingConstructionModule(Operation *operation) { + return operation->getParentOfType(); +} + +std::string definitionKey(Operation *operation) { + StringAttr name = symbolName(operation); + if (!name) + return {}; + if (isa(operation)) + return name.getValue().str(); + if (ModuleOp module = enclosingConstructionModule(operation)) { + std::string key = module.getSymName().str(); + key.append("::"); + key.append(name.getValue()); + return key; + } + return name.getValue().str(); +} + +uint64_t arrayVolume(ArrayRef shape) { + uint64_t volume = 1; + for (int64_t extent : shape) { + if (extent == 0) + return 0; + volume *= static_cast(extent); + } + return volume; +} + +uint64_t arrayVolume(DenseI64ArrayAttr shape) { + return arrayVolume(shape.asArrayRef()); +} + +SmallVector lexicographicIndices(ArrayRef shape, + uint64_t ordinal) { + SmallVector indices(shape.size(), 0); + for (size_t index = shape.size(); index > 0; --index) { + uint64_t extent = static_cast(shape[index - 1]); + if (!extent) + return indices; + indices[index - 1] = static_cast(ordinal % extent); + ordinal /= extent; + } + return indices; +} + +bool lexicographicallyLess(ArrayRef left, ArrayRef right) { + return std::lexicographical_compare(left.begin(), left.end(), right.begin(), + right.end()); +} + +struct ModelIndex { + SmallVector ordered; + llvm::StringMap definitions; + llvm::DenseMap positions; +}; + +LogicalResult verifySourceMap(Operation *operation, Attribute attribute) { + auto records = dyn_cast(attribute); + constexpr std::array keys = {"column", "end_column", + "end_line", "file", "line"}; + if (!records) + return operation->emitOpError( + "acsim.source_map must be an array of exact source records"); + for (Attribute item : records) { + auto record = dyn_cast(item); + auto file = record ? record.getAs("file") : StringAttr(); + auto line = record ? record.getAs("line") : IntegerAttr(); + auto column = record ? record.getAs("column") : IntegerAttr(); + auto endLine = + record ? record.getAs("end_line") : IntegerAttr(); + auto endColumn = + record ? record.getAs("end_column") : IntegerAttr(); + if (!hasExactKeys(record, keys) || !file || file.getValue().empty() || + !line || !column || !endLine || !endColumn || line.getInt() <= 0 || + column.getInt() <= 0 || endLine.getInt() < line.getInt() || + endColumn.getInt() <= 0 || + (endLine.getInt() == line.getInt() && + endColumn.getInt() < column.getInt())) + return operation->emitOpError( + "acsim.source_map records require exact ordered positive ranges"); + } + return success(); +} + +LogicalResult preflightModel(ModelOp model) { + struct Frame { + Operation *operation; + uint64_t depth; + }; + SmallVector stack{{model.getOperation(), 0}}; + uint64_t nodes = 0; + uint64_t edges = 0; + uint64_t totalArrayVolume = 0; + uint64_t attributeElements = 0; + uint64_t attributeStringBytes = 0; + const detail::ModelVerificationLimits &limits = + currentModelVerificationLimits(); + + while (!stack.empty()) { + Frame frame = stack.pop_back_val(); + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->preflightOperationVisits; + if (++nodes > limits.maxNodes) + return model.emitOpError() << "model node count exceeds ACSim " + "capability " + << limits.maxNodes; + if (frame.depth > limits.maxRegionDepth) + return frame.operation->emitOpError() + << "region nesting exceeds ACSim capability " + << limits.maxRegionDepth; + SmallVector attributeStack; + if (frame.operation->getAttrs().size() > + limits.maxAttributeElements - attributeElements) + return frame.operation->emitOpError( + "attribute element count exceeds ACSim capability"); + for (NamedAttribute named : frame.operation->getAttrs()) { + if (named.getName().size() > + limits.maxAttributeStringBytes - attributeStringBytes) + return frame.operation->emitOpError( + "attribute string bytes exceed ACSim capability"); + attributeStringBytes += named.getName().size(); + attributeStack.push_back(named.getValue()); + } + while (!attributeStack.empty()) { + Attribute attribute = attributeStack.pop_back_val(); + if (++attributeElements > limits.maxAttributeElements) + return frame.operation->emitOpError( + "attribute element count exceeds ACSim capability"); + auto addString = [&](StringRef value) -> LogicalResult { + if (value.size() > + limits.maxAttributeStringBytes - attributeStringBytes) + return frame.operation->emitOpError( + "attribute string bytes exceed ACSim capability"); + attributeStringBytes += value.size(); + return success(); + }; + if (auto string = dyn_cast(attribute)) { + if (failed(addString(string.getValue()))) + return failure(); + } else if (auto symbol = dyn_cast(attribute)) { + if (failed(addString(symbolKey(symbol)))) + return failure(); + } else if (auto array = dyn_cast(attribute)) { + if (attributeStack.size() > + limits.maxAttributeElements - attributeElements || + array.size() > limits.maxAttributeElements - attributeElements - + attributeStack.size()) + return frame.operation->emitOpError( + "attribute element count exceeds ACSim capability"); + attributeStack.append(array.begin(), array.end()); + } else if (auto dictionary = dyn_cast(attribute)) { + for (NamedAttribute named : dictionary) { + if (failed(addString(named.getName().getValue()))) + return failure(); + if (attributeStack.size() >= + limits.maxAttributeElements - attributeElements) + return frame.operation->emitOpError( + "attribute element count exceeds ACSim capability"); + attributeStack.push_back(named.getValue()); + } + } else if (auto dense = dyn_cast(attribute)) { + uint64_t count = dense.size(); + if (count > limits.maxAttributeElements - attributeElements) + return frame.operation->emitOpError( + "attribute element count exceeds ACSim capability"); + attributeElements += count; + } else if (auto dense = dyn_cast(attribute)) { + uint64_t count = dense.getNumElements(); + if (count > limits.maxAttributeElements - attributeElements) + return frame.operation->emitOpError( + "attribute element count exceeds ACSim capability"); + attributeElements += count; + } + } + uint64_t operandEdges = frame.operation->getNumOperands(); + if (modelVerificationWorkCollector) + modelVerificationWorkCollector->edgeVisits += operandEdges; + if (operandEdges > limits.maxEdges || + edges > limits.maxEdges - operandEdges) + return frame.operation->emitOpError() + << "model edge count exceeds ACSim capability " << limits.maxEdges; + edges += operandEdges; + if (auto array = dyn_cast(frame.operation)) { + auto type = dyn_cast(array.getResult().getType()); + if (type) { + uint64_t volume = 1; + for (int64_t extent : type.getShape().asArrayRef()) { + if (extent < 0 || + (extent != 0 && volume > limits.maxExpandedObjects / + static_cast(extent))) + return array.emitOpError() + << "expanded array volume exceeds ACSim capability " + << limits.maxExpandedObjects; + volume *= static_cast(extent); + } + if (volume > limits.maxExpandedObjects || + totalArrayVolume > limits.maxExpandedObjects - volume) + return array.emitOpError() + << "expanded array volume exceeds ACSim capability " + << limits.maxExpandedObjects; + totalArrayVolume += volume; + } + } + for (Region ®ion : frame.operation->getRegions()) { + for (Block &block : region) { + if (Operation *terminator = block.getTerminator()) { + uint64_t successors = terminator->getNumSuccessors(); + if (modelVerificationWorkCollector) + modelVerificationWorkCollector->edgeVisits += successors; + if (successors > limits.maxEdges || + edges > limits.maxEdges - successors) + return terminator->emitOpError() + << "model edge count exceeds ACSim capability " + << limits.maxEdges; + edges += successors; + } + for (Operation &child : llvm::reverse(block)) { + if (stack.size() >= limits.maxNodes - nodes) + return frame.operation->emitOpError() + << "model node count exceeds ACSim capability " + << limits.maxNodes; + stack.push_back({&child, frame.depth + 1}); + } + } + } + } + return success(); +} + +SmallVector collectPreorder(ModelOp model) { + SmallVector result; + SmallVector stack{model.getOperation()}; + while (!stack.empty()) { + Operation *operation = stack.pop_back_val(); + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->preorderOperationVisits; + result.push_back(operation); + for (Region ®ion : llvm::reverse(operation->getRegions())) + for (Block &block : llvm::reverse(region)) + for (Operation &child : llvm::reverse(block)) + stack.push_back(&child); + } + return result; +} + +bool isProcessOperation(Operation *operation) { + return isa(operation); +} + +bool isModuleOperation(Operation *operation) { + return isa(operation); +} + +bool isModelOperation(Operation *operation) { + return isa(operation); +} + +LogicalResult verifyClosedLegality(ModelOp model, + ArrayRef ordered) { + for (Operation *operation : ordered) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->closureOperationVisits; + for (NamedAttribute attribute : operation->getDiscardableAttrs()) { + StringRef name = attribute.getName().getValue(); + if (name != kSourceMapAttrName) + return operation->emitOpError() << "unknown public attribute '" << name + << "' is not legal in canonical ACSim"; + if (failed(verifySourceMap(operation, attribute.getValue()))) + return failure(); + } + + if (operation == model.getOperation()) + continue; + + if (ProcessOp process = operation->getParentOfType()) { + (void)process; + if (isProcessOperation(operation)) + continue; + if (isa(operation)) + continue; + if (isa(operation)) + return operation->emitOpError( + "conversion placeholders are not legal in canonical ACSim"); + StringRef dialect = operation->getName().getDialectNamespace(); + if ((dialect == "builtin" || dialect == "arith" || dialect == "index" || + dialect == "cf") && + isMemoryEffectFree(operation) && operation->getNumRegions() == 0) + continue; + return operation->emitOpError() + << "operation '" << operation->getName() + << "' is not legal in an acsim.process body"; + } + + Operation *parent = operation->getParentOp(); + if (parent == model.getOperation() && isModelOperation(operation)) + continue; + if (isa(parent) && isModuleOperation(operation)) + continue; + return operation->emitOpError() << "operation '" << operation->getName() + << "' is not legal in canonical ACSim"; + } + return success(); +} + +LogicalResult buildIndex(ModelOp model, ModelIndex &index) { + index.ordered = collectPreorder(model); + uint64_t position = 0; + for (Operation *operation : index.ordered) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->indexOperationVisits; + index.positions[operation] = position++; + } + + auto addDefinition = [&](Operation *operation) -> LogicalResult { + std::string key = definitionKey(operation); + if (key.empty()) + return success(); + auto [iterator, inserted] = index.definitions.try_emplace(key, operation); + if (!inserted) + return operation->emitOpError() + << "duplicate canonical symbol or placement '" << key << "'"; + return success(); + }; + + for (Operation *operation : index.ordered) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->indexOperationVisits; + if (isa( + operation) && + failed(addDefinition(operation))) + return failure(); + } + return success(); +} + +Operation *resolveReference(const ModelIndex &index, Operation *from, + SymbolRefAttr reference) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->referenceLookups; + std::string key = symbolKey(reference); + if (!reference.getNestedReferences().empty()) + return index.definitions.lookup(key); + if (ModuleOp module = enclosingConstructionModule(from)) { + std::string local = module.getSymName().str(); + local.append("::"); + local.append(key); + if (Operation *definition = index.definitions.lookup(local)) + return definition; + } + return index.definitions.lookup(key); +} + +template +FailureOr +requireReference(const ModelIndex &index, Operation *from, + SymbolRefAttr reference, StringRef label, + bool requireEarlier = true) { + Operation *definition = resolveReference(index, from, reference); + if (!definition) + return from->emitOpError() + << label << " reference '" << reference << "' is unresolved"; + if (!isa(definition)) + return from->emitOpError() << label << " reference '" << reference + << "' resolves to incompatible operation '" + << definition->getName() << "'"; + if (requireEarlier && definition != from && + index.positions.lookup(definition) >= index.positions.lookup(from)) + return from->emitOpError() << label << " reference '" << reference + << "' appears before its construction"; + return definition; +} + +FailureOr requireCallCallee(const ModelIndex &index, + Operation *call, + FlatSymbolRefAttr callee) { + Operation *definition = resolveReference(index, call, callee); + if (!definition) + return call->emitOpError() + << "callee reference '" << callee << "' is unresolved"; + if (!isa(definition)) + return call->emitOpError() << "callee reference '" << callee + << "' resolves to incompatible operation '" + << definition->getName() << "'"; + if (definition != call && + index.positions.lookup(definition) >= index.positions.lookup(call)) + return call->emitOpError() << "callee reference '" << callee + << "' appears before its construction"; + return definition; +} + +LogicalResult requireTypeKind(const ModelIndex &index, Operation *from, + SymbolRefAttr reference, + ArrayRef allowedKinds, + StringRef label) { + FailureOr definition = + requireReference(index, from, reference, label); + if (failed(definition)) + return failure(); + StringRef kind = cast(*definition).getKind(); + if (llvm::is_contained(allowedKinds, kind)) + return success(); + return from->emitOpError() + << label << " reference '" << reference + << "' has incompatible acsim.type kind '" << kind << "'"; +} + +LogicalResult verifyCanonicalType(Type type, Operation *from, + const ModelIndex &index) { + return llvm::TypeSwitch(type) + .Case([&](auto valueType) { + const std::array kinds = {"value", "packet"}; + return requireTypeKind(index, from, valueType.getSymbol(), kinds, + "C++ type"); + }) + .Case([&](auto ownerType) -> LogicalResult { + FailureOr definition = + requireReference( + index, from, ownerType.getRealization(), "realization"); + if (failed(definition)) + return failure(); + if (auto binding = dyn_cast(*definition); + binding && binding.getEffect() != "stateful") + return from->emitOpError() << "owner/ref type requires a generated " + "module or stateful binding"; + return success(); + }) + .Case([&](PortType port) { + const std::array interfaceKinds = {"interface"}; + const std::array roleKinds = {"role"}; + const std::array payloadKinds = {"value", "packet"}; + const std::array protocolKinds = {"protocol"}; + if (failed(requireTypeKind(index, from, port.getInterface(), + interfaceKinds, "interface")) || + failed(requireTypeKind(index, from, port.getRole(), roleKinds, + "role")) || + failed(requireTypeKind(index, from, port.getPayload(), payloadKinds, + "payload")) || + failed(requireTypeKind(index, from, port.getProtocol(), + protocolKinds, "protocol"))) + return failure(); + return success(); + }) + .Case([&](ResourceType resource) { + const std::array resourceKinds = {"resource"}; + const std::array roleKinds = {"role"}; + return success( + succeeded(requireTypeKind(index, from, resource.getResource(), + resourceKinds, "resource")) && + succeeded(requireTypeKind(index, from, resource.getRole(), + roleKinds, "role"))); + }) + .Case([&](ArrayType array) { + return verifyCanonicalType(array.getElementType(), from, index); + }) + .Case([&](PcType pc) { + return success(succeeded(requireReference( + index, from, pc.getSymbol(), "process"))); + }) + .Case([&](WakeType wake) { + const std::array kinds = {"wake"}; + return requireTypeKind(index, from, wake.getSymbol(), kinds, + "wake kind"); + }) + .Case([](auto) { return success(); }) + .Default([&](Type other) -> LogicalResult { + if ((isa(from) || from->getParentOfType() || + isa(from)) && + isa(other)) + return success(); + return from->emitOpError() + << "type '" << other << "' is not legal in canonical ACSim"; + }); +} + +LogicalResult verifyModelFingerprints(ModelOp model) { + constexpr std::array expected = { + "binding_lock", "frozen_acir", "profile", + "provider", "schema_set", "toolchain"}; + DictionaryAttr fingerprints = model.getFingerprints(); + if (!fingerprints || fingerprints.size() != expected.size()) + return model.emitOpError( + "fingerprints must contain exactly frozen_acir, binding_lock, " + "provider, profile, toolchain, and schema_set"); + for (StringLiteral name : expected) { + auto value = fingerprints.getAs(name); + if (!value || !isSha256(value.getValue())) + return model.emitOpError() + << "fingerprint field '" << name + << "' must be sha256: followed by 64 lowercase hexadecimal " + "digits"; + } + return success(); +} + +unsigned modelRank(Operation *operation) { + return llvm::TypeSwitch(operation) + .Case([](auto) { return 0; }) + .Case([](auto) { return 1; }) + .Case([](auto) { return 2; }) + .Case([](auto) { return 3; }) + .Case([](auto) { return 4; }) + .Default([](auto) { return 5; }); +} + +unsigned moduleRank(Operation *operation) { + if (auto bind = dyn_cast(operation)) { + if (bind.getKind() == "pure_view") + return 5; + if (bind.getKind() == "export") + return 7; + return 3; + } + return llvm::TypeSwitch(operation) + .Case([](auto) { return 0; }) + .Case([](auto) { return 1; }) + .Case([](auto) { return 2; }) + .Case([](auto) { return 4; }) + .Case([](auto) { return 6; }) + .Case([](auto) { return 8; }) + .Case([](auto) { return 100; }) + .Default([](auto) { return 99; }); +} + +LogicalResult verifyDeterministicOrder(ModelOp model) { + unsigned previousRank = 0; + std::string previousName; + bool first = true; + for (Operation &operation : model.getBody().front()) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->orderingOperationVisits; + unsigned rank = modelRank(&operation); + if (!first && rank < previousRank) + return operation.emitOpError( + "model declarations are not in deterministic canonical order"); + StringAttr name = symbolName(&operation); + if (!first && rank == previousRank && rank != 2 && name && + name.getValue() <= previousName) + return operation.emitOpError( + "same-kind model declarations must be strictly symbol-sorted"); + previousRank = rank; + previousName = name ? name.getValue().str() : std::string(); + first = false; + } + + SmallVector moduleOrder; + llvm::StringMap moduleIndex; + for (ModuleOp module : model.getOps()) { + moduleIndex[module.getSymName()] = moduleOrder.size(); + moduleOrder.push_back(module); + } + SmallVector dependencyCount(moduleOrder.size()); + SmallVector> parentsByChild(moduleOrder.size()); + for (auto [ownerIndex, module] : llvm::enumerate(moduleOrder)) { + llvm::SmallSet dependencies; + for (Operation &operation : module.getBody().front()) { + SymbolRefAttr target; + if (auto instance = dyn_cast(operation)) + target = instance.getTargetAttr(); + else if (auto array = dyn_cast(operation)) + target = array.getTargetAttr(); + if (!target) + continue; + auto child = moduleIndex.find(target.getRootReference().getValue()); + if (child != moduleIndex.end()) + dependencies.insert(child->second); + } + dependencyCount[ownerIndex] = dependencies.size(); + for (unsigned childIndex : dependencies) + parentsByChild[childIndex].push_back(ownerIndex); + } + std::set> readyModules; + for (auto [index, module] : llvm::enumerate(moduleOrder)) + if (dependencyCount[index] == 0) + readyModules.emplace(module.getSymName().str(), index); + SmallVector expectedModuleOrder; + while (!readyModules.empty()) { + unsigned childIndex = readyModules.begin()->second; + readyModules.erase(readyModules.begin()); + expectedModuleOrder.push_back(childIndex); + for (unsigned parentIndex : parentsByChild[childIndex]) + if (--dependencyCount[parentIndex] == 0) + readyModules.emplace(moduleOrder[parentIndex].getSymName().str(), + parentIndex); + } + if (expectedModuleOrder.size() != moduleOrder.size()) + return model.emitOpError( + "module instantiation cycle has no canonical declaration order"); + for (auto [actualIndex, expectedIndex] : llvm::enumerate(expectedModuleOrder)) + if (actualIndex != expectedIndex) + return moduleOrder[actualIndex].emitOpError( + "module declarations must use child-before-parent topological " + "order with symbol-sorted ties"); + + for (Operation &operation : model.getBody().front()) { + auto module = dyn_cast(operation); + if (!module) + continue; + unsigned prior = 0; + bool moduleFirst = true; + std::string priorPlacement; + std::string priorProcess; + for (Operation &child : module.getBody().front()) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->orderingOperationVisits; + unsigned rank = moduleRank(&child); + if (!moduleFirst && rank < prior) + return child.emitOpError( + "module construction is not in deterministic canonical order"); + if (rank == 0) { + StringAttr name = symbolName(&child); + if (!moduleFirst && prior == rank && name && + name.getValue() <= priorPlacement) + return child.emitOpError( + "owned placements must be strictly symbol-sorted"); + priorPlacement = name ? name.getValue().str() : std::string(); + } else if (rank == 8) { + StringAttr name = symbolName(&child); + if (!moduleFirst && prior == rank && name && + name.getValue() <= priorProcess) + return child.emitOpError( + "process declarations must be strictly symbol-sorted"); + priorProcess = name ? name.getValue().str() : std::string(); + } + prior = rank; + moduleFirst = false; + } + } + return success(); +} + +struct ExpandedRuntimeRow { + Operation *placement = nullptr; + Operation *realization = nullptr; + unsigned context = 0; + std::string target; + std::string path; + SmallVector indices; + int64_t objectId = -1; + int64_t activationId = -1; +}; + +struct ExpandedOwnerRow { + Operation *placement = nullptr; + unsigned context = 0; + std::string path; + SmallVector indices; +}; + +struct ExpansionContext { + ModuleOp module; + std::string path; + llvm::DenseMap> objectIds; + llvm::DenseMap> childContexts; + + ExpansionContext(ModuleOp module, std::string path) + : module(module), path(std::move(path)) {} +}; + +struct HierarchyExpansion { + SmallVector ownerRows; + SmallVector runtimeRows; + SmallVector contexts; + llvm::StringMap ownerByPath; +}; + +std::string expandedPath(StringRef parent, StringRef name, + ArrayRef indices = {}) { + std::string path = parent.str(); + if (!path.empty()) + path.push_back('.'); + path.append(name); + llvm::raw_string_ostream os(path); + for (int64_t index : indices) + os << '[' << index << ']'; + return path; +} + +std::string moduleSpecializationKey(ModuleOp module) { + std::string key = module.getSymName().str(); + key.push_back(':'); + key.append(module.getSpecializationFingerprint()); + key.push_back(':'); + llvm::raw_string_ostream(key) << module.getStaticParams(); + return key; +} + +LogicalResult expandSelectedRootOwners(ModelOp model, const ModelIndex &index, + HierarchyExpansion &expansion) { + FailureOr root = requireReference( + index, model, model.getRootAttr(), "root", false); + if (failed(root)) + return failure(); + + enum class ActionKind { Enter, Exit, Row }; + struct Action { + ActionKind kind; + ModuleOp module; + Operation *placement = nullptr; + unsigned context = 0; + std::string path; + SmallVector indices; + std::string specializationKey; + }; + SmallVector stack; + ModuleOp rootModule = cast(*root); + std::string rootPath = rootModule.getSymName().str(); + if (!model.getConstructionOrder().empty()) { + auto firstPath = + dyn_cast(model.getConstructionOrder().getValue().front()); + if (!firstPath) + return model.emitOpError( + "construction order paths must be canonical strings"); + rootPath = firstPath.getValue().split('.').first.str(); + if (rootPath.empty()) + return model.emitOpError("construction order has an empty root path"); + } + stack.push_back({ActionKind::Enter, + rootModule, + nullptr, + 0, + rootPath, + {}, + moduleSpecializationKey(rootModule)}); + llvm::StringSet<> activeSpecializations; + const uint64_t expansionLimit = + currentModelVerificationLimits().maxExpandedObjects; + + while (!stack.empty()) { + Action action = std::move(stack.back()); + stack.pop_back(); + if (action.kind == ActionKind::Exit) { + activeSpecializations.erase(action.specializationKey); + continue; + } + if (action.kind == ActionKind::Row) { + if (expansion.ownerRows.size() >= expansionLimit) + return action.placement->emitOpError() + << "expanded hierarchy exceeds ACSim capability " + << expansionLimit; + if (expansion.ownerByPath.contains(action.path)) + return action.placement->emitOpError() + << "derived hierarchy path is not unique: '" << action.path + << "'"; + SymbolRefAttr target; + if (auto instance = dyn_cast(action.placement)) { + target = instance.getTargetAttr(); + } else if (auto array = dyn_cast(action.placement)) { + target = array.getTargetAttr(); + } + Operation *targetDefinition = + target ? index.definitions.lookup(symbolKey(target)) : nullptr; + if (auto binding = dyn_cast_or_null(targetDefinition); + binding && binding.getEffect() != "stateful") + return action.placement->emitOpError( + "ownership expansion requires a generated module or stateful " + "binding target"); + unsigned ownerOrdinal = expansion.ownerRows.size(); + expansion.ownerByPath.try_emplace(action.path, ownerOrdinal); + expansion.ownerRows.push_back( + {action.placement, action.context, action.path, action.indices}); + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->expandedOwnerRows; + + if (auto childModule = dyn_cast_or_null(targetDefinition)) + stack.push_back({ActionKind::Enter, childModule, action.placement, + action.context, action.path, action.indices, + moduleSpecializationKey(childModule)}); + continue; + } + + if (!activeSpecializations.insert(action.specializationKey).second) + return action.module.emitOpError( + "active module specialization cycle in selected hierarchy"); + unsigned context = expansion.contexts.size(); + expansion.contexts.emplace_back(action.module, action.path); + if (action.placement) + expansion.contexts[action.context] + .childContexts[action.placement] + .push_back(context); + stack.push_back({ActionKind::Exit, + action.module, + nullptr, + context, + {}, + {}, + action.specializationKey}); + SmallVector placements; + for (Operation &operation : action.module.getBody().front()) + if (isa(operation)) + placements.push_back(&operation); + for (Operation *placement : llvm::reverse(placements)) { + if (auto array = dyn_cast(placement)) { + uint64_t volume = arrayVolume(array.getShape()); + for (uint64_t ordinal = volume; ordinal > 0; --ordinal) { + SmallVector indices = + lexicographicIndices(array.getShape(), ordinal - 1); + stack.push_back( + {ActionKind::Row, + {}, + placement, + context, + expandedPath(action.path, array.getSymName(), indices), + std::move(indices), + {}}); + } + } else { + StringRef name = cast(symbolName(placement)).getValue(); + stack.push_back({ActionKind::Row, + {}, + placement, + context, + expandedPath(action.path, name), + {}, + {}}); + } + } + } + return success(); +} + +LogicalResult expandRuntime(const ModelIndex &index, + HierarchyExpansion &expansion) { + const uint64_t expansionLimit = + currentModelVerificationLimits().maxExpandedObjects; + for (const ExpandedOwnerRow &owner : expansion.ownerRows) { + Operation *realization = nullptr; + if (isa(owner.placement)) { + realization = owner.placement; + } else { + SymbolRefAttr target = + isa(owner.placement) + ? cast(owner.placement).getTargetAttr() + : cast(owner.placement).getTargetAttr(); + Operation *definition = index.definitions.lookup(symbolKey(target)); + if (isa_and_nonnull(definition)) + realization = definition; + } + if (!realization) + continue; + if (expansion.runtimeRows.size() >= expansionLimit) + return owner.placement->emitOpError() + << "runtime expansion exceeds ACSim capability " << expansionLimit; + int64_t id = static_cast(expansion.runtimeRows.size()); + expansion.runtimeRows.push_back( + {owner.placement, realization, owner.context, + definitionKey(owner.placement), owner.path, owner.indices, id, id}); + expansion.contexts[owner.context].objectIds[owner.placement].push_back(id); + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->expandedRuntimeRows; + } + return success(); +} + +LogicalResult verifyConstructionOrder(ModelOp model, + const HierarchyExpansion &expansion) { + auto readOrder = [&](ArrayAttr order, StringRef label, + SmallVectorImpl &paths) -> LogicalResult { + llvm::StringSet<> unique; + for (Attribute attribute : order) { + auto path = dyn_cast(attribute); + if (!path || path.getValue().empty()) + return model.emitOpError() + << label << " entries must be concrete hierarchy-path strings"; + if (!unique.insert(path.getValue()).second) + return model.emitOpError() + << label << " contains duplicate '" << path << "'"; + paths.push_back(path.getValue().str()); + } + return success(); + }; + + SmallVector actual; + actual.reserve(expansion.ownerRows.size()); + for (const ExpandedOwnerRow &row : expansion.ownerRows) + actual.push_back(row.path); + SmallVector construction; + SmallVector destruction; + if (failed(readOrder(model.getConstructionOrder(), "construction order", + construction)) || + failed(readOrder(model.getDestructionOrder(), "destruction order", + destruction))) + return failure(); + if (construction != actual) + return model.emitOpError( + "construction order must equal canonical ownership preorder"); + SmallVector reversed(actual.rbegin(), actual.rend()); + if (destruction != reversed) + return model.emitOpError( + "destruction order must be the exact reverse of construction order"); + return success(); +} + +LogicalResult verifyProcess(ProcessOp process, const ModelIndex &index) { + if (process.getFairnessCap() <= 0 || + static_cast(process.getFairnessCap()) > kMaxModelNodes) + return process.emitOpError( + "fairness cap must be a positive bounded static integer"); + SmallVector pcs; + llvm::StringSet<> pcSet; + for (Attribute attribute : process.getPcs()) { + auto reference = dyn_cast(attribute); + if (!reference || !pcSet.insert(reference.getValue()).second) + return process.emitOpError( + "pcs must be a non-empty ordered list of unique flat symbols"); + pcs.push_back(reference.getValue().str()); + } + if (pcs.empty() || !pcSet.contains(process.getEntryPc())) + return process.emitOpError( + "entry PC must occur exactly once in the closed PC list"); + if (process.getStates().size() != pcs.size()) + return process.emitOpError( + "process requires exactly one ordered state region per PC"); + if (pcs.front() != process.getEntryPc()) + return process.emitOpError( + "the first ordered state region must be the entry PC"); + + if (process.getCaptureNames().size() != process.getCaptures().size()) + return process.emitOpError( + "process captures must have one exact ordered name per operand"); + llvm::StringSet<> captureNames; + for (Attribute attribute : process.getCaptureNames()) { + auto name = dyn_cast(attribute); + if (!name || name.getValue().empty() || + !captureNames.insert(name.getValue()).second) + return process.emitOpError( + "process capture names must be unique non-empty strings"); + } + + llvm::StringMap slots; + for (Attribute attribute : process.getLiveSlots()) { + auto dictionary = dyn_cast(attribute); + auto name = + dictionary ? dictionary.getAs("name") : StringAttr(); + auto type = dictionary ? dictionary.getAs("type") : TypeAttr(); + if (!dictionary || dictionary.size() != 2 || !name || !type || + !isa(type.getValue()) || + !slots.try_emplace(name.getValue(), type.getValue()).second) + return process.emitOpError( + "live slots require unique exact {name, type} value records"); + if (failed(verifyCanonicalType(type.getValue(), process, index))) + return failure(); + } + + for (Region &state : process.getStates()) { + if (state.empty()) + return process.emitOpError("every PC requires a non-empty state region"); + Block &entry = state.front(); + if (entry.getNumArguments() != process.getCaptures().size()) + return process.emitOpError( + "process state arguments must exactly match declared typed captures"); + for (auto [argument, capture] : + llvm::zip_equal(entry.getArguments(), process.getCaptures())) + if (argument.getType() != capture.getType()) + return process.emitOpError("process state arguments must exactly match " + "declared typed captures"); + llvm::DenseMap blockOrdinals; + unsigned ordinal = 0; + for (Block &block : state) { + blockOrdinals[&block] = ordinal++; + if (block.empty()) + return process.emitOpError("every process block requires operations"); + Operation *terminator = block.getTerminator(); + if (!terminator || + (!isa(terminator) && + terminator->getName().getDialectNamespace() != "cf")) + return block.front().emitOpError( + "every process path must continue, suspend, terminate, or use cf"); + } + for (Block &block : state) { + Operation *terminator = block.getTerminator(); + for (Block *successor : terminator->getSuccessors()) { + if (successor->getParent() != &state) + return terminator->emitOpError("ordinary cf edges cannot cross " + "process PC suspension boundaries"); + if (blockOrdinals.lookup(successor) <= blockOrdinals.lookup(&block)) + return terminator->emitOpError( + "intra-PC control flow must prove bounded acyclic progress"); + } + } + } + + auto verifyTarget = [&](Operation *operation, + FlatSymbolRefAttr target) -> LogicalResult { + if (pcSet.contains(target.getValue())) + return success(); + return operation->emitOpError() + << "target PC '" << target << "' is not in the closed PC list"; + }; + for (Region &state : process.getStates()) { + for (Block &block : state) { + for (Operation &operation : block) { + if (auto load = dyn_cast(operation)) { + if (resolveReference(index, load, load.getProcessAttr()) != process || + !slots.contains(load.getSlot()) || + slots.lookup(load.getSlot()) != load.getResult().getType()) + return load.emitOpError("live load must resolve to an exact typed " + "slot of this process"); + } else if (auto store = dyn_cast(operation)) { + if (resolveReference(index, store, store.getProcessAttr()) != + process || + !slots.contains(store.getSlot()) || + slots.lookup(store.getSlot()) != store.getValue().getType()) + return store.emitOpError("live store must resolve to an exact " + "typed slot of this process"); + } else if (auto next = dyn_cast(operation)) { + if (failed(verifyTarget(next, next.getTargetPcAttr()))) + return failure(); + } else if (auto suspend = dyn_cast(operation)) { + if (!isa(suspend.getWake().getType()) || + failed(verifyTarget(suspend, suspend.getTargetPcAttr()))) + return suspend.emitOpError( + "suspend requires one exact typed wake and a closed next PC"); + } else if (auto terminate = dyn_cast(operation)) { + if (terminate.getStatus() != "success" && + terminate.getStatus() != "failure") + return terminate.emitOpError( + "terminal status must be exactly 'success' or 'failure'"); + } + } + } + } + + llvm::StringMap pcOrdinals; + for (auto [ordinal, pc] : llvm::enumerate(pcs)) + pcOrdinals[pc] = ordinal; + const uint64_t dependencyLimit = + currentModelVerificationLimits().maxDependencyNodes; + if (pcs.size() > dependencyLimit) + return process.emitOpError() + << "dependency graph exceeds ACSim capability " << dependencyLimit; + SmallVector> successors(pcs.size()); + SmallVector indegree(pcs.size(), 0); + uint64_t dependencyNodes = pcs.size(); + for (auto [ordinal, state] : llvm::enumerate(process.getStates())) { + llvm::SmallSet unique; + for (Block &block : state) { + auto next = dyn_cast(block.getTerminator()); + if (!next) + continue; + auto found = pcOrdinals.find(next.getTargetPc()); + if (found == pcOrdinals.end()) + return next.emitOpError("target PC is not in the closed PC list"); + if (unique.insert(found->second).second) { + if (++dependencyNodes > dependencyLimit) + return process.emitOpError() + << "dependency graph exceeds ACSim capability " + << dependencyLimit; + successors[ordinal].push_back(found->second); + ++indegree[found->second]; + } + } + } + std::set ready; + for (auto [ordinal, degree] : llvm::enumerate(indegree)) + if (degree == 0) + ready.insert(ordinal); + SmallVector topological; + while (!ready.empty()) { + unsigned ordinal = *ready.begin(); + ready.erase(ready.begin()); + topological.push_back(ordinal); + for (unsigned successor : successors[ordinal]) + if (--indegree[successor] == 0) + ready.insert(successor); + } + if (topological.size() != pcs.size()) + return process.emitOpError( + "process continue graph must prove bounded acyclic progress"); + + SmallVector memo(pcs.size(), 0); + uint64_t maximumPath = 0; + for (unsigned stateOrdinal : llvm::reverse(topological)) { + Region &state = process.getStates()[stateOrdinal]; + llvm::DenseMap blockCost; + uint64_t stateMaximum = 0; + for (Block &block : llvm::reverse(state)) { + uint64_t suffix = 0; + Operation *terminator = block.getTerminator(); + if (auto next = dyn_cast(terminator)) + suffix = memo[pcOrdinals.lookup(next.getTargetPc())]; + else + for (Block *successor : terminator->getSuccessors()) + suffix = std::max(suffix, blockCost.lookup(successor)); + uint64_t own = block.getOperations().size(); + if (suffix > kMaxModelNodes - own) + return process.emitOpError("process fairness work count overflows"); + blockCost[&block] = own + suffix; + stateMaximum = std::max(stateMaximum, own + suffix); + } + memo[stateOrdinal] = stateMaximum; + maximumPath = std::max(maximumPath, stateMaximum); + } + if (maximumPath > static_cast(process.getFairnessCap())) + return process.emitOpError() + << "fairness cap " << process.getFairnessCap() + << " is below maximum local execution path " << maximumPath; + return success(); +} + +Operation *realizationForBase(Value value, const ModelIndex &index) { + SymbolRefAttr symbol; + if (auto owner = dyn_cast(value.getType())) + symbol = owner.getRealization(); + else if (auto reference = dyn_cast(value.getType())) + symbol = reference.getRealization(); + if (!symbol) + return nullptr; + Operation *definition = index.definitions.lookup(symbolKey(symbol)); + return isa_and_nonnull(definition) ? definition + : nullptr; +} + +ArrayAttr realizationRecords(Operation *realization, StringRef field) { + if (auto binding = dyn_cast_or_null(realization)) + return binding.getRecord().getAs(field); + if (auto module = dyn_cast_or_null(realization)) + return module.getInterface().getAs(field); + return {}; +} + +DictionaryAttr findEndpoint(Operation *realization, StringRef field, + FlatSymbolRefAttr accessor) { + auto records = realizationRecords(realization, field); + if (!records) + return {}; + for (Attribute attribute : records) { + auto record = dyn_cast(attribute); + if (record && record.getAs("accessor") == accessor) + return record; + } + return {}; +} + +DictionaryAttr findProjectedEndpoint(Value value, const ModelIndex &index) { + if (auto port = value.getDefiningOp()) + return findEndpoint(realizationForBase(port.getBase(), index), "ports", + port.getAccessorAttr()); + if (auto resource = value.getDefiningOp()) + return findEndpoint(realizationForBase(resource.getBase(), index), + "resources", resource.getAccessorAttr()); + return {}; +} + +ExportOp findModuleEndpointExport(ModuleOp module, StringRef field, + FlatSymbolRefAttr accessor) { + SmallVector exports; + for (Operation &operation : module.getBody().front()) + if (auto exportOp = dyn_cast(operation)) + exports.push_back(exportOp); + + unsigned ordinal = 0; + for (StringRef candidateField : + {StringRef("ports"), StringRef("resources"), StringRef("results")}) { + for (Attribute attribute : + module.getInterface().getAs(candidateField)) { + auto record = cast(attribute); + if (candidateField == field && + record.getAs("accessor") == accessor) + return ordinal < exports.size() ? exports[ordinal] : ExportOp(); + ++ordinal; + } + } + return {}; +} + +LogicalResult verifyBindingReferenceFingerprint(BindingOp binding, + const ModelIndex &index, + FlatSymbolRefAttr reference, + StringRef fingerprintField, + StringRef label) { + Operation *definition = resolveReference(index, binding, reference); + auto type = dyn_cast_or_null(definition); + auto fingerprint = binding.getRecord().getAs(fingerprintField); + if (!type || !fingerprint || fingerprint != type.getFingerprintAttr()) + return binding.emitOpError() + << label << " fingerprint must exactly match referenced acsim.type"; + return success(); +} + +ArrayAttr bindingStaticValues(BindingOp binding) { + SmallVector values; + auto parameters = binding.getRecord().getAs("parameters"); + if (!parameters) + return {}; + for (Attribute attribute : parameters) { + auto record = dyn_cast(attribute); + if (!record || !record.get("value")) + return {}; + values.push_back(record.get("value")); + } + return ArrayAttr::get(binding.getContext(), values); +} + +LogicalResult verifyPlacementTarget(Operation *operation, + SymbolRefAttr targetReference, + ArrayAttr staticArguments, + StringAttr specialization, + const ModelIndex &index) { + FailureOr targetDefinition = + requireReference(index, operation, targetReference, + "realization target"); + if (failed(targetDefinition)) + return failure(); + Operation *target = *targetDefinition; + if (auto targetBinding = dyn_cast(target)) { + if (targetBinding.getEffect() != "stateful") + return operation->emitOpError( + "placement target binding must be stateful"); + if (bindingStaticValues(targetBinding) != staticArguments) + return operation->emitOpError("static arguments must exactly match " + "ordered binding-lock parameters"); + } else { + auto targetModule = cast(target); + if (targetModule.getStaticParams() != staticArguments || + targetModule.getSpecializationFingerprintAttr() != specialization) + return operation->emitOpError( + "generated module target requires exact static arguments and " + "specialization fingerprint"); + } + return success(); +} + +FailureOr capturedPlacement(Value value, Operation *reporter) { + llvm::SmallPtrSet visited; + uint64_t nodes = 0; + const uint64_t limit = currentModelVerificationLimits().maxDependencyNodes; + while (value) { + if (++nodes > limit) + return reporter->emitOpError() + << "dependency graph exceeds ACSim capability " << limit; + void *key = value.getAsOpaquePointer(); + if (!visited.insert(key).second) + return reporter->emitOpError( + "typed SSA dependency graph contains a cycle"); + Operation *definition = value.getDefiningOp(); + if (!definition) + return static_cast(nullptr); + if (isa(definition)) + return definition; + if (auto element = dyn_cast(definition)) { + value = element.getArray(); + continue; + } + if (auto port = dyn_cast(definition)) { + value = port.getBase(); + continue; + } + if (auto resource = dyn_cast(definition)) { + value = resource.getBase(); + continue; + } + return static_cast(nullptr); + } + return static_cast(nullptr); +} + +LogicalResult verifyCaptureBoundary(ProcessOp process, Value capture, + const ModelIndex &index) { + if (!isa(capture.getType())) + return success(); + FailureOr placement = capturedPlacement(capture, process); + if (failed(placement)) + return failure(); + if (*placement && (*placement)->getParentOfType() == + process->getParentOfType()) + return success(); + return process.emitOpError("captured owner/ref must remain within the " + "lexically enclosing module boundary"); +} + +LogicalResult verifyModulesAndTypedGraph(ModelOp model, + const ModelIndex &index) { + llvm::DenseMap> lastProjection; + llvm::SmallSet, 16> bindingPairs; + llvm::StringMap specializationByKey; + llvm::StringMap keyBySpecialization; + llvm::StringMap generatedCallEffects; + auto recordSpecialization = [&](Operation *placement, SymbolRefAttr target, + ArrayAttr arguments, + StringAttr fingerprint) -> LogicalResult { + std::string key = symbolKey(target); + std::string printedArguments; + llvm::raw_string_ostream(printedArguments) << arguments; + key.push_back(':'); + key.append(printedArguments); + auto [sameKey, inserted] = + specializationByKey.try_emplace(key, fingerprint); + if (!inserted && sameKey->second != fingerprint) + return placement->emitOpError("identical target and static arguments " + "require one specialization fingerprint"); + auto [sameFingerprint, unique] = + keyBySpecialization.try_emplace(fingerprint.getValue(), key); + if (!unique && sameFingerprint->second != key) + return placement->emitOpError( + "different specialization inputs require distinct fingerprints"); + return success(); + }; + + for (Operation *operation : index.ordered) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->semanticOperationVisits; + for (Type type : operation->getOperandTypes()) + if (failed(verifyCanonicalType(type, operation, index))) + return failure(); + for (Type type : operation->getResultTypes()) + if (failed(verifyCanonicalType(type, operation, index))) + return failure(); + for (Region ®ion : operation->getRegions()) + for (Block &block : region) + for (BlockArgument argument : block.getArguments()) + if (failed(verifyCanonicalType(argument.getType(), operation, index))) + return failure(); + + if (auto binding = dyn_cast(operation)) { + // The field projections below assume the exact lock shape; malformed + // records must be rejected before any of them are dereferenced. + if (failed(verifyBindingLockShape(binding))) + return failure(); + const std::array cppKinds = {"value", "packet"}; + const std::array schemaKinds = {"schema"}; + const std::array providerKinds = {"provider"}; + const std::array implementationKinds = {"implementation"}; + if (failed(requireTypeKind(index, binding, binding.getCppTypeAttr(), + cppKinds, "C++ type")) || + failed(requireTypeKind(index, binding, binding.getSchemaAttr(), + schemaKinds, "schema")) || + failed(requireTypeKind(index, binding, binding.getProviderAttr(), + providerKinds, "provider")) || + failed(requireTypeKind(index, binding, + binding.getImplementationAttr(), + implementationKinds, "implementation"))) + return failure(); + if (failed(verifyBindingReferenceFingerprint( + binding, index, binding.getSchemaAttr(), + "component_schema_fingerprint", "component schema")) || + failed(verifyBindingReferenceFingerprint( + binding, index, binding.getImplementationAttr(), + "provider_implementation_fingerprint", "implementation"))) + return failure(); + for (StringRef field : {StringRef("ports"), StringRef("resources")}) + for (Attribute item : binding.getRecord().getAs(field)) { + auto endpoint = cast(item); + const std::array accessorKinds = {"accessor"}; + const std::array roleKinds = {"role"}; + const std::array timeDomainKinds = {"time_domain"}; + if (failed(requireTypeKind( + index, binding, endpoint.getAs("accessor"), + accessorKinds, "endpoint accessor")) || + failed(requireTypeKind(index, binding, + endpoint.getAs("role"), + roleKinds, "endpoint role")) || + failed(requireTypeKind( + index, binding, + endpoint.getAs("time_domain"), + timeDomainKinds, "time-domain"))) + return failure(); + } + for (Attribute item : binding.getRecord().getAs("ports")) { + auto endpoint = cast(item); + const std::array interfaceKinds = {"interface"}; + const std::array payloadKinds = {"value", "packet"}; + const std::array protocolKinds = {"protocol"}; + if (failed(requireTypeKind( + index, binding, endpoint.getAs("interface"), + interfaceKinds, "endpoint interface")) || + failed(requireTypeKind(index, binding, + endpoint.getAs("payload"), + payloadKinds, "endpoint payload")) || + failed(requireTypeKind( + index, binding, endpoint.getAs("protocol"), + protocolKinds, "endpoint protocol"))) + return failure(); + } + for (Attribute item : binding.getRecord().getAs("resources")) { + auto endpoint = cast(item); + const std::array resourceKinds = {"resource"}; + if (failed(requireTypeKind( + index, binding, endpoint.getAs("resource"), + resourceKinds, "endpoint resource"))) + return failure(); + } + for (Attribute item : binding.getRecord().getAs("results")) { + auto result = cast(item); + const std::array resultKinds = {"value", "packet"}; + if (failed(requireTypeKind(index, binding, + result.getAs("cpp_type"), + resultKinds, "result C++ type"))) + return failure(); + } + for (Attribute item : + binding.getRecord().getAs("activation_sources")) { + auto source = cast(item); + const std::array wakeKinds = {"wake"}; + if (failed(requireTypeKind(index, binding, + source.getAs("kind"), + wakeKinds, "activation source kind"))) + return failure(); + } + } else if (auto module = dyn_cast(operation)) { + // As with binding locks, the interface projections below assume the + // exact module interface shape. + if (failed(verifyModuleInterfaceShape(module))) + return failure(); + for (StringRef field : {StringRef("ports"), StringRef("resources")}) + for (Attribute item : module.getInterface().getAs(field)) { + auto endpoint = cast(item); + const std::array accessorKinds = {"accessor"}; + const std::array roleKinds = {"role"}; + const std::array timeDomainKinds = {"time_domain"}; + if (failed(requireTypeKind( + index, module, endpoint.getAs("accessor"), + accessorKinds, "interface accessor")) || + failed(requireTypeKind(index, module, + endpoint.getAs("role"), + roleKinds, "interface role")) || + failed(requireTypeKind( + index, module, + endpoint.getAs("time_domain"), + timeDomainKinds, "interface time-domain"))) + return failure(); + } + for (Attribute item : module.getInterface().getAs("ports")) { + auto endpoint = cast(item); + const std::array interfaceKinds = {"interface"}; + const std::array payloadKinds = {"value", "packet"}; + const std::array protocolKinds = {"protocol"}; + if (failed(requireTypeKind( + index, module, endpoint.getAs("interface"), + interfaceKinds, "interface kind")) || + failed(requireTypeKind(index, module, + endpoint.getAs("payload"), + payloadKinds, "interface payload")) || + failed(requireTypeKind( + index, module, endpoint.getAs("protocol"), + protocolKinds, "interface protocol"))) + return failure(); + } + for (Attribute item : + module.getInterface().getAs("resources")) { + const std::array resourceKinds = {"resource"}; + if (failed(requireTypeKind( + index, module, + cast(item).getAs("resource"), + resourceKinds, "interface resource"))) + return failure(); + } + for (Attribute item : module.getInterface().getAs("results")) { + const std::array resultKinds = {"value", "packet"}; + if (failed(requireTypeKind( + index, module, + cast(item).getAs("cpp_type"), + resultKinds, "interface result C++ type"))) + return failure(); + } + } else if (auto instance = dyn_cast(operation)) { + auto ownerType = dyn_cast(instance.getResult().getType()); + if (failed(verifyPlacementTarget( + instance, instance.getTargetAttr(), instance.getStaticArgs(), + instance.getSpecializationFingerprintAttr(), index)) || + failed(recordSpecialization( + instance, instance.getTargetAttr(), instance.getStaticArgs(), + instance.getSpecializationFingerprintAttr()))) + return failure(); + if (!ownerType || ownerType.getRealization() != instance.getTargetAttr()) + return instance.emitOpError( + "instance result must own its exact realization target"); + } else if (auto array = dyn_cast(operation)) { + if (failed(verifyPlacementTarget( + array, array.getTargetAttr(), array.getStaticArgs(), + array.getSpecializationFingerprintAttr(), index)) || + failed(recordSpecialization( + array, array.getTargetAttr(), array.getStaticArgs(), + array.getSpecializationFingerprintAttr()))) + return failure(); + auto type = dyn_cast(array.getResult().getType()); + auto element = + type ? dyn_cast(type.getElementType()) : OwnerType(); + if (!type || type.getShape().asArrayRef() != array.getShape() || + !element || element.getRealization() != array.getTargetAttr()) + return array.emitOpError( + "array result shape and owning element realization must be exact"); + } else if (auto element = dyn_cast(operation)) { + auto arrayType = dyn_cast(element.getArray().getType()); + auto owner = arrayType ? dyn_cast(arrayType.getElementType()) + : OwnerType(); + auto reference = dyn_cast(element.getResult().getType()); + if (!arrayType || !owner || !reference || + owner.getRealization() != reference.getRealization() || + element.getIndices().size() != arrayType.getShape().size()) + return element.emitOpError( + "element must be a constant typed reference projection"); + for (auto [indexValue, extent] : llvm::zip_equal( + element.getIndices(), arrayType.getShape().asArrayRef())) + if (indexValue < 0 || indexValue >= extent) + return element.emitOpError("element index is out of static bounds"); + SmallVector current(element.getIndices()); + auto found = lastProjection.find(element.getArray()); + if (found != lastProjection.end() && + !lexicographicallyLess(found->second, current)) + return element.emitOpError( + "array element projections must be strictly lexicographic"); + lastProjection[element.getArray()] = std::move(current); + } else if (auto port = dyn_cast(operation)) { + if (!isa(port.getBase().getType()) || + !isa(port.getResult().getType())) + return port.emitOpError( + "port projection requires a typed owner/ref and typed port result"); + const std::array kinds = {"accessor"}; + if (failed(requireTypeKind(index, port, port.getAccessorAttr(), kinds, + "port accessor"))) + return failure(); + Operation *realization = realizationForBase(port.getBase(), index); + DictionaryAttr endpoint = + findEndpoint(realization, "ports", port.getAccessorAttr()); + PortType type = cast(port.getResult().getType()); + if (!endpoint || + endpoint.getAs("interface") != + type.getInterface() || + endpoint.getAs("role") != type.getRole() || + endpoint.getAs("payload") != type.getPayload() || + endpoint.getAs("protocol") != type.getProtocol()) + return port.emitOpError("port projection must exactly match its " + "binding-lock endpoint record"); + } else if (auto resource = dyn_cast(operation)) { + if (!isa(resource.getBase().getType()) || + !isa(resource.getResult().getType())) + return resource.emitOpError("resource projection requires a typed " + "owner/ref and resource result"); + const std::array kinds = {"accessor"}; + if (failed(requireTypeKind(index, resource, resource.getAccessorAttr(), + kinds, "resource accessor"))) + return failure(); + Operation *realization = realizationForBase(resource.getBase(), index); + DictionaryAttr endpoint = + findEndpoint(realization, "resources", resource.getAccessorAttr()); + ResourceType type = cast(resource.getResult().getType()); + if (!endpoint || + endpoint.getAs("resource") != type.getResource() || + endpoint.getAs("role") != type.getRole()) + return resource.emitOpError("resource projection must exactly match " + "its binding-lock endpoint record"); + } else if (auto bind = dyn_cast(operation)) { + if (!llvm::is_contained({StringRef("port"), StringRef("resource"), + StringRef("export"), StringRef("pure_view")}, + bind.getKind())) + return bind.emitOpError("unknown closed typed binding kind"); + if (bind.getKind() == "port") { + auto source = bind.getSource().getDefiningOp(); + auto target = bind.getTarget().getDefiningOp(); + DictionaryAttr sourceRecord = + source ? findEndpoint(realizationForBase(source.getBase(), index), + "ports", source.getAccessorAttr()) + : DictionaryAttr(); + DictionaryAttr targetRecord = + target ? findEndpoint(realizationForBase(target.getBase(), index), + "ports", target.getAccessorAttr()) + : DictionaryAttr(); + if (!sourceRecord || !targetRecord || + sourceRecord.getAs("direction").getValue() != + "output" || + targetRecord.getAs("direction").getValue() != "input") + return bind.emitOpError("port binding must connect exact output and " + "input endpoint records"); + if (sourceRecord.get("interface") != targetRecord.get("interface") || + sourceRecord.get("payload") != targetRecord.get("payload") || + sourceRecord.get("protocol") != targetRecord.get("protocol") || + sourceRecord.get("cardinality") != + targetRecord.get("cardinality") || + sourceRecord.get("delegation") != targetRecord.get("delegation") || + sourceRecord.get("ownership") != targetRecord.get("ownership") || + sourceRecord.get("time_domain") != targetRecord.get("time_domain")) + return bind.emitOpError( + "port bind endpoints must have identical interface, payload, " + "protocol, cardinality, delegation, ownership, and time domain"); + } else if (bind.getKind() == "resource") { + auto source = bind.getSource().getDefiningOp(); + auto target = bind.getTarget().getDefiningOp(); + DictionaryAttr sourceRecord = + source ? findEndpoint(realizationForBase(source.getBase(), index), + "resources", source.getAccessorAttr()) + : DictionaryAttr(); + DictionaryAttr targetRecord = + target ? findEndpoint(realizationForBase(target.getBase(), index), + "resources", target.getAccessorAttr()) + : DictionaryAttr(); + if (!sourceRecord || !targetRecord || + sourceRecord.getAs("mode").getValue() != "initiator" || + targetRecord.getAs("mode").getValue() != "target") + return bind.emitOpError("resource binding must connect exact " + "initiator and target endpoint records"); + if (sourceRecord.get("resource") != targetRecord.get("resource") || + sourceRecord.get("delegation") != targetRecord.get("delegation") || + sourceRecord.get("ownership") != targetRecord.get("ownership") || + sourceRecord.get("time_domain") != targetRecord.get("time_domain")) + return bind.emitOpError( + "resource bind endpoints must have identical resource kind, " + "delegation, ownership, and time domain"); + } else if (bind.getKind() == "pure_view") { + auto target = bind.getTarget().getDefiningOp(); + if (!target || !llvm::is_contained(target.getArgs(), bind.getSource())) + return bind.emitOpError( + "pure_view target must directly consume the source expression"); + } else { + auto target = bind.getTarget().getDefiningOp(); + if (!target || bind.getTarget() != target.getResult()) + return bind.emitOpError( + "export bind target must be the exact result of acsim.export"); + if (bind.getSource() != target.getValue()) + return bind.emitOpError( + "export bind source must be the exact input of its target " + "acsim.export"); + if (bind.getSource().getType() != bind.getTarget().getType()) + return bind.emitOpError( + "export binding endpoints must have exactly equal types"); + } + std::pair key{bind.getSource().getAsOpaquePointer(), + bind.getTarget().getAsOpaquePointer()}; + if (!bindingPairs.insert(key).second) + return bind.emitOpError( + "each typed construction relation must lower exactly once"); + } else if (auto inlineOp = dyn_cast(operation)) { + Type result = inlineOp.getResult().getType(); + if (inlineOp->getParentOfType()) { + if (!isa(result)) + return inlineOp.emitOpError( + "process inline result must be an integer, float, index, or " + "!acsim.value"); + } else if (!isa(result)) { + return inlineOp.emitOpError( + "module inline result must be exactly !acsim.expr"); + } + FailureOr definition = + requireCallCallee(index, inlineOp, inlineOp.getCalleeAttr()); + if (failed(definition)) + return failure(); + if (auto binding = dyn_cast(*definition)) { + if (binding.getEffect() != "pure") + return inlineOp.emitOpError() + << "inline callee '" << inlineOp.getCalleeAttr() + << "' requires effect 'pure'"; + } else { + auto type = cast(*definition); + if (type.getKind() != "implementation") + return inlineOp.emitOpError() + << "callee reference '" << inlineOp.getCalleeAttr() + << "' resolves to non-implementation acsim.type"; + std::string key = definitionKey(type); + auto [entry, inserted] = + generatedCallEffects.try_emplace(key, "inline"); + if (!inserted && entry->second != "inline") + return inlineOp.emitOpError() + << "generated implementation callee '" + << inlineOp.getCalleeAttr() + << "' cannot be used by both acsim.inline and acsim.invoke"; + } + if (!isMemoryEffectFree(inlineOp)) + return inlineOp.emitOpError("inline must remain effect-free"); + } else if (auto invoke = dyn_cast(operation)) { + FailureOr definition = + requireCallCallee(index, invoke, invoke.getCalleeAttr()); + if (failed(definition)) + return failure(); + if (auto binding = dyn_cast(*definition)) { + if (binding.getEffect() != "stateful") + return invoke.emitOpError() + << "invoke callee '" << invoke.getCalleeAttr() + << "' requires effect 'stateful'"; + } else { + auto type = cast(*definition); + if (type.getKind() != "implementation") + return invoke.emitOpError() + << "callee reference '" << invoke.getCalleeAttr() + << "' resolves to non-implementation acsim.type"; + std::string key = definitionKey(type); + auto [entry, inserted] = + generatedCallEffects.try_emplace(key, "invoke"); + if (!inserted && entry->second != "invoke") + return invoke.emitOpError() + << "generated implementation callee '" + << invoke.getCalleeAttr() + << "' cannot be used by both acsim.inline and acsim.invoke"; + } + for (Type type : invoke.getResultTypes()) + if (!isa(type)) + return invoke.emitOpError( + "invoke results must be exact !acsim.value or !acsim.wake types"); + } else if (auto exportOp = dyn_cast(operation)) { + if (exportOp.getValue().getType() != exportOp.getResult().getType()) + return exportOp.emitOpError( + "export result type must exactly preserve its internal value"); + const std::array kinds = {"role"}; + if (failed(requireTypeKind(index, exportOp, exportOp.getRoleAttr(), kinds, + "export role"))) + return failure(); + } else if (auto process = dyn_cast(operation)) { + for (Value capture : process.getCaptures()) + if (failed(verifyCaptureBoundary(process, capture, index))) + return failure(); + if (failed(verifyProcess(process, index))) + return failure(); + } + } + + for (Operation &operation : model.getBody().front()) { + auto module = dyn_cast(operation); + if (!module) + continue; + if (module.getBody().empty() || module.getBody().front().empty() || + !isa(module.getBody().front().back())) + return module.emitOpError("module must end in one acsim.return"); + SmallVector exports; + for (Operation &child : module.getBody().front()) + if (auto exportOp = dyn_cast(child)) + exports.push_back(exportOp); + SmallVector> interfaceRecords; + for (StringRef field : + {StringRef("ports"), StringRef("resources"), StringRef("results")}) + for (Attribute attribute : module.getInterface().getAs(field)) + interfaceRecords.emplace_back(field, cast(attribute)); + if (module.getExports().size() != exports.size() || + exports.size() != interfaceRecords.size()) + return module.emitOpError( + "module exports must exactly cover its ordered interface records"); + for (auto [attribute, exportOp, interfaceRecord] : + llvm::zip_equal(module.getExports(), exports, interfaceRecords)) { + auto reference = dyn_cast(attribute); + DictionaryAttr record = interfaceRecord.second; + StringRef exportName = exportOp.getSymName(); + if (!reference || reference.getValue() != exportName || + record.getAs("name").getValue() != exportName) + return module.emitOpError( + "module export names must match ordered interface records and " + "acsim.export declarations"); + if (interfaceRecord.first == "ports") { + auto projection = exportOp.getValue().getDefiningOp(); + auto type = dyn_cast(exportOp.getValue().getType()); + DictionaryAttr endpoint = + findProjectedEndpoint(exportOp.getValue(), index); + if (!projection || !type || !endpoint || + projection.getAccessorAttr() != + record.getAs("accessor") || + endpoint.getAs("delegation") != + record.getAs("delegation") || + exportOp.getRoleAttr() != record.getAs("role") || + type.getInterface() != + record.getAs("interface") || + type.getRole() != record.getAs("role") || + type.getPayload() != record.getAs("payload") || + type.getProtocol() != record.getAs("protocol")) + return exportOp.emitOpError( + "port export must exactly match its module interface record"); + } else if (interfaceRecord.first == "resources") { + auto projection = exportOp.getValue().getDefiningOp(); + auto type = dyn_cast(exportOp.getValue().getType()); + DictionaryAttr endpoint = + findProjectedEndpoint(exportOp.getValue(), index); + if (!projection || !type || !endpoint || + projection.getAccessorAttr() != + record.getAs("accessor") || + endpoint.getAs("delegation") != + record.getAs("delegation") || + exportOp.getRoleAttr() != record.getAs("role") || + type.getResource() != record.getAs("resource") || + type.getRole() != record.getAs("role")) + return exportOp.emitOpError( + "resource export must exactly match its module interface record"); + } else { + SymbolRefAttr cppType; + if (auto value = dyn_cast(exportOp.getValue().getType())) + cppType = value.getSymbol(); + else if (auto expr = dyn_cast(exportOp.getValue().getType())) + cppType = expr.getSymbol(); + if (!cppType || cppType != record.getAs("cpp_type")) + return exportOp.emitOpError( + "result export must exactly match its module interface record"); + } + } + auto returnOp = cast(module.getBody().front().back()); + if (returnOp.getValues().size() != exports.size()) + return returnOp.emitOpError( + "return values must exactly match ordered module exports"); + for (auto [value, exportOp] : + llvm::zip_equal(returnOp.getValues(), exports)) + if (value != exportOp.getResult()) + return returnOp.emitOpError( + "return values must be the exact ordered export results"); + } + return success(); +} + +std::string generatedProcessThunk(ProcessOp process, StringRef kind) { + ModuleOp module = process->getParentOfType(); + std::string result = "acsim_generated::"; + result.append(module.getSymName()); + result.append("::s"); + result.append(module.getSpecializationFingerprint().drop_front(7)); + result.append("::"); + result.append(process.getSymName()); + result.append("::p"); + result.append(process.getSpecializationFingerprint().drop_front(7)); + result.append("::"); + result.append(kind); + return result; +} + +LogicalResult verifyDispatchAndActivation(ModelOp model, + const ModelIndex &index, + HierarchyExpansion &expansion) { + llvm::DenseMap dispatchByObject; + for (Operation &operation : model.getBody().front()) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->runtimeOperationVisits; + auto dispatch = dyn_cast(operation); + if (!dispatch) + continue; + int64_t id = dispatch.getObjectId(); + if (id < 0 || static_cast(id) >= expansion.runtimeRows.size()) + return dispatch.emitOpError( + "dispatch object ID has no expanded runtime object"); + const ExpandedRuntimeRow &row = expansion.runtimeRows[id]; + FailureOr targetDefinition = + requireReference( + index, dispatch, dispatch.getTargetAttr(), "dispatch target"); + if (failed(targetDefinition)) + return failure(); + std::string target = symbolKey(dispatch.getTargetAttr()); + if (*targetDefinition != row.placement || target != row.target || + dispatch.getPath() != row.path || + dispatch.getIndices() != ArrayRef(row.indices) || + dispatch.getActivationId() != row.activationId) + return dispatch.emitOpError( + "dispatch target, path, indices, and IDs must jointly match one " + "derived expanded row"); + if (!dispatchByObject.try_emplace(id, dispatch).second) + return dispatch.emitOpError( + "runtime object has more than one dispatch row"); + if (auto binding = dyn_cast(row.realization)) { + auto entryPoints = + binding.getCppRecord().getAs("entry_points"); + if (dispatch.getWorkAttr() != entryPoints.getAs("work") || + dispatch.getXferAttr() != entryPoints.getAs("xfer") || + dispatch.getResetAttr() != entryPoints.getAs("reset") || + dispatch.getValidateAttr() != + entryPoints.getAs("validate")) + return dispatch.emitOpError( + "dispatch thunks must exactly match the placement binding lock"); + } else { + auto process = cast(row.realization); + if (dispatch.getWork() != generatedProcessThunk(process, "work") || + dispatch.getXfer() != generatedProcessThunk(process, "xfer") || + dispatch.getReset() != generatedProcessThunk(process, "reset") || + dispatch.getValidate() != generatedProcessThunk(process, "validate")) + return dispatch.emitOpError( + "dispatch thunks must exactly match the generated process " + "realization"); + } + } + if (dispatchByObject.size() != expansion.runtimeRows.size()) + return model.emitOpError( + "every runtime object requires exactly one typed dispatch row"); + + using DependencyKey = std::pair; + std::map> dependencyMemo; + uint64_t dependencyNodes = 0; + auto collectIds = [&](Value rootValue, unsigned context, + Operation *reporter) -> FailureOr> { + struct Dependency { + unsigned context; + Value value; + }; + struct Frame { + DependencyKey key; + Value value; + SmallVector dependencies; + size_t next = 0; + std::set result; + bool initialized = false; + }; + DependencyKey rootKey{context, rootValue.getAsOpaquePointer()}; + if (auto found = dependencyMemo.find(rootKey); + found != dependencyMemo.end()) + return found->second; + SmallVector stack{{rootKey, rootValue}}; + std::set active; + const uint64_t limit = currentModelVerificationLimits().maxDependencyNodes; + while (!stack.empty()) { + Frame &frame = stack.back(); + if (!frame.initialized) { + if (!active.insert(frame.key).second) + return reporter->emitOpError( + "typed SSA dependency graph contains a cycle"); + if (++dependencyNodes > limit) + return reporter->emitOpError() + << "dependency graph exceeds ACSim capability " << limit; + frame.initialized = true; + if (auto argument = dyn_cast(frame.value)) { + if (auto process = + dyn_cast(argument.getOwner()->getParentOp())) + if (argument.getArgNumber() < process.getCaptures().size()) + frame.dependencies.push_back( + {frame.key.first, + process.getCaptures()[argument.getArgNumber()]}); + } else if (Operation *definition = frame.value.getDefiningOp()) { + unsigned currentContext = frame.key.first; + auto appendGeneratedEndpoint = [&](Value base, StringRef field, + FlatSymbolRefAttr accessor) { + Operation *wrapper = base.getDefiningOp(); + std::optional elementOrdinal; + if (auto element = dyn_cast_or_null(wrapper)) { + auto array = element.getArray().getDefiningOp(); + wrapper = array; + if (array) { + uint64_t ordinal = 0; + for (auto [indexValue, extent] : + llvm::zip_equal(element.getIndices(), array.getShape())) + ordinal = ordinal * static_cast(extent) + + static_cast(indexValue); + elementOrdinal = ordinal; + } + } + auto children = + expansion.contexts[currentContext].childContexts.find(wrapper); + if (children == + expansion.contexts[currentContext].childContexts.end()) + return false; + uint64_t ordinal = elementOrdinal.value_or(0); + if (ordinal >= children->second.size()) + return true; + unsigned childContext = children->second[ordinal]; + ExportOp exportOp = findModuleEndpointExport( + expansion.contexts[childContext].module, field, accessor); + if (exportOp) + frame.dependencies.push_back({childContext, exportOp.getValue()}); + return true; + }; + auto found = + expansion.contexts[currentContext].objectIds.find(definition); + if (isa(definition) && + found != expansion.contexts[currentContext].objectIds.end()) { + frame.result.insert(found->second.begin(), found->second.end()); + } else if (auto element = dyn_cast(definition)) { + auto array = element.getArray().getDefiningOp(); + auto rows = + array ? expansion.contexts[currentContext].objectIds.find(array) + : expansion.contexts[currentContext].objectIds.end(); + if (rows != expansion.contexts[currentContext].objectIds.end()) { + uint64_t ordinal = 0; + for (auto [indexValue, extent] : + llvm::zip_equal(element.getIndices(), array.getShape())) + ordinal = ordinal * static_cast(extent) + + static_cast(indexValue); + if (ordinal < rows->second.size()) + frame.result.insert(rows->second[ordinal]); + } + } else if (auto port = dyn_cast(definition)) { + if (!appendGeneratedEndpoint(port.getBase(), "ports", + port.getAccessorAttr())) + frame.dependencies.push_back({currentContext, port.getBase()}); + } else if (auto resource = dyn_cast(definition)) { + if (!appendGeneratedEndpoint(resource.getBase(), "resources", + resource.getAccessorAttr())) + frame.dependencies.push_back( + {currentContext, resource.getBase()}); + } else { + for (Value operand : definition->getOperands()) + frame.dependencies.push_back({currentContext, operand}); + } + } + } + if (frame.next < frame.dependencies.size()) { + Dependency dependency = frame.dependencies[frame.next]; + DependencyKey key{dependency.context, + dependency.value.getAsOpaquePointer()}; + if (auto found = dependencyMemo.find(key); + found != dependencyMemo.end()) { + frame.result.insert(found->second.begin(), found->second.end()); + ++frame.next; + continue; + } + if (active.contains(key)) + return reporter->emitOpError( + "typed SSA dependency graph contains a cycle"); + stack.push_back({key, dependency.value}); + continue; + } + dependencyMemo[frame.key] = frame.result; + active.erase(frame.key); + stack.pop_back(); + } + return dependencyMemo.find(rootKey)->second; + }; + + std::set> expected; + for (const ExpandedRuntimeRow &row : expansion.runtimeRows) + expected.insert({row.activationId, row.objectId}); + for (auto [contextOrdinal, expansionContext] : + llvm::enumerate(expansion.contexts)) { + for (Operation &operation : expansionContext.module.getBody().front()) { + if (auto bind = dyn_cast(operation)) { + FailureOr> sources = + collectIds(bind.getSource(), contextOrdinal, bind); + FailureOr> targets = + collectIds(bind.getTarget(), contextOrdinal, bind); + if (failed(sources) || failed(targets)) + return failure(); + for (int64_t source : *sources) + for (int64_t target : *targets) + expected.insert( + {expansion.runtimeRows[source].activationId, target}); + } else if (auto process = dyn_cast(operation)) { + auto rows = expansionContext.objectIds.find(process); + if (rows == expansionContext.objectIds.end() || + rows->second.size() != 1) + return process.emitOpError( + "process must instantiate once per concrete module context"); + int64_t processId = rows->second.front(); + for (Value capture : process.getCaptures()) { + FailureOr> sources = + collectIds(capture, contextOrdinal, process); + if (failed(sources)) + return failure(); + for (int64_t source : *sources) + expected.insert( + {expansion.runtimeRows[source].activationId, processId}); + } + LogicalResult invokeStatus = success(); + process.walk([&](InvokeOp invoke) -> WalkResult { + for (Value argument : invoke.getArgs()) { + FailureOr> sources = + collectIds(argument, contextOrdinal, invoke); + if (failed(sources)) { + invokeStatus = failure(); + return WalkResult::interrupt(); + } + for (int64_t source : *sources) + expected.insert( + {expansion.runtimeRows[source].activationId, processId}); + } + return WalkResult::advance(); + }); + if (failed(invokeStatus)) + return failure(); + } + } + } + + std::pair previous{-1, -1}; + std::set> actual; + for (Operation &operation : model.getBody().front()) { + if (modelVerificationWorkCollector) + ++modelVerificationWorkCollector->runtimeOperationVisits; + auto activate = dyn_cast(operation); + if (!activate) + continue; + auto sourceDispatch = activate.getSource().getDefiningOp(); + auto targetDispatch = activate.getTarget().getDefiningOp(); + if (!sourceDispatch || !targetDispatch || + activate.getSource() != sourceDispatch.getActivation() || + activate.getTarget() != targetDispatch.getObject()) + return activate.emitOpError( + "activation edge must consume dispatch-produced typed IDs"); + std::pair edge{sourceDispatch.getActivationId(), + targetDispatch.getObjectId()}; + if (edge <= previous) + return activate.emitOpError( + "activation edges must be deduplicated and sorted by source,target"); + previous = edge; + actual.insert(edge); + } + if (actual != expected) + return model.emitOpError( + "activation edges must exactly equal computed static dependencies"); + return success(); +} + +} // namespace + +ParseResult ProcessOp::parse(OpAsmParser &parser, OperationState &result) { + Builder &builder = parser.getBuilder(); + StringAttr name; + SmallVector captures; + SmallVector captureTypes; + ArrayAttr captureNames; + FlatSymbolRefAttr entryPc; + ArrayAttr pcs; + ArrayAttr liveSlots; + int64_t fairnessCap; + StringAttr specializationFingerprint; + + if (parser.parseSymbolName(name, SymbolTable::getSymbolAttrName(), + result.attributes) || + parser.parseKeyword("captures") || parser.parseLParen()) + return failure(); + if (failed(parser.parseOptionalRParen())) { + do { + captures.emplace_back(); + Type type; + if (parser.parseOperand(captures.back()) || parser.parseColonType(type)) + return failure(); + captureTypes.push_back(type); + } while (succeeded(parser.parseOptionalComma())); + if (parser.parseRParen()) + return failure(); + } + if (parser.resolveOperands(captures, captureTypes, + parser.getCurrentLocation(), result.operands) || + parser.parseKeyword("names") || parser.parseAttribute(captureNames) || + parser.parseKeyword("entry") || parser.parseAttribute(entryPc) || + parser.parseKeyword("pcs") || parser.parseAttribute(pcs) || + parser.parseKeyword("live") || parser.parseAttribute(liveSlots) || + parser.parseKeyword("fairness") || parser.parseInteger(fairnessCap) || + parser.parseKeyword("specialization") || + parser.parseAttribute(specializationFingerprint)) + return failure(); + + result.addAttribute("capture_names", captureNames); + result.addAttribute("entry_pc", entryPc); + result.addAttribute("pcs", pcs); + result.addAttribute("live_slots", liveSlots); + result.addAttribute("fairness_cap", builder.getI64IntegerAttr(fairnessCap)); + result.addAttribute("specialization_fingerprint", specializationFingerprint); + if (parser.parseOptionalAttrDictWithKeyword(result.attributes) || + parser.parseLBrace()) + return failure(); + + for (Attribute attribute : pcs) { + auto expected = dyn_cast(attribute); + FlatSymbolRefAttr label; + if (!expected || parser.parseKeyword("state") || + parser.parseAttribute(label)) + return parser.emitError(parser.getCurrentLocation(), + "expected one flat state label per PC"); + if (label != expected) + return parser.emitError(parser.getCurrentLocation()) + << "state label " << label << " does not match ordered PC " + << expected; + Region *state = result.addRegion(); + if (parser.parseRegion(*state)) + return failure(); + } + return parser.parseRBrace(); +} + +void ProcessOp::print(OpAsmPrinter &printer) { + printer << ' '; + printer.printSymbolName(getSymName()); + printer << " captures("; + llvm::interleaveComma(getCaptures(), printer, [&](Value capture) { + printer << capture << " : " << capture.getType(); + }); + printer << ") names " << getCaptureNamesAttr() << " entry " + << getEntryPcAttr() << " pcs " << getPcsAttr() << " live " + << getLiveSlotsAttr() << " fairness " << getFairnessCap() + << " specialization " << getSpecializationFingerprintAttr(); + printer.printOptionalAttrDictWithKeyword( + (*this)->getAttrs(), + {SymbolTable::getSymbolAttrName(), "capture_names", "entry_pc", "pcs", + "live_slots", "fairness_cap", "specialization_fingerprint"}); + printer << " {"; + for (auto [pc, state] : llvm::zip(getPcs(), getStates())) { + printer << "\nstate " << pc << ' '; + printer.printRegion(state, /*printEntryBlockArgs=*/true, + /*printBlockTerminators=*/true, + /*printEmptyBlock=*/true); + } + printer << "\n}"; +} + +LogicalResult ModelOp::verify() { + if (getContractEpoch() != "0.3") + return emitOpError("contract epoch must be exactly \"0.3\""); + auto parentModule = dyn_cast_or_null((*this)->getParentOp()); + if (!parentModule) + return emitOpError("acsim.model must be directly inside builtin.module"); + unsigned models = 0; + for (Operation &operation : *parentModule.getBody()) + models += isa(operation); + if (models != 1) + return emitOpError("canonical ACSim requires exactly one acsim.model"); + if (parentModule.getBody()->getOperations().size() != 1) + return emitOpError( + "acsim.model must be the sole operation in the canonical file"); + if (failed(verifyModelFingerprints(*this))) + return failure(); + if (getBody().empty()) + return emitOpError("model requires one closed body block"); + + if (failed(preflightModel(*this))) + return failure(); + ModelIndex index; + HierarchyExpansion expansion; + if (failed(buildIndex(*this, index)) || + failed(verifyClosedLegality(*this, index.ordered)) || + failed(verifyDeterministicOrder(*this)) || + failed(expandSelectedRootOwners(*this, index, expansion)) || + failed(expandRuntime(index, expansion)) || + failed(verifyConstructionOrder(*this, expansion)) || + failed(verifyModulesAndTypedGraph(*this, index)) || + failed(verifyDispatchAndActivation(*this, index, expansion))) + return failure(); + return success(); +} + +LogicalResult TypeOp::verify() { + constexpr std::array kinds = { + "accessor", "implementation", "interface", "packet", "policy", + "protocol", "provider", "resource", "role", "schema", + "time_domain", "value", "wake", "payload"}; + if (!llvm::is_contained(kinds, getKind())) + return emitOpError("kind is not a closed ACSim C++ realization kind"); + if (getCppName().empty() || hasRawCppFragment(getCppName())) + return emitOpError( + "C++ spelling must be a non-empty declarative symbol/type spelling"); + if (failed(verifyFingerprint(*this, getFingerprintAttr()))) + return failure(); + + IntegerAttr period = getPeriodAttr(); + IntegerAttr phase = getPhaseAttr(); + IntegerAttr tickScale = getTickScaleAttr(); + FlatSymbolRefAttr parent = getParentAttr(); + DictionaryAttr bridge = getBridgeAttr(); + const bool hasRuntimeMetadata = + period || phase || tickScale || parent || bridge; + if (!hasRuntimeMetadata) + return success(); + if (getKind() != "time_domain") + return emitOpError("runtime domain metadata is legal only for time_domain"); + if (!period || !phase || !tickScale || + !period.getType().isSignlessInteger(64) || + !phase.getType().isSignlessInteger(64) || + !tickScale.getType().isSignlessInteger(64) || period.getInt() <= 0 || + phase.getInt() < 0 || tickScale.getInt() <= 0) + return emitOpError( + "time_domain requires exact positive period/tick_scale and " + "non-negative phase i64 metadata"); + if (static_cast(parent) != static_cast(bridge)) + return emitOpError("time_domain parent requires exact bridge metadata"); + if (bridge) { + auto kind = bridge.getAs("kind"); + auto owner = bridge.getAs("owner"); + if (!hasExactKeys(bridge, {"kind", "owner"}) || !kind || + kind.getValue() != "explicit" || !owner) + return emitOpError( + "time_domain bridge requires exact explicit kind and owner"); + } + return success(); +} + +LogicalResult BindingOp::verify() { return verifyBindingLockShape(*this); } + +LogicalResult ModuleOp::verify() { + if (!isCanonicalIdentifier(getSymName())) + return emitOpError( + "generated module symbol must be a canonical C++ identifier"); + if (failed(verifyFingerprint(*this, getSpecializationFingerprintAttr(), + "specialization fingerprint")) || + failed(verifyModuleInterfaceShape(*this))) + return failure(); + if (getBody().empty() || getBody().front().empty() || + !isa(getBody().front().back())) + return emitOpError("module must end in acsim.return"); + return success(); +} + +LogicalResult InstanceOp::verify() { + if (failed(verifyFingerprint(*this, getSpecializationFingerprintAttr(), + "specialization fingerprint"))) + return failure(); + auto owner = dyn_cast(getResult().getType()); + if (!owner || owner.getRealization() != getTargetAttr()) + return emitOpError("result must own the exact realization target"); + return success(); +} + +LogicalResult ArrayOp::verify() { + if (failed(verifyFingerprint(*this, getSpecializationFingerprintAttr(), + "specialization fingerprint"))) + return failure(); + auto type = dyn_cast(getResult().getType()); + auto owner = type ? dyn_cast(type.getElementType()) : OwnerType(); + if (!type || type.getShape().asArrayRef() != getShape() || !owner || + owner.getRealization() != getTargetAttr()) + return emitOpError( + "result array type must exactly match shape and realization target"); + return success(); +} + +LogicalResult ElementOp::verify() { + auto array = dyn_cast(getArray().getType()); + if (!array || getIndices().size() != array.getShape().size()) + return emitOpError("indices must have the exact static array rank"); + return success(); +} + +LogicalResult PortOp::verify() { + if (isa(getBase().getType()) && + isa(getResult().getType())) + return success(); + return emitOpError("requires owner/ref input and typed port result"); +} + +LogicalResult ResourceOp::verify() { + if (isa(getBase().getType()) && + isa(getResult().getType())) + return success(); + return emitOpError("requires owner/ref input and resource result"); +} + +LogicalResult BindOp::verify() { + auto findRealization = [&](Value base) -> Operation * { + SymbolRefAttr reference; + if (auto owner = dyn_cast(base.getType())) + reference = owner.getRealization(); + else if (auto ref = dyn_cast(base.getType())) + reference = ref.getRealization(); + if (!reference) + return nullptr; + ModelOp model = (*this)->getParentOfType(); + if (!model) + return nullptr; + for (Operation &operation : model.getBody().front()) + if (isa(operation) && + symbolName(&operation).getValue() == + reference.getRootReference().getValue()) + return &operation; + return nullptr; + }; + if (getKind() == "port") { + auto sourceOp = getSource().getDefiningOp(); + auto targetOp = getTarget().getDefiningOp(); + auto source = dyn_cast(getSource().getType()); + auto target = dyn_cast(getTarget().getType()); + DictionaryAttr sourceRecord = + sourceOp ? findEndpoint(findRealization(sourceOp.getBase()), "ports", + sourceOp.getAccessorAttr()) + : DictionaryAttr(); + DictionaryAttr targetRecord = + targetOp ? findEndpoint(findRealization(targetOp.getBase()), "ports", + targetOp.getAccessorAttr()) + : DictionaryAttr(); + if (!source || !target || !sourceRecord || !targetRecord || + sourceRecord.getAs("direction").getValue() != "output" || + targetRecord.getAs("direction").getValue() != "input" || + sourceRecord.getAs("interface") != + source.getInterface() || + sourceRecord.getAs("role") != source.getRole() || + sourceRecord.getAs("payload") != + source.getPayload() || + sourceRecord.getAs("protocol") != + source.getProtocol() || + targetRecord.getAs("interface") != + target.getInterface() || + targetRecord.getAs("role") != target.getRole() || + targetRecord.getAs("payload") != + target.getPayload() || + targetRecord.getAs("protocol") != + target.getProtocol()) + return emitOpError("port bind endpoints must match exact output/input " + "binding-lock records"); + if (sourceRecord.get("interface") != targetRecord.get("interface") || + sourceRecord.get("payload") != targetRecord.get("payload") || + sourceRecord.get("protocol") != targetRecord.get("protocol") || + sourceRecord.get("cardinality") != targetRecord.get("cardinality") || + sourceRecord.get("delegation") != targetRecord.get("delegation") || + sourceRecord.get("ownership") != targetRecord.get("ownership") || + sourceRecord.get("time_domain") != targetRecord.get("time_domain")) + return emitOpError( + "port bind endpoints must have identical interface, payload, " + "protocol, cardinality, delegation, ownership, and time domain"); + return success(); + } else if (getKind() == "resource") { + auto sourceOp = getSource().getDefiningOp(); + auto targetOp = getTarget().getDefiningOp(); + auto source = dyn_cast(getSource().getType()); + auto target = dyn_cast(getTarget().getType()); + DictionaryAttr sourceRecord = + sourceOp ? findEndpoint(findRealization(sourceOp.getBase()), + "resources", sourceOp.getAccessorAttr()) + : DictionaryAttr(); + DictionaryAttr targetRecord = + targetOp ? findEndpoint(findRealization(targetOp.getBase()), + "resources", targetOp.getAccessorAttr()) + : DictionaryAttr(); + if (!source || !target || !sourceRecord || !targetRecord || + sourceRecord.getAs("mode").getValue() != "initiator" || + targetRecord.getAs("mode").getValue() != "target" || + sourceRecord.getAs("resource") != + source.getResource() || + sourceRecord.getAs("role") != source.getRole() || + targetRecord.getAs("resource") != + target.getResource() || + targetRecord.getAs("role") != target.getRole()) + return emitOpError("resource bind endpoints must match exact " + "initiator/target binding-lock records"); + if (sourceRecord.get("resource") != targetRecord.get("resource") || + sourceRecord.get("delegation") != targetRecord.get("delegation") || + sourceRecord.get("ownership") != targetRecord.get("ownership") || + sourceRecord.get("time_domain") != targetRecord.get("time_domain")) + return emitOpError( + "resource bind endpoints must have identical resource kind, " + "delegation, ownership, and time domain"); + return success(); + } else if (getKind() == "pure_view") { + if (getSource() != getTarget() && + getSource().getType() == getTarget().getType() && + isa(getSource().getType())) + return success(); + } else if (getKind() == "export") { + auto target = getTarget().getDefiningOp(); + if (!target || getTarget() != target.getResult()) + return emitOpError( + "export bind target must be the exact result of acsim.export"); + if (getSource() != target.getValue()) + return emitOpError( + "export bind source must be the exact input of its target " + "acsim.export"); + if (getSource().getType() == getTarget().getType()) + return success(); + } + return emitOpError("typed binding endpoints are not exactly compatible"); +} + +LogicalResult InlineOp::verify() { + if (!isMemoryEffectFree(*this)) + return emitOpError("inline must remain effect-free"); + Type result = getResult().getType(); + if (getOperation()->getParentOfType()) { + if (!isa(result)) + return emitOpError( + "process inline result must be an integer, float, index, or " + "!acsim.value"); + } else if (!isa(result)) { + return emitOpError("module inline result must be exactly !acsim.expr"); + } + return success(); +} + +LogicalResult ProcessOp::verify() { + if (!isCanonicalIdentifier(getSymName())) + return emitOpError( + "generated process symbol must be a canonical C++ identifier"); + return verifyFingerprint(*this, getSpecializationFingerprintAttr(), + "specialization fingerprint"); +} + +LogicalResult LiveLoadOp::verify() { + return isa(getResult().getType()) + ? success() + : emitOpError("live load must produce a typed value"); +} + +LogicalResult LiveStoreOp::verify() { + return isa(getValue().getType()) + ? success() + : emitOpError("live store requires a typed value"); +} + +LogicalResult InvokeOp::verify() { + for (Type type : getResultTypes()) + if (!isa(type)) + return emitOpError( + "invoke results must be exact !acsim.value or !acsim.wake types"); + return success(); +} + +LogicalResult ContinueOp::verify() { return success(); } + +LogicalResult SuspendOp::verify() { + return isa(getWake().getType()) + ? success() + : emitOpError("requires an exact typed wake"); +} + +LogicalResult TerminateOp::verify() { + if (getStatus() != "success" && getStatus() != "failure") + return emitOpError("status must be exactly 'success' or 'failure'"); + return success(); +} + +LogicalResult ExportOp::verify() { + if (getValue().getType() != getResult().getType()) + return emitOpError("must preserve the exact exported type"); + if (isa( + getResult().getType())) + return emitOpError("owners, IDs, PCs, and wakes cannot escape a module"); + return success(); +} + +LogicalResult DispatchOp::verify() { + if (getObjectId() < 0 || getActivationId() < 0 || getPath().empty()) + return emitOpError( + "object and activation IDs must be non-negative and path non-empty"); + for (StringRef thunk : {getWork(), getXfer(), getReset(), getValidate()}) + if (!isCppQualifiedSymbol(thunk)) + return emitOpError( + "dispatch thunks must be non-empty declarative C++ symbols"); + return success(); +} + +LogicalResult ActivateOp::verify() { return success(); } + +LogicalResult ReturnOp::verify() { + if (!isa_and_nonnull((*this)->getParentOp())) + return emitOpError("must terminate an acsim.module body"); + return success(); +} + +LogicalResult verifyCanonicalACSimFile(mlir::ModuleOp module) { + unsigned models = 0; + bool hasACSimOperation = false; + ModelOp model; + SmallVector stack; + for (Operation &operation : llvm::reverse(*module.getBody())) + stack.push_back(&operation); + while (!stack.empty()) { + Operation *operation = stack.pop_back_val(); + if (operation->getName().getDialectNamespace() == "acsim") + hasACSimOperation = true; + if (auto candidate = dyn_cast(operation)) { + ++models; + model = candidate; + } + for (Region ®ion : llvm::reverse(operation->getRegions())) + for (Block &block : llvm::reverse(region)) + for (Operation &child : llvm::reverse(block)) + stack.push_back(&child); + } + if (!hasACSimOperation) + return success(); + if (models != 1) + return module.emitError("canonical ACSim requires exactly one acsim.model"); + if (model->getParentOp() != module) + return model.emitOpError( + "canonical acsim.model must be directly inside the file module"); + auto epoch = module->getAttrOfType("ac.contract_epoch"); + auto discardable = module->getDiscardableAttrs(); + if (!epoch || epoch.getValue() != "0.3" || + std::distance(discardable.begin(), discardable.end()) != 1) + return module.emitError("canonical ACSim file attributes must be exactly " + "ac.contract_epoch = \"0.3\""); + return success(); +} + +} // namespace acir::acsim + +#define GET_OP_CLASSES +#include "acir/Dialect/ACSim/ACSimOps.cpp.inc" diff --git a/components/agentic-circuit/lib/Dialect/ACSim/ACSimOpsTestHooks.h b/components/agentic-circuit/lib/Dialect/ACSim/ACSimOpsTestHooks.h new file mode 100644 index 00000000..a035a712 --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACSim/ACSimOpsTestHooks.h @@ -0,0 +1,70 @@ +#ifndef ACIR_LIB_DIALECT_ACSIM_ACSIMOPSTESTHOOKS_H +#define ACIR_LIB_DIALECT_ACSIM_ACSIMOPSTESTHOOKS_H + +#include + +namespace acir::acsim::detail { + +struct ModelVerificationWork { + uint64_t preflightOperationVisits = 0; + uint64_t preorderOperationVisits = 0; + uint64_t indexOperationVisits = 0; + uint64_t closureOperationVisits = 0; + uint64_t orderingOperationVisits = 0; + uint64_t constructionOperationVisits = 0; + uint64_t semanticOperationVisits = 0; + uint64_t runtimeOperationVisits = 0; + uint64_t edgeVisits = 0; + uint64_t referenceLookups = 0; + uint64_t expandedOwnerRows = 0; + uint64_t expandedRuntimeRows = 0; + + uint64_t total() const { + return preflightOperationVisits + preorderOperationVisits + + indexOperationVisits + closureOperationVisits + + orderingOperationVisits + constructionOperationVisits + + semanticOperationVisits + runtimeOperationVisits + edgeVisits + + referenceLookups + expandedOwnerRows + expandedRuntimeRows; + } +}; + +class ScopedModelVerificationWorkCollector { +public: + explicit ScopedModelVerificationWorkCollector(ModelVerificationWork &work); + ~ScopedModelVerificationWorkCollector(); + + ScopedModelVerificationWorkCollector( + const ScopedModelVerificationWorkCollector &) = delete; + ScopedModelVerificationWorkCollector & + operator=(const ScopedModelVerificationWorkCollector &) = delete; + +private: + ModelVerificationWork *previous; +}; + +struct ModelVerificationLimits { + uint64_t maxNodes = 1ULL << 20; + uint64_t maxEdges = 1ULL << 22; + uint64_t maxRegionDepth = 512; + uint64_t maxExpandedObjects = 1ULL << 20; + uint64_t maxAttributeElements = 1ULL << 20; + uint64_t maxAttributeStringBytes = 1ULL << 24; + uint64_t maxDependencyNodes = 1ULL << 20; +}; + +class ScopedModelVerificationLimits { +public: + explicit ScopedModelVerificationLimits(const ModelVerificationLimits &limits); + ~ScopedModelVerificationLimits(); + + ScopedModelVerificationLimits(const ScopedModelVerificationLimits &) = delete; + ScopedModelVerificationLimits & + operator=(const ScopedModelVerificationLimits &) = delete; + +private: + const ModelVerificationLimits *previous; +}; + +} // namespace acir::acsim::detail + +#endif // ACIR_LIB_DIALECT_ACSIM_ACSIMOPSTESTHOOKS_H diff --git a/components/agentic-circuit/lib/Dialect/ACSim/ACSimTypes.cpp b/components/agentic-circuit/lib/Dialect/ACSim/ACSimTypes.cpp new file mode 100644 index 00000000..63ddebe6 --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACSim/ACSimTypes.cpp @@ -0,0 +1,84 @@ +#include "acir/Dialect/ACSim/ACSimTypes.h" +#include "acir/Dialect/ACSim/ACSimOps.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/DialectImplementation.h" +#include "llvm/ADT/TypeSwitch.h" + +using namespace mlir; + +namespace acir::acsim { + +Type ArrayType::parse(AsmParser &parser) { + if (failed(parser.parseLess()) || failed(parser.parseLSquare())) + return {}; + + SmallVector shape; + if (failed(parser.parseOptionalRSquare())) { + do { + int64_t extent; + if (failed(parser.parseInteger(extent))) + return {}; + shape.push_back(extent); + } while (succeeded(parser.parseOptionalComma())); + if (failed(parser.parseRSquare())) + return {}; + } + + Type elementType; + if (failed(parser.parseComma()) || failed(parser.parseType(elementType)) || + failed(parser.parseGreater())) + return {}; + return ArrayType::getChecked( + [&] { return parser.emitError(parser.getCurrentLocation()); }, + parser.getContext(), DenseI64ArrayAttr::get(parser.getContext(), shape), + elementType); +} + +void ArrayType::print(AsmPrinter &printer) const { + printer << "<["; + llvm::interleaveComma(getShape().asArrayRef(), printer, + [&](int64_t extent) { printer << extent; }); + printer << "], " << getElementType() << ">"; +} + +LogicalResult ArrayType::verify(function_ref emitError, + DenseI64ArrayAttr shape, Type elementType) { + if (!shape) + return emitError() << "array shape must be concrete"; + if (shape.empty()) + return emitError() << "array shape must have at least one extent"; + uint64_t volume = 1; + for (int64_t extent : shape.asArrayRef()) { + if (extent < 0) + return emitError() << "array extents must be non-negative"; + if (extent == 0) { + volume = 0; + continue; + } + uint64_t unsignedExtent = static_cast(extent); + if (volume > kMaxArrayVolume / unsignedExtent) + return emitError() << "array volume exceeds ACSim capability " + << kMaxArrayVolume; + volume *= unsignedExtent; + } + if (!elementType) + return emitError() << "array element type must be concrete"; + return success(); +} + +} // namespace acir::acsim + +#define GET_TYPEDEF_CLASSES +#include "acir/Dialect/ACSim/ACSimTypes.cpp.inc" + +void acir::acsim::ACSimDialect::initialize() { + addTypes< +#define GET_TYPEDEF_LIST +#include "acir/Dialect/ACSim/ACSimTypes.cpp.inc" + >(); + addOperations< +#define GET_OP_LIST +#include "acir/Dialect/ACSim/ACSimOps.cpp.inc" + >(); +} diff --git a/components/agentic-circuit/lib/Dialect/ACSim/CMakeLists.txt b/components/agentic-circuit/lib/Dialect/ACSim/CMakeLists.txt new file mode 100644 index 00000000..46fc6ba2 --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/ACSim/CMakeLists.txt @@ -0,0 +1,20 @@ +add_acir_library(ACSimDialect SOURCES + ACSimDialect.cpp + ACSimOps.cpp + ACSimTypes.cpp +) +add_dependencies(ACSimDialect + ACSimDialectIncGen + ACSimOpsIncGen + ACSimTypesIncGen +) +target_link_libraries(ACSimDialect PUBLIC + MLIRControlFlowDialect + MLIRSideEffectInterfaces +) +target_include_directories(ACSimDialect PUBLIC + "$" + "$" + "$" +) +add_library(AgenticCircuit::ACSimDialect ALIAS ACSimDialect) diff --git a/components/agentic-circuit/lib/Dialect/CMakeLists.txt b/components/agentic-circuit/lib/Dialect/CMakeLists.txt new file mode 100644 index 00000000..a0fe9b40 --- /dev/null +++ b/components/agentic-circuit/lib/Dialect/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(ACIR) +add_subdirectory(ACSim) diff --git a/components/agentic-circuit/lib/Transforms/CMakeLists.txt b/components/agentic-circuit/lib/Transforms/CMakeLists.txt new file mode 100644 index 00000000..d3b5b828 --- /dev/null +++ b/components/agentic-circuit/lib/Transforms/CMakeLists.txt @@ -0,0 +1,25 @@ +add_acir_library(ACIRTransforms SOURCES + VerifyModel.cpp + FreezeTopology.cpp + CanonicalizeModel.cpp + ResolveBindings.cpp + NormalizeACIRFile.cpp + VerifyACIRFile.cpp + LowerProcessState.cpp +) +add_dependencies(ACIRTransforms ACIRTransformsIncGen) +target_link_libraries(ACIRTransforms PUBLIC + ACIRAnalysis + ACIRBindings + ACIRDialect + MLIRPass +) +target_include_directories(ACIRTransforms PUBLIC + "$" + "$" + "$" +) +target_include_directories(ACIRTransforms PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +add_library(AgenticCircuit::ACIRTransforms ALIAS ACIRTransforms) diff --git a/components/agentic-circuit/lib/Transforms/CanonicalizeModel.cpp b/components/agentic-circuit/lib/Transforms/CanonicalizeModel.cpp new file mode 100644 index 00000000..82b6cca1 --- /dev/null +++ b/components/agentic-circuit/lib/Transforms/CanonicalizeModel.cpp @@ -0,0 +1,179 @@ +#include "acir/Transforms/Passes.h" + +#include "Analysis/ModelAnalysisInternal.h" +#include "acir/Analysis/ModelAnalysis.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/ACIRResources.h" +#include "mlir/IR/SymbolTable.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringSwitch.h" + +using namespace mlir; + +namespace acir { +namespace { + +std::string token(Attribute attribute) { + std::string storage; + llvm::raw_string_ostream stream(storage); + stream << attribute; + return storage; +} + +unsigned topLevelRank(Operation *operation) { + StringRef name = operation->getName().getStringRef(); + return llvm::StringSwitch(name) + .Case("ac.system", 0) + .Cases({"ac.type_scope", "ac.type_alias", "ac.struct", "ac.enum", + "ac.union", "ac.packet", "ac.transaction"}, + 1) + .Cases({"ac.interface", "ac.protocol"}, 2) + .Case("func.func", 3) + .Cases({"ac.module", "ac.module.extern", "ac.module.generated"}, 4) + .Default(5); +} + +unsigned graphRank(Operation *operation) { + StringRef name = operation->getName().getStringRef(); + if (name == "ac.return") + return 100; + return llvm::StringSwitch(name) + .Cases({"ac.instance", "ac.array", "ac.instances", "ac.view"}, 0) + .Cases({"ac.queue", "ac.event_queue", "ac.resource", "ac.address_space", + "ac.address_map", "ac.time_domain"}, + 1) + .Case("ac.process", 2) + .Case("ac.stat", 3) + .Cases({"ac.require", "ac.ensure"}, 4) + .Default(5); +} + +unsigned typeScopeRank(Operation *) { return 0; } + +unsigned interfaceRank(Operation *operation) { + return llvm::StringSwitch(operation->getName().getStringRef()) + .Case("ac.role", 0) + .Case("ac.port", 1) + .Case("ac.guarantee", 2) + .Default(3); +} + +unsigned protocolRank(Operation *operation) { + return llvm::StringSwitch(operation->getName().getStringRef()) + .Case("ac.role", 0) + .Case("ac.state", 1) + .Case("ac.event", 2) + // Transition order is semantically ordered and must remain stable. + .Case("ac.transition", 3) + .Case("ac.guarantee", 4) + .Default(5); +} + +std::string stableKey(Operation *operation, unsigned rank) { + std::string rankKey = std::to_string(rank); + rankKey.insert(rankKey.begin(), 3 - std::min(3, rankKey.size()), '0'); + std::string key = rankKey + '|'; + key.append(operation->getName().getStringRef()); + key.push_back('|'); + if (auto name = operation->getAttrOfType( + SymbolTable::getSymbolAttrName())) + key.append(name.getValue()); + key.push_back('|'); + SmallVector attributes(operation->getAttrs().begin(), + operation->getAttrs().end()); + llvm::sort(attributes, [](NamedAttribute left, NamedAttribute right) { + return left.getName().getValue() < right.getName().getValue(); + }); + for (NamedAttribute attribute : attributes) { + StringRef name = attribute.getName().getValue(); + if (name.starts_with("ac.frozen_") || name == "ac.freeze_proven") + continue; + key.append(name); + key.push_back('='); + key.append(token(attribute.getValue())); + key.push_back(';'); + } + return key; +} + +LogicalResult sortBlock(Block &block, bool topologyFrozen, + llvm::function_ref rank) { + SmallVector current; + for (Operation &operation : block) + current.push_back(&operation); + SmallVector sorted = current; + llvm::stable_sort(sorted, [&](Operation *left, Operation *right) { + unsigned leftRank = rank(left); + unsigned rightRank = rank(right); + if (leftRank == rightRank && isa(left) && + isa(right)) + return false; + return stableKey(left, leftRank) < stableKey(right, rightRank); + }); + if (current == sorted) + return success(); + if (topologyFrozen) + return block.getParentOp()->emitError( + "topology declaration order was mutated after ac-freeze-topology"); + for (Operation *operation : sorted) + operation->moveBefore(&block, block.end()); + return success(); +} + +} // namespace + +LogicalResult canonicalizeModel(ModuleOp model) { + if (failed(detail::preflightModelStructure(model))) + return failure(); + if (detail::hasTopologyFreezeEvidence(model)) + return verifyModel(model); + bool frozen = false; + ac::normalizeAddressMaps(model); + if (failed(sortBlock(*model.getBody(), frozen, topLevelRank))) + return failure(); + for (ac::ModuleOp module : model.getOps()) + if (!module.getBody().empty() && + failed(sortBlock(module.getBody().front(), frozen, graphRank))) + return failure(); + for (ac::TypeScopeOp scope : model.getOps()) + if (!scope.getBody().empty() && + failed(sortBlock(scope.getBody().front(), frozen, typeScopeRank))) + return failure(); + for (ac::InterfaceOp interface : model.getOps()) + if (!interface.getBody().empty() && + failed(sortBlock(interface.getBody().front(), frozen, interfaceRank))) + return failure(); + for (ac::ProtocolOp protocol : model.getOps()) + if (!protocol.getBody().empty() && + failed(sortBlock(protocol.getBody().front(), frozen, protocolRank))) + return failure(); + + UnknownLoc unknown = UnknownLoc::get(model.getContext()); + model.walk([&](Operation *operation) { + operation->setLoc(unknown); + for (Region ®ion : operation->getRegions()) + for (Block &block : region) + for (BlockArgument argument : block.getArguments()) + argument.setLoc(unknown); + }); + return success(); +} + +namespace { +#define GEN_PASS_DEF_CANONICALIZEMODELPASS +#include "acir/Transforms/Passes.h.inc" + +struct CanonicalizeModelPass + : impl::CanonicalizeModelPassBase { + void runOnOperation() override { + if (failed(canonicalizeModel(getOperation()))) + signalPassFailure(); + } +}; +} // namespace + +std::unique_ptr createCanonicalizeModelPass() { + return std::make_unique(); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Transforms/FreezeTopology.cpp b/components/agentic-circuit/lib/Transforms/FreezeTopology.cpp new file mode 100644 index 00000000..f140a4dc --- /dev/null +++ b/components/agentic-circuit/lib/Transforms/FreezeTopology.cpp @@ -0,0 +1,268 @@ +#include "acir/Transforms/Passes.h" + +#include "Analysis/ModelAnalysisInternal.h" +#include "Analysis/ModelAnalysisTestHooks.h" +#include "acir/Analysis/ModelAnalysis.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "mlir/IR/SymbolTable.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" + +using namespace mlir; + +namespace acir { +namespace { + +std::string manifestKey(SymbolRefAttr owner, StringRef kind) { + std::string key; + llvm::raw_string_ostream stream(key); + stream << owner << '\0' << kind; + return key; +} + +class ManifestOwnerIndex { +public: + explicit ManifestOwnerIndex(ArrayAttr manifest) { + for (Attribute attribute : manifest) { + if (detail::activeFreezeWork) + ++detail::activeFreezeWork->manifestIndexInsertions; + auto record = cast(attribute); + index[manifestKey(record.getAs("owner"), + record.getAs("kind").getValue())] + .push_back(record); + } + } + + FailureOr lookup(SymbolRefAttr owner, StringRef kind) const { + if (detail::activeFreezeWork) + ++detail::activeFreezeWork->manifestOwnerLookups; + auto found = index.find(manifestKey(owner, kind)); + if (found == index.end() || found->second.size() != 1) + return failure(); + return found->second.front(); + } + +private: + llvm::StringMap> index; +}; + +std::string declarationKey(SymbolRefAttr reference) { + std::string key; + llvm::raw_string_ostream(key) << reference; + return key; +} + +class DeclarationIndex { +public: + LogicalResult build(ModuleOp model) { + for (ac::ModuleOp definition : model.getOps()) { + if (definition.getBody().empty()) + continue; + for (Operation &operation : definition.getBody().front()) { + auto name = operation.getAttrOfType( + SymbolTable::getSymbolAttrName()); + if (!name) + continue; + SymbolRefAttr reference = SymbolRefAttr::get( + model.getContext(), definition.getSymName(), + {FlatSymbolRefAttr::get(model.getContext(), name.getValue())}); + if (detail::activeFreezeWork) + ++detail::activeFreezeWork->declarationIndexInsertions; + if (!index.try_emplace(declarationKey(reference), &operation).second) + return operation.emitOpError() + << "duplicate freeze declaration index key '" << reference + << "'"; + } + } + return success(); + } + + Operation *lookup(SymbolRefAttr reference) const { + if (detail::activeFreezeWork) + ++detail::activeFreezeWork->declarationLookups; + auto found = index.find(declarationKey(reference)); + return found == index.end() ? nullptr : found->second; + } + +private: + llvm::StringMap index; +}; + +LogicalResult freezeTopology(ModuleOp model) { + if (failed(detail::preflightModelStructure(model))) + return failure(); + if (detail::hasTopologyFreezeEvidence(model)) + return verifyModel(model); + if (failed(canonicalizeModel(model))) + return failure(); + + ModelAnalysis analysis(model); + if (failed(analysis.verify()) || failed(analysis.verifyFreezeContracts())) + return failure(); + + ac::SystemOp selected; + for (ac::SystemOp system : model.getOps()) + if (system.getSelected()) { + selected = system; + break; + } + if (!selected) + return model.emitError( + "topology freeze requires exactly one selected ac.system"); + if (!selected.getPrimaryWorkloadAttr()) + return selected.emitOpError( + "selected system requires one canonical primary workload at topology " + "freeze"); + + FailureOr ownerManifest = detail::buildFrozenOwnerManifest(model); + if (failed(ownerManifest)) + return failure(); + Builder builder(model.getContext()); + model->setAttr("ac.freeze_epoch", builder.getStringAttr("0.3")); + model->setAttr( + "ac.frozen_system", + FlatSymbolRefAttr::get(model.getContext(), selected.getSymName())); + model->setAttr("ac.frozen_owners", *ownerManifest); + ManifestOwnerIndex ownerIndex(*ownerManifest); + DeclarationIndex declarationIndex; + if (failed(declarationIndex.build(model))) + return failure(); + + SymbolRefAttr workload = selected.getPrimaryWorkloadAttr(); + FailureOr workloadOwner = + ownerIndex.lookup(workload, "ac.process"); + if (failed(workloadOwner)) + return selected.emitOpError() + << "primary workload '" << workload + << "' has no unique elaborated absolute owner"; + model->setAttr( + "ac.frozen_primary_workload", + builder.getDictionaryAttr({ + builder.getNamedAttr("reference", workload), + builder.getNamedAttr("path", (*workloadOwner).get("path")), + builder.getNamedAttr("stable_id", (*workloadOwner).get("stable_id")), + })); + + SmallVector frozenInstrumentation; + for (Attribute attribute : selected.getInstrumentation()) { + auto reference = cast(attribute); + ArrayRef nested = reference.getNestedReferences(); + SymbolRefAttr processReference = SymbolRefAttr::get( + model.getContext(), reference.getRootReference(), {nested.front()}); + FailureOr processOwner = + ownerIndex.lookup(processReference, "ac.process"); + if (failed(processOwner)) + return selected.emitOpError() + << "instrumentation '" << reference + << "' has no unique elaborated process owner"; + StringRef local = nested.back().getValue(); + frozenInstrumentation.push_back(builder.getDictionaryAttr({ + builder.getNamedAttr("reference", reference), + builder.getNamedAttr( + "path", builder.getStringAttr( + ((*processOwner).getAs("path").getValue() + + "." + local) + .str())), + builder.getNamedAttr( + "stable_id", + builder.getStringAttr( + ((*processOwner).getAs("stable_id").getValue() + + "/" + local) + .str())), + })); + } + model->setAttr("ac.frozen_instrumentation", + builder.getArrayAttr(frozenInstrumentation)); + + // Persist the relevant absolute owner set on each declaration. Reused + // definitions intentionally receive multiple records rather than a lossy + // definition-local identity. + llvm::DenseMap> byDeclaration; + for (Attribute attribute : *ownerManifest) { + auto record = cast(attribute); + if (record.getAs("kind").getValue() == "ac.system_root") + continue; + Operation *declaration = + declarationIndex.lookup(record.getAs("owner")); + if (!declaration) + return model.emitError() + << "frozen owner manifest references an unindexed declaration '" + << record.getAs("owner") << "'"; + byDeclaration[declaration].push_back(record); + + if (record.getAs("kind").getValue() != "ac.process") + continue; + ArrayAttr traces = record.getAs("trace_sources"); + if (!traces) + continue; + auto process = dyn_cast(declaration); + if (!process) + return declaration->emitOpError( + "process owner record does not resolve to ac.process"); + process.getBody().walk([&](ac::TraceOpenOp trace) { + if (!llvm::is_contained(traces, builder.getStringAttr(trace.getSource()))) + return; + trace->setAttr( + "ac.frozen_owner", + builder.getDictionaryAttr({ + builder.getNamedAttr("path", record.get("path")), + builder.getNamedAttr("stable_id", record.get("stable_id")), + builder.getNamedAttr("source", trace.getSourceAttr()), + })); + }); + } + for (auto &[declaration, records] : byDeclaration) { + llvm::sort(records, [](Attribute left, Attribute right) { + auto leftRecord = cast(left); + auto rightRecord = cast(right); + return leftRecord.getAs("path").getValue() < + rightRecord.getAs("path").getValue(); + }); + declaration->setAttr("ac.frozen_owners", builder.getArrayAttr(records)); + } + + for (ac::ModuleOp module : model.getOps()) { + if (module.getBody().empty()) + continue; + for (Operation &operation : module.getBody().front()) + if (isa(operation)) + operation.setAttr("ac.freeze_proven", builder.getBoolAttr(true)); + } + + LogicalResult skeletonResult = success(); + model.walk([&](ac::ProcessOp process) { + if (failed(skeletonResult)) + return; + FailureOr skeleton = detail::buildFrozenProcessSkeleton(process); + if (failed(skeleton)) { + skeletonResult = failure(); + return; + } + process->setAttr("ac.frozen_process_skeleton", *skeleton); + }); + if (failed(skeletonResult)) + return failure(); + + model->setAttr("ac.topology_frozen", builder.getBoolAttr(true)); + model->setAttr("ac.topology_digest", + builder.getStringAttr(detail::computeTopologyDigest(model))); + return verifyModel(model); +} + +#define GEN_PASS_DEF_FREEZETOPOLOGYPASS +#include "acir/Transforms/Passes.h.inc" + +struct FreezeTopologyPass : impl::FreezeTopologyPassBase { + void runOnOperation() override { + if (failed(freezeTopology(getOperation()))) + signalPassFailure(); + } +}; + +} // namespace + +std::unique_ptr createFreezeTopologyPass() { + return std::make_unique(); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Transforms/LowerProcessState.cpp b/components/agentic-circuit/lib/Transforms/LowerProcessState.cpp new file mode 100644 index 00000000..bf18eaff --- /dev/null +++ b/components/agentic-circuit/lib/Transforms/LowerProcessState.cpp @@ -0,0 +1,39 @@ +#include "acir/Transforms/Passes.h" + +#include "acir/Analysis/ProcessStatePlan.h" + +#include "mlir/Pass/Pass.h" + +using namespace mlir; + +namespace acir { +namespace { +#define GEN_PASS_DEF_LOWERPROCESSSTATEPASS +#include "acir/Transforms/Passes.h.inc" + +struct LowerProcessStatePass + : impl::LowerProcessStatePassBase { + using Base = impl::LowerProcessStatePassBase; + using Base::Base; + + void runOnOperation() final { + ProcessStateLimits limits; + auto plans = planProcessState(getOperation(), limits); + if (failed(plans)) { + signalPassFailure(); + return; + } + if (failed(verifyProcessStatePlan(*plans, limits))) { + signalPassFailure(); + return; + } + } +}; + +} // namespace + +std::unique_ptr createLowerProcessStatePass() { + return std::make_unique(); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Transforms/NormalizeACIRFile.cpp b/components/agentic-circuit/lib/Transforms/NormalizeACIRFile.cpp new file mode 100644 index 00000000..812b4dc9 --- /dev/null +++ b/components/agentic-circuit/lib/Transforms/NormalizeACIRFile.cpp @@ -0,0 +1,37 @@ +#include "acir/Transforms/Passes.h" + +#include "Dialect/ACIR/ProcessLowerability.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/ACIRResources.h" + +namespace acir { +namespace { + +class NormalizeACIRFilePass final + : public mlir::PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(NormalizeACIRFilePass) + + llvm::StringRef getArgument() const override { return "normalize-ac-file"; } + llvm::StringRef getDescription() const override { + return "Normalize deterministic Agentic Circuit declaration order"; + } + + void runOnOperation() override { + mlir::ModuleOp module = getOperation(); + if (mlir::failed(ac::preflightRawModelStructure(module))) { + signalPassFailure(); + return; + } + ac::normalizeAddressMaps(module); + } +}; + +} // namespace + +std::unique_ptr createNormalizeACIRFilePass() { + return std::make_unique(); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Transforms/ResolveBindings.cpp b/components/agentic-circuit/lib/Transforms/ResolveBindings.cpp new file mode 100644 index 00000000..b8bded06 --- /dev/null +++ b/components/agentic-circuit/lib/Transforms/ResolveBindings.cpp @@ -0,0 +1,279 @@ +#include "acir/Transforms/ResolveBindings.h" + +#include "acir/Analysis/ModelAnalysis.h" +#include "acir/Dialect/ACIR/ACIROps.h" + +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/Operation.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringSet.h" +#include "llvm/Support/Errc.h" + +#include +#include +#include + +using namespace mlir; + +namespace acir { +namespace { + +llvm::Error parameterError(const llvm::Twine &message) { + return llvm::createStringError(llvm::errc::invalid_argument, + "ACLOWER-PARAM-PHASE: %s", + message.str().c_str()); +} + +llvm::Error bindingError(llvm::StringRef code, ac::ModuleExternOp module, + const llvm::Twine &detail) { + llvm::StringRef binding = + module.getImplementation().getAs("name").getValue(); + return llvm::createStringError( + llvm::errc::invalid_argument, "%s: key=@%s binding=%s %s", + code.str().c_str(), module.getSymName().str().c_str(), + binding.str().c_str(), detail.str().c_str()); +} + +std::string token(Type type) { + std::string storage; + llvm::raw_string_ostream output(storage); + output << type; + return storage; +} + +std::string token(Attribute attribute) { + std::string storage; + llvm::raw_string_ostream output(storage); + output << attribute; + return storage; +} + +std::string attributeTypeToken(Attribute attribute) { + if (auto typed = dyn_cast(attribute)) + return token(typed.getType()); + if (auto dictionary = dyn_cast(attribute)) + if (auto amount = dictionary.getAs("value")) + if (dictionary.size() == 2 && dictionary.getAs("unit")) + return token(amount.getType()); + return "symbol"; +} + +llvm::Expected staticValue(Attribute attribute) { + if (auto boolean = dyn_cast(attribute)) + return llvm::json::Value(boolean.getValue()); + if (auto integer = dyn_cast(attribute)) { + const llvm::APInt &value = integer.getValue(); + if (!value.isSignedIntN(64)) + return parameterError("integer static value exceeds signed i64"); + return llvm::json::Value(value.getSExtValue()); + } + if (auto floating = dyn_cast(attribute)) { + double value = floating.getValueAsDouble(); + if (!std::isfinite(value) || (std::signbit(value) && value == 0.0)) + return parameterError( + "floating static value is non-finite or negative zero"); + return llvm::json::Value(value); + } + if (auto string = dyn_cast(attribute)) + return llvm::json::Value(string.getValue()); + if (auto array = dyn_cast(attribute)) { + llvm::json::Array values; + values.reserve(array.size()); + for (Attribute element : array) { + auto value = staticValue(element); + if (!value) + return value.takeError(); + values.push_back(std::move(*value)); + } + return llvm::json::Value(std::move(values)); + } + if (auto dictionary = dyn_cast(attribute)) { + llvm::json::Object values; + for (NamedAttribute named : dictionary) { + auto value = staticValue(named.getValue()); + if (!value) + return value.takeError(); + values[named.getName().getValue()] = std::move(*value); + } + return llvm::json::Value(std::move(values)); + } + if (isa(attribute)) + return llvm::json::Value(token(attribute)); + return parameterError(llvm::Twine("unsupported static attribute ") + + token(attribute)); +} + +llvm::Error normalizeRequest(ac::ModuleExternOp module, + const bindings::BindingRequest &request) { + std::string expectedKey = (llvm::Twine("@") + module.getSymName()).str(); + llvm::StringRef declaredBinding = + module.getImplementation().getAs("name").getValue(); + if (request.resolutionKey != expectedKey || + request.binding != declaredBinding) + return bindingError( + "ACLOWER-BINDING-MISSING", module, + "frozen request identity does not match the declaration"); + if (request.functionType != token(module.getFunctionType())) + return bindingError("ACLOWER-TYPE-MISMATCH", module, + llvm::Twine("function_type expected=") + + request.functionType + + " actual=" + token(module.getFunctionType())); + if (request.results.size() != module.getFunctionType().getNumResults()) + return bindingError("ACLOWER-TYPE-MISMATCH", module, + "frozen result count differs from the declaration"); + for (auto [required, actual] : + llvm::zip_equal(request.results, module.getFunctionType().getResults())) + if (required.acirType != token(actual)) + return bindingError("ACLOWER-TYPE-MISMATCH", module, + llvm::Twine("result '") + required.name + + "' expected=" + required.acirType + + " actual=" + token(actual)); + + DictionaryAttr staticParameters = module.getStaticParams(); + llvm::StringSet<> consumed; + for (const bindings::ParameterRequirement ¶meter : request.parameters) { + Attribute attribute = staticParameters.get(parameter.name); + if (!attribute) + return bindingError("ACLOWER-PARAM-PHASE", module, + llvm::Twine("missing frozen parameter '") + + parameter.name + "'"); + auto value = staticValue(attribute); + if (!value) + return value.takeError(); + if (parameter.acirType != attributeTypeToken(attribute) || + parameter.value != *value) + return bindingError("ACLOWER-PARAM-PHASE", module, + llvm::Twine("normalized parameter '") + + parameter.name + "' differs"); + consumed.insert(parameter.name); + } + for (NamedAttribute named : staticParameters) { + llvm::StringRef name = named.getName().getValue(); + if (!consumed.contains(name)) + return bindingError("ACLOWER-PARAM-PHASE", module, + llvm::Twine("unexpected frozen parameter '") + name + + "'"); + } + return llvm::Error::success(); +} + +llvm::Expected> +collectRequests(ModuleOp module, + llvm::ArrayRef explicitRequests) { + std::vector requests(explicitRequests.begin(), + explicitRequests.end()); + llvm::sort(requests, [](const bindings::BindingRequest &left, + const bindings::BindingRequest &right) { + return std::tie(left.resolutionKey, left.binding) < + std::tie(right.resolutionKey, right.binding); + }); + llvm::StringSet<> consumed; + for (const bindings::BindingRequest &request : requests) { + ac::ModuleExternOp matched; + for (ac::ModuleExternOp external : module.getOps()) + if (request.resolutionKey == + (llvm::Twine("@") + external.getSymName()).str()) { + matched = external; + break; + } + if (!matched) + return llvm::createStringError( + llvm::errc::invalid_argument, + "ACLOWER-BINDING-MISSING: key=%s binding=%s frozen request has no " + "external declaration", + request.resolutionKey.c_str(), request.binding.c_str()); + if (!consumed.insert(request.resolutionKey).second) + return bindingError("ACLOWER-BINDING-AMBIGUOUS", matched, + "duplicate frozen request key"); + if (llvm::Error error = normalizeRequest(matched, request)) + return std::move(error); + } + for (ac::ModuleExternOp external : module.getOps()) { + std::string key = (llvm::Twine("@") + external.getSymName()).str(); + if (!consumed.contains(key)) + return bindingError("ACLOWER-BINDING-MISSING", external, + "exact frozen architecture request is absent"); + } + return requests; +} + +class ResolveBindingsPass + : public PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(ResolveBindingsPass) + + explicit ResolveBindingsPass(ResolveBindingsPassOptions options) + : options(std::move(options)) {} + + llvm::StringRef getArgument() const override { + return "ac-resolve-gfsim-bindings"; + } + + llvm::StringRef getDescription() const override { + return "Resolve exact metadata-only gfsim bindings and emit a canonical " + "lock"; + } + + void runOnOperation() override { + ModuleOp module = getOperation(); + if (options.lockOutputPath.empty()) { + module.emitError( + "ACLOWER-BINDING-OPTIONS: binding lock output path is required"); + signalPassFailure(); + return; + } + auto result = resolveModuleBindings(module, options); + if (!result) { + module.emitError(llvm::toString(result.takeError())); + signalPassFailure(); + return; + } + if (llvm::Error error = bindings::emitBindingLockAtomically( + *result, options.lockOutputPath)) { + module.emitError(llvm::toString(std::move(error))); + signalPassFailure(); + return; + } + markAllAnalysesPreserved(); + } + +private: + ResolveBindingsPassOptions options; +}; + +} // namespace + +llvm::Expected +resolveModuleBindings(ModuleOp module, + const ResolveBindingsPassOptions &options) { + auto frozen = module->getAttrOfType("ac.topology_frozen"); + auto epoch = module->getAttrOfType("ac.freeze_epoch"); + if (!frozen || !frozen.getValue() || !epoch || epoch.getValue() != "0.3") + return llvm::createStringError( + llvm::errc::invalid_argument, + "ACLOWER-BINDING-MISSING: frozen ACIR topology epoch 0.3 is required"); + if (failed(verifyModel(module))) + return llvm::createStringError(llvm::errc::invalid_argument, + "ACLOWER-FINGERPRINT: frozen ACIR topology " + "integrity verification failed"); + if (options.profile.empty() || options.target.empty()) + return llvm::createStringError( + llvm::errc::invalid_argument, + "ACLOWER-PROFILE: selected profile and target must be explicit"); + for (const bindings::BindingCandidate &candidate : options.candidates) + if (llvm::Error error = candidate.record().validateFingerprint()) + return std::move(error); + auto requests = collectRequests(module, options.requests); + if (!requests) + return requests.takeError(); + return bindings::resolveBindings(options.candidates, *requests, + options.profile, options.target); +} + +std::unique_ptr +createResolveBindingsPass(ResolveBindingsPassOptions options) { + return std::make_unique(std::move(options)); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Transforms/VerifyACIRFile.cpp b/components/agentic-circuit/lib/Transforms/VerifyACIRFile.cpp new file mode 100644 index 00000000..24849a0e --- /dev/null +++ b/components/agentic-circuit/lib/Transforms/VerifyACIRFile.cpp @@ -0,0 +1,100 @@ +#include "acir/Transforms/Passes.h" + +#include "Dialect/ACIR/ProcessLowerability.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/ACIRResources.h" +#include "acir/Dialect/ACSim/ACSimOps.h" + +namespace acir { +namespace { + +class VerifyACIRFilePass final + : public mlir::PassWrapper> { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(VerifyACIRFilePass) + + llvm::StringRef getArgument() const override { return "verify-ac-file"; } + llvm::StringRef getDescription() const override { + return "Verify the Agentic Circuit epoch and whole-file legality"; + } + + void runOnOperation() override { + mlir::ModuleOp module = getOperation(); + if (mlir::failed(ac::preflightRawModelStructure(module))) { + signalPassFailure(); + return; + } + auto epoch = module->getAttrOfType("ac.contract_epoch"); + if (!epoch || epoch.getValue() != "0.3") { + module.emitError( + "expected top-level 'ac.contract_epoch' string attribute equal to " + "\"0.3\""); + signalPassFailure(); + return; + } + if (mlir::failed(acsim::verifyCanonicalACSimFile(module))) { + signalPassFailure(); + return; + } + + mlir::WalkResult result = module.walk([&](mlir::Operation *operation) { + if (mlir::failed(ac::verifyTopologyTypeUses(operation))) + return mlir::WalkResult::interrupt(); + auto rejectChannel = [&](mlir::Attribute attribute) { + return attribute && attribute + .walk([](ac::ChannelType) { + return mlir::WalkResult::interrupt(); + }) + .wasInterrupted(); + }; + for (mlir::NamedAttribute attribute : operation->getAttrs()) { + if (mlir::isa(operation) && attribute.getName() == "type") + continue; + if (rejectChannel(attribute.getValue())) { + operation->emitError("channel type is only permitted in an " + "ac.interface channel declaration"); + return mlir::WalkResult::interrupt(); + } + } + if ((!mlir::isa(operation) && + rejectChannel(operation->getPropertiesAsAttribute())) || + rejectChannel(mlir::LocationAttr(operation->getLoc()))) { + operation->emitError("channel type is only permitted in an " + "ac.interface channel declaration"); + return mlir::WalkResult::interrupt(); + } + for (mlir::Type type : operation->getOperandTypes()) + if (ac::containsChannelType(type)) { + operation->emitError("channel type is only permitted in an " + "ac.interface channel declaration"); + return mlir::WalkResult::interrupt(); + } + for (mlir::Type type : operation->getResultTypes()) + if (ac::containsChannelType(type)) { + operation->emitError("channel type is only permitted in an " + "ac.interface channel declaration"); + return mlir::WalkResult::interrupt(); + } + for (mlir::Region ®ion : operation->getRegions()) + for (mlir::Block &block : region) + for (mlir::BlockArgument argument : block.getArguments()) + if (ac::containsChannelType(argument.getType())) { + operation->emitError("channel type is only permitted in an " + "ac.interface channel declaration"); + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + if (result.wasInterrupted()) + signalPassFailure(); + } +}; + +} // namespace + +std::unique_ptr createVerifyACIRFilePass() { + return std::make_unique(); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/Transforms/VerifyModel.cpp b/components/agentic-circuit/lib/Transforms/VerifyModel.cpp new file mode 100644 index 00000000..2cb190ed --- /dev/null +++ b/components/agentic-circuit/lib/Transforms/VerifyModel.cpp @@ -0,0 +1,24 @@ +#include "acir/Transforms/Passes.h" + +#include "acir/Analysis/ModelAnalysis.h" + +using namespace mlir; + +namespace acir { +namespace { +#define GEN_PASS_DEF_VERIFYMODELPASS +#include "acir/Transforms/Passes.h.inc" + +struct VerifyModelPass : impl::VerifyModelPassBase { + void runOnOperation() override { + if (failed(verifyModel(getOperation()))) + signalPassFailure(); + } +}; +} // namespace + +std::unique_ptr createVerifyModelPass() { + return std::make_unique(); +} + +} // namespace acir diff --git a/components/agentic-circuit/lib/gfsim/CMakeLists.txt b/components/agentic-circuit/lib/gfsim/CMakeLists.txt new file mode 100644 index 00000000..46596219 --- /dev/null +++ b/components/agentic-circuit/lib/gfsim/CMakeLists.txt @@ -0,0 +1,19 @@ +add_library(gfsim STATIC + harness.cpp + npu.cpp + observation.cpp + showcase.cpp + system.cpp + trace.cpp +) +add_dependencies(gfsim acir_runtime_headers) +target_include_directories(gfsim PUBLIC + "$" + "$" +) +target_compile_features(gfsim PUBLIC cxx_std_20) +target_link_libraries(gfsim PUBLIC ACIRBindings) +if(NOT LLVM_ENABLE_RTTI) + target_compile_options(gfsim PRIVATE -fno-rtti) +endif() +set_target_properties(gfsim PROPERTIES EXPORT_NAME Gfsim) diff --git a/components/agentic-circuit/lib/gfsim/harness.cpp b/components/agentic-circuit/lib/gfsim/harness.cpp new file mode 100644 index 00000000..4657f2f3 --- /dev/null +++ b/components/agentic-circuit/lib/gfsim/harness.cpp @@ -0,0 +1,740 @@ +#include "gfsim/harness.h" + +#include "acir/Bindings/Binding.h" +#include "gfsim/trace.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { +namespace { + +llvm::Error harnessError(const llvm::Twine &message) { + return llvm::createStringError(llvm::errc::invalid_argument, + "ACRUN-PREFLIGHT-001: " + message); +} + +bool isFingerprint(llvm::StringRef value) { + if (!value.consume_front("sha256:") || value.size() != 64) + return false; + return llvm::all_of(value, [](char character) { + return (character >= '0' && character <= '9') || + (character >= 'a' && character <= 'f'); + }); +} + +bool isNormalizedRelativePath(llvm::StringRef value) { + if (value.empty() || value.starts_with('/') || value.ends_with('/') || + value.contains('\\')) + return false; + while (!value.empty()) { + auto [component, rest] = value.split('/'); + if (component.empty() || component == "." || component == "..") + return false; + value = rest; + } + return true; +} + +bool isDomainName(llvm::StringRef value) { + if (value.empty() || + !(std::isalpha(static_cast(value.front())) || + value.front() == '_')) + return false; + return llvm::all_of(value.drop_front(), [](char character) { + return std::isalnum(static_cast(character)) || + character == '_' || character == '.' || character == '-'; + }); +} + +bool hasExactKeys(const llvm::json::Object &object, + llvm::ArrayRef keys) { + if (object.size() != keys.size()) + return false; + return llvm::all_of(keys, + [&](llvm::StringRef key) { return object.get(key); }); +} + +llvm::Expected requiredString(const llvm::json::Object &object, + llvm::StringRef key) { + auto value = object.getString(key); + if (!value || value->empty()) + return harnessError("field '" + key + "' must be a non-empty string"); + return value->str(); +} + +llvm::Expected requiredUInt64(const llvm::json::Object &object, + llvm::StringRef key, + bool allowZero = true) { + const llvm::json::Value *value = object.get(key); + auto integer = value ? value->getAsUINT64() : std::nullopt; + if (!integer || (!allowZero && *integer == 0)) + return harnessError("field '" + key + "' must be an unsigned integer"); + return *integer; +} + +llvm::Expected> +optionalPositiveUInt64(const llvm::json::Object &object, llvm::StringRef key) { + const llvm::json::Value *value = object.get(key); + if (!value) + return harnessError("field '" + key + "' is required"); + if (value->kind() == llvm::json::Value::Null) + return std::optional{}; + auto integer = value->getAsUINT64(); + if (!integer || *integer == 0) + return harnessError("field '" + key + "' must be null or positive"); + return integer; +} + +llvm::Expected parseFileHash(const llvm::json::Value *value, + llvm::StringRef label) { + const llvm::json::Object *object = value ? value->getAsObject() : nullptr; + constexpr std::array keys{"path", "sha256"}; + if (!object || !hasExactKeys(*object, keys)) + return harnessError(label + " must be an exact file-hash object"); + auto path = requiredString(*object, "path"); + auto hash = requiredString(*object, "sha256"); + if (!path || !hash) + return path ? hash.takeError() : path.takeError(); + if (!isNormalizedRelativePath(*path) || !isFingerprint(*hash)) + return harnessError(label + " path or fingerprint is invalid"); + return HarnessFileHash{std::move(*path), std::move(*hash)}; +} + +llvm::Expected +parseTraceIdentity(const llvm::json::Value *value) { + const llvm::json::Object *object = value ? value->getAsObject() : nullptr; + constexpr std::array keys{"path", "schema", "version", + "sha256"}; + if (!object || !hasExactKeys(*object, keys)) + return harnessError("trace must be an exact identity object"); + auto path = requiredString(*object, "path"); + auto schema = requiredString(*object, "schema"); + auto version = requiredString(*object, "version"); + auto hash = requiredString(*object, "sha256"); + if (!path || !schema || !version || !hash) { + if (!path) + return path.takeError(); + if (!schema) + return schema.takeError(); + if (!version) + return version.takeError(); + return hash.takeError(); + } + if (!isNormalizedRelativePath(*path) || *schema != "pto-trace" || + *version != "0.1" || !isFingerprint(*hash)) + return harnessError("trace identity is invalid"); + return TraceIdentity{std::move(*path), std::move(*schema), + std::move(*version), std::move(*hash)}; +} + +llvm::Expected readFile(llvm::StringRef path) { + auto buffer = llvm::MemoryBuffer::getFile(path); + if (!buffer) + return llvm::createStringError(buffer.getError(), + "ACRUN-PREFLIGHT-001: cannot read '%s'", + path.str().c_str()); + return buffer.get()->getBuffer().str(); +} + +llvm::Expected resolvedInputPath(const RunManifest &manifest, + llvm::StringRef relative) { + if (!isNormalizedRelativePath(relative)) + return harnessError("input path is not normalized and relative"); + std::filesystem::path root = + std::filesystem::weakly_canonical(manifest.rootDirectory); + std::filesystem::path candidate = + std::filesystem::weakly_canonical(root / relative.str()); + auto [rootEnd, candidateEnd] = std::mismatch( + root.begin(), root.end(), candidate.begin(), candidate.end()); + if (rootEnd != root.end()) + return harnessError("input path escapes the run-manifest root"); + return candidate.string(); +} + +llvm::Error writeExclusive(llvm::StringRef root, llvm::StringRef relative, + llvm::StringRef bytes) { + llvm::SmallString<256> path(root); + llvm::sys::path::append(path, relative); + llvm::SmallString<256> parent(path); + llvm::sys::path::remove_filename(parent); + if (std::error_code error = llvm::sys::fs::create_directories(parent)) + return llvm::createStringError(error, "cannot create result directory"); + int descriptor = -1; + if (std::error_code error = llvm::sys::fs::openFileForWrite( + path, descriptor, llvm::sys::fs::CD_CreateNew, + llvm::sys::fs::OF_None)) + return llvm::createStringError(error, "cannot create result artifact"); + llvm::raw_fd_ostream output(descriptor, true); + output << bytes; + output.flush(); + if (output.has_error()) + return llvm::createStringError(output.error(), + "cannot write result artifact"); + return llvm::Error::success(); +} + +llvm::StringRef statusName(RunStatus status) { + switch (status) { + case RunStatus::Completed: + return "completed"; + case RunStatus::Incomplete: + return "incomplete"; + case RunStatus::Failed: + return "failed"; + } + return "failed"; +} + +bool expectationMatches(const TerminationExpectation &expectation, + const RunResultDocument &result) { + const bool kindMatches = expectation.kind == "any" || + (expectation.kind == "complete" && + result.status == RunStatus::Completed) || + (expectation.kind == "incomplete" && + result.status == RunStatus::Incomplete); + return kindMatches && (!expectation.reason || + *expectation.reason == result.terminationReason); +} + +llvm::Error validateResultDocument(const RunResultDocument &result) { + constexpr std::array completedReasons{ + "trace_drained", "declared_model_termination"}; + constexpr std::array incompleteReasons{ + "max_ticks", "max_domain_cycles", "max_events", + "max_deltas_per_tick", "max_trace_records", "max_validation_work", + "interrupted"}; + constexpr std::array failedReasons{ + "deadlock", "invariant_violation", "trace_error", "runtime_error"}; + llvm::ArrayRef allowed; + switch (result.status) { + case RunStatus::Completed: + allowed = completedReasons; + break; + case RunStatus::Incomplete: + allowed = incompleteReasons; + break; + case RunStatus::Failed: + allowed = failedReasons; + break; + } + if (!llvm::is_contained(allowed, result.terminationReason)) + return harnessError("run result status and termination reason disagree"); + if (!isNormalizedRelativePath(result.runManifest.path) || + !isFingerprint(result.runManifest.sha256)) + return harnessError("run result manifest identity is invalid"); + std::string previous; + for (const auto &[name, count] : result.domainCycles) + if (!isDomainName(name)) + return harnessError("run result contains an invalid domain name"); + for (const HarnessFileHash &output : result.outputs) { + if (!isNormalizedRelativePath(output.path) || + !isFingerprint(output.sha256) || + (!previous.empty() && previous >= output.path)) + return harnessError("run result outputs are not canonical"); + previous = output.path; + } + if (result.validation.status != "passed" && + result.validation.status != "failed" && + result.validation.status != "not_run") + return harnessError("run result validation status is invalid"); + if ((result.validation.status == "not_run") == + result.validation.reportSha256.has_value()) + return harnessError("run result validation report identity is invalid"); + if (result.validation.reportSha256 && + !isFingerprint(*result.validation.reportSha256)) + return harnessError("run result validation hash is invalid"); + return llvm::Error::success(); +} + +llvm::Expected canonicalResult(const RunResultDocument &result) { + llvm::json::Object cycles; + for (const auto &[name, count] : result.domainCycles) + cycles[name] = count; + llvm::json::Array outputs; + for (const HarnessFileHash &output : result.outputs) + outputs.push_back( + llvm::json::Object{{"path", output.path}, {"sha256", output.sha256}}); + llvm::json::Object document{ + {"schema", "agentic-circuit-run-result"}, + {"version", "0.1"}, + {"contract_epoch", "0.3"}, + {"run_manifest", + llvm::json::Object{{"path", result.runManifest.path}, + {"sha256", result.runManifest.sha256}}}, + {"status", statusName(result.status)}, + {"termination_reason", result.terminationReason}, + {"simulated_ticks", result.simulatedTicks}, + {"domain_cycles", std::move(cycles)}, + {"event_count", result.eventCount}, + {"trace_position", + llvm::json::Object{ + {"next_record_index", result.tracePosition.nextRecordIndex}, + {"last_committed_sequence_id", + result.tracePosition.lastCommittedSequenceId + ? llvm::json::Value( + *result.tracePosition.lastCommittedSequenceId) + : llvm::json::Value(nullptr)}}}, + {"outputs", std::move(outputs)}, + {"validation", + llvm::json::Object{ + {"status", result.validation.status}, + {"report_sha256", + result.validation.reportSha256 + ? llvm::json::Value(*result.validation.reportSha256) + : llvm::json::Value(nullptr)}}}}; + return acir::bindings::canonicalizeJson( + llvm::json::Value(std::move(document))); +} + +llvm::StringRef statisticKindName(StatisticKind kind) { + switch (kind) { + case StatisticKind::Counter: + return "counter"; + case StatisticKind::Gauge: + return "gauge"; + case StatisticKind::Histogram: + return "histogram"; + } + return "counter"; +} + +llvm::Expected +canonicalStatistics(std::span statistics) { + llvm::json::Array output; + std::pair previous; + bool hasPrevious = false; + for (const StatSnapshot &snapshot : statistics) { + const auto key = std::pair(std::string_view(snapshot.objectPath), + std::string_view(snapshot.name)); + if (snapshot.objectPath.empty() || snapshot.name.empty() || + (hasPrevious && previous >= key)) + return harnessError("statistics are not in canonical path/name order"); + previous = key; + hasPrevious = true; + llvm::json::Array buckets; + uint64_t previousBound = 0; + bool hasBound = false; + for (const HistogramBucket &bucket : snapshot.buckets) { + if (hasBound && previousBound >= bucket.upperBound) + return harnessError("histogram bounds are not strictly increasing"); + previousBound = bucket.upperBound; + hasBound = true; + buckets.push_back(llvm::json::Object{{"upper_bound", bucket.upperBound}, + {"count", bucket.count}}); + } + output.push_back(llvm::json::Object{ + {"name", snapshot.name}, + {"object_path", snapshot.objectPath}, + {"kind", statisticKindName(snapshot.kind)}, + {"value", snapshot.value}, + {"count", snapshot.count}, + {"sum", snapshot.sum}, + {"minimum", snapshot.minimum}, + {"maximum", snapshot.maximum}, + {"buckets", std::move(buckets)}, + {"last_update", + llvm::json::Object{{"time", snapshot.lastUpdate.time}, + {"delta", snapshot.lastUpdate.delta}}}}); + } + auto bytes = + acir::bindings::canonicalizeJson(llvm::json::Value(std::move(output))); + if (bytes) + bytes->push_back('\n'); + return bytes; +} + +llvm::StringRef tracePhaseName(TraceEventPhase phase) { + switch (phase) { + case TraceEventPhase::Instant: + return "i"; + case TraceEventPhase::Complete: + return "X"; + case TraceEventPhase::Counter: + return "C"; + case TraceEventPhase::FlowStart: + return "s"; + case TraceEventPhase::FlowEnd: + return "f"; + } + return "i"; +} + +llvm::json::Value observationValue(const ObservationValue &value) { + return std::visit([](const auto &item) -> llvm::json::Value { return item; }, + value); +} + +llvm::Expected +canonicalEvents(std::span events) { + std::string output; + std::optional> previous; + for (const CommittedEvent &event : events) { + const auto key = + std::tuple(event.epoch, event.ownerId, event.localCommittedIndex); + if (event.ownerId == kInvalidObjectId || + event.epoch.delta >= kMaxDeltasPerTick || + (previous && *previous >= key)) + return harnessError("events are not in canonical committed order"); + previous = key; + if (event.epoch.time > + (std::numeric_limits::max() - event.epoch.delta) / + kMaxDeltasPerTick) + return harnessError("event presentation timestamp overflowed"); + const uint64_t timestamp = + event.epoch.time * kMaxDeltasPerTick + event.epoch.delta; + llvm::json::Object arguments{ + {"gfsim_epoch_time", event.epoch.time}, + {"gfsim_epoch_delta", event.epoch.delta}, + {"gfsim_object_id", event.ownerId}, + {"gfsim_local_committed_index", event.localCommittedIndex}, + }; + if (event.rootSequenceId) + arguments["gfsim_root_sequence_id"] = *event.rootSequenceId; + for (const ObservationArgument &argument : event.arguments) { + if (llvm::StringRef(argument.name).starts_with("gfsim_") || + arguments.get(argument.name)) + return harnessError("event argument collides with runtime metadata"); + arguments[argument.name] = observationValue(argument.value); + } + llvm::json::Object encoded{{"name", event.name}, + {"cat", event.category}, + {"ph", tracePhaseName(event.phase)}, + {"ts", timestamp}, + {"pid", 0}, + {"tid", event.ownerId}, + {"args", std::move(arguments)}}; + if (event.phase == TraceEventPhase::Instant) + encoded["s"] = "t"; + if (event.duration) + encoded["dur"] = *event.duration; + if (event.flowId) + encoded["id"] = *event.flowId; + if (event.phase == TraceEventPhase::FlowEnd) + encoded["bp"] = "e"; + auto line = + acir::bindings::canonicalizeJson(llvm::json::Value(std::move(encoded))); + if (!line) + return line.takeError(); + output.append(*line).push_back('\n'); + } + return output; +} + +} // namespace + +llvm::Expected loadRunManifest(llvm::StringRef bytes, + llvm::StringRef rootDirectory) { + auto parsed = acir::bindings::parseIJson(bytes); + if (!parsed) + return parsed.takeError(); + const llvm::json::Object *object = parsed->getAsObject(); + constexpr std::array keys{"schema", + "version", + "contract_epoch", + "build_manifest", + "trace", + "seed", + "output_directory", + "deadlock_window", + "max_ticks", + "max_domain_cycles", + "stats_format", + "event_log", + "termination_expectation"}; + if (!object || !hasExactKeys(*object, keys) || + object->getString("schema") != "agentic-circuit-run-manifest" || + object->getString("version") != "0.1" || + object->getString("contract_epoch") != "0.3") + return harnessError("run manifest has an invalid closed envelope"); + + RunManifest manifest; + auto build = parseFileHash(object->get("build_manifest"), "build_manifest"); + auto trace = parseTraceIdentity(object->get("trace")); + auto seed = requiredUInt64(*object, "seed"); + auto output = requiredString(*object, "output_directory"); + auto deadlock = optionalPositiveUInt64(*object, "deadlock_window"); + auto maxTicks = optionalPositiveUInt64(*object, "max_ticks"); + auto stats = requiredString(*object, "stats_format"); + auto eventLog = requiredString(*object, "event_log"); + if (!build || !trace || !seed || !output || !deadlock || !maxTicks || + !stats || !eventLog) { + if (!build) + return build.takeError(); + if (!trace) + return trace.takeError(); + if (!seed) + return seed.takeError(); + if (!output) + return output.takeError(); + if (!deadlock) + return deadlock.takeError(); + if (!maxTicks) + return maxTicks.takeError(); + if (!stats) + return stats.takeError(); + return eventLog.takeError(); + } + if (!isNormalizedRelativePath(*output) || *stats != "json" || + (*eventLog != "disabled" && *eventLog != "jsonl")) + return harnessError("run output configuration is invalid"); + + const llvm::json::Object *domainLimits = + object->getObject("max_domain_cycles"); + if (!domainLimits) + return harnessError("max_domain_cycles must be an object"); + for (const auto &[name, value] : *domainLimits) { + auto maximum = value.getAsUINT64(); + if (!isDomainName(name) || !maximum || *maximum == 0) + return harnessError("max_domain_cycles contains an invalid bound"); + manifest.limits.maxDomainCycles.emplace(name.str(), *maximum); + } + + const llvm::json::Object *expectation = + object->getObject("termination_expectation"); + constexpr std::array expectationKeys{"kind", "reason"}; + if (!expectation || !hasExactKeys(*expectation, expectationKeys)) + return harnessError("termination_expectation is invalid"); + auto kind = expectation->getString("kind"); + const llvm::json::Value *reasonValue = expectation->get("reason"); + if (!kind || + (*kind != "complete" && *kind != "incomplete" && *kind != "any") || + !reasonValue) + return harnessError("termination expectation kind is invalid"); + std::optional reason; + if (reasonValue->kind() != llvm::json::Value::Null) { + auto value = reasonValue->getAsString(); + if (!value || (*value != "trace_drained" && *value != "max_ticks" && + *value != "max_domain_cycles" && + *value != "declared_model_termination")) + return harnessError("termination expectation reason is invalid"); + reason = value->str(); + } + if (reason && ((*kind == "complete" && *reason != "trace_drained" && + *reason != "declared_model_termination") || + (*kind == "incomplete" && *reason != "max_ticks" && + *reason != "max_domain_cycles"))) + return harnessError( + "termination expectation kind and reason are incompatible"); + + manifest.buildManifest = std::move(*build); + manifest.trace = std::move(*trace); + manifest.seed = *seed; + manifest.outputDirectory = std::move(*output); + manifest.limits.deadlockWindow = *deadlock; + manifest.limits.maxTicks = *maxTicks; + manifest.statsFormat = std::move(*stats); + manifest.eventLog = std::move(*eventLog); + manifest.expectation = {kind->str(), std::move(reason)}; + manifest.rootDirectory = + std::filesystem::weakly_canonical(rootDirectory.str()).string(); + manifest.manifestSha256 = acir::bindings::sha256Fingerprint(bytes); + return manifest; +} + +llvm::Expected +preflightRunManifest(const RunManifest &manifest, + llvm::StringRef buildFingerprint, + std::span timeDomains, + llvm::StringRef resultStage) { + if (!isFingerprint(buildFingerprint)) + return harnessError("generated build fingerprint is invalid"); + auto buildPath = resolvedInputPath(manifest, manifest.buildManifest.path); + if (!buildPath) + return buildPath.takeError(); + auto buildBytes = readFile(*buildPath); + if (!buildBytes) + return buildBytes.takeError(); + if (acir::bindings::sha256Fingerprint(*buildBytes) != + manifest.buildManifest.sha256) + return harnessError("build manifest hash does not match"); + auto buildDocument = acir::bindings::parseIJson(*buildBytes); + const llvm::json::Object *build = + buildDocument ? buildDocument->getAsObject() : nullptr; + if (!buildDocument) + return buildDocument.takeError(); + if (!build || + !hasExactKeys(*build, + {"schema", "version", "contract_epoch", "project", "system", + "source_files", "normalized_acir_sha256", "compiler", + "pass_pipeline", "providers", "component_specializations", + "protocol_identities", "artifacts", "validation_gates", + "build_profile", "instrumentation_layers", + "specialization_inputs", "build_fingerprint"}) || + build->getString("schema") != "agentic-circuit-build-manifest" || + build->getString("version") != "0.1" || + build->getString("contract_epoch") != "0.3" || + build->getString("build_fingerprint") != buildFingerprint) + return harnessError("build manifest identity does not match the model"); + + std::set knownDomains; + for (const TimeDomainRuntime &domain : timeDomains) { + if (!isDomainName(domain.name) || domain.period == 0 || + domain.tickScale == 0 || !knownDomains.insert(domain.name).second) + return harnessError("generated time-domain metadata is invalid"); + } + for (const auto &[name, maximum] : manifest.limits.maxDomainCycles) + if (!knownDomains.contains(name)) + return harnessError("max_domain_cycles names unknown domain '" + name + + "'"); + + auto tracePath = resolvedInputPath(manifest, manifest.trace.path); + if (!tracePath) + return tracePath.takeError(); + auto traceBytes = readFile(*tracePath); + if (!traceBytes) + return traceBytes.takeError(); + if (acir::bindings::sha256Fingerprint(*traceBytes) != manifest.trace.sha256) + return harnessError("trace hash does not match"); + TraceLoadResult trace = parsePtoTrace(*traceBytes); + if (!trace.succeeded()) + return harnessError("trace document failed exact preflight"); + + std::filesystem::path root = + std::filesystem::weakly_canonical(manifest.rootDirectory); + auto outputPath = resolvedInputPath(manifest, manifest.outputDirectory); + if (!outputPath) + return outputPath.takeError(); + std::filesystem::path stage = + std::filesystem::weakly_canonical(resultStage.str()); + auto [rootEnd, stageEnd] = + std::mismatch(root.begin(), root.end(), stage.begin(), stage.end()); + if (rootEnd != root.end() || stage == root) + return harnessError("result stage escapes the run-manifest root"); + return std::move(*trace.document); +} + +RunResultDocument makeRunResult(const RunManifest &manifest, + const TerminationResult &termination) { + RunResultDocument result; + result.runManifest = {"run-manifest.json", manifest.manifestSha256}; + result.simulatedTicks = termination.finalEpoch.time; + result.domainCycles = termination.domainCycles; + result.eventCount = termination.committedEventCount; + result.tracePosition = {termination.tracePosition, + termination.traceLastCommittedSequenceId}; + switch (termination.classification) { + case TerminationClass::Completed: + result.status = RunStatus::Completed; + result.terminationReason = + termination.diagnosticCode == "declared_model_termination" + ? "declared_model_termination" + : "trace_drained"; + break; + case TerminationClass::Incomplete: + result.status = RunStatus::Incomplete; + if (termination.diagnosticCode == "max_ticks_reached") + result.terminationReason = "max_ticks"; + else if (termination.diagnosticCode == "max_domain_cycles_reached") + result.terminationReason = "max_domain_cycles"; + else if (termination.diagnosticCode == "max_events_reached") + result.terminationReason = "max_events"; + else if (termination.diagnosticCode == "max_trace_records_reached") + result.terminationReason = "max_trace_records"; + else + result.terminationReason = "interrupted"; + break; + case TerminationClass::Failed: + result.status = RunStatus::Failed; + result.terminationReason = + termination.diagnosticCode == "no_progress" || + termination.diagnosticCode == "deadlock_window_reached" + ? "deadlock" + : "invariant_violation"; + break; + } + result.validation = {"not_run", std::nullopt}; + return result; +} + +llvm::Error publishRunResult(const RunManifest &manifest, + RunResultDocument &result, + std::span statistics, + std::span events, + llvm::StringRef resultStage) { + auto stats = canonicalStatistics(statistics); + if (!stats) + return stats.takeError(); + llvm::Expected eventBytes = std::string(); + if (manifest.eventLog == "jsonl") { + eventBytes = canonicalEvents(events); + if (!eventBytes) + return eventBytes.takeError(); + } + if (llvm::sys::fs::exists(resultStage)) + return harnessError("result stage already exists"); + if (std::error_code error = llvm::sys::fs::create_directories(resultStage)) + return llvm::createStringError(error, "cannot create result stage"); + struct Cleanup { + llvm::StringRef path; + bool keep = false; + ~Cleanup() { + if (!keep) + llvm::sys::fs::remove_directories(path); + } + } cleanup{resultStage}; + + if (llvm::Error error = writeExclusive(resultStage, "stats.json", *stats)) + return error; + result.outputs.push_back( + {"stats.json", acir::bindings::sha256Fingerprint(*stats)}); + if (manifest.eventLog == "jsonl") { + if (llvm::Error error = + writeExclusive(resultStage, "events.jsonl", *eventBytes)) + return error; + result.outputs.push_back( + {"events.jsonl", acir::bindings::sha256Fingerprint(*eventBytes)}); + } + const bool expectationPassed = + expectationMatches(manifest.expectation, result); + std::string validation = + expectationPassed ? "{\"status\":\"passed\"}\n" + : "{\"diagnostic_code\":\"ACRUN-EXPECTATION-001\"," + "\"status\":\"failed\"}\n"; + if (llvm::Error error = + writeExclusive(resultStage, "validation-report.json", validation)) + return error; + result.validation = {expectationPassed ? "passed" : "failed", + acir::bindings::sha256Fingerprint(validation)}; + if (!expectationPassed) { + result.status = RunStatus::Failed; + result.terminationReason = "invariant_violation"; + } + result.outputs.push_back( + {"validation-report.json", *result.validation.reportSha256}); + std::sort(result.outputs.begin(), result.outputs.end(), + [](const HarnessFileHash &left, const HarnessFileHash &right) { + return left.path < right.path; + }); + if (llvm::Error error = validateResultDocument(result)) + return error; + auto bytes = canonicalResult(result); + if (!bytes) + return bytes.takeError(); + bytes->push_back('\n'); + if (llvm::Error error = + writeExclusive(resultStage, "run-result.json", *bytes)) + return error; + cleanup.keep = true; + return llvm::Error::success(); +} + +} // namespace gfsim diff --git a/components/agentic-circuit/lib/gfsim/npu.cpp b/components/agentic-circuit/lib/gfsim/npu.cpp new file mode 100644 index 00000000..8337e055 --- /dev/null +++ b/components/agentic-circuit/lib/gfsim/npu.cpp @@ -0,0 +1,1782 @@ +#include "gfsim/npu.h" + +#include "gfsim/components.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { +namespace { + +constexpr uint64_t kMaxPortableJsonInteger = 9007199254740991ULL; + +// Frozen from davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb +// model/include/davincioo/model/pto_inst.hpp. +constexpr auto kTmaOpcodes = std::to_array({ + "TLOAD", + "TSTORE", + "TSTORE_FP", + "MGATHER", + "MSCATTER", + "TPUT", + "TGET", + "TPUT_ASYNC", + "TGET_ASYNC", + "TBROADCAST", + "TREDUCE", + "TPREFETCH", + "TIMG2COL", +}); + +constexpr auto kScalarOpcodes = std::to_array({ + "RECORD_EVENT", "WAIT_EVENT", + "BARRIER", "TSYNC", + "TALLOC", "TFREE", + "TPUSH", "TPOP", + "TNOTIFY", "TWAIT", + "TTEST", "TRESHAPE", + "TASSIGN", "TCI", + "TPRINT", "SETFMATRIX", + "SET_IMG2COL_RPT", "SET_IMG2COL_PADDING", + "TGET_SCALE_ADDR", +}); + +constexpr auto kCubeOpcodes = std::to_array({ + "TMATMUL", + "TMATMUL_MX", + "TMATMUL_ACC", + "TMATMUL_BIAS", + "TMATMUL_MX_ACC", + "TMATMUL_MX_BIAS", + "TGEMV", + "TGEMV_ACC", + "TGEMV_BIAS", + "TGEMV_MX", +}); + +constexpr auto kVectorOpcodes = std::to_array({ + "TADD", + "TSUB", + "TMUL", + "TDIV", + "TREM", + "TAND", + "TOR", + "TXOR", + "TSHL", + "TSHR", + "TMAX", + "TMIN", + "TPRELU", + "TCMP", + "TABS", + "TEXP", + "TLOG", + "TSQRT", + "TRSQRT", + "TRECIP", + "TNEG", + "TNOT", + "TRELU", + "TCVT", + "TADDC", + "TSUBC", + "TSEL", + "TADDS", + "TSUBS", + "TMULS", + "TDIVS", + "TREMS", + "TANDS", + "TORS", + "TXORS", + "TSHLS", + "TSHRS", + "TMAXS", + "TMINS", + "TLRELU", + "TAXPY", + "TCMPS", + "TADDSC", + "TSUBSC", + "TSELS", + "TEXPANDS", + "TROWSUM", + "TROWPROD", + "TROWMAX", + "TROWMIN", + "TROWARGMAX", + "TROWARGMIN", + "TROWEXPAND", + "TCOLSUM", + "TCOLPROD", + "TCOLMAX", + "TCOLMIN", + "TCOLARGMAX", + "TCOLARGMIN", + "TCOLEXPAND", + "TROWEXPANDDIV", + "TROWEXPANDMUL", + "TROWEXPANDSUB", + "TROWEXPANDADD", + "TROWEXPANDMAX", + "TROWEXPANDMIN", + "TROWEXPANDEXPDIF", + "TCOLEXPANDDIV", + "TCOLEXPANDMUL", + "TCOLEXPANDSUB", + "TCOLEXPANDADD", + "TCOLEXPANDMAX", + "TCOLEXPANDMIN", + "TCOLEXPANDEXPDIF", + "TFILLPAD", + "TFILLPAD_INPLACE", + "TFILLPAD_EXPAND", + "TMOV", + "TMOV_FP", + "TTRANS", + "TEXTRACT", + "TEXTRACT_FP", + "TTRI", + "TGATHER", + "TGATHERB", + "TSCATTER", + "TSORT32", + "TMRGSORT", + "TPARTADD", + "TPARTMUL", + "TPARTMAX", + "TPARTMIN", + "TPARTARGMAX", + "TPARTARGMIN", + "TRANDOM", + "TCONCAT", + "TDEQUANT", + "TINSERT", + "TINSERT_FP", + "TFMOD", + "TFMODS", + "THISTOGRAM", + "TQUANT", + "TSUBVIEW", +}); + +static_assert(kTmaOpcodes.size() + kScalarOpcodes.size() + kCubeOpcodes.size() + + kVectorOpcodes.size() == + 146); + +template +constexpr bool contains(const std::array &values, + std::string_view value) { + for (std::string_view candidate : values) + if (candidate == value) + return true; + return false; +} + +std::optional classify(std::string_view opcode) { + if (contains(kScalarOpcodes, opcode)) + return NpuEngineClass::Scalar; + if (contains(kVectorOpcodes, opcode)) + return NpuEngineClass::Vector; + if (contains(kCubeOpcodes, opcode)) + return NpuEngineClass::Cube; + if (contains(kTmaOpcodes, opcode)) + return NpuEngineClass::Tma; + return std::nullopt; +} + +NpuDecodeResult reject(std::string code, std::string message) { + NpuDecodeResult result; + result.diagnostics.push_back( + {.code = std::move(code), .message = std::move(message)}); + return result; +} + +template const T *get(const PtoValue &value) { + return std::get_if(&value.value); +} + +std::optional unsignedInteger(const PtoValue &value) { + if (const auto *integer = get(value)) + return *integer; + if (const auto *integer = get(value); integer && *integer >= 0) + return static_cast(*integer); + return std::nullopt; +} + +const PtoValue *find(const PtoValue::Object &object, std::string_view key) { + auto iterator = object.find(std::string(key)); + return iterator == object.end() ? nullptr : &iterator->second; +} + +bool hasExactKeys(const PtoValue::Object &object, + std::initializer_list keys) { + if (object.size() != keys.size()) + return false; + for (std::string_view key : keys) + if (!object.contains(std::string(key))) + return false; + return true; +} + +bool isScalarValue(const PtoValue &value) { + return !std::holds_alternative(value.value) && + !std::holds_alternative(value.value) && + !std::holds_alternative(value.value); +} + +struct TileAttribute { + std::string_view address; + std::string_view type; + std::string_view layout; + const PtoValue::Array *shape = nullptr; +}; + +std::optional parseTile(const PtoValue &value) { + const auto *object = get(value); + if (!object || + !hasExactKeys(*object, {"address", "dtype", "layout", "shape"})) + return std::nullopt; + const auto *address = find(*object, "address"); + const auto *dtype = find(*object, "dtype"); + const auto *layout = find(*object, "layout"); + const auto *shape = find(*object, "shape"); + if (!address || !dtype || !layout || !shape) + return std::nullopt; + const auto *addressText = get(*address); + const auto *dtypeText = get(*dtype); + const auto *layoutText = get(*layout); + const auto *dimensions = get(*shape); + if (!addressText || addressText->empty() || !dtypeText || + dtypeText->empty() || !layoutText || layoutText->empty() || !dimensions) + return std::nullopt; + for (const PtoValue &dimension : *dimensions) + if (!get(dimension) && !get(dimension)) + return std::nullopt; + return TileAttribute{.address = *addressText, + .type = *dtypeText, + .layout = *layoutText, + .shape = dimensions}; +} + +struct ScalarAttribute { + std::string_view type; + const PtoValue *value = nullptr; +}; + +std::optional parseScalar(const PtoValue &value) { + const auto *object = get(value); + if (!object || !hasExactKeys(*object, {"dtype", "value"})) + return std::nullopt; + const PtoValue *dtype = find(*object, "dtype"); + const PtoValue *scalarValue = find(*object, "value"); + if (!dtype || !scalarValue) + return std::nullopt; + const auto *type = get(*dtype); + if (!type || type->empty() || !isScalarValue(*scalarValue)) + return std::nullopt; + return ScalarAttribute{.type = *type, .value = scalarValue}; +} + +std::string tileIdentity(uint64_t blockId, std::string_view address) { + return "block/" + std::to_string(blockId) + "/tile/" + std::string(address); +} + +EventProposal observation(std::string name, const NpuInstruction &instruction) { + return {.ownerId = kInvalidObjectId, + .category = "instruction", + .name = std::move(name), + .phase = TraceEventPhase::Instant, + .rootSequenceId = instruction.sequenceId, + .arguments = {{.name = "block_id", .value = instruction.blockId}, + {.name = "engine", + .value = std::string(toString(instruction.engine))}, + {.name = "opcode", .value = instruction.opcode}}}; +} + +uint64_t dependencyFlowId(uint64_t blockId, uint64_t producerSequenceId, + uint64_t consumerSequenceId, + std::string_view tileIdentity) { + constexpr uint64_t kOffset = 14695981039346656037ULL; + constexpr uint64_t kPrime = 1099511628211ULL; + uint64_t hash = kOffset; + auto appendByte = [&](uint8_t byte) { + hash ^= byte; + hash *= kPrime; + }; + auto appendInteger = [&](uint64_t value) { + for (unsigned shift = 0; shift != 64; shift += 8) + appendByte(static_cast(value >> shift)); + }; + appendInteger(blockId); + appendInteger(producerSequenceId); + appendInteger(consumerSequenceId); + for (char character : tileIdentity) + appendByte(static_cast(character)); + return hash % kMaxPortableJsonInteger + 1; +} + +bool issueEntryLess(const NpuIssueEntry &left, const NpuIssueEntry &right) { + return std::tie(left.instruction.sequenceId, left.stableObjectId) < + std::tie(right.instruction.sequenceId, right.stableObjectId); +} + +} // namespace + +std::string_view toString(NpuEngineClass engine) { + switch (engine) { + case NpuEngineClass::Scalar: + return "scalar"; + case NpuEngineClass::Vector: + return "vector"; + case NpuEngineClass::Cube: + return "cube"; + case NpuEngineClass::Tma: + return "tma"; + } + return "unknown"; +} + +NpuDecodeResult NpuDecoder::decode(const PtoTraceRecord &record) const { + std::optional engine = classify(record.opcode); + if (!engine) + return reject("npu_unsupported_opcode", + "opcode is not in the pinned DavinciOO opcode table"); + + auto attributesIterator = record.attributes.find("davincioo"); + if (attributesIterator == record.attributes.end()) + return reject("npu_missing_davincioo_attributes", + "record does not contain attributes.davincioo"); + const auto *attributes = get(attributesIterator->second); + if (!attributes || + !hasExactKeys(*attributes, {"block_idx", "input_tiles", "operand_roles", + "output_tiles", "scalar_inputs"})) + return reject("npu_invalid_davincioo_attributes", + "attributes.davincioo is not the closed imported shape"); + + const PtoValue *blockValue = find(*attributes, "block_idx"); + const PtoValue *inputValue = find(*attributes, "input_tiles"); + const PtoValue *roleValue = find(*attributes, "operand_roles"); + const PtoValue *outputValue = find(*attributes, "output_tiles"); + const PtoValue *scalarValue = find(*attributes, "scalar_inputs"); + if (!blockValue || !inputValue || !roleValue || !outputValue || !scalarValue) + return reject("npu_invalid_davincioo_attributes", + "attributes.davincioo is incomplete"); + + std::optional blockId = unsignedInteger(*blockValue); + const auto *inputs = get(*inputValue); + const auto *roles = get(*roleValue); + const auto *outputs = get(*outputValue); + const auto *scalars = get(*scalarValue); + if (!blockId || !inputs || !roles || !outputs || !scalars) + return reject("npu_invalid_davincioo_attributes", + "attributes.davincioo contains a value of the wrong type"); + if (roles->size() != record.operands.size()) + return reject("npu_operand_role_mismatch", + "operand_roles must have one entry per ordered operand"); + + NpuInstruction instruction; + instruction.sequenceId = record.sequenceId; + instruction.blockId = *blockId; + instruction.opcode = record.opcode; + instruction.dependencies = record.dependencies; + instruction.operands = record.operands; + instruction.engine = *engine; + + size_t inputIndex = 0; + size_t outputIndex = 0; + size_t scalarIndex = 0; + for (size_t operandIndex = 0; operandIndex < roles->size(); ++operandIndex) { + const auto *role = get((*roles)[operandIndex]); + if (!role) + return reject("npu_operand_role_mismatch", + "every operand role must be a string"); + const PtoTraceOperand &operand = record.operands[operandIndex]; + if (*role == "input_tile" || *role == "output_tile") { + const bool input = *role == "input_tile"; + const PtoValue::Array &tiles = input ? *inputs : *outputs; + size_t &tileIndex = input ? inputIndex : outputIndex; + if (tileIndex >= tiles.size()) + return reject("npu_operand_role_mismatch", + "tile role count does not match imported tile arrays"); + std::optional tile = parseTile(tiles[tileIndex++]); + if (!tile) + return reject("npu_invalid_davincioo_attributes", + "tile attributes are malformed"); + std::string identity = tileIdentity(*blockId, tile->address); + if (operand.kind != PtoOperandKind::Tile || operand.id != identity) + return reject( + "npu_tile_identity_mismatch", + "ordered tile operand does not match its imported identity"); + (input ? instruction.inputTiles : instruction.outputTiles) + .push_back(std::move(identity)); + (input ? instruction.inputTileDescriptors + : instruction.outputTileDescriptors) + .push_back({.identity = tileIdentity(*blockId, tile->address), + .address = std::string(tile->address), + .type = std::string(tile->type), + .layout = std::string(tile->layout), + .shape = *tile->shape}); + continue; + } + if (*role == "scalar_input") { + if (scalarIndex >= scalars->size()) + return reject("npu_operand_role_mismatch", + "scalar role count does not match scalar_inputs"); + std::optional scalar = + parseScalar((*scalars)[scalarIndex++]); + if (!scalar) + return reject("npu_invalid_davincioo_attributes", + "scalar input attributes are malformed"); + if (operand.kind != PtoOperandKind::Immediate || + operand.type != scalar->type || !operand.immediate || + *operand.immediate != *scalar->value) + return reject("npu_scalar_mismatch", + "ordered immediate does not match its imported scalar"); + instruction.scalarInputs.push_back( + {.type = std::string(scalar->type), .value = *scalar->value}); + continue; + } + return reject("npu_operand_role_mismatch", + "operand role is not supported by the DavinciOO adapter"); + } + + if (inputIndex != inputs->size() || outputIndex != outputs->size() || + scalarIndex != scalars->size()) + return reject("npu_operand_role_mismatch", + "imported operand arrays contain unreferenced entries"); + + NpuDecodeResult result; + result.observations.push_back(observation("decode", instruction)); + result.observations.push_back(observation("dispatch", instruction)); + result.instruction = std::move(instruction); + return result; +} + +NpuDependencyTracker::NpuDependencyTracker(std::string name, ObjectId id, + SimObject *parent, + NpuIssueQueueCapacities capacities, + ObservationSink *observations) + : SimObject(ObjectKind::Scheduler, std::move(name), id, parent, + observations), + capacities_(capacities) {} + +size_t NpuDependencyTracker::engineIndex(NpuEngineClass engine) { + switch (engine) { + case NpuEngineClass::Scalar: + return 0; + case NpuEngineClass::Vector: + return 1; + case NpuEngineClass::Cube: + return 2; + case NpuEngineClass::Tma: + return 3; + } + return 0; +} + +size_t NpuDependencyTracker::capacity(NpuEngineClass engine) const { + switch (engine) { + case NpuEngineClass::Scalar: + return capacities_.scalar; + case NpuEngineClass::Vector: + return capacities_.vector; + case NpuEngineClass::Cube: + return capacities_.cube; + case NpuEngineClass::Tma: + return capacities_.tma; + } + return 0; +} + +bool NpuDependencyTracker::knownSequence(uint64_t sequenceId) const { + if (completedSequences_.contains(sequenceId) || + outstandingSequences_.contains(sequenceId)) + return true; + if (std::ranges::any_of(dispatchProposals_, + [&](const auto &proposal) { + return proposal.instruction.sequenceId == + sequenceId; + }) || + std::ranges::any_of(acceptedDispatches_, [&](const auto &entry) { + return entry.instruction.sequenceId == sequenceId; + })) + return true; + for (const auto &queue : queues_) + if (std::ranges::any_of(queue, [&](const auto &entry) { + return entry.instruction.sequenceId == sequenceId; + })) + return true; + return false; +} + +bool NpuDependencyTracker::ready(const NpuIssueEntry &entry) const { + return std::ranges::all_of( + entry.derivedDependencies, [&](const NpuDependency &dependency) { + return completedSequences_.contains(dependency.producerSequenceId); + }); +} + +bool NpuDependencyTracker::proposeDispatch(const NpuInstruction &instruction, + ObjectId stableObjectId) { + if (stableObjectId == kInvalidObjectId || + (lastDispatchedSequence_ && + instruction.sequenceId <= *lastDispatchedSequence_) || + knownSequence(instruction.sequenceId)) + return false; + dispatchProposals_.push_back({instruction, stableObjectId}); + return true; +} + +bool NpuDependencyTracker::proposeIssue(NpuEngineClass engine) { + const size_t index = engineIndex(engine); + if (issueProposals_[index]) + return false; + const auto &queue = queues_[index]; + const NpuIssueEntry *candidate = nullptr; + for (const NpuIssueEntry &entry : queue) + if (ready(entry) && (!candidate || issueEntryLess(entry, *candidate))) + candidate = &entry; + if (!candidate) + return false; + issueProposals_[index] = *candidate; + return true; +} + +bool NpuDependencyTracker::proposeComplete(uint64_t sequenceId) { + if (!outstandingSequences_.contains(sequenceId) || + std::ranges::find(completionProposals_, sequenceId) != + completionProposals_.end()) + return false; + completionProposals_.push_back(sequenceId); + return true; +} + +void NpuDependencyTracker::doArbitrate(Epoch) { + std::sort( + dispatchProposals_.begin(), dispatchProposals_.end(), + [](const DispatchProposal &left, const DispatchProposal &right) { + return std::tie(left.instruction.sequenceId, left.stableObjectId) < + std::tie(right.instruction.sequenceId, right.stableObjectId); + }); + + std::array remaining{}; + for (size_t index = 0; index < queues_.size(); ++index) { + const size_t released = issueProposals_[index] ? 1 : 0; + const size_t occupied = queues_[index].size() - released; + NpuEngineClass engine = static_cast(index); + remaining[index] = + capacity(engine) > occupied ? capacity(engine) - occupied : 0; + } + + std::set shadowFlowIds = usedFlowIds_; + bool dispatchBlocked = false; + for (const DispatchProposal &proposal : dispatchProposals_) { + const size_t index = engineIndex(proposal.instruction.engine); + if (dispatchBlocked || remaining[index] == 0) { + proposedRejectedDispatches_.push_back(proposal.instruction.sequenceId); + dispatchBlocked = true; + continue; + } + + NpuIssueEntry entry{.instruction = proposal.instruction, + .stableObjectId = proposal.stableObjectId}; + for (const std::string &tile : entry.instruction.inputTiles) { + auto producer = producers_.find({entry.instruction.blockId, tile}); + if (producer == producers_.end()) + continue; + uint64_t flow = + dependencyFlowId(entry.instruction.blockId, producer->second, + entry.instruction.sequenceId, tile); + for (size_t attempts = 0; shadowFlowIds.contains(flow); ++attempts) { + if (attempts >= shadowFlowIds.size()) { + setRuntimeFailureCode("npu_dependency_flow_id_exhausted"); + break; + } + flow = flow == kMaxPortableJsonInteger ? 1 : flow + 1; + } + if (!runtimeFailureCode().empty()) + break; + shadowFlowIds.insert(flow); + entry.derivedDependencies.push_back( + {.producerSequenceId = producer->second, + .tileIdentity = tile, + .flowId = flow}); + } + if (!runtimeFailureCode().empty()) { + proposedRejectedDispatches_.push_back(proposal.instruction.sequenceId); + dispatchBlocked = true; + continue; + } + --remaining[index]; + acceptedDispatches_.push_back(std::move(entry)); + } + + for (const NpuIssueEntry &entry : acceptedDispatches_) { + emitObservation( + {.category = "instruction", + .name = "dispatch", + .phase = TraceEventPhase::Instant, + .rootSequenceId = entry.instruction.sequenceId, + .arguments = { + {"engine", std::string(toString(entry.instruction.engine))}, + {"stable_object_id", + static_cast(entry.stableObjectId)}}}); + for (const NpuDependency &dependency : entry.derivedDependencies) + emitObservation( + {.category = "dependency", + .name = "tile", + .phase = TraceEventPhase::FlowStart, + .rootSequenceId = entry.instruction.sequenceId, + .flowId = dependency.flowId, + .arguments = { + {"producer_sequence_id", dependency.producerSequenceId}, + {"tile_identity", dependency.tileIdentity}}}); + } + for (uint64_t sequenceId : proposedRejectedDispatches_) + emitObservation({.category = "stall", + .name = "issue_queue_capacity", + .phase = TraceEventPhase::Instant, + .rootSequenceId = sequenceId}); + + std::vector issues; + for (const auto &proposal : issueProposals_) + if (proposal) + issues.push_back(&*proposal); + std::sort(issues.begin(), issues.end(), + [](const auto *left, const auto *right) { + return issueEntryLess(*left, *right); + }); + for (const NpuIssueEntry *entry : issues) { + emitObservation( + {.category = "instruction", + .name = "issue", + .phase = TraceEventPhase::Instant, + .rootSequenceId = entry->instruction.sequenceId, + .arguments = { + {"engine", std::string(toString(entry->instruction.engine))}, + {"stable_object_id", + static_cast(entry->stableObjectId)}}}); + for (const NpuDependency &dependency : entry->derivedDependencies) + emitObservation( + {.category = "dependency", + .name = "tile", + .phase = TraceEventPhase::FlowEnd, + .rootSequenceId = entry->instruction.sequenceId, + .flowId = dependency.flowId, + .arguments = { + {"producer_sequence_id", dependency.producerSequenceId}, + {"tile_identity", dependency.tileIdentity}}}); + } + + std::sort(completionProposals_.begin(), completionProposals_.end()); + for (uint64_t producer : completionProposals_) + for (const auto &queue : queues_) + for (const NpuIssueEntry &entry : queue) + if (std::ranges::any_of(entry.derivedDependencies, + [&](const NpuDependency &dependency) { + return dependency.producerSequenceId == + producer; + })) + emitObservation({.category = "dependency", + .name = "ready", + .phase = TraceEventPhase::Instant, + .rootSequenceId = entry.instruction.sequenceId, + .arguments = {{"producer_sequence_id", producer}}}); +} + +void NpuDependencyTracker::doXfer(Epoch epoch) { + const bool changed = hasPendingCommit(); + issued_.clear(); + acceptedDispatchSequences_.clear(); + rejectedDispatches_ = proposedRejectedDispatches_; + + for (NpuIssueEntry &entry : acceptedDispatches_) { + entry.instruction.timestamps.dispatched = epoch.time; + for (const std::string &tile : entry.instruction.outputTiles) + producers_[{entry.instruction.blockId, tile}] = + entry.instruction.sequenceId; + for (const NpuDependency &dependency : entry.derivedDependencies) + usedFlowIds_.insert(dependency.flowId); + acceptedDispatchSequences_.insert(entry.instruction.sequenceId); + lastDispatchedSequence_ = entry.instruction.sequenceId; + queues_[engineIndex(entry.instruction.engine)].push_back(std::move(entry)); + ++totalDispatches_; + } + totalDispatchStalls_ += proposedRejectedDispatches_.size(); + + for (size_t index = 0; index < issueProposals_.size(); ++index) { + if (!issueProposals_[index]) + continue; + auto &queue = queues_[index]; + auto position = + std::ranges::find_if(queue, [&](const NpuIssueEntry &entry) { + return entry.instruction.sequenceId == + issueProposals_[index]->instruction.sequenceId && + entry.stableObjectId == issueProposals_[index]->stableObjectId; + }); + if (position == queue.end()) { + setRuntimeFailureCode("npu_issue_entry_missing"); + continue; + } + position->instruction.timestamps.issued = epoch.time; + outstandingSequences_.insert(position->instruction.sequenceId); + issued_.push_back(std::move(*position)); + queue.erase(position); + ++totalIssues_; + } + std::sort(issued_.begin(), issued_.end(), issueEntryLess); + + for (uint64_t sequenceId : completionProposals_) { + for (const auto &queue : queues_) + for (const NpuIssueEntry &entry : queue) + totalDependencyWakeups_ += static_cast(std::ranges::count_if( + entry.derivedDependencies, [&](const NpuDependency &dependency) { + return dependency.producerSequenceId == sequenceId; + })); + completedSequences_.insert(sequenceId); + outstandingSequences_.erase(sequenceId); + } + + for (size_t index = 0; index < queues_.size(); ++index) { + std::sort(queues_[index].begin(), queues_[index].end(), issueEntryLess); + highWatermarks_[index] = + std::max(highWatermarks_[index], queues_[index].size()); + } + + dispatchProposals_.clear(); + acceptedDispatches_.clear(); + proposedRejectedDispatches_.clear(); + for (auto &proposal : issueProposals_) + proposal.reset(); + completionProposals_.clear(); + if (changed) + lastUpdate_ = epoch; +} + +bool NpuDependencyTracker::hasPendingCommit() const { + return !dispatchProposals_.empty() || !acceptedDispatches_.empty() || + !proposedRejectedDispatches_.empty() || + std::ranges::any_of( + issueProposals_, + [](const auto &entry) { return entry.has_value(); }) || + !completionProposals_.empty(); +} + +bool NpuDependencyTracker::isRunnable(Epoch) const { + return !dispatchProposals_.empty() || !completionProposals_.empty(); +} + +RuntimeObjectState NpuDependencyTracker::runtimeState(Epoch epoch) const { + RuntimeObjectState state = SimObject::runtimeState(epoch); + for (const auto &queue : queues_) + state.queueOccupancy += queue.size(); + state.pendingOffers = dispatchProposals_.size() + completionProposals_.size(); + state.activeReservations = outstandingSequences_.size(); + state.quiescent = state.queueOccupancy == 0 && state.pendingOffers == 0 && + state.activeReservations == 0 && !hasPendingCommit(); + if (!state.quiescent) + state.reason = + state.pendingOffers != 0 + ? "npu_pending_proposal" + : (state.queueOccupancy != 0 ? "npu_issue_queue_not_empty" + : "npu_execution_outstanding"); + return state; +} + +void NpuDependencyTracker::collectStatistics( + std::vector &out) const { + auto append = [&](std::string name, StatisticKind kind, uint64_t value) { + out.push_back({.name = std::move(name), + .objectPath = std::string(path()), + .kind = kind, + .value = value, + .lastUpdate = lastUpdate_}); + }; + constexpr std::array names = {"scalar", "vector", "cube", "tma"}; + for (size_t index = 0; index < queues_.size(); ++index) { + append("issue_queue_occupancy_" + std::string(names[index]), + StatisticKind::Gauge, queues_[index].size()); + append("issue_queue_peak_" + std::string(names[index]), + StatisticKind::Gauge, highWatermarks_[index]); + } + append("dispatch_stalls", StatisticKind::Counter, totalDispatchStalls_); + append("dispatched_instructions", StatisticKind::Counter, totalDispatches_); + append("issued_instructions", StatisticKind::Counter, totalIssues_); + append("dependency_wakeups", StatisticKind::Counter, totalDependencyWakeups_); +} + +const NpuIssueEntry * +NpuDependencyTracker::proposedIssue(NpuEngineClass engine) const { + const auto &proposal = issueProposals_[engineIndex(engine)]; + return proposal ? &*proposal : nullptr; +} + +std::vector +NpuDependencyTracker::queued(NpuEngineClass engine) const { + return queues_[engineIndex(engine)]; +} + +std::vector +NpuDependencyTracker::dependencies(uint64_t sequenceId) const { + for (const auto &queue : queues_) + for (const NpuIssueEntry &entry : queue) + if (entry.instruction.sequenceId == sequenceId) + return entry.derivedDependencies; + return {}; +} + +bool NpuDependencyTracker::isReady(uint64_t sequenceId) const { + for (const auto &queue : queues_) + for (const NpuIssueEntry &entry : queue) + if (entry.instruction.sequenceId == sequenceId) + return ready(entry); + return false; +} + +bool NpuDependencyTracker::dispatchAccepted(uint64_t sequenceId) const { + return acceptedDispatchSequences_.contains(sequenceId); +} + +size_t NpuDependencyTracker::queueSize(NpuEngineClass engine) const { + return queues_[engineIndex(engine)].size(); +} + +void NpuDependencyTracker::reset() { + for (auto &queue : queues_) + queue.clear(); + dispatchProposals_.clear(); + acceptedDispatches_.clear(); + proposedRejectedDispatches_.clear(); + rejectedDispatches_.clear(); + acceptedDispatchSequences_.clear(); + for (auto &proposal : issueProposals_) + proposal.reset(); + issued_.clear(); + completionProposals_.clear(); + outstandingSequences_.clear(); + completedSequences_.clear(); + producers_.clear(); + usedFlowIds_.clear(); + lastDispatchedSequence_.reset(); + highWatermarks_.fill(0); + totalDispatches_ = 0; + totalDispatchStalls_ = 0; + totalIssues_ = 0; + totalDependencyWakeups_ = 0; + lastUpdate_ = {}; + clearRuntimeFailureCode(); +} + +struct NpuExecutionPipeline::Impl { + struct RobEntry { + NpuInstruction instruction; + bool completed = false; + }; + + struct ActiveExecution { + NpuIssueEntry entry; + Epoch issueEpoch; + Epoch readyEpoch; + size_t unitIndex = 0; + bool memoryDelivered = false; + std::optional memoryRequest; + }; + + explicit Impl(NpuExecutionConfig configuration) + : config(configuration), + memoryProtocol("memory_protocol", kInvalidObjectId, nullptr, + configuration.memoryRequests), + memoryController("memory_controller", kInvalidObjectId, nullptr, + static_cast(std::min( + configuration.memoryRequests, + std::numeric_limits::max()))) {} + + NpuExecutionConfig config; + SimSystem *system = nullptr; + RequestResponse memoryProtocol; + Resource memoryController; + std::map> rob; + std::map lastAdmittedSequence; + std::vector admissionProposals; + std::vector executionProposals; + std::map active; + std::vector deliveryProposals; + std::vector completionProposals; + bool traceExhaustedProposal = false; + bool traceExhausted = false; + std::set executedSequences; + std::vector completed; + std::vector retired; + std::vector memoryRequests; + std::vector memoryResponses; + std::map scratchpad; + std::map globalMemory; + NpuArchitecturalResult result; + std::array unitHighWatermarks{}; + uint64_t totalExecutions = 0; + uint64_t totalCompletions = 0; + uint64_t totalUnitStalls = 0; + uint64_t unitStallProposals = 0; + uint64_t totalMemoryRequests = 0; + Epoch lastUpdate; +}; + +namespace { + +constexpr uint32_t kNpuCompletionEvent = 0x4e505501; +constexpr uint32_t kNpuMemoryServiceEvent = 0x4e505502; +constexpr uint32_t kNpuMemoryCleanupEvent = 0x4e505503; + +size_t configuredUnits(const NpuExecutionConfig &config, + NpuEngineClass engine) { + switch (engine) { + case NpuEngineClass::Scalar: + return config.scalarUnits; + case NpuEngineClass::Vector: + return config.vectorUnits; + case NpuEngineClass::Cube: + return config.cubeUnits; + case NpuEngineClass::Tma: + return config.tmaUnits; + } + return 0; +} + +std::optional parseAddress(std::string_view text) { + if (text.size() < 3 || !text.starts_with("0x")) + return std::nullopt; + uint64_t result = 0; + auto [position, error] = + std::from_chars(text.data() + 2, text.data() + text.size(), result, 16); + if (error != std::errc{} || position != text.data() + text.size()) + return std::nullopt; + return result; +} + +void updateDigest(uint64_t &digest, const NpuInstruction &instruction) { + constexpr uint64_t kPrime = 1099511628211ULL; + auto append = [&](uint8_t byte) { + digest ^= byte; + digest *= kPrime; + }; + auto appendInteger = [&](uint64_t value) { + for (unsigned shift = 0; shift != 64; shift += 8) + append(static_cast(value >> shift)); + }; + appendInteger(instruction.sequenceId); + appendInteger(instruction.blockId); + for (char character : instruction.opcode) + append(static_cast(character)); + append(0); + for (const std::string &tile : instruction.outputTiles) { + for (char character : tile) + append(static_cast(character)); + append(0); + } +} + +} // namespace + +NpuExecutionPipeline::NpuExecutionPipeline(std::string name, ObjectId id, + SimObject *parent, + NpuExecutionConfig config, + SimSystem *system, + ObservationSink *observations) + : SimObject(ObjectKind::Compute, std::move(name), id, parent, observations), + impl_(std::make_unique(config)) { + impl_->system = system; + if (config.memoryRequests > std::numeric_limits::max()) + setRuntimeFailureCode("npu_memory_capacity_invalid"); +} + +NpuExecutionPipeline::~NpuExecutionPipeline() = default; + +bool NpuExecutionPipeline::proposeAdmit(const NpuInstruction &instruction) { + auto matches = [&](const NpuInstruction &candidate) { + return candidate.sequenceId == instruction.sequenceId; + }; + if (std::ranges::any_of(impl_->admissionProposals, matches)) + return false; + if (auto previous = impl_->lastAdmittedSequence.find(instruction.blockId); + previous != impl_->lastAdmittedSequence.end() && + instruction.sequenceId <= previous->second) + return false; + for (const auto &[blockId, entries] : impl_->rob) + if (std::ranges::any_of(entries, [&](const Impl::RobEntry &entry) { + return entry.instruction.sequenceId == instruction.sequenceId; + })) + return false; + impl_->admissionProposals.push_back(instruction); + return true; +} + +uint64_t +NpuExecutionPipeline::executionLatency(const NpuInstruction &instruction) { + switch (instruction.engine) { + case NpuEngineClass::Scalar: + return 1; + case NpuEngineClass::Vector: + if (instruction.opcode == "TEXP" || instruction.opcode == "TLOG" || + instruction.opcode == "TSQRT" || instruction.opcode == "TRSQRT") + return 3; + return 2; + case NpuEngineClass::Cube: + if (instruction.opcode.starts_with("TMATMUL_MX")) + return 5; + return 4; + case NpuEngineClass::Tma: + if (instruction.opcode == "TSTORE" || instruction.opcode == "TSTORE_FP") + return 4; + return 3; + } + return 1; +} + +bool NpuExecutionPipeline::proposeExecute(const NpuIssueEntry &entry, + Epoch issueEpoch) { + const uint64_t sequenceId = entry.instruction.sequenceId; + const NpuInstruction *admitted = nullptr; + for (const auto &[blockId, entries] : impl_->rob) + for (const Impl::RobEntry &rob : entries) + if (rob.instruction.sequenceId == sequenceId) + admitted = &rob.instruction; + if (!admitted || impl_->executedSequences.contains(sequenceId) || + impl_->active.contains(sequenceId) || + std::ranges::any_of(impl_->executionProposals, + [&](const Impl::ActiveExecution &proposal) { + return proposal.entry.instruction.sequenceId == + sequenceId; + })) + return false; + NpuInstruction admittedIdentity = *admitted; + NpuInstruction issuedIdentity = entry.instruction; + admittedIdentity.timestamps = {}; + issuedIdentity.timestamps = {}; + if (admittedIdentity != issuedIdentity) { + setRuntimeFailureCode("npu_execution_identity_mismatch"); + return false; + } + + size_t occupied = activeExecutions(entry.instruction.engine); + occupied += std::ranges::count_if( + impl_->executionProposals, [&](const Impl::ActiveExecution &proposal) { + return proposal.entry.instruction.engine == entry.instruction.engine; + }); + if (occupied >= configuredUnits(impl_->config, entry.instruction.engine)) { + ++impl_->unitStallProposals; + return false; + } + + const uint64_t latency = executionLatency(entry.instruction); + if (issueEpoch.time > std::numeric_limits::max() - latency) { + setRuntimeFailureCode("npu_execution_time_overflow"); + return false; + } + Epoch readyEpoch{issueEpoch.time + latency, 0}; + size_t unit = 0; + auto occupiedUnit = [&](size_t candidate) { + return std::ranges::any_of( + impl_->active, + [&](const auto &active) { + return active.second.entry.instruction.engine == + entry.instruction.engine && + active.second.unitIndex == candidate; + }) || + std::ranges::any_of(impl_->executionProposals, + [&](const Impl::ActiveExecution &proposal) { + return proposal.entry.instruction.engine == + entry.instruction.engine && + proposal.unitIndex == candidate; + }); + }; + while (occupiedUnit(unit)) + ++unit; + + Impl::ActiveExecution proposal{.entry = entry, + .issueEpoch = issueEpoch, + .readyEpoch = readyEpoch, + .unitIndex = unit}; + const bool store = entry.instruction.opcode.starts_with("TSTORE"); + std::set pendingTiles; + for (const auto &[activeSequence, active] : impl_->active) + if (!active.entry.instruction.opcode.starts_with("TSTORE")) + for (const NpuTileDescriptor &tile : + active.entry.instruction.outputTileDescriptors) + if (!impl_->scratchpad.contains(tile.identity)) + pendingTiles.insert(tile.identity); + for (const Impl::ActiveExecution &pending : impl_->executionProposals) + if (!pending.entry.instruction.opcode.starts_with("TSTORE")) + for (const NpuTileDescriptor &tile : + pending.entry.instruction.outputTileDescriptors) + if (!impl_->scratchpad.contains(tile.identity)) + pendingTiles.insert(tile.identity); + if (!store) + for (const NpuTileDescriptor &tile : + entry.instruction.outputTileDescriptors) + if (!impl_->scratchpad.contains(tile.identity)) + pendingTiles.insert(tile.identity); + if (impl_->scratchpad.size() + pendingTiles.size() > + impl_->config.scratchpadTiles) { + ++impl_->unitStallProposals; + return false; + } + if (store && (entry.instruction.inputTileDescriptors.empty() || + !impl_->scratchpad.contains( + entry.instruction.inputTileDescriptors.front().identity))) { + setRuntimeFailureCode("npu_scratchpad_input_missing"); + return false; + } + + if (entry.instruction.engine == NpuEngineClass::Tma) { + size_t memoryOccupied = + std::ranges::count_if(impl_->active, [](const auto &active) { + return active.second.memoryRequest.has_value(); + }); + memoryOccupied += std::ranges::count_if( + impl_->executionProposals, [](const Impl::ActiveExecution &pending) { + return pending.memoryRequest.has_value(); + }); + if (memoryOccupied >= impl_->config.memoryRequests) { + ++impl_->unitStallProposals; + return false; + } + const bool write = store; + const auto &descriptors = write ? entry.instruction.outputTileDescriptors + : entry.instruction.inputTileDescriptors; + if (descriptors.empty() || + (!write && entry.instruction.outputTileDescriptors.empty())) { + setRuntimeFailureCode("npu_memory_descriptor_missing"); + return false; + } + std::optional address = parseAddress(descriptors.front().address); + if (!address) { + setRuntimeFailureCode("npu_memory_address_invalid"); + return false; + } + NpuMemoryRequest request{ + .sequenceId = sequenceId, + .correlationId = sequenceId, + .address = *address, + .write = write, + .tileIdentity = + write ? entry.instruction.inputTileDescriptors.front().identity + : entry.instruction.outputTileDescriptors.front().identity}; + proposal.memoryRequest = request; + } + + if (impl_->system) { + Epoch wake = entry.instruction.engine == NpuEngineClass::Tma + ? Epoch{issueEpoch.time + 1, 0} + : readyEpoch; + uint32_t kind = entry.instruction.engine == NpuEngineClass::Tma + ? kNpuMemoryServiceEvent + : kNpuCompletionEvent; + if (!impl_->system->scheduleEvent({wake, id(), kind, sequenceId})) { + setRuntimeFailureCode("npu_completion_schedule_failed"); + return false; + } + } + impl_->executionProposals.push_back(std::move(proposal)); + return true; +} + +bool NpuExecutionPipeline::proposeTraceExhausted() { + if (impl_->traceExhausted || impl_->traceExhaustedProposal) + return false; + impl_->traceExhaustedProposal = true; + return true; +} + +void NpuExecutionPipeline::doWork(Epoch epoch) { + while (auto response = impl_->memoryProtocol.proposePopResponse()) + (void)response; + + while (const auto *request = impl_->memoryProtocol.peekRequest()) { + auto active = impl_->active.find(request->correlationId); + if (active == impl_->active.end()) { + setRuntimeFailureCode("npu_memory_correlation_missing"); + break; + } + if (active->second.issueEpoch.time == + std::numeric_limits::max() || + epoch.time < active->second.issueEpoch.time + 1) + break; + if (active->second.memoryDelivered) { + setRuntimeFailureCode("npu_memory_request_delivered_twice"); + break; + } + auto popped = impl_->memoryProtocol.proposePopRequest(); + if (!popped) + break; + impl_->deliveryProposals.push_back(popped->correlationId); + } + + for (const auto &[sequenceId, active] : impl_->active) { + if (active.readyEpoch != epoch || + std::ranges::find(impl_->completionProposals, sequenceId) != + impl_->completionProposals.end()) + continue; + if (active.memoryRequest) { + if (!active.memoryDelivered) + continue; + uint64_t value = + active.memoryRequest->write + ? impl_->scratchpad.at(active.memoryRequest->tileIdentity) + : active.memoryRequest->address ^ sequenceId; + if (!impl_->memoryProtocol.proposeResponse( + {.correlationId = sequenceId, .value = value}, sequenceId) || + !impl_->memoryController.proposeCancel(id(), sequenceId)) { + setRuntimeFailureCode("npu_memory_response_failed"); + continue; + } + } + impl_->completionProposals.push_back(sequenceId); + } +} + +void NpuExecutionPipeline::doArbitrate(Epoch) { + std::sort(impl_->admissionProposals.begin(), impl_->admissionProposals.end(), + [](const NpuInstruction &left, const NpuInstruction &right) { + return std::tie(left.sequenceId, left.blockId) < + std::tie(right.sequenceId, right.blockId); + }); + std::sort(impl_->executionProposals.begin(), impl_->executionProposals.end(), + [](const Impl::ActiveExecution &left, + const Impl::ActiveExecution &right) { + return issueEntryLess(left.entry, right.entry); + }); + for (const Impl::ActiveExecution &execution : impl_->executionProposals) + if (execution.memoryRequest && + (!impl_->memoryProtocol.proposeRequest( + *execution.memoryRequest, + execution.entry.instruction.sequenceId) || + !impl_->memoryController.proposeReserve( + id(), 1, execution.issueEpoch, + execution.entry.instruction.sequenceId, execution.readyEpoch, + execution.entry.instruction.sequenceId))) + setRuntimeFailureCode("npu_memory_request_failed"); + impl_->memoryProtocol.doArbitrate({}); + impl_->memoryController.doArbitrate({}); + for (const Impl::ActiveExecution &execution : impl_->executionProposals) + emitObservation( + {.category = "instruction", + .name = "execute", + .phase = TraceEventPhase::Complete, + .rootSequenceId = execution.entry.instruction.sequenceId, + .duration = executionLatency(execution.entry.instruction), + .arguments = { + {"engine", + std::string(toString(execution.entry.instruction.engine))}, + {"unit_index", static_cast(execution.unitIndex)}}}); + for (uint64_t sequenceId : impl_->completionProposals) + emitObservation({.category = "instruction", + .name = "complete", + .phase = TraceEventPhase::Instant, + .rootSequenceId = sequenceId}); +} + +void NpuExecutionPipeline::doXfer(Epoch epoch) { + const bool changed = hasPendingCommit(); + impl_->completed.clear(); + impl_->retired.clear(); + impl_->memoryProtocol.doXfer(epoch); + impl_->memoryController.doXfer(epoch); + + for (NpuInstruction &instruction : impl_->admissionProposals) { + auto &entries = impl_->rob[instruction.blockId]; + entries.push_back({.instruction = std::move(instruction)}); + impl_->lastAdmittedSequence[entries.back().instruction.blockId] = + entries.back().instruction.sequenceId; + std::sort(entries.begin(), entries.end(), + [](const auto &left, const auto &right) { + return left.instruction.sequenceId < + right.instruction.sequenceId; + }); + } + + for (Impl::ActiveExecution &execution : impl_->executionProposals) { + const uint64_t sequenceId = execution.entry.instruction.sequenceId; + impl_->executedSequences.insert(sequenceId); + if (execution.memoryRequest) { + impl_->memoryRequests.push_back(*execution.memoryRequest); + ++impl_->totalMemoryRequests; + } + impl_->active.emplace(sequenceId, std::move(execution)); + ++impl_->totalExecutions; + } + + for (uint64_t sequenceId : impl_->deliveryProposals) { + auto active = impl_->active.find(sequenceId); + if (active == impl_->active.end()) { + setRuntimeFailureCode("npu_memory_correlation_missing"); + continue; + } + active->second.memoryDelivered = true; + if (impl_->system && + !impl_->system->scheduleEvent( + {active->second.readyEpoch, id(), kNpuCompletionEvent, sequenceId})) + setRuntimeFailureCode("npu_completion_schedule_failed"); + } + + std::sort(impl_->completionProposals.begin(), + impl_->completionProposals.end()); + for (uint64_t sequenceId : impl_->completionProposals) { + auto active = impl_->active.find(sequenceId); + if (active == impl_->active.end()) { + setRuntimeFailureCode("npu_completion_correlation_missing"); + continue; + } + active->second.entry.instruction.timestamps.completed = epoch.time; + NpuIssueEntry completed = active->second.entry; + for (auto &[blockId, entries] : impl_->rob) + for (Impl::RobEntry &entry : entries) + if (entry.instruction.sequenceId == sequenceId) { + entry.instruction = completed.instruction; + entry.completed = true; + } + + uint64_t value = sequenceId; + if (active->second.memoryRequest) { + const NpuMemoryRequest &request = *active->second.memoryRequest; + value = request.write ? impl_->scratchpad.at(request.tileIdentity) + : request.address ^ sequenceId; + if (request.write) + impl_->globalMemory[request.address] = value; + impl_->memoryResponses.push_back( + {.correlationId = request.correlationId, .value = value}); + if (impl_->system) { + if (epoch.time == std::numeric_limits::max() || + !impl_->system->scheduleEvent({{epoch.time + 1, 0}, + id(), + kNpuMemoryCleanupEvent, + sequenceId})) + setRuntimeFailureCode("npu_completion_schedule_failed"); + } + } + if (!completed.instruction.opcode.starts_with("TSTORE")) + for (const NpuTileDescriptor &tile : + completed.instruction.outputTileDescriptors) + impl_->scratchpad[tile.identity] = value; + impl_->completed.push_back(std::move(completed)); + impl_->active.erase(active); + ++impl_->totalCompletions; + } + std::sort(impl_->completed.begin(), impl_->completed.end(), issueEntryLess); + + for (auto &[blockId, entries] : impl_->rob) { + while (!entries.empty() && entries.front().completed) { + NpuInstruction instruction = std::move(entries.front().instruction); + entries.erase(entries.begin()); + instruction.timestamps.retired = epoch.time; + updateDigest(impl_->result.digest, instruction); + ++impl_->result.retiredInstructions; + impl_->result.retiredSequenceIds.push_back(instruction.sequenceId); + emitObservation({.category = "instruction", + .name = "retire", + .phase = TraceEventPhase::Instant, + .rootSequenceId = instruction.sequenceId}); + impl_->retired.push_back(std::move(instruction)); + } + } + std::sort(impl_->retired.begin(), impl_->retired.end(), + [](const NpuInstruction &left, const NpuInstruction &right) { + return std::tie(left.blockId, left.sequenceId) < + std::tie(right.blockId, right.sequenceId); + }); + + impl_->traceExhausted = + impl_->traceExhausted || impl_->traceExhaustedProposal; + impl_->totalUnitStalls += impl_->unitStallProposals; + for (size_t index = 0; index < impl_->unitHighWatermarks.size(); ++index) { + NpuEngineClass engine = static_cast(index); + impl_->unitHighWatermarks[index] = + std::max(impl_->unitHighWatermarks[index], activeExecutions(engine)); + } + impl_->admissionProposals.clear(); + impl_->executionProposals.clear(); + impl_->deliveryProposals.clear(); + impl_->completionProposals.clear(); + impl_->traceExhaustedProposal = false; + impl_->unitStallProposals = 0; + if (changed) + impl_->lastUpdate = epoch; +} + +bool NpuExecutionPipeline::hasPendingCommit() const { + return !impl_->admissionProposals.empty() || + !impl_->executionProposals.empty() || + !impl_->deliveryProposals.empty() || + !impl_->completionProposals.empty() || impl_->traceExhaustedProposal || + impl_->unitStallProposals != 0 || + impl_->memoryProtocol.hasPendingCommit() || + impl_->memoryController.hasPendingCommit(); +} + +bool NpuExecutionPipeline::isRunnable(Epoch epoch) const { + if (hasPendingCommit()) + return true; + return std::ranges::any_of(impl_->active, [&](const auto &active) { + return active.second.readyEpoch == epoch; + }); +} + +RuntimeObjectState NpuExecutionPipeline::runtimeState(Epoch epoch) const { + RuntimeObjectState state = SimObject::runtimeState(epoch); + for (const auto &[blockId, entries] : impl_->rob) + state.queueOccupancy += entries.size(); + state.activeReservations = impl_->active.size(); + state.pendingOffers = impl_->admissionProposals.size() + + impl_->executionProposals.size() + + impl_->completionProposals.size(); + state.quiescent = + complete() || + (state.queueOccupancy == 0 && state.activeReservations == 0 && + state.pendingOffers == 0 && !hasPendingCommit()); + if (!state.quiescent) + state.reason = state.activeReservations ? "npu_execution_active" + : "npu_retirement_pending"; + return state; +} + +void NpuExecutionPipeline::collectStatistics( + std::vector &out) const { + auto append = [&](std::string name, StatisticKind kind, uint64_t value) { + out.push_back({.name = std::move(name), + .objectPath = std::string(path()), + .kind = kind, + .value = value, + .lastUpdate = impl_->lastUpdate}); + }; + append("active_executions", StatisticKind::Gauge, impl_->active.size()); + append("executed_instructions", StatisticKind::Counter, + impl_->totalExecutions); + append("completed_instructions", StatisticKind::Counter, + impl_->totalCompletions); + append("retired_instructions", StatisticKind::Counter, + impl_->result.retiredInstructions); + append("execution_unit_stalls", StatisticKind::Counter, + impl_->totalUnitStalls); + append("memory_requests", StatisticKind::Counter, impl_->totalMemoryRequests); + append("scratchpad_tiles", StatisticKind::Gauge, impl_->scratchpad.size()); +} + +void NpuExecutionPipeline::bindSystem(SimSystem *system) { + impl_->system = system; +} + +Epoch NpuExecutionPipeline::completionEpoch(uint64_t sequenceId) const { + auto active = impl_->active.find(sequenceId); + return active == impl_->active.end() ? Epoch{} : active->second.readyEpoch; +} + +bool NpuExecutionPipeline::canAccept(NpuEngineClass engine) const { + size_t occupied = activeExecutions(engine); + occupied += std::ranges::count_if( + impl_->executionProposals, [&](const Impl::ActiveExecution &proposal) { + return proposal.entry.instruction.engine == engine; + }); + if (occupied >= configuredUnits(impl_->config, engine)) + return false; + if (engine != NpuEngineClass::Tma) + return true; + size_t memoryOccupied = + std::ranges::count_if(impl_->active, [](const auto &active) { + return active.second.memoryRequest.has_value(); + }); + memoryOccupied += std::ranges::count_if( + impl_->executionProposals, [](const Impl::ActiveExecution &proposal) { + return proposal.memoryRequest.has_value(); + }); + return memoryOccupied < impl_->config.memoryRequests; +} + +size_t NpuExecutionPipeline::activeExecutions(NpuEngineClass engine) const { + return std::ranges::count_if(impl_->active, [&](const auto &active) { + return active.second.entry.instruction.engine == engine; + }); +} + +const std::vector &NpuExecutionPipeline::completed() const { + return impl_->completed; +} + +const std::vector &NpuExecutionPipeline::retired() const { + return impl_->retired; +} + +const std::vector & +NpuExecutionPipeline::memoryRequests() const { + return impl_->memoryRequests; +} + +const std::vector & +NpuExecutionPipeline::memoryResponses() const { + return impl_->memoryResponses; +} + +bool NpuExecutionPipeline::scratchpadContains( + std::string_view tileIdentity) const { + return impl_->scratchpad.contains(std::string(tileIdentity)); +} + +std::optional +NpuExecutionPipeline::globalMemoryValue(uint64_t address) const { + auto value = impl_->globalMemory.find(address); + return value == impl_->globalMemory.end() + ? std::nullopt + : std::optional(value->second); +} + +const NpuArchitecturalResult & +NpuExecutionPipeline::architecturalResult() const { + return impl_->result; +} + +bool NpuExecutionPipeline::complete() const { + if (!impl_->traceExhausted || !impl_->active.empty() || + !impl_->admissionProposals.empty() || + !impl_->executionProposals.empty() || hasPendingCommit()) + return false; + for (const auto &[blockId, entries] : impl_->rob) + if (!entries.empty()) + return false; + return impl_->memoryProtocol.runtimeState({}).quiescent && + impl_->memoryController.runtimeState({}).quiescent; +} + +void NpuExecutionPipeline::reset() { + NpuExecutionConfig config = impl_->config; + SimSystem *system = impl_->system; + impl_ = std::make_unique(config); + impl_->system = system; + clearRuntimeFailureCode(); +} + +NpuTraceSource::NpuTraceSource(std::string name, ObjectId id, SimObject *parent, + ObservationSink *observations) + : SimObject(ObjectKind::TraceSource, std::move(name), id, parent, + observations) {} + +bool NpuTraceSource::loadDocument(PtoTraceDocument document) { + if (loaded_ || pending_ || committed_) + return false; + document_ = std::move(document); + loaded_ = true; + return true; +} + +void NpuTraceSource::doWork(Epoch) { + if (pending_ || committed_) + return; + if (!loaded_) { + setRuntimeFailureCode("npu_trace_not_loaded"); + return; + } + + struct LocalSink final : ObservationSink { + ObservationRecorder recorder; + bool proposeObservation(EventProposal proposal) override { + return recorder.propose(std::move(proposal)); + } + } sink; + + const size_t queueCapacity = std::max(document_.records.size(), 1); + NpuDependencyTracker tracker( + "dependencies", 10, nullptr, + {queueCapacity, queueCapacity, queueCapacity, queueCapacity}, &sink); + NpuExecutionPipeline execution( + "execution", 11, nullptr, + {.scalarUnits = 1, + .vectorUnits = 2, + .cubeUnits = 1, + .tmaUnits = 1, + .memoryRequests = 2, + .scratchpadTiles = std::max(document_.records.size() * 2, 8)}, + nullptr, &sink); + + auto commitTracker = [&](Epoch epoch) { + tracker.doArbitrate(epoch); + tracker.doXfer(epoch); + return sink.recorder.commitOwner(tracker.id(), epoch); + }; + auto commitExecution = [&](Epoch epoch) { + execution.doArbitrate(epoch); + execution.doXfer(epoch); + return sink.recorder.commitOwner(execution.id(), epoch); + }; + + NpuDecoder decoder; + uint64_t tick = 0; + for (const PtoTraceRecord &record : document_.records) { + NpuDecodeResult decoded = decoder.decode(record); + if (!decoded.succeeded()) { + setRuntimeFailureCode("npu_decode_failed"); + return; + } + NpuInstruction instruction = std::move(*decoded.instruction); + ObjectId stableObjectId = + static_cast(20 + static_cast(instruction.engine)); + if (!tracker.proposeDispatch(instruction, stableObjectId) || + !execution.proposeAdmit(instruction) || !commitTracker({tick, 0}) || + !tracker.dispatchAccepted(record.sequenceId) || + !commitExecution({tick, 0})) { + setRuntimeFailureCode("npu_dispatch_failed"); + return; + } + if (!tracker.runtimeFailureCode().empty() || + !execution.runtimeFailureCode().empty()) { + setRuntimeFailureCode("npu_dispatch_runtime_failed"); + return; + } + ++tick; + } + + if (!execution.proposeTraceExhausted()) { + setRuntimeFailureCode("npu_trace_exhaustion_failed"); + return; + } + + std::vector pendingIssues; + constexpr std::array engines = {NpuEngineClass::Scalar, + NpuEngineClass::Vector, NpuEngineClass::Cube, + NpuEngineClass::Tma}; + const uint64_t maximumTicks = + std::max(128, document_.records.size() * 32); + bool finished = false; + for (uint64_t iteration = 0; iteration < maximumTicks; ++iteration, ++tick) { + Epoch epoch{tick, 0}; + for (const NpuIssueEntry &issue : pendingIssues) + if (!execution.proposeExecute(issue, epoch)) { + setRuntimeFailureCode("npu_execution_admission_failed"); + return; + } + pendingIssues.clear(); + + execution.doWork(epoch); + if (!commitExecution(epoch)) { + setRuntimeFailureCode("npu_execution_observation_failed"); + return; + } + if (!execution.runtimeFailureCode().empty()) { + setRuntimeFailureCode("npu_execution_runtime_failed"); + return; + } + for (const NpuIssueEntry &completed : execution.completed()) + if (!tracker.proposeComplete(completed.instruction.sequenceId)) { + setRuntimeFailureCode("npu_completion_broadcast_failed"); + return; + } + for (NpuEngineClass engine : engines) + if (execution.canAccept(engine)) + tracker.proposeIssue(engine); + if (!commitTracker(epoch)) { + setRuntimeFailureCode("npu_dependency_observation_failed"); + return; + } + if (!tracker.runtimeFailureCode().empty()) { + setRuntimeFailureCode("npu_dependency_runtime_failed"); + return; + } + pendingIssues.assign(tracker.issued().begin(), tracker.issued().end()); + + if (execution.complete() && pendingIssues.empty() && + tracker.runtimeState(epoch).quiescent) { + finished = true; + break; + } + } + if (!finished) { + setRuntimeFailureCode("npu_execution_limit_reached"); + return; + } + + result_ = execution.architecturalResult(); + eventCount_ = sink.recorder.events().size(); + for (const CommittedEvent &event : sink.recorder.events()) { + std::vector arguments = event.arguments; + arguments.push_back( + {.name = "npu_epoch_delta", .value = uint64_t{event.epoch.delta}}); + arguments.push_back({.name = "npu_epoch_time", .value = event.epoch.time}); + std::sort( + arguments.begin(), arguments.end(), + [](const ObservationArgument &left, const ObservationArgument &right) { + return left.name < right.name; + }); + if (!emitObservation({.category = event.category, + .name = event.name, + .phase = event.phase, + .rootSequenceId = event.rootSequenceId, + .duration = event.duration, + .flowId = event.flowId, + .arguments = std::move(arguments)})) + return; + } + pending_ = true; +} + +void NpuTraceSource::doXfer(Epoch epoch) { + if (!pending_) + return; + pending_ = false; + committed_ = true; + lastUpdate_ = epoch; +} + +bool NpuTraceSource::hasPendingCommit() const { return pending_; } + +RuntimeObjectState NpuTraceSource::runtimeState(Epoch) const { + return {.quiescent = committed_ && !pending_, + .runnable = loaded_ && !committed_ && !pending_, + .pendingCommit = pending_, + .reason = committed_ ? "" : "npu_trace_pending", + .traceOwner = true, + .tracePosition = committed_ ? document_.records.size() : 0, + .traceLastCommittedSequenceId = + committed_ && !document_.records.empty() + ? std::optional(document_.records.back().sequenceId) + : std::nullopt, + .traceEof = committed_}; +} + +void NpuTraceSource::collectStatistics(std::vector &out) const { + if (!committed_) + return; + auto append = [&](std::string name, uint64_t value) { + out.push_back({.name = std::move(name), + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = value, + .lastUpdate = lastUpdate_}); + }; + append("trace_records", document_.records.size()); + append("npu_events", eventCount_); + append("architectural_retired_instructions", result_.retiredInstructions); + append("architectural_digest", result_.digest); +} + +void NpuTraceSource::reset() { + document_ = {}; + result_ = {}; + eventCount_ = 0; + loaded_ = false; + pending_ = false; + committed_ = false; + lastUpdate_ = {}; + clearRuntimeFailureCode(); +} + +bool NpuTraceSource::validate() const { return true; } + +} // namespace gfsim diff --git a/components/agentic-circuit/lib/gfsim/observation.cpp b/components/agentic-circuit/lib/gfsim/observation.cpp new file mode 100644 index 00000000..0bde79d3 --- /dev/null +++ b/components/agentic-circuit/lib/gfsim/observation.cpp @@ -0,0 +1,132 @@ +#include "gfsim/observation.h" + +#include +#include +#include +#include + +namespace gfsim { +namespace { + +bool validName(std::string_view value) { + auto alpha = [](char character) { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z'); + }; + if (value.empty() || !(alpha(value.front()) || value.front() == '_')) + return false; + return std::all_of(value.begin() + 1, value.end(), [&](char character) { + return alpha(character) || (character >= '0' && character <= '9') || + character == '_' || character == '.' || character == '-'; + }); +} + +bool lessEvent(const CommittedEvent &left, const CommittedEvent &right) { + return std::tie(left.epoch, left.ownerId, left.localCommittedIndex) < + std::tie(right.epoch, right.ownerId, right.localCommittedIndex); +} + +} // namespace + +bool ObservationRecorder::reject(std::string message) { + lastError_ = std::move(message); + return false; +} + +bool ObservationRecorder::propose(EventProposal proposal) { + lastError_.clear(); + if (proposal.ownerId == kInvalidObjectId || !validName(proposal.category) || + !validName(proposal.name)) + return reject("observation owner, category, or name is invalid"); + std::string previous; + for (const ObservationArgument &argument : proposal.arguments) { + if (!validName(argument.name) || + std::string_view(argument.name).starts_with("gfsim_") || + (!previous.empty() && previous >= argument.name)) + return reject("observation arguments must be sorted and unique"); + previous = argument.name; + } + const bool flow = proposal.phase == TraceEventPhase::FlowStart || + proposal.phase == TraceEventPhase::FlowEnd; + if (flow != proposal.flowId.has_value()) + return reject("flow observations require exactly one flow identity"); + if (proposal.phase == TraceEventPhase::Complete) { + if (!proposal.duration) + return reject("complete observations require a duration"); + } else if (proposal.duration) { + return reject("only complete observations may carry a duration"); + } + pending_[proposal.ownerId].push_back(std::move(proposal)); + return true; +} + +bool ObservationRecorder::commitOwner(ObjectId owner, Epoch epoch) { + lastError_.clear(); + if (owner == kInvalidObjectId) + return reject("observation commit owner is invalid"); + if (lastCommitEpoch_ && epoch < *lastCommitEpoch_) + return reject("observation commit time regressed"); + auto pending = pending_.find(owner); + if (pending == pending_.end()) + return true; + + uint64_t next = nextIndices_[owner]; + if (pending->second.size() > std::numeric_limits::max() - next) + return reject("observation owner-local index overflowed"); + std::map flows = activeFlows_; + for (const EventProposal &proposal : pending->second) { + if (proposal.phase == TraceEventPhase::FlowStart) { + if (flows[*proposal.flowId]) + return reject("observation flow started more than once"); + flows[*proposal.flowId] = true; + } else if (proposal.phase == TraceEventPhase::FlowEnd) { + auto flow = flows.find(*proposal.flowId); + if (flow == flows.end() || !flow->second) + return reject("observation flow ended before it started"); + flow->second = false; + } + } + + std::vector additions; + additions.reserve(pending->second.size()); + for (EventProposal &proposal : pending->second) { + additions.push_back({.epoch = epoch, + .ownerId = owner, + .localCommittedIndex = next++, + .category = std::move(proposal.category), + .name = std::move(proposal.name), + .phase = proposal.phase, + .rootSequenceId = proposal.rootSequenceId, + .duration = proposal.duration, + .flowId = proposal.flowId, + .arguments = std::move(proposal.arguments)}); + } + pending_.erase(pending); + nextIndices_[owner] = next; + activeFlows_ = std::move(flows); + for (CommittedEvent &event : additions) { + auto position = std::lower_bound(committed_.begin(), committed_.end(), + event, lessEvent); + committed_.insert(position, std::move(event)); + } + lastCommitEpoch_ = epoch; + return true; +} + +void ObservationRecorder::rejectOwner(ObjectId owner) { pending_.erase(owner); } + +size_t ObservationRecorder::pendingCount(ObjectId owner) const { + auto pending = pending_.find(owner); + return pending == pending_.end() ? 0 : pending->second.size(); +} + +void ObservationRecorder::reset() { + pending_.clear(); + nextIndices_.clear(); + activeFlows_.clear(); + committed_.clear(); + lastCommitEpoch_.reset(); + lastError_.clear(); +} + +} // namespace gfsim diff --git a/components/agentic-circuit/lib/gfsim/showcase.cpp b/components/agentic-circuit/lib/gfsim/showcase.cpp new file mode 100644 index 00000000..289c9949 --- /dev/null +++ b/components/agentic-circuit/lib/gfsim/showcase.cpp @@ -0,0 +1,840 @@ +#include "gfsim/showcase.h" + +#include "gfsim/components.h" +#include "gfsim/dispatch.h" +#include "gfsim/object.h" +#include "gfsim/process.h" +#include "gfsim/queue.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfsim { +namespace { + +std::vector workOrder(size_t count, ShowcaseWorkOrder order, + uint64_t seed, Tick tick) { + std::vector result(count); + std::iota(result.begin(), result.end(), 0); + if (order == ShowcaseWorkOrder::Descending) + std::reverse(result.begin(), result.end()); + else if (order == ShowcaseWorkOrder::Seeded) { + std::mt19937_64 random(seed ^ (tick * 0x9e3779b97f4a7c15ULL)); + std::shuffle(result.begin(), result.end(), random); + } + return result; +} + +bool installRuntime(SimSystem &system, std::span rows, + std::vector &offsets, + std::vector &targets) { + for (const DispatchRow &row : rows) { + auto *object = static_cast(row.object); + object->bindSystem(&system); + object->setObservationSink(&system); + } + offsets.assign(rows.size() + 1, 0); + targets.clear(); + targets.reserve(rows.size() * rows.size()); + for (size_t source = 0; source < rows.size(); ++source) { + for (ObjectId target = 0; target < rows.size(); ++target) + targets.push_back(target); + offsets[source + 1] = static_cast(targets.size()); + } + return system.setDispatchTable(rows) && + system.setActivationPlan(offsets, targets); +} + +ShowcaseResult finish(SimSystem &system, TerminationResult termination, + std::span objects, + std::map values, + uint64_t tracePosition) { + ShowcaseResult result; + result.termination = std::move(termination); + result.termination.tracePosition = tracePosition; + result.architecturalValues = std::move(values); + result.tracePosition = tracePosition; + result.statistics = system.statistics(); + auto events = system.observations(); + result.events.assign(events.begin(), events.end()); + std::vector sorted(objects.begin(), objects.end()); + std::sort(sorted.begin(), sorted.end(), + [](const SimObject *left, const SimObject *right) { + return left->id() < right->id(); + }); + for (const SimObject *object : sorted) + result.hierarchy.push_back( + {object->id(), std::string(object->path()), object->kind()}); + return result; +} + +class QueueScenarioActor final : public SimObject { +public: + QueueScenarioActor(ObjectId id, std::span values, + Queue &queue, Sink &sink) + : SimObject(ObjectKind::Process, "producer", id), values_(values), + queue_(queue), sink_(sink) {} + + void doWork(Epoch) override { + if (nextValue_ < values_.size() && queue_.proposePush(values_[nextValue_])) + advance_ = true; + if (auto value = queue_.proposePop()) + sink_.receive(*value); + } + + void doXfer(Epoch) override { + if (advance_) + ++nextValue_; + advance_ = false; + } + + bool hasPendingCommit() const override { return advance_; } + RuntimeObjectState runtimeState(Epoch) const override { + const bool done = nextValue_ == values_.size() && queue_.isEmpty(); + return {.quiescent = done && !advance_, + .runnable = !done, + .pendingCommit = advance_, + .reason = done ? "" : "showcase_queue_flow"}; + } + +private: + std::span values_; + Queue &queue_; + Sink &sink_; + size_t nextValue_ = 0; + bool advance_ = false; +}; + +ShowcaseResult +runProducerQueueConsumer(const ProducerQueueConsumerPolicy &policy) { + SimSystem system("showcase"); + Queue queue("queue", 1, nullptr, policy.queueCapacity, SIZE_MAX, + &system); + Sink sink("consumer", 2, nullptr, &system); + QueueScenarioActor actor(0, policy.values, queue, sink); + system.root().attachChild(actor); + system.root().attachChild(queue); + system.root().attachChild(sink); + std::array rows = {makeDispatchRow(&actor), makeDispatchRow(&queue), + makeDispatchRow(&sink)}; + std::vector offsets; + std::vector targets; + installRuntime(system, rows, offsets, targets); + TerminationResult termination = system.run(); + uint64_t sum = + std::accumulate(sink.received().begin(), sink.received().end(), 0ULL); + std::array objects = {&actor, &queue, &sink}; + return finish( + system, std::move(termination), objects, + {{"consumed_count", sink.totalReceived()}, {"consumed_sum", sum}}, + sink.totalReceived()); +} + +class BackpressureActor final : public SimObject { +public: + BackpressureActor(ObjectId id, const BackpressuredPipelinePolicy &policy, + ReadyValid &channel, Sink &sink) + : SimObject(ObjectKind::Process, "producer", id), policy_(policy), + channel_(channel), sink_(sink) {} + + void doWork(Epoch epoch) override { + if (channel_.transferCount() > observedTransfers_) { + sink_.receive(channel_.lastTransferred()); + proposedObservedTransfers_ = channel_.transferCount(); + } + if (nextValue_ < policy_.values.size() && !channel_.hasOffer() && + channel_.proposeOffer(policy_.values[nextValue_])) + advance_ = true; + if (channel_.transferCount() < policy_.values.size() || + nextValue_ < policy_.values.size() || channel_.hasOffer()) { + const bool ready = std::ranges::find(policy_.readyTicks, epoch.time) != + policy_.readyTicks.end(); + channel_.proposeReady(ready); + } + } + + void doXfer(Epoch) override { + if (advance_) + ++nextValue_; + if (proposedObservedTransfers_) + observedTransfers_ = *proposedObservedTransfers_; + advance_ = false; + proposedObservedTransfers_.reset(); + } + + bool hasPendingCommit() const override { + return advance_ || proposedObservedTransfers_.has_value(); + } + RuntimeObjectState runtimeState(Epoch) const override { + const bool done = observedTransfers_ == policy_.values.size() && + nextValue_ == policy_.values.size() && + !channel_.hasOffer(); + return {.quiescent = done && !hasPendingCommit(), + .runnable = !done, + .pendingCommit = hasPendingCommit(), + .reason = done ? "" : "showcase_backpressure"}; + } + +private: + const BackpressuredPipelinePolicy &policy_; + ReadyValid &channel_; + Sink &sink_; + size_t nextValue_ = 0; + uint64_t observedTransfers_ = 0; + std::optional proposedObservedTransfers_; + bool advance_ = false; +}; + +ShowcaseResult runBackpressured(const BackpressuredPipelinePolicy &policy) { + SimSystem system("showcase"); + ReadyValid channel("ready_valid", 1, nullptr, &system); + Sink sink("consumer", 2, nullptr, &system); + BackpressureActor actor(0, policy, channel, sink); + system.root().attachChild(actor); + system.root().attachChild(channel); + system.root().attachChild(sink); + std::array rows = {makeDispatchRow(&actor), makeDispatchRow(&channel), + makeDispatchRow(&sink)}; + std::vector offsets; + std::vector targets; + installRuntime(system, rows, offsets, targets); + TerminationResult termination = system.run(); + uint64_t sum = + std::accumulate(sink.received().begin(), sink.received().end(), 0ULL); + std::array objects = {&actor, &channel, &sink}; + return finish(system, std::move(termination), objects, + {{"consumed_count", sink.totalReceived()}, + {"consumed_sum", sum}, + {"transfer_count", channel.transferCount()}}, + channel.transferCount()); +} + +class MemoryScenarioActor final : public SimObject { +public: + MemoryScenarioActor(ObjectId id, const RequestResponseMemoryPolicy &policy, + RequestResponse &protocol, + Memory &memory) + : SimObject(ObjectKind::Process, "requester", id), policy_(policy), + protocol_(protocol), memory_(memory) {} + + void doWork(Epoch) override { + if (nextRequest_ == policy_.requests.size()) + return; + const MemoryWorkItem &request = policy_.requests[nextRequest_]; + switch (phase_) { + case Phase::Submit: + if (protocol_.proposeRequest(request, request.correlationId)) + proposedPhase_ = Phase::ConsumeRequest; + break; + case Phase::ConsumeRequest: { + auto envelope = protocol_.proposePopRequest(); + if (!envelope) + break; + if (!memory_.proposeWrite(envelope->payload.address, + envelope->payload.value)) { + setRuntimeFailureCode("showcase_memory_address_out_of_range"); + break; + } + proposedPhase_ = Phase::Respond; + break; + } + case Phase::Respond: + if (protocol_.proposeResponse(request.value, request.correlationId)) + proposedPhase_ = Phase::ConsumeResponse; + break; + case Phase::ConsumeResponse: + if (protocol_.proposePopResponse()) { + proposedPhase_ = Phase::Submit; + completeRequest_ = true; + } + break; + } + } + + void doXfer(Epoch) override { + if (proposedPhase_) + phase_ = *proposedPhase_; + if (completeRequest_) { + ++nextRequest_; + ++completed_; + } + proposedPhase_.reset(); + completeRequest_ = false; + } + + bool hasPendingCommit() const override { + return proposedPhase_.has_value() || completeRequest_; + } + RuntimeObjectState runtimeState(Epoch) const override { + const bool done = nextRequest_ == policy_.requests.size(); + return {.quiescent = done && !hasPendingCommit(), + .runnable = !done, + .pendingCommit = hasPendingCommit(), + .reason = done ? "" : "showcase_memory_protocol"}; + } + uint64_t completed() const { return completed_; } + +private: + enum class Phase { Submit, ConsumeRequest, Respond, ConsumeResponse }; + const RequestResponseMemoryPolicy &policy_; + RequestResponse &protocol_; + Memory &memory_; + size_t nextRequest_ = 0; + uint64_t completed_ = 0; + Phase phase_ = Phase::Submit; + std::optional proposedPhase_; + bool completeRequest_ = false; +}; + +ShowcaseResult runRequestResponse(const RequestResponseMemoryPolicy &policy) { + SimSystem system("showcase"); + RequestResponse protocol("request_response", 1, + nullptr, 2, &system); + Memory memory("memory", 2, nullptr, policy.memoryCapacity, &system); + MemoryScenarioActor actor(0, policy, protocol, memory); + system.root().attachChild(actor); + system.root().attachChild(protocol); + system.root().attachChild(memory); + std::array rows = {makeDispatchRow(&actor), makeDispatchRow(&protocol), + makeDispatchRow(&memory)}; + std::vector offsets; + std::vector targets; + installRuntime(system, rows, offsets, targets); + TerminationResult termination = system.run(); + std::map values = { + {"completed_responses", actor.completed()}}; + for (size_t address = 0; address < memory.capacity(); ++address) + values.emplace("memory." + std::to_string(address), memory.read(address)); + std::array objects = {&actor, &protocol, &memory}; + return finish(system, std::move(termination), objects, std::move(values), + actor.completed()); +} + +class NestedArrayActor final : public SimObject { +public: + NestedArrayActor(ObjectId id, const NestedArraysPolicy &policy, + std::span *const> queues, + std::span *const> sinks, + ShowcaseWorkOrder order, uint64_t seed) + : SimObject(ObjectKind::Process, "producer", id), policy_(policy), + queues_(queues), sinks_(sinks), order_(order), seed_(seed), + nextValues_(queues.size()), advance_(queues.size()) {} + + void doWork(Epoch epoch) override { + for (size_t lane : workOrder(queues_.size(), order_, seed_, epoch.time)) { + if (nextValues_[lane] < policy_.laneValues[lane].size() && + queues_[lane]->proposePush( + policy_.laneValues[lane][nextValues_[lane]])) + advance_[lane] = true; + if (auto value = queues_[lane]->proposePop()) + sinks_[lane]->receive(*value); + } + } + + void doXfer(Epoch) override { + for (size_t lane = 0; lane < advance_.size(); ++lane) { + if (advance_[lane]) + ++nextValues_[lane]; + advance_[lane] = false; + } + } + + bool hasPendingCommit() const override { + return std::ranges::find(advance_, true) != advance_.end(); + } + RuntimeObjectState runtimeState(Epoch) const override { + bool done = true; + for (size_t lane = 0; lane < queues_.size(); ++lane) + done = done && nextValues_[lane] == policy_.laneValues[lane].size() && + queues_[lane]->isEmpty(); + return {.quiescent = done && !hasPendingCommit(), + .runnable = !done, + .pendingCommit = hasPendingCommit(), + .reason = done ? "" : "showcase_nested_arrays"}; + } + +private: + const NestedArraysPolicy &policy_; + std::span *const> queues_; + std::span *const> sinks_; + ShowcaseWorkOrder order_; + uint64_t seed_; + std::vector nextValues_; + std::vector advance_; +}; + +ShowcaseResult runNestedArrays(const NestedArraysPolicy &policy, + ShowcaseWorkOrder order, uint64_t seed) { + SimSystem system("showcase"); + Module lanes("lanes", kInvalidObjectId); + system.root().attachChild(lanes); + std::vector> laneModules; + std::vector>> queueStorage; + std::vector>> sinkStorage; + std::vector *> queues; + std::vector *> sinks; + std::vector objects; + std::vector rows(policy.laneValues.size() * 2 + 1); + laneModules.reserve(policy.laneValues.size()); + for (size_t lane = 0; lane < policy.laneValues.size(); ++lane) { + auto module = + std::make_unique(std::to_string(lane), kInvalidObjectId); + lanes.attachChild(*module); + ObjectId queueId = static_cast(1 + lane * 2); + ObjectId sinkId = queueId + 1; + auto queue = std::make_unique>( + "queue", queueId, nullptr, policy.queueCapacity, SIZE_MAX, &system); + auto sink = + std::make_unique>("sink", sinkId, nullptr, &system); + module->attachChild(*queue); + module->attachChild(*sink); + queues.push_back(queue.get()); + sinks.push_back(sink.get()); + rows[queueId] = makeDispatchRow(queue.get()); + rows[sinkId] = makeDispatchRow(sink.get()); + queueStorage.push_back(std::move(queue)); + sinkStorage.push_back(std::move(sink)); + laneModules.push_back(std::move(module)); + } + NestedArrayActor actor(0, policy, queues, sinks, order, seed); + system.root().attachChild(actor); + rows[0] = makeDispatchRow(&actor); + objects.push_back(&actor); + for (size_t lane = 0; lane < queues.size(); ++lane) { + objects.push_back(queues[lane]); + objects.push_back(sinks[lane]); + } + std::vector offsets; + std::vector targets; + installRuntime(system, rows, offsets, targets); + TerminationResult termination = system.run(); + std::map values; + uint64_t tracePosition = 0; + for (size_t lane = 0; lane < sinks.size(); ++lane) { + uint64_t sum = std::accumulate(sinks[lane]->received().begin(), + sinks[lane]->received().end(), 0ULL); + values.emplace("lane." + std::to_string(lane) + ".sum", sum); + tracePosition += sinks[lane]->totalReceived(); + } + return finish(system, std::move(termination), objects, std::move(values), + tracePosition); +} + +class TimeDomainActor final : public SimObject { +public: + TimeDomainActor(ObjectId id, const MultiTimeDomainBridgePolicy &policy, + ReadyValid &bridge, Sink &sink) + : SimObject(ObjectKind::Process, "source", id), policy_(policy), + bridge_(bridge), sink_(sink) {} + + void doWork(Epoch epoch) override { + if (bridge_.transferCount() > observedTransfers_) { + sink_.receive(bridge_.lastTransferred()); + proposedObservedTransfers_ = bridge_.transferCount(); + proposedLastTransferTick_ = epoch.time == 0 ? 0 : epoch.time - 1; + } + if (nextValue_ < policy_.values.size() && !bridge_.hasOffer() && + epoch.time % policy_.sourcePeriod == 0 && + bridge_.proposeOffer(policy_.values[nextValue_])) + advance_ = true; + if (bridge_.transferCount() < policy_.values.size() || + nextValue_ < policy_.values.size() || bridge_.hasOffer()) + bridge_.proposeReady(epoch.time % policy_.targetPeriod == 0); + } + + void doXfer(Epoch) override { + if (advance_) + ++nextValue_; + if (proposedObservedTransfers_) { + observedTransfers_ = *proposedObservedTransfers_; + lastTransferTick_ = *proposedLastTransferTick_; + } + advance_ = false; + proposedObservedTransfers_.reset(); + proposedLastTransferTick_.reset(); + } + bool hasPendingCommit() const override { + return advance_ || proposedObservedTransfers_.has_value(); + } + RuntimeObjectState runtimeState(Epoch) const override { + const bool done = observedTransfers_ == policy_.values.size() && + nextValue_ == policy_.values.size() && + !bridge_.hasOffer(); + return {.quiescent = done && !hasPendingCommit(), + .runnable = !done, + .pendingCommit = hasPendingCommit(), + .reason = done ? "" : "showcase_time_domain_bridge"}; + } + Tick lastTransferTick() const { return lastTransferTick_; } + +private: + const MultiTimeDomainBridgePolicy &policy_; + ReadyValid &bridge_; + Sink &sink_; + size_t nextValue_ = 0; + uint64_t observedTransfers_ = 0; + Tick lastTransferTick_ = 0; + bool advance_ = false; + std::optional proposedObservedTransfers_; + std::optional proposedLastTransferTick_; +}; + +ShowcaseResult runTimeDomains(const MultiTimeDomainBridgePolicy &policy) { + SimSystem system("showcase"); + ReadyValid bridge("bridge", 1, nullptr, &system); + Sink sink("consumer", 2, nullptr, &system); + TimeDomainActor actor(0, policy, bridge, sink); + system.root().attachChild(actor); + system.root().attachChild(bridge); + system.root().attachChild(sink); + std::array rows = {makeDispatchRow(&actor), makeDispatchRow(&bridge), + makeDispatchRow(&sink)}; + std::vector offsets; + std::vector targets; + installRuntime(system, rows, offsets, targets); + const std::array domains = { + TimeDomainRuntime{"source", policy.sourcePeriod, 0, 1}, + TimeDomainRuntime{"target", policy.targetPeriod, 0, 1}}; + system.setTimeDomains(domains); + TerminationResult termination = system.run(); + uint64_t sum = + std::accumulate(sink.received().begin(), sink.received().end(), 0ULL); + std::array objects = {&actor, &bridge, &sink}; + return finish(system, std::move(termination), objects, + {{"bridged_count", sink.totalReceived()}, + {"bridged_sum", sum}, + {"last_transfer_tick", actor.lastTransferTick()}}, + sink.totalReceived()); +} + +class ShowcaseProcess final : public ProcessRuntime { +public: + explicit ShowcaseProcess(const SuspendedProcessPolicy &policy) + : ProcessRuntime("process", 0, nullptr, 0, 2), policy_(policy) {} + + ProcessStep executeProcessStep(uint32_t pc, Epoch) { + if (pc == 0) { + proposedValue_ = policy_.initialValue; + return ProcessStep::suspendAt( + 1, {.kind = ProcessWakeKind::EventQueue, .id = 7}, 42); + } + proposedValue_ = liveValue_ + policy_.incrementAfterWake; + proposedResume_ = true; + return ProcessStep::terminate(); + } + + void doWork(Epoch epoch) override { + const uint32_t activePc = pc(); + ProcessRuntime::doWork(epoch); + if (!hasPendingCommit()) + return; + if (activePc == 0) + emitObservation({.category = "process", + .name = "suspended", + .phase = TraceEventPhase::Instant}); + else + emitObservation({.category = "process", + .name = "resumed", + .phase = TraceEventPhase::Instant}); + } + + void doXfer(Epoch epoch) override { + const bool commitValue = hasPendingCommit(); + ProcessRuntime::doXfer(epoch); + if (commitValue) { + liveValue_ = proposedValue_; + if (proposedResume_) + ++resumeCount_; + } + proposedResume_ = false; + } + uint64_t result() const { return liveValue_; } + uint64_t resumeCount() const { return resumeCount_; } + +private: + const SuspendedProcessPolicy &policy_; + uint64_t liveValue_ = 0; + uint64_t proposedValue_ = 0; + uint64_t resumeCount_ = 0; + bool proposedResume_ = false; +}; + +class WakeDriver final : public SimObject { +public: + WakeDriver(SimSystem &system, ShowcaseProcess &process, Tick wakeTick) + : SimObject(ObjectKind::Process, "wake", 1), system_(system), + process_(process), wakeTick_(wakeTick) {} + + void doWork(Epoch epoch) override { + if (!scheduled_) { + system_.scheduleEvent({{wakeTick_, 0}, id(), 7, 42}); + pendingSchedule_ = true; + return; + } + if (epoch.time == wakeTick_ && + process_.wake({ProcessWakeKind::EventQueue, 7}, 42)) { + system_.scheduleWork(process_.id(), epoch); + emitObservation({.category = "process", + .name = "wake", + .phase = TraceEventPhase::Instant}); + pendingWake_ = true; + } + } + void doXfer(Epoch) override { + if (pendingSchedule_) + scheduled_ = true; + pendingSchedule_ = false; + pendingWake_ = false; + } + bool hasPendingCommit() const override { + return pendingSchedule_ || pendingWake_; + } + +private: + SimSystem &system_; + ShowcaseProcess &process_; + Tick wakeTick_; + bool scheduled_ = false; + bool pendingSchedule_ = false; + bool pendingWake_ = false; +}; + +ShowcaseResult runSuspended(const SuspendedProcessPolicy &policy) { + SimSystem system("showcase"); + ShowcaseProcess process(policy); + WakeDriver wake(system, process, policy.wakeTick); + system.root().attachChild(process); + system.root().attachChild(wake); + std::array rows = {makeDispatchRow(&process), makeDispatchRow(&wake)}; + std::vector offsets; + std::vector targets; + installRuntime(system, rows, offsets, targets); + TerminationResult termination = system.run(); + std::array objects = {&process, &wake}; + return finish(system, std::move(termination), objects, + {{"process_result", process.result()}, + {"resume_count", process.resumeCount()}, + {"wake_tick", policy.wakeTick}}, + 0); +} + +template +void appendOptional(std::ostringstream &output, const std::optional &value) { + if (value) + output << *value; + else + output << '-'; +} + +void appendObservationValue(std::ostringstream &output, + const ObservationValue &value) { + std::visit([&](const auto &item) { output << item; }, value); +} + +} // namespace + +ShowcaseTraceSource::ShowcaseTraceSource(std::string name, ObjectId id, + SimObject *parent, uint64_t scenario) + : SimObject(ObjectKind::TraceSource, std::move(name), id, parent), + scenario_(scenario) {} + +bool ShowcaseTraceSource::loadDocument(PtoTraceDocument document) { + if (loaded_ || pending_ || committed_) + return false; + document_ = std::move(document); + loaded_ = true; + return true; +} + +void ShowcaseTraceSource::doWork(Epoch) { + if (pending_ || committed_) + return; + if (!loaded_) { + setRuntimeFailureCode("showcase_trace_not_loaded"); + return; + } + ShowcasePolicy policy; + switch (scenario_) { + case 0: + policy = ProducerQueueConsumerPolicy{}; + break; + case 1: + policy = BackpressuredPipelinePolicy{}; + break; + case 2: + policy = RequestResponseMemoryPolicy{}; + break; + case 3: + policy = NestedArraysPolicy{}; + break; + case 4: + policy = MultiTimeDomainBridgePolicy{}; + break; + case 5: + policy = SuspendedProcessPolicy{}; + break; + default: + setRuntimeFailureCode("invalid_showcase_scenario"); + return; + } + result_ = runShowcase(policy, ShowcaseWorkOrder::Ascending); + if (result_.termination.classification != TerminationClass::Completed) { + setRuntimeFailureCode(result_.termination.diagnosticCode.empty() + ? "showcase_execution_failed" + : result_.termination.diagnosticCode); + return; + } + for (const CommittedEvent &event : result_.events) { + std::vector arguments = event.arguments; + arguments.push_back({.name = "showcase_epoch_delta", + .value = static_cast(event.epoch.delta)}); + arguments.push_back( + {.name = "showcase_epoch_time", .value = event.epoch.time}); + if (!emitObservation({.category = event.category, + .name = event.name, + .phase = event.phase, + .rootSequenceId = event.rootSequenceId, + .duration = event.duration, + .flowId = event.flowId, + .arguments = std::move(arguments)})) + return; + } + pending_ = true; +} + +void ShowcaseTraceSource::doXfer(Epoch epoch) { + if (!pending_) + return; + pending_ = false; + committed_ = true; + lastUpdate_ = epoch; +} + +bool ShowcaseTraceSource::hasPendingCommit() const { return pending_; } + +RuntimeObjectState ShowcaseTraceSource::runtimeState(Epoch) const { + return {.quiescent = committed_ && !pending_, + .runnable = loaded_ && !committed_ && !pending_, + .pendingCommit = pending_, + .reason = committed_ ? "" : "showcase_trace_pending", + .traceOwner = true, + .tracePosition = committed_ ? document_.records.size() : 0, + .traceLastCommittedSequenceId = + committed_ && !document_.records.empty() + ? std::optional(document_.records.back().sequenceId) + : std::nullopt, + .traceEof = committed_}; +} + +void ShowcaseTraceSource::collectStatistics( + std::vector &out) const { + if (!committed_) + return; + auto append = [&](std::string name, uint64_t value) { + std::replace(name.begin(), name.end(), '.', '_'); + out.push_back({.name = std::move(name), + .objectPath = std::string(path()), + .kind = StatisticKind::Counter, + .value = value, + .lastUpdate = lastUpdate_}); + }; + append("trace_records", document_.records.size()); + append("showcase_events", result_.events.size()); + for (const auto &[name, value] : result_.architecturalValues) + append("architectural_" + name, value); +} + +void ShowcaseTraceSource::reset() { + document_ = {}; + result_ = {}; + loaded_ = false; + pending_ = false; + committed_ = false; + lastUpdate_ = {}; + clearRuntimeFailureCode(); +} + +bool ShowcaseTraceSource::validate() const { return scenario_ < 6; } + +ShowcaseResult runShowcase(const ShowcasePolicy &policy, + ShowcaseWorkOrder order, uint64_t permutationSeed) { + return std::visit( + [&](const auto &typedPolicy) -> ShowcaseResult { + using Policy = std::decay_t; + if constexpr (std::is_same_v) + return runProducerQueueConsumer(typedPolicy); + else if constexpr (std::is_same_v) + return runBackpressured(typedPolicy); + else if constexpr (std::is_same_v) + return runRequestResponse(typedPolicy); + else if constexpr (std::is_same_v) + return runNestedArrays(typedPolicy, order, permutationSeed); + else if constexpr (std::is_same_v) + return runTimeDomains(typedPolicy); + else + return runSuspended(typedPolicy); + }, + policy); +} + +std::string canonicalShowcaseResult(const ShowcaseResult &result) { + std::ostringstream output; + const auto &termination = result.termination; + output << "termination|" << static_cast(termination.classification) + << '|' << termination.finalEpoch.time << '|' + << termination.finalEpoch.delta << '|' + << termination.committedEventCount << '|' << termination.tracePosition + << '|'; + appendOptional(output, termination.traceLastCommittedSequenceId); + output << '|'; + appendOptional(output, termination.terminationCap); + output << '|' << termination.diagnosticCode << '|'; + appendOptional(output, termination.message); + output << '\n'; + for (const auto &[name, cycles] : termination.domainCycles) + output << "domain|" << name << '|' << cycles << '\n'; + for (const auto &[name, value] : result.architecturalValues) + output << "value|" << name << '|' << value << '\n'; + output << "trace|" << result.tracePosition << '\n'; + for (const auto &entry : result.hierarchy) + output << "hierarchy|" << entry.id << '|' << entry.path << '|' + << static_cast(entry.kind) << '\n'; + for (const auto &stat : result.statistics) { + output << "stat|" << stat.objectPath << '|' << stat.name << '|' + << static_cast(stat.kind) << '|' << stat.value << '|' + << stat.count << '|' << stat.sum << '|' << stat.minimum << '|' + << stat.maximum << '|' << stat.lastUpdate.time << '|' + << stat.lastUpdate.delta; + for (const auto &bucket : stat.buckets) + output << '|' << bucket.upperBound << ':' << bucket.count; + output << '\n'; + } + for (const auto &event : result.events) { + output << "event|" << event.epoch.time << '|' << event.epoch.delta << '|' + << event.ownerId << '|' << event.localCommittedIndex << '|' + << event.category << '|' << event.name << '|' + << static_cast(event.phase) << '|'; + appendOptional(output, event.rootSequenceId); + output << '|'; + appendOptional(output, event.duration); + output << '|'; + appendOptional(output, event.flowId); + for (const auto &argument : event.arguments) { + output << '|' << argument.name << '='; + appendObservationValue(output, argument.value); + } + output << '\n'; + } + return output.str(); +} + +} // namespace gfsim diff --git a/components/agentic-circuit/lib/gfsim/system.cpp b/components/agentic-circuit/lib/gfsim/system.cpp new file mode 100644 index 00000000..bf76df10 --- /dev/null +++ b/components/agentic-circuit/lib/gfsim/system.cpp @@ -0,0 +1,688 @@ +#include "gfsim/object.h" +#include "gfsim/queue.h" + +#include +#include +#include +#include +#include +#include + +namespace gfsim { + +struct SimSystem::Impl { + std::map objects; + std::map> scheduledWork; + EventQueue eventQueue{"events", kInvalidObjectId, nullptr}; + DispatchTable dispatch; + ActivationPlan activation; + uint64_t committedEventCount = 0; + bool executingEpoch = false; + std::optional activeProposalOwner; + NoProgressReport noProgress; + size_t traceOwnerCount = 0; + bool traceEof = true; + bool preflightValidated = false; + std::map lastCommitTick; + std::optional eventQueueLastCommitTick; + std::map timeDomains; + std::map maxDomainCycles; + std::map domainCycles; + ObservationRecorder observations; + std::optional deadlockWindow; + Tick lastProgressTick = 0; +}; + +SimSystem::~SimSystem() = default; + +SimSystem::SimSystem(std::string name) + : SimObject(ObjectKind::System, std::move(name), kSystemObjectId), + root_(std::make_unique("root", kRootObjectId, this)), + impl_(std::make_unique()) { + setPath("/" + std::string(this->name())); + root_->setPath(std::string(path())); + impl_->objects[kSystemObjectId] = this; +} + +bool SimSystem::fail(std::string code, std::string message) { + terminated_ = true; + impl_->executingEpoch = false; + result_.classification = TerminationClass::Failed; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.domainCycles = impl_->domainCycles; + result_.diagnosticCode = std::move(code); + result_.message = std::move(message); + return false; +} + +std::vector SimSystem::runtimeObjects() const { + std::map objects; + for (const auto &[id, object] : impl_->objects) + if (id != kSystemObjectId && object) + objects[id] = object; + root_->walk([&](const SimObject &object) { + if (object.kind() != ObjectKind::Module) + objects[object.id()] = const_cast(&object); + }); + for (ObjectId id = 0; id < impl_->dispatch.size(); ++id) + if (const DispatchRow *row = impl_->dispatch.lookup(id)) + objects[id] = static_cast(row->object); + + std::vector result; + result.reserve(objects.size()); + for (const auto &[id, object] : objects) + result.push_back(object); + return result; +} + +bool SimSystem::validateRuntimeIdentities() { + std::map ids; + std::map paths; + std::string conflict; + auto record = [&](const SimObject *object) { + if (!object || !conflict.empty() || object == this) + return; + if (object->id() == kInvalidObjectId && object->asModule() == nullptr) { + conflict = "runtime object has the invalid object ID"; + return; + } + if (object->id() != kInvalidObjectId) { + if (auto [position, inserted] = ids.emplace(object->id(), object); + !inserted && position->second != object) { + conflict = "stable object ID " + std::to_string(object->id()) + + " names more than one runtime object"; + return; + } + } + if (object->path().empty()) + return; + if (auto [position, inserted] = + paths.emplace(std::string(object->path()), object); + !inserted && position->second != object) + conflict = "canonical object path " + std::string(object->path()) + + " names more than one runtime object"; + }; + + for (const auto &[id, object] : impl_->objects) + record(object); + root_->walk([&](const SimObject &object) { record(&object); }); + for (ObjectId id = 0; id < impl_->dispatch.size(); ++id) + if (const DispatchRow *row = impl_->dispatch.lookup(id)) + record(static_cast(row->object)); + if (!conflict.empty()) + return fail("duplicate_object_identity", std::move(conflict)); + impl_->preflightValidated = true; + return true; +} + +void SimSystem::refreshRuntimeSummary() { + impl_->noProgress = {}; + impl_->noProgress.nextEvent = impl_->eventQueue.nextEvent(); + impl_->traceOwnerCount = 0; + impl_->traceEof = true; + + for (SimObject *object : runtimeObjects()) { + RuntimeObjectState state = object->runtimeState(epoch_); + if (state.traceOwner) { + ++impl_->traceOwnerCount; + impl_->traceEof = impl_->traceEof && state.traceEof; + impl_->noProgress.tracePosition = state.tracePosition; + impl_->noProgress.lastCommittedSequenceId = + state.traceLastCommittedSequenceId; + } + impl_->noProgress.queueOccupancy += state.queueOccupancy; + impl_->noProgress.pendingOffers += state.pendingOffers; + impl_->noProgress.activeReservations += state.activeReservations; + if (state.quiescent) + continue; + impl_->noProgress.blockedObjects.push_back( + {.id = object->id(), + .path = std::string(object->path()), + .reason = std::move(state.reason), + .subscriptions = std::move(state.subscriptions), + .dependencyChain = std::move(state.dependencyChain), + .correlationChain = std::move(state.correlationChain), + .queueOccupancy = state.queueOccupancy, + .pendingOffers = state.pendingOffers, + .activeReservations = state.activeReservations, + .protocolState = std::move(state.protocolState)}); + } + result_.tracePosition = impl_->noProgress.tracePosition; + result_.traceLastCommittedSequenceId = + impl_->noProgress.lastCommittedSequenceId; + if (!impl_->noProgress.blockedObjects.empty()) + impl_->noProgress.summary = + "unfinished runtime state has no scheduled wake or future event"; +} + +bool SimSystem::stopAtTraceCap() { + refreshRuntimeSummary(); + if (impl_->traceOwnerCount > 1) { + fail("multiple_trace_owners", + "the runtime must have exactly one committed trace cursor owner"); + return true; + } + if (impl_->traceOwnerCount == 0 || impl_->traceEof || + result_.tracePosition < maxTraceRecords_) + return false; + terminated_ = true; + impl_->executingEpoch = false; + result_.classification = TerminationClass::Incomplete; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.domainCycles = impl_->domainCycles; + result_.terminationCap = maxTraceRecords_; + result_.diagnosticCode = "max_trace_records_reached"; + return true; +} + +NoProgressReport SimSystem::noProgressReport() const { + return impl_->noProgress; +} + +std::vector SimSystem::statistics() const { + std::vector snapshots; + for (const SimObject *object : runtimeObjects()) + object->collectStatistics(snapshots); + std::stable_sort(snapshots.begin(), snapshots.end(), + [](const StatSnapshot &left, const StatSnapshot &right) { + return std::tie(left.objectPath, left.name) < + std::tie(right.objectPath, right.name); + }); + return snapshots; +} + +std::span SimSystem::observations() const { + return impl_->observations.events(); +} + +bool SimSystem::proposeObservation(EventProposal proposal) { + if (terminated_ || !impl_->executingEpoch || !impl_->activeProposalOwner) + return false; + if (proposal.ownerId != *impl_->activeProposalOwner || + !lookup(proposal.ownerId)) + return fail("invalid_observation_owner", + "observation owner must be the active runtime object"); + if (!impl_->observations.propose(std::move(proposal))) + return fail("invalid_observation_proposal", + std::string(impl_->observations.lastError())); + return true; +} + +void SimSystem::registerObject(SimObject *obj) { + if (!obj || obj->id() == kInvalidObjectId || + impl_->objects.contains(obj->id())) { + fail("invalid_object_registration", + "runtime object registration is null, invalid, or duplicate"); + return; + } + impl_->objects[obj->id()] = obj; + impl_->preflightValidated = false; +} + +bool SimSystem::setDispatchTable(std::span rows) { + DispatchTable candidate(rows); + if (!candidate.validate()) + return fail("invalid_dispatch_table", + "dispatch rows must be complete and densely indexed"); + impl_->dispatch = candidate; + impl_->activation = ActivationPlan{}; + impl_->preflightValidated = false; + return true; +} + +bool SimSystem::setActivationPlan(std::span offsets, + std::span targets) { + ActivationPlan candidate(offsets, targets); + if (!candidate.validate(impl_->dispatch.size())) + return fail("invalid_activation_plan", + "activation offsets and targets must be canonical and dense"); + impl_->activation = candidate; + return true; +} + +bool SimSystem::setTimeDomains(std::span domains) { + if (terminated_) + return false; + std::map candidate; + std::string previous; + for (const TimeDomainRuntime &domain : domains) { + if (domain.name.empty() || domain.period == 0 || domain.tickScale == 0 || + (!previous.empty() && previous >= domain.name) || + !candidate.emplace(domain.name, domain).second) + return fail("invalid_time_domains", + "time domains must be sorted, unique, and positive"); + previous = domain.name; + } + impl_->timeDomains = std::move(candidate); + impl_->domainCycles.clear(); + for (const auto &[name, domain] : impl_->timeDomains) + impl_->domainCycles.emplace(name, 0); + return true; +} + +bool SimSystem::setDeadlockWindow(std::optional window) { + if (terminated_) + return false; + if (window && *window == 0) + return fail("invalid_runtime_limits", + "the deadlock window must be positive"); + impl_->deadlockWindow = window; + impl_->lastProgressTick = epoch_.time; + return true; +} + +bool SimSystem::setMaxDomainCycles( + const std::map &limits) { + if (terminated_) + return false; + for (const auto &[name, maximum] : limits) + if (maximum == 0 || !impl_->timeDomains.contains(name)) + return fail("invalid_runtime_limits", + "domain limits must name configured time domains"); + impl_->maxDomainCycles = limits; + return true; +} + +bool SimSystem::setRuntimeLimits(const RuntimeLimits &limits) { + if (terminated_) + return false; + if (limits.maxTicks && *limits.maxTicks == 0) + return fail("invalid_runtime_limits", "runtime limits must be positive"); + if (!setDeadlockWindow(limits.deadlockWindow) || + !setMaxDomainCycles(limits.maxDomainCycles)) + return false; + maxTicks_ = limits.maxTicks.value_or(UINT64_MAX); + return true; +} + +SimObject *SimSystem::lookup(ObjectId id) const { + if (const DispatchRow *row = impl_->dispatch.lookup(id)) + return static_cast(row->object); + auto it = impl_->objects.find(id); + return it != impl_->objects.end() ? it->second : nullptr; +} + +bool SimSystem::scheduleWork(ObjectId id, Epoch epoch) { + if (terminated_) + return false; + if (epoch.delta >= kMaxDeltasPerTick) + return fail("max_deltas_exceeded", + "scheduled work exceeds the causal delta limit"); + if (epoch < epoch_) + return fail("work_before_current_epoch", + "work cannot be scheduled before the committed epoch"); + if (!lookup(id)) + return fail("unknown_work_target", + "work target is absent from the static dispatch table"); + if (impl_->executingEpoch && epoch == epoch_) { + if (epoch_.delta + 1 >= kMaxDeltasPerTick) + return fail("max_deltas_exceeded", + "causal continuation exceeds the delta limit"); + epoch = epoch_.nextDelta(); + } + impl_->scheduledWork[epoch].insert(id); + return true; +} + +bool SimSystem::scheduleEvent(Event event) { + if (terminated_) + return false; + if (event.readyTime.delta >= kMaxDeltasPerTick) + return fail("max_deltas_exceeded", + "scheduled event exceeds the causal delta limit"); + if (event.readyTime < epoch_) + return fail("event_before_current_epoch", + "events cannot be scheduled before the committed epoch"); + if (!lookup(event.targetId)) + return fail("unknown_event_target", + "event target is absent from the static dispatch table"); + if (!impl_->eventQueue.proposeSchedule(event)) + return fail("event_queue_capacity_exceeded", + "the global event queue capacity was exceeded"); + return true; +} + +std::optional SimSystem::nextEvent() const { + return impl_->eventQueue.nextEvent(); +} + +bool SimSystem::step() { + if (terminated_) + return false; + if (!impl_->preflightValidated && !validateRuntimeIdentities()) + return false; + if (stopAtTraceCap()) + return false; + + if (epoch_.time >= maxTicks_) { + terminated_ = true; + result_.classification = TerminationClass::Incomplete; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.domainCycles = impl_->domainCycles; + result_.terminationCap = maxTicks_; + result_.diagnosticCode = "max_ticks_reached"; + return false; + } + + if (epoch_.delta == 0) { + for (const auto &[name, domain] : impl_->timeDomains) { + if (epoch_.time < domain.phase || + (epoch_.time - domain.phase) % domain.period != 0) + continue; + uint64_t &cycles = impl_->domainCycles[name]; + if (auto maximum = impl_->maxDomainCycles.find(name); + maximum != impl_->maxDomainCycles.end() && + cycles >= maximum->second) { + terminated_ = true; + impl_->executingEpoch = false; + result_.classification = TerminationClass::Incomplete; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.terminationCap = maximum->second; + result_.domainCycles = impl_->domainCycles; + result_.diagnosticCode = "max_domain_cycles_reached"; + return false; + } + ++cycles; + } + } + + auto stopAtEventCap = [this] { + if (impl_->committedEventCount < maxEvents_) + return false; + terminated_ = true; + impl_->executingEpoch = false; + result_.classification = TerminationClass::Incomplete; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.domainCycles = impl_->domainCycles; + result_.terminationCap = maxEvents_; + result_.diagnosticCode = "max_events_reached"; + return true; + }; + + if (stopAtEventCap()) + return false; + + // Events committed by a previous epoch activate their target at their exact + // ready epoch before the immutable Work snapshot is observed. + while (auto event = impl_->eventQueue.nextEvent()) { + if (event->readyTime < epoch_) + return fail("event_before_current_epoch", + "the event queue contains a stale event"); + if (event->readyTime != epoch_) + break; + if (stopAtEventCap()) + return false; + impl_->eventQueue.popNext(); + if (!scheduleWork(event->targetId, epoch_)) + return false; + ++impl_->committedEventCount; + impl_->lastProgressTick = epoch_.time; + } + + std::set currentWork; + if (auto current = impl_->scheduledWork.find(epoch_); + current != impl_->scheduledWork.end()) { + currentWork = std::move(current->second); + impl_->scheduledWork.erase(current); + } + + impl_->executingEpoch = true; + for (ObjectId id : currentWork) { + impl_->activeProposalOwner = id; + if (const DispatchRow *row = impl_->dispatch.lookup(id)) + row->work(row->object, epoch_); + else if (SimObject *object = lookup(id)) + object->doWork(epoch_); + impl_->activeProposalOwner.reset(); + if (terminated_) + return false; + } + + for (ObjectId id : currentWork) { + impl_->activeProposalOwner = id; + if (const DispatchRow *row = impl_->dispatch.lookup(id)) + row->xfer(row->object, epoch_, XferPhase::Arbitrate); + else if (SimObject *object = lookup(id)) + object->doArbitrate(epoch_); + impl_->activeProposalOwner.reset(); + if (terminated_) + return false; + } + + std::vector committedSources; + for (ObjectId id : currentWork) { + SimObject *object = lookup(id); + const DispatchRow *row = impl_->dispatch.lookup(id); + bool willCommit = row ? row->xfer(row->object, epoch_, XferPhase::Probe) + : object && object->hasPendingCommit(); + if (willCommit) { + auto previousCommit = impl_->lastCommitTick.find(id); + if (previousCommit != impl_->lastCommitTick.end() && + previousCommit->second == epoch_.time) + return fail("multiple_stateful_commits", + "a stateful object cannot commit twice in one tick"); + } + + bool committed = false; + if (row) + committed = row->xfer(row->object, epoch_, XferPhase::Commit); + else if (object) { + object->doXfer(epoch_); + committed = willCommit; + } + if (committed != willCommit) + return fail("xfer_probe_mismatch", + "Xfer pending state changed between probe and commit"); + if (committed) { + committedSources.push_back(id); + impl_->lastCommitTick[id] = epoch_.time; + impl_->lastProgressTick = epoch_.time; + } + if (terminated_) + return false; + if (object && !object->runtimeFailureCode().empty()) + return fail(std::string(object->runtimeFailureCode()), + "runtime object reported a committed failure"); + if (committed) { + if (!impl_->observations.commitOwner(id, epoch_)) + return fail("invalid_observation_commit", + std::string(impl_->observations.lastError())); + } else { + impl_->observations.rejectOwner(id); + } + } + if (impl_->eventQueue.hasPendingCommit()) { + if (impl_->eventQueueLastCommitTick == epoch_.time) + return fail("multiple_stateful_commits", + "the event queue cannot commit twice in one tick"); + impl_->eventQueueLastCommitTick = epoch_.time; + impl_->lastProgressTick = epoch_.time; + } + impl_->eventQueue.doXfer(epoch_); + + if (!committedSources.empty() && !impl_->activation.empty()) { + if (epoch_.time == std::numeric_limits::max()) + return fail("tick_overflow", "activation would overflow simulation time"); + Epoch activationEpoch{epoch_.time + 1, 0}; + for (ObjectId source : committedSources) + for (ObjectId target : impl_->activation.targetsFor(source)) + if (!scheduleWork(target, activationEpoch)) + return false; + } + + // An event committed for the active epoch is a causal continuation. Its + // target runs at the next delta, never inside the closed Work snapshot. + while (auto event = impl_->eventQueue.nextEvent()) { + if (event->readyTime < epoch_) + return fail("event_before_current_epoch", + "the event queue contains a stale event"); + if (event->readyTime != epoch_) + break; + if (stopAtEventCap()) + return false; + impl_->eventQueue.popNext(); + if (!scheduleWork(event->targetId, epoch_)) + return false; + ++impl_->committedEventCount; + impl_->lastProgressTick = epoch_.time; + } + impl_->executingEpoch = false; + impl_->activeProposalOwner.reset(); + + std::optional nextEpoch; + bool nextEpochIsEvent = false; + if (!impl_->scheduledWork.empty()) + nextEpoch = impl_->scheduledWork.begin()->first; + if (auto event = impl_->eventQueue.nextEvent(); + event && (!nextEpoch || event->readyTime <= *nextEpoch)) { + nextEpoch = event->readyTime; + nextEpochIsEvent = true; + } + + if (!nextEpoch) { + if (stopAtTraceCap()) + return false; + refreshRuntimeSummary(); + if (impl_->traceOwnerCount == 1 && impl_->traceEof) { + std::vector traceEndProcesses; + for (SimObject *object : runtimeObjects()) + if (object->requestTraceEnd()) + traceEndProcesses.push_back(object->id()); + if (!traceEndProcesses.empty()) { + if (epoch_.time == std::numeric_limits::max()) + return fail("tick_overflow", + "trace-end process termination exceeds tick range"); + Epoch shutdownEpoch{epoch_.time + 1, 0}; + if (shutdownEpoch.time >= maxTicks_) { + epoch_ = {maxTicks_, 0}; + terminated_ = true; + result_.classification = TerminationClass::Incomplete; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.terminationCap = maxTicks_; + result_.domainCycles = impl_->domainCycles; + result_.diagnosticCode = "max_ticks_reached"; + return false; + } + for (ObjectId id : traceEndProcesses) + if (!scheduleWork(id, shutdownEpoch)) + return false; + epoch_ = shutdownEpoch; + return true; + } + } + if (!impl_->noProgress.blockedObjects.empty() && impl_->deadlockWindow) { + const Tick window = *impl_->deadlockWindow; + if (impl_->lastProgressTick > std::numeric_limits::max() - window) + return fail("tick_overflow", "deadlock window exceeds tick range"); + Tick deadline = impl_->lastProgressTick + window; + if (deadline >= maxTicks_) { + epoch_ = {maxTicks_, 0}; + terminated_ = true; + result_.classification = TerminationClass::Incomplete; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.terminationCap = maxTicks_; + result_.domainCycles = impl_->domainCycles; + result_.diagnosticCode = "max_ticks_reached"; + return false; + } + epoch_ = {deadline, 0}; + return fail("deadlock_window_reached", impl_->noProgress.summary); + } + if (!impl_->noProgress.blockedObjects.empty()) + return fail("no_progress", impl_->noProgress.summary); + terminated_ = true; + result_.classification = TerminationClass::Completed; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.domainCycles = impl_->domainCycles; + return false; + } + if (*nextEpoch <= epoch_) + return fail("non_monotonic_epoch", + "scheduler failed to advance beyond the committed epoch"); + if (!nextEpochIsEvent && impl_->deadlockWindow) { + const Tick window = *impl_->deadlockWindow; + if (impl_->lastProgressTick > std::numeric_limits::max() - window) + return fail("tick_overflow", "deadlock window exceeds tick range"); + const Tick deadline = impl_->lastProgressTick + window; + if (nextEpoch->time >= deadline) { + epoch_ = {deadline, 0}; + return fail("deadlock_window_reached", + "scheduled work made no declared progress within the " + "deadlock window"); + } + } + if (nextEpoch->time >= maxTicks_) { + epoch_ = {maxTicks_, 0}; + terminated_ = true; + result_.classification = TerminationClass::Incomplete; + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.domainCycles = impl_->domainCycles; + result_.terminationCap = maxTicks_; + result_.diagnosticCode = "max_ticks_reached"; + return false; + } + epoch_ = *nextEpoch; + return true; +} + +TerminationResult SimSystem::run() { + epoch_ = {0, 0}; + + for (SimObject *object : runtimeObjects()) + if (object->kind() == ObjectKind::Process || + object->kind() == ObjectKind::TraceSource) + scheduleWork(object->id(), epoch_); + + while (!terminated_) + if (!step()) + break; + + result_.finalEpoch = epoch_; + result_.committedEventCount = impl_->committedEventCount; + result_.domainCycles = impl_->domainCycles; + refreshRuntimeSummary(); + return result_; +} + +void SimSystem::reset() { + epoch_ = {0, 0}; + terminated_ = false; + result_ = TerminationResult{}; + impl_->scheduledWork.clear(); + impl_->eventQueue.reset(); + impl_->committedEventCount = 0; + impl_->executingEpoch = false; + impl_->activeProposalOwner.reset(); + impl_->noProgress = {}; + impl_->observations.reset(); + impl_->traceOwnerCount = 0; + impl_->traceEof = true; + impl_->preflightValidated = false; + impl_->lastCommitTick.clear(); + impl_->eventQueueLastCommitTick.reset(); + impl_->lastProgressTick = 0; + impl_->domainCycles.clear(); + for (const auto &[name, domain] : impl_->timeDomains) + impl_->domainCycles.emplace(name, 0); + if (!impl_->dispatch.empty()) { + for (ObjectId id = 0; id < impl_->dispatch.size(); ++id) { + const DispatchRow *row = impl_->dispatch.lookup(id); + row->reset(row->object); + } + } else { + for (SimObject *object : runtimeObjects()) + if (object->kind() != ObjectKind::Module) + object->reset(); + } +} + +} // namespace gfsim diff --git a/components/agentic-circuit/lib/gfsim/trace.cpp b/components/agentic-circuit/lib/gfsim/trace.cpp new file mode 100644 index 00000000..d434d62d --- /dev/null +++ b/components/agentic-circuit/lib/gfsim/trace.cpp @@ -0,0 +1,613 @@ +#include "gfsim/trace.h" + +#include "acir/Bindings/Binding.h" + +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" + +#include +#include +#include +#include +#include +#include + +namespace gfsim { +namespace { + +using llvm::json::Array; +using llvm::json::Object; +using llvm::json::Value; + +std::string pointerToken(llvm::StringRef token) { + std::string escaped; + for (char character : token) { + if (character == '~') + escaped += "~0"; + else if (character == '/') + escaped += "~1"; + else + escaped += character; + } + return escaped; +} + +bool contains(std::initializer_list values, + llvm::StringRef candidate) { + return std::any_of(values.begin(), values.end(), [&](std::string_view value) { + return candidate == llvm::StringRef(value.data(), value.size()); + }); +} + +class TraceParser { +public: + explicit TraceParser(const TraceValidationLimits &limits) : limits_(limits) {} + + TraceLoadResult parse(std::string_view input) { + acir::bindings::JsonParseLimits jsonLimits; + jsonLimits.maxInputBytes = limits_.maxDocumentBytes; + jsonLimits.maxDepth = limits_.maxNestingDepth; + jsonLimits.maxStringBytes = limits_.maxStringBytes; + jsonLimits.maxTotalStringBytes = limits_.maxAggregateDecodedBytes; + jsonLimits.maxArrayElements = + std::max({limits_.maxRecordCount, limits_.maxOperandsPerRecord, + limits_.maxDependenciesPerRecord}); + jsonLimits.maxObjectMembers = limits_.maxAttributeMembers; + jsonLimits.maxStructuralWork = + std::max(limits_.maxAggregateDecodedBytes, 1); + + auto parsed = acir::bindings::parseIJson( + llvm::StringRef(input.data(), input.size()), jsonLimits); + if (!parsed) { + std::string message = llvm::toString(parsed.takeError()); + std::string code = + message.find("limit exceeded") != std::string::npos || + message.find("maximum depth") != std::string::npos + ? "ACTRACE-LIMIT" + : "ACTRACE-JSON"; + fail(code, "", std::nullopt, std::move(message)); + return std::move(result_); + } + const Object *root = parsed->getAsObject(); + if (!root) { + fail("ACTRACE-SCHEMA", "", std::nullopt, + "trace document must be an object"); + return std::move(result_); + } + + PtoTraceDocument document; + if (!parseRoot(*root, document)) + return std::move(result_); + result_.document = std::move(document); + return std::move(result_); + } + +private: + bool fail(std::string code, std::string pointer, + std::optional sequenceId, std::string message) { + if (result_.diagnostics.size() < limits_.maxDiagnostics) + result_.diagnostics.push_back({std::move(code), std::move(pointer), + sequenceId, std::move(message)}); + return false; + } + + bool spendDecoded(size_t bytes, std::string_view pointer, + std::optional sequenceId = std::nullopt) { + if (bytes > limits_.maxAggregateDecodedBytes - + std::min(decodedBytes_, limits_.maxAggregateDecodedBytes)) + return fail("ACTRACE-LIMIT", std::string(pointer), sequenceId, + "aggregate decoded byte limit exceeded"); + decodedBytes_ += bytes; + return true; + } + + bool checkObject(const Object &object, std::string_view pointer, + std::initializer_list required, + std::initializer_list allowed, + std::optional sequenceId = std::nullopt) { + for (const auto &entry : object) { + llvm::StringRef key = entry.first; + if (!contains(allowed, key)) + return fail("ACTRACE-SCHEMA", + std::string(pointer) + "/" + pointerToken(key), sequenceId, + "unknown object member"); + } + for (std::string_view key : required) + if (!object.get(key)) + return fail("ACTRACE-SCHEMA", + std::string(pointer) + "/" + std::string(key), sequenceId, + "required object member is missing"); + return true; + } + + bool readString(const Value *value, std::string &output, + std::string_view pointer, + std::optional sequenceId = std::nullopt, + bool allowEmpty = false) { + auto string = value ? value->getAsString() : std::nullopt; + if (!string || (!allowEmpty && string->empty())) + return fail("ACTRACE-SCHEMA", std::string(pointer), sequenceId, + "expected a non-empty string"); + output = string->str(); + return spendDecoded(output.size(), pointer, sequenceId); + } + + bool readUInt(const Value *value, uint64_t &output, std::string_view pointer, + std::optional sequenceId = std::nullopt) { + auto integer = value ? value->getAsUINT64() : std::nullopt; + if (!integer) + return fail("ACTRACE-SCHEMA", std::string(pointer), sequenceId, + "expected an unsigned 64-bit integer"); + output = *integer; + return spendDecoded(sizeof(output), pointer, sequenceId); + } + + bool parseRoot(const Object &root, PtoTraceDocument &document) { + if (!checkObject( + root, "", + {"schema", "version", "contract_epoch", "metadata", "records"}, + {"schema", "version", "contract_epoch", "metadata", "records"})) + return false; + auto schema = root.getString("schema"); + auto version = root.getString("version"); + auto epoch = root.getString("contract_epoch"); + if (!schema || *schema != "pto-trace") + return fail("ACTRACE-SCHEMA", "/schema", std::nullopt, + "schema must equal pto-trace"); + if (!version || *version != "0.1") + return fail("ACTRACE-SCHEMA", "/version", std::nullopt, + "version must equal 0.1"); + if (!epoch || *epoch != "0.3") + return fail("ACTRACE-SCHEMA", "/contract_epoch", std::nullopt, + "contract_epoch must equal 0.3"); + + const Object *metadata = root.getObject("metadata"); + if (!metadata) + return fail("ACTRACE-SCHEMA", "/metadata", std::nullopt, + "metadata must be an object"); + if (!parseMetadata(*metadata, document.metadata)) + return false; + + const Array *records = root.getArray("records"); + if (!records) + return fail("ACTRACE-SCHEMA", "/records", std::nullopt, + "records must be an array"); + if (records->size() > limits_.maxRecordCount) + return fail("ACTRACE-LIMIT", "/records", std::nullopt, + "record count limit exceeded"); + + document.records.reserve(records->size()); + for (size_t index = 0; index < records->size(); ++index) { + const Object *record = (*records)[index].getAsObject(); + std::string pointer = "/records/" + std::to_string(index); + if (!record) + return fail("ACTRACE-SCHEMA", pointer, std::nullopt, + "record must be an object"); + PtoTraceRecord decoded; + if (!parseRecord(*record, pointer, decoded)) + return false; + document.records.push_back(std::move(decoded)); + } + if (document.metadata.recordCount && + *document.metadata.recordCount != document.records.size()) + return fail("ACTRACE-METADATA", "/metadata/record_count", std::nullopt, + "record_count does not match records length"); + if (!validateContentHash(*records, document.metadata.contentHash)) + return false; + return true; + } + + bool parseMetadata(const Object &metadata, PtoTraceMetadata &decoded) { + if (!checkObject(metadata, "/metadata", {}, + {"producer", "pto_identity", "source_program", + "address_spaces", "data_layout", "record_count", + "content_hash"})) + return false; + if (const Value *value = metadata.get("producer"); + value && !readString(value, decoded.producer, "/metadata/producer")) + return false; + if (const Value *value = metadata.get("pto_identity"); + value && + !readString(value, decoded.ptoIdentity, "/metadata/pto_identity")) + return false; + if (const Value *value = metadata.get("source_program"); + value && + !readString(value, decoded.sourceProgram, "/metadata/source_program")) + return false; + if (const Value *value = metadata.get("data_layout"); + value && + !readString(value, decoded.dataLayout, "/metadata/data_layout")) + return false; + if (const Value *value = metadata.get("record_count")) { + uint64_t count = 0; + if (!readUInt(value, count, "/metadata/record_count")) + return false; + decoded.recordCount = count; + } + if (const Value *value = metadata.get("content_hash")) { + if (!readString(value, decoded.contentHash, "/metadata/content_hash")) + return false; + if (decoded.contentHash.size() != 71 || + !decoded.contentHash.starts_with("sha256:") || + !std::all_of(decoded.contentHash.begin() + 7, + decoded.contentHash.end(), [](unsigned char character) { + return std::isdigit(character) || + (character >= 'a' && character <= 'f'); + })) + return fail("ACTRACE-SCHEMA", "/metadata/content_hash", std::nullopt, + "content_hash must be a lowercase sha256 fingerprint"); + } + if (const Value *value = metadata.get("address_spaces")) { + const Array *spaces = value->getAsArray(); + if (!spaces) + return fail("ACTRACE-SCHEMA", "/metadata/address_spaces", std::nullopt, + "address_spaces must be an array"); + std::set unique; + for (size_t index = 0; index < spaces->size(); ++index) { + std::string space; + std::string pointer = + "/metadata/address_spaces/" + std::to_string(index); + if (!readString(&(*spaces)[index], space, pointer)) + return false; + if (!unique.insert(space).second) + return fail("ACTRACE-SCHEMA", pointer, std::nullopt, + "address_spaces entries must be unique"); + declaredAddressSpaces_.insert(space); + decoded.addressSpaces.push_back(std::move(space)); + } + } + return true; + } + + bool parseRecord(const Object &record, std::string_view pointer, + PtoTraceRecord &decoded) { + if (!checkObject( + record, pointer, + {"sequence_id", "opcode", "operands", "dependencies", "attributes"}, + {"sequence_id", "opcode", "operands", "dependencies", "attributes", + "issue_time", "source"})) + return false; + if (!readUInt(record.get("sequence_id"), decoded.sequenceId, + std::string(pointer) + "/sequence_id")) + return false; + if (lastSequenceId_ && decoded.sequenceId <= *lastSequenceId_) + return fail("ACTRACE-IDENTITY", std::string(pointer) + "/sequence_id", + decoded.sequenceId, + "sequence_id values must be strictly increasing"); + lastSequenceId_ = decoded.sequenceId; + + if (!readString(record.get("opcode"), decoded.opcode, + std::string(pointer) + "/opcode", decoded.sequenceId)) + return false; + if (!isOpcode(decoded.opcode)) + return fail("ACTRACE-SCHEMA", std::string(pointer) + "/opcode", + decoded.sequenceId, "opcode has an invalid spelling"); + + const Array *operands = record.getArray("operands"); + if (!operands) + return fail("ACTRACE-SCHEMA", std::string(pointer) + "/operands", + decoded.sequenceId, "operands must be an array"); + if (operands->size() > limits_.maxOperandsPerRecord) + return fail("ACTRACE-LIMIT", std::string(pointer) + "/operands", + decoded.sequenceId, "operand count limit exceeded"); + decoded.operands.reserve(operands->size()); + for (size_t index = 0; index < operands->size(); ++index) { + const Object *operand = (*operands)[index].getAsObject(); + std::string operandPointer = + std::string(pointer) + "/operands/" + std::to_string(index); + if (!operand) + return fail("ACTRACE-SCHEMA", operandPointer, decoded.sequenceId, + "operand must be an object"); + PtoTraceOperand decodedOperand; + if (!parseOperand(*operand, operandPointer, decoded.sequenceId, + decodedOperand)) + return false; + decoded.operands.push_back(std::move(decodedOperand)); + } + + const Array *dependencies = record.getArray("dependencies"); + if (!dependencies) + return fail("ACTRACE-SCHEMA", std::string(pointer) + "/dependencies", + decoded.sequenceId, "dependencies must be an array"); + if (dependencies->size() > limits_.maxDependenciesPerRecord) + return fail("ACTRACE-LIMIT", std::string(pointer) + "/dependencies", + decoded.sequenceId, "dependency count limit exceeded"); + std::set uniqueDependencies; + for (size_t index = 0; index < dependencies->size(); ++index) { + uint64_t dependency = 0; + std::string dependencyPointer = + std::string(pointer) + "/dependencies/" + std::to_string(index); + if (!readUInt(&(*dependencies)[index], dependency, dependencyPointer, + decoded.sequenceId)) + return false; + if (!uniqueDependencies.insert(dependency).second) + return fail("ACTRACE-DEPENDENCY", dependencyPointer, decoded.sequenceId, + "duplicate dependency"); + if (!seenSequenceIds_.contains(dependency)) + return fail("ACTRACE-DEPENDENCY", dependencyPointer, decoded.sequenceId, + "dependency must reference an earlier record"); + decoded.dependencies.push_back(dependency); + } + + const Object *attributes = record.getObject("attributes"); + if (!attributes) + return fail("ACTRACE-SCHEMA", std::string(pointer) + "/attributes", + decoded.sequenceId, "attributes must be an object"); + if (attributes->size() > limits_.maxAttributeMembers) + return fail("ACTRACE-LIMIT", std::string(pointer) + "/attributes", + decoded.sequenceId, "attribute member limit exceeded"); + for (const auto &entry : *attributes) { + llvm::StringRef key = entry.first; + PtoValue value; + std::string attributePointer = + std::string(pointer) + "/attributes/" + pointerToken(key); + if (!parseValue(entry.second, attributePointer, decoded.sequenceId, + value)) + return false; + decoded.attributes.emplace(key.str(), std::move(value)); + } + + if (const Value *issue = record.get("issue_time")) { + uint64_t issueTime = 0; + if (!readUInt(issue, issueTime, std::string(pointer) + "/issue_time", + decoded.sequenceId)) + return false; + decoded.issueTime = issueTime; + } + if (const Object *source = record.getObject("source")) { + PtoSourceLocation location; + if (!parseSource(*source, std::string(pointer) + "/source", + decoded.sequenceId, location)) + return false; + decoded.source = std::move(location); + } else if (record.get("source")) { + return fail("ACTRACE-SCHEMA", std::string(pointer) + "/source", + decoded.sequenceId, "source must be an object"); + } + seenSequenceIds_.insert(decoded.sequenceId); + return true; + } + + bool parseOperand(const Object &operand, std::string_view pointer, + uint64_t rootSequenceId, PtoTraceOperand &decoded) { + std::string kind; + if (!readString(operand.get("kind"), kind, std::string(pointer) + "/kind", + rootSequenceId)) + return false; + if (kind == "immediate") { + if (!checkObject(operand, pointer, {"kind", "type", "value"}, + {"kind", "type", "value"}, rootSequenceId)) + return false; + decoded.kind = PtoOperandKind::Immediate; + if (!readString(operand.get("type"), decoded.type, + std::string(pointer) + "/type", rootSequenceId)) + return false; + PtoValue value; + if (!parseValue(*operand.get("value"), std::string(pointer) + "/value", + rootSequenceId, value, true)) + return false; + decoded.immediate = std::move(value); + return true; + } + if (kind == "buffer" || kind == "tile" || kind == "symbol") { + if (!checkObject(operand, pointer, {"kind", "id"}, {"kind", "id"}, + rootSequenceId)) + return false; + decoded.kind = kind == "buffer" ? PtoOperandKind::Buffer + : kind == "tile" ? PtoOperandKind::Tile + : PtoOperandKind::Symbol; + return readString(operand.get("id"), decoded.id, + std::string(pointer) + "/id", rootSequenceId); + } + if (kind == "address") { + if (!checkObject(operand, pointer, {"kind", "space", "value"}, + {"kind", "space", "value"}, rootSequenceId)) + return false; + decoded.kind = PtoOperandKind::Address; + if (!readString(operand.get("space"), decoded.addressSpace, + std::string(pointer) + "/space", rootSequenceId) || + !readUInt(operand.get("value"), decoded.address, + std::string(pointer) + "/value", rootSequenceId)) + return false; + if (!declaredAddressSpaces_.contains(decoded.addressSpace)) + return fail("ACTRACE-ADDRESS", std::string(pointer) + "/space", + rootSequenceId, + "address space is absent from trace metadata"); + return true; + } + if (kind == "record_result") { + if (!checkObject(operand, pointer, + {"kind", "sequence_id", "result_index"}, + {"kind", "sequence_id", "result_index"}, rootSequenceId)) + return false; + decoded.kind = PtoOperandKind::RecordResult; + if (!readUInt(operand.get("sequence_id"), decoded.sequenceId, + std::string(pointer) + "/sequence_id", rootSequenceId) || + !readUInt(operand.get("result_index"), decoded.resultIndex, + std::string(pointer) + "/result_index", rootSequenceId)) + return false; + if (!seenSequenceIds_.contains(decoded.sequenceId)) + return fail("ACTRACE-DEPENDENCY", std::string(pointer) + "/sequence_id", + rootSequenceId, + "record_result must reference an earlier record"); + return true; + } + return fail("ACTRACE-SCHEMA", std::string(pointer) + "/kind", + rootSequenceId, "unknown operand kind"); + } + + bool parseSource(const Object &source, std::string_view pointer, + uint64_t sequenceId, PtoSourceLocation &decoded) { + if (!checkObject(source, pointer, {"file", "line", "column"}, + {"file", "line", "column"}, sequenceId) || + !readString(source.get("file"), decoded.file, + std::string(pointer) + "/file", sequenceId) || + !readUInt(source.get("line"), decoded.line, + std::string(pointer) + "/line", sequenceId) || + !readUInt(source.get("column"), decoded.column, + std::string(pointer) + "/column", sequenceId)) + return false; + if (decoded.line == 0 || decoded.column == 0) + return fail("ACTRACE-SCHEMA", std::string(pointer), sequenceId, + "source line and column must be positive"); + return true; + } + + bool parseValue(const Value &value, std::string_view pointer, + uint64_t sequenceId, PtoValue &decoded, + bool scalarOnly = false) { + if (!spendDecoded(1, pointer, sequenceId)) + return false; + if (value.kind() == Value::Null) { + decoded.value = std::monostate{}; + return true; + } + if (auto boolean = value.getAsBoolean()) { + decoded.value = *boolean; + return true; + } + if (auto integer = value.getAsInteger()) { + decoded.value = *integer; + return true; + } + if (auto integer = value.getAsUINT64()) { + decoded.value = *integer; + return true; + } + if (auto number = value.getAsNumber()) { + decoded.value = *number; + return true; + } + if (auto string = value.getAsString()) { + if (!spendDecoded(string->size(), pointer, sequenceId)) + return false; + decoded.value = string->str(); + return true; + } + if (scalarOnly) + return fail("ACTRACE-SCHEMA", std::string(pointer), sequenceId, + "immediate value must be a JSON scalar"); + if (const Array *array = value.getAsArray()) { + PtoValue::Array output; + output.reserve(array->size()); + for (size_t index = 0; index < array->size(); ++index) { + PtoValue element; + if (!parseValue((*array)[index], + std::string(pointer) + "/" + std::to_string(index), + sequenceId, element)) + return false; + output.push_back(std::move(element)); + } + decoded.value = std::move(output); + return true; + } + if (const Object *object = value.getAsObject()) { + PtoValue::Object output; + for (const auto &entry : *object) { + PtoValue member; + llvm::StringRef key = entry.first; + if (!parseValue(entry.second, + std::string(pointer) + "/" + pointerToken(key), + sequenceId, member) || + !spendDecoded(key.size(), pointer, sequenceId)) + return false; + output.emplace(key.str(), std::move(member)); + } + decoded.value = std::move(output); + return true; + } + return fail("ACTRACE-SCHEMA", std::string(pointer), sequenceId, + "unsupported JSON value"); + } + + bool validateContentHash(const Array &records, const std::string &declared) { + if (declared.empty()) + return true; + Array copy; + copy.reserve(records.size()); + for (const Value &record : records) + copy.push_back(record); + acir::bindings::JsonParseLimits limits; + limits.maxInputBytes = limits_.maxDocumentBytes; + limits.maxDepth = limits_.maxNestingDepth; + limits.maxStringBytes = limits_.maxStringBytes; + limits.maxTotalStringBytes = limits_.maxAggregateDecodedBytes; + limits.maxArrayElements = + std::max({limits_.maxRecordCount, limits_.maxOperandsPerRecord, + limits_.maxDependenciesPerRecord}); + limits.maxObjectMembers = limits_.maxAttributeMembers; + auto canonical = + acir::bindings::canonicalizeJson(Value(std::move(copy)), limits); + if (!canonical) + return fail("ACTRACE-LIMIT", "/metadata/content_hash", std::nullopt, + llvm::toString(canonical.takeError())); + if (acir::bindings::sha256Fingerprint(*canonical) != declared) + return fail("ACTRACE-METADATA", "/metadata/content_hash", std::nullopt, + "content_hash does not match canonical records bytes"); + return true; + } + + static bool isOpcode(std::string_view opcode) { + if (opcode.empty() || + !(std::isalpha(static_cast(opcode.front())) || + opcode.front() == '_')) + return false; + return std::all_of(opcode.begin() + 1, opcode.end(), [](unsigned char c) { + return std::isalnum(c) || c == '_' || c == '.'; + }); + } + + const TraceValidationLimits &limits_; + TraceLoadResult result_; + size_t decodedBytes_ = 0; + std::optional lastSequenceId_; + std::set seenSequenceIds_; + std::set declaredAddressSpaces_; +}; + +} // namespace + +std::string TraceLoadResult::primaryDiagnostic() const { + if (diagnostics.empty()) + return {}; + const TraceDiagnostic &diagnostic = diagnostics.front(); + std::ostringstream output; + output << diagnostic.code; + if (!diagnostic.jsonPointer.empty()) + output << " at " << diagnostic.jsonPointer; + if (diagnostic.sequenceId) + output << " for sequence_id " << *diagnostic.sequenceId; + output << ": " << diagnostic.message; + return output.str(); +} + +TraceLoadResult parsePtoTrace(std::string_view input, + const TraceValidationLimits &limits) { + return TraceParser(limits).parse(input); +} + +bool PtoTraceStream::append(std::string_view bytes) { + if (finished_ || exceededByteLimit_) + return false; + if (buffer_.size() > limits_.maxDocumentBytes || + bytes.size() > limits_.maxDocumentBytes - buffer_.size()) { + exceededByteLimit_ = true; + return false; + } + buffer_.append(bytes); + return true; +} + +TraceLoadResult PtoTraceStream::finish() { + if (finished_) + return {.diagnostics = {{"ACTRACE-STREAM", "", std::nullopt, + "trace stream was already finished"}}}; + finished_ = true; + if (exceededByteLimit_) + return {.diagnostics = {{"ACTRACE-LIMIT", "", std::nullopt, + "document byte limit exceeded"}}}; + return parsePtoTrace(buffer_, limits_); +} + +} // namespace gfsim diff --git a/components/agentic-circuit/pyproject.toml b/components/agentic-circuit/pyproject.toml new file mode 100644 index 00000000..b35e33ca --- /dev/null +++ b/components/agentic-circuit/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "agentic-circuit" +version = "0.1.0" +description = "IR-first architecture construction and graph-flow simulation" +readme = "README.md" +requires-python = ">=3.11" +license = "BSD-3-Clause" +authors = [{ name = "PTO-ISA Contributors" }] +dependencies = [] + +[project.optional-dependencies] +test = ["jsonschema>=4.18", "lit>=18,<19"] + +[project.scripts] +agentic-circuit = "agentic_circuit._cli:main" + +[tool.setuptools] +package-dir = { "" = "src" } + +[tool.setuptools.packages.find] +where = ["src"] + +[project.urls] +Repository = "https://github.com/PTO-ISA/pyCircuit" +Issues = "https://github.com/PTO-ISA/pyCircuit/issues" + +[tool.agentic-circuit] +contract-epoch = "0.3" +llvm-release = "22.1.8" diff --git a/components/agentic-circuit/python/CMakeLists.txt b/components/agentic-circuit/python/CMakeLists.txt new file mode 100644 index 00000000..48d1235b --- /dev/null +++ b/components/agentic-circuit/python/CMakeLists.txt @@ -0,0 +1,65 @@ +find_package(Python3 3.11 REQUIRED COMPONENTS Interpreter Development.Module) + +set( + ACIR_PYTHON_INSTALL_DIR + "lib/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages" + CACHE STRING + "Install destination for the Agentic Circuit Python package" +) + +add_subdirectory(native) + +set( + ACIR_PYTHON_BUILD_DATA_DIR + "${PROJECT_BINARY_DIR}/python/agentic_circuit/_data" +) +file(MAKE_DIRECTORY "${ACIR_PYTHON_BUILD_DATA_DIR}") +file( + COPY "${PROJECT_SOURCE_DIR}/schemas" + DESTINATION "${ACIR_PYTHON_BUILD_DATA_DIR}" +) +file( + COPY "${PROJECT_SOURCE_DIR}/resources" + DESTINATION "${ACIR_PYTHON_BUILD_DATA_DIR}" +) +configure_file( + "${PROJECT_SOURCE_DIR}/python/resource-package-init.py" + "${ACIR_PYTHON_BUILD_DATA_DIR}/schemas/__init__.py" + COPYONLY +) +configure_file( + "${PROJECT_SOURCE_DIR}/python/resource-package-init.py" + "${ACIR_PYTHON_BUILD_DATA_DIR}/resources/__init__.py" + COPYONLY +) + +install( + DIRECTORY "${PROJECT_SOURCE_DIR}/src/agentic_circuit" + DESTINATION "${ACIR_PYTHON_INSTALL_DIR}" + PATTERN "__pycache__" EXCLUDE + PATTERN "*.pyc" EXCLUDE +) +install( + DIRECTORY "${PROJECT_SOURCE_DIR}/schemas" + DESTINATION "${ACIR_PYTHON_INSTALL_DIR}/agentic_circuit/_data" +) +install( + DIRECTORY "${PROJECT_SOURCE_DIR}/resources" + DESTINATION "${ACIR_PYTHON_INSTALL_DIR}/agentic_circuit/_data" + PATTERN "__pycache__" EXCLUDE + PATTERN "*.pyc" EXCLUDE +) +install( + FILES "${PROJECT_SOURCE_DIR}/python/resource-package-init.py" + DESTINATION "${ACIR_PYTHON_INSTALL_DIR}/agentic_circuit/_data/schemas" + RENAME "__init__.py" +) +install( + FILES "${PROJECT_SOURCE_DIR}/python/resource-package-init.py" + DESTINATION "${ACIR_PYTHON_INSTALL_DIR}/agentic_circuit/_data/resources" + RENAME "__init__.py" +) +install( + PROGRAMS "${PROJECT_SOURCE_DIR}/python/agentic-circuit" + DESTINATION bin +) diff --git a/components/agentic-circuit/python/agentic-circuit b/components/agentic-circuit/python/agentic-circuit new file mode 100644 index 00000000..1ef0760c --- /dev/null +++ b/components/agentic-circuit/python/agentic-circuit @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Relocatable launcher for the installed Agentic Circuit CLI.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def _site_packages() -> Path: + prefix = Path(__file__).resolve().parents[1] + preferred = prefix.joinpath( + "lib", + f"python{sys.version_info.major}.{sys.version_info.minor}", + "site-packages", + ) + if preferred.is_dir(): + return preferred + candidates = sorted((prefix / "lib").glob("python*/site-packages")) + if len(candidates) != 1: + raise RuntimeError("installed Agentic Circuit Python package is unavailable") + return candidates[0] + + +sys.path.insert(0, str(_site_packages())) + +from agentic_circuit._cli import main # noqa: E402 + + +raise SystemExit(main()) diff --git a/components/agentic-circuit/python/native/CMakeLists.txt b/components/agentic-circuit/python/native/CMakeLists.txt new file mode 100644 index 00000000..cfe91b0e --- /dev/null +++ b/components/agentic-circuit/python/native/CMakeLists.txt @@ -0,0 +1,24 @@ +Python3_add_library(agentic_circuit_native MODULE NativeModule.cpp) +string(JOIN "|" ACIR_NATIVE_LLVM_LINK_FLAGS ${ACIR_LLVM_LINK_FLAGS}) +target_compile_definitions(agentic_circuit_native PRIVATE Py_LIMITED_API=0x030B0000) +target_compile_definitions(agentic_circuit_native PRIVATE + ACIR_NATIVE_CXX_COMPILER="${CMAKE_CXX_COMPILER}" + ACIR_NATIVE_LLVM_LINK_FLAGS="${ACIR_NATIVE_LLVM_LINK_FLAGS}" +) +if(NOT LLVM_ENABLE_RTTI) + target_compile_options(agentic_circuit_native PRIVATE -fno-rtti) +endif() +target_link_libraries(agentic_circuit_native PRIVATE + ACIRCompiler + Python3::Module +) +set_target_properties(agentic_circuit_native PROPERTIES + OUTPUT_NAME "_native" + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/python/agentic_circuit" +) + +install( + TARGETS agentic_circuit_native + LIBRARY DESTINATION "${ACIR_PYTHON_INSTALL_DIR}/agentic_circuit" + RUNTIME DESTINATION "${ACIR_PYTHON_INSTALL_DIR}/agentic_circuit" +) diff --git a/components/agentic-circuit/python/native/NativeModule.cpp b/components/agentic-circuit/python/native/NativeModule.cpp new file mode 100644 index 00000000..392fae99 --- /dev/null +++ b/components/agentic-circuit/python/native/NativeModule.cpp @@ -0,0 +1,708 @@ +#include "acir/Compiler/Driver.h" + +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +using acir::codegen::ArtifactKind; +using acir::codegen::FileHash; +using acir::codegen::NamedFingerprint; +using acir::compiler::CompilerArtifact; +using acir::compiler::CompilerDiagnostic; +using acir::compiler::CompilerProfile; +using acir::compiler::CompilerRequest; +using acir::compiler::CompilerResult; +using acir::compiler::CompilerStage; + +class OwnedPy { +public: + OwnedPy(PyObject *object = nullptr) : object_(object) {} + ~OwnedPy() { Py_XDECREF(object_); } + + OwnedPy(const OwnedPy &) = delete; + OwnedPy &operator=(const OwnedPy &) = delete; + + OwnedPy(OwnedPy &&other) noexcept + : object_(std::exchange(other.object_, nullptr)) {} + OwnedPy &operator=(OwnedPy &&other) noexcept { + if (this != &other) { + Py_XDECREF(object_); + object_ = std::exchange(other.object_, nullptr); + } + return *this; + } + + explicit operator bool() const { return object_ != nullptr; } + PyObject *get() const { return object_; } + PyObject *release() { return std::exchange(object_, nullptr); } + +private: + PyObject *object_; +}; + +OwnedPy pyString(llvm::StringRef value) { + return OwnedPy(PyUnicode_FromStringAndSize(value.data(), value.size())); +} + +OwnedPy pyBytes(llvm::StringRef value) { + return OwnedPy(PyBytes_FromStringAndSize(value.data(), value.size())); +} + +OwnedPy pyNone() { + Py_INCREF(Py_None); + return OwnedPy(Py_None); +} + +bool setItem(PyObject *dictionary, const char *key, OwnedPy value) { + return value && PyDict_SetItemString(dictionary, key, value.get()) == 0; +} + +std::optional pythonString(PyObject *value, + llvm::StringRef label) { + if (!PyUnicode_Check(value)) { + PyErr_Format(PyExc_TypeError, "%s must be a string", label.str().c_str()); + return std::nullopt; + } + OwnedPy encoded(PyUnicode_AsUTF8String(value)); + if (!encoded) + return std::nullopt; + char *data = nullptr; + Py_ssize_t size = 0; + if (PyBytes_AsStringAndSize(encoded.get(), &data, &size) != 0) + return std::nullopt; + return std::string(data, static_cast(size)); +} + +std::optional pythonBytes(PyObject *value, llvm::StringRef label) { + if (!PyBytes_Check(value)) { + PyErr_Format(PyExc_TypeError, "%s must be bytes", label.str().c_str()); + return std::nullopt; + } + char *data = nullptr; + Py_ssize_t size = 0; + if (PyBytes_AsStringAndSize(value, &data, &size) != 0) + return std::nullopt; + return std::string(data, static_cast(size)); +} + +bool hasOnlyKeys(PyObject *dictionary, llvm::ArrayRef keys, + llvm::StringRef label) { + if (!PyDict_Check(dictionary)) { + PyErr_Format(PyExc_TypeError, "%s must be a dictionary", + label.str().c_str()); + return false; + } + Py_ssize_t position = 0; + PyObject *key = nullptr; + PyObject *value = nullptr; + while (PyDict_Next(dictionary, &position, &key, &value)) { + auto name = pythonString(key, "dictionary key"); + if (!name) + return false; + bool known = false; + for (llvm::StringRef candidate : keys) + known |= candidate == *name; + if (!known) { + PyErr_Format(PyExc_ValueError, "%s contains unknown key '%s'", + label.str().c_str(), name->c_str()); + return false; + } + } + return true; +} + +std::optional parseStage(llvm::StringRef name) { + static constexpr std::array, 11> + stages{{ + {"acir-parse", CompilerStage::AcirParse}, + {"acir-verify", CompilerStage::AcirVerify}, + {"acir-normalize", CompilerStage::AcirNormalize}, + {"acir-freeze", CompilerStage::AcirFreeze}, + {"acsim-lower", CompilerStage::AcsimLower}, + {"acsim-verify", CompilerStage::AcsimVerify}, + {"cxx-emit", CompilerStage::CxxEmit}, + {"cxx-contract", CompilerStage::CxxContract}, + {"compile", CompilerStage::Compile}, + {"link", CompilerStage::Link}, + {"publish", CompilerStage::Publish}, + }}; + for (const auto &[spelling, stage] : stages) + if (name == spelling) + return stage; + return std::nullopt; +} + +std::optional parseArtifactKind(llvm::StringRef name) { + if (name == "acpy") + return ArtifactKind::Acpy; + if (name == "frozen-acir" || name == "acir") + return ArtifactKind::Acir; + if (name == "acsim") + return ArtifactKind::Acsim; + if (name == "cpp-source") + return ArtifactKind::CppSource; + if (name == "cpp-header") + return ArtifactKind::CppHeader; + if (name == "executable") + return ArtifactKind::Executable; + if (name == "report") + return ArtifactKind::Report; + return std::nullopt; +} + +llvm::StringRef artifactKindName(ArtifactKind kind) { + switch (kind) { + case ArtifactKind::Acpy: + return "acpy"; + case ArtifactKind::Acir: + return "frozen-acir"; + case ArtifactKind::Acsim: + return "acsim"; + case ArtifactKind::CppSource: + return "cpp-source"; + case ArtifactKind::CppHeader: + return "cpp-header"; + case ArtifactKind::Executable: + return "executable"; + case ArtifactKind::Report: + return "report"; + } + return "report"; +} + +bool parseStringTuple(PyObject *value, llvm::StringRef label, + std::vector &result) { + if (!PyTuple_Check(value)) { + PyErr_Format(PyExc_TypeError, "%s must be a tuple", label.str().c_str()); + return false; + } + const Py_ssize_t size = PyTuple_Size(value); + if (size < 0) + return false; + result.reserve(static_cast(size)); + for (Py_ssize_t index = 0; index < size; ++index) { + auto item = pythonString(PyTuple_GetItem(value, index), label); + if (!item) + return false; + result.push_back(std::move(*item)); + } + return true; +} + +bool parseStringList(PyObject *value, llvm::StringRef label, + std::vector &result) { + if (!PyList_Check(value)) { + PyErr_Format(PyExc_TypeError, "%s must be a list", label.str().c_str()); + return false; + } + const Py_ssize_t size = PyList_Size(value); + if (size < 0) + return false; + result.reserve(static_cast(size)); + for (Py_ssize_t index = 0; index < size; ++index) { + auto item = pythonString(PyList_GetItem(value, index), label); + if (!item) + return false; + result.push_back(std::move(*item)); + } + return true; +} + +bool parseNamedFingerprints(PyObject *value, llvm::StringRef label, + std::vector &result) { + if (!PyList_Check(value)) { + PyErr_Format(PyExc_TypeError, "%s must be a list", label.str().c_str()); + return false; + } + const Py_ssize_t size = PyList_Size(value); + if (size < 0) + return false; + for (Py_ssize_t index = 0; index < size; ++index) { + PyObject *entry = PyList_GetItem(value, index); + static constexpr std::array keys{"name", "fingerprint"}; + if (!hasOnlyKeys(entry, keys, label)) + return false; + auto name = pythonString(PyDict_GetItemString(entry, "name"), label); + auto fingerprint = + pythonString(PyDict_GetItemString(entry, "fingerprint"), label); + if (!name || !fingerprint) + return false; + result.push_back({std::move(*name), std::move(*fingerprint)}); + } + return true; +} + +bool parseFileHashes(PyObject *value, std::vector &result) { + if (!PyList_Check(value)) { + PyErr_SetString(PyExc_TypeError, "source_files must be a list"); + return false; + } + const Py_ssize_t size = PyList_Size(value); + if (size < 0) + return false; + for (Py_ssize_t index = 0; index < size; ++index) { + PyObject *entry = PyList_GetItem(value, index); + static constexpr std::array keys{"path", "sha256"}; + if (!hasOnlyKeys(entry, keys, "source file")) + return false; + auto path = + pythonString(PyDict_GetItemString(entry, "path"), "source path"); + auto fingerprint = + pythonString(PyDict_GetItemString(entry, "sha256"), "source hash"); + if (!path || !fingerprint) + return false; + result.push_back({std::move(*path), std::move(*fingerprint)}); + } + return true; +} + +bool parseBuildOptions(PyObject *value, CompilerRequest &request) { + static constexpr std::array keys{ + "project_name", "project_identity", + "system_name", "system_identity", + "source_files", "python_version", + "helper_identities", "compiler", + "standard_library", "instrumentation_layers", + "output_root", "include_roots", + "link_inputs"}; + if (!hasOnlyKeys(value, keys, "native build options")) + return false; + for (llvm::StringRef key : keys) + if (!PyDict_GetItemString(value, key.data())) { + PyErr_Format(PyExc_ValueError, "native build options are missing '%s'", + key.str().c_str()); + return false; + } + auto projectName = + pythonString(PyDict_GetItemString(value, "project_name"), "project_name"); + auto projectIdentity = pythonString( + PyDict_GetItemString(value, "project_identity"), "project_identity"); + auto systemName = + pythonString(PyDict_GetItemString(value, "system_name"), "system_name"); + auto systemIdentity = pythonString( + PyDict_GetItemString(value, "system_identity"), "system_identity"); + auto pythonVersion = pythonString( + PyDict_GetItemString(value, "python_version"), "python_version"); + auto compiler = + pythonString(PyDict_GetItemString(value, "compiler"), "compiler"); + auto standardLibrary = pythonString( + PyDict_GetItemString(value, "standard_library"), "standard_library"); + auto outputRoot = + pythonString(PyDict_GetItemString(value, "output_root"), "output_root"); + if (!projectName || !projectIdentity || !systemName || !systemIdentity || + !pythonVersion || !compiler || !standardLibrary || !outputRoot) + return false; + + auto toolchain = + acir::codegen::identifyToolchain(*compiler, *standardLibrary, "default", +#ifdef __APPLE__ + "mach-o", +#else + "elf", +#endif + {"-std=c++20"}); + if (!toolchain) { + std::string message = llvm::toString(toolchain.takeError()); + PyErr_SetString(PyExc_ValueError, message.c_str()); + return false; + } + + auto &build = request.build; + build.project = {std::move(*projectName), std::move(*projectIdentity)}; + build.system = {std::move(*systemName), std::move(*systemIdentity)}; + build.frontend.pythonVersion = std::move(*pythonVersion); + if (!parseFileHashes(PyDict_GetItemString(value, "source_files"), + build.frontend.sourceFiles) || + !parseNamedFingerprints(PyDict_GetItemString(value, "helper_identities"), + "helper_identities", + build.frontend.helperIdentities) || + !parseStringList(PyDict_GetItemString(value, "instrumentation_layers"), + "instrumentation_layers", build.instrumentationLayers) || + !parseStringList(PyDict_GetItemString(value, "include_roots"), + "include_roots", build.includeRoots) || + !parseStringList(PyDict_GetItemString(value, "link_inputs"), + "link_inputs", build.linkInputs)) + return false; + build.frontend.acpy = { + "input/model.acpy.json", ArtifactKind::Acpy, + acir::codegen::computeFingerprint(build.frontend.acpyBytes)}; + build.frontend.canonicalAcir = { + "input/model.ac.mlir", ArtifactKind::Acir, + acir::codegen::computeFingerprint(build.frontend.canonicalAcirBytes)}; + build.toolchain = std::move(*toolchain); + llvm::StringRef llvmLinkerFlags(ACIR_NATIVE_LLVM_LINK_FLAGS); + while (!llvmLinkerFlags.empty()) { + auto [flag, remainder] = llvmLinkerFlags.split('|'); + if (!flag.empty()) + build.linkerFlags.push_back(flag.str()); + llvmLinkerFlags = remainder; + } + build.outputRoot = std::move(*outputRoot); + return true; +} + +bool parseOptions(PyObject *options, CompilerRequest &request) { + static constexpr std::array keys{ + "profile", + "binding_lock", + "binding_registry", + "custom_pipeline", + "dump_before", + "dump_after", + "dump_after_each", + "verify_after_each", + "frontend_acpy", + "frontend_acir", + "build", + }; + if (!hasOnlyKeys(options, keys, "native compiler options")) + return false; + + if (PyObject *value = PyDict_GetItemString(options, "profile")) { + auto profile = pythonString(value, "profile"); + if (!profile) + return false; + if (*profile == "fast") + request.profile = CompilerProfile::Fast; + else if (*profile == "validated") + request.profile = CompilerProfile::Validated; + else if (*profile == "custom") + request.profile = CompilerProfile::Custom; + else { + PyErr_SetString(PyExc_ValueError, + "profile must be fast, validated, or custom"); + return false; + } + } + if (PyObject *value = PyDict_GetItemString(options, "binding_lock")) { + auto bytes = pythonBytes(value, "binding_lock"); + if (!bytes) + return false; + request.bindingLockBytes = std::move(*bytes); + } + if (PyObject *value = PyDict_GetItemString(options, "binding_registry")) { + auto bytes = pythonBytes(value, "binding_registry"); + if (!bytes) + return false; + request.bindingRegistryBytes = std::move(*bytes); + } + if (PyObject *value = PyDict_GetItemString(options, "frontend_acpy")) { + auto bytes = pythonBytes(value, "frontend_acpy"); + if (!bytes) + return false; + request.build.frontend.acpyBytes = std::move(*bytes); + } + if (PyObject *value = PyDict_GetItemString(options, "frontend_acir")) { + auto bytes = pythonBytes(value, "frontend_acir"); + if (!bytes) + return false; + request.build.frontend.canonicalAcirBytes = std::move(*bytes); + } + if (PyObject *value = PyDict_GetItemString(options, "build")) + if (!parseBuildOptions(value, request)) + return false; + if (PyObject *value = PyDict_GetItemString(options, "custom_pipeline")) { + auto pipeline = pythonString(value, "custom_pipeline"); + if (!pipeline) + return false; + request.customPipeline = std::move(*pipeline); + } + if (PyObject *value = PyDict_GetItemString(options, "dump_before")) + if (!parseStringTuple(value, "dump_before", request.dumpBefore)) + return false; + if (PyObject *value = PyDict_GetItemString(options, "dump_after")) + if (!parseStringTuple(value, "dump_after", request.dumpAfter)) + return false; + auto parseBoolean = [&](const char *key, bool &target) { + PyObject *value = PyDict_GetItemString(options, key); + if (!value) + return true; + if (!PyBool_Check(value)) { + PyErr_Format(PyExc_TypeError, "%s must be a boolean", key); + return false; + } + target = value == Py_True; + return true; + }; + return parseBoolean("dump_after_each", request.dumpAfterEach) && + parseBoolean("verify_after_each", request.verifyAfterEach); +} + +std::optional parseRequest(PyObject *value) { + static constexpr std::array keys{"acir", "stop_after", + "emits", "options"}; + if (!hasOnlyKeys(value, keys, "native compiler request")) + return std::nullopt; + for (llvm::StringRef key : keys) + if (!PyDict_GetItemString(value, key.data())) { + PyErr_Format(PyExc_ValueError, "native compiler request is missing '%s'", + key.str().c_str()); + return std::nullopt; + } + + CompilerRequest request; + auto bytes = pythonBytes(PyDict_GetItemString(value, "acir"), "acir"); + if (!bytes) + return std::nullopt; + request.acirBytes = std::move(*bytes); + + PyObject *stop = PyDict_GetItemString(value, "stop_after"); + if (stop != Py_None) { + auto name = pythonString(stop, "stop_after"); + if (!name) + return std::nullopt; + request.stopAfter = parseStage(*name); + if (!request.stopAfter) { + PyErr_SetString(PyExc_ValueError, "unknown native compiler stop stage"); + return std::nullopt; + } + } + + PyObject *emits = PyDict_GetItemString(value, "emits"); + if (!PyTuple_Check(emits)) { + PyErr_SetString(PyExc_TypeError, "emits must be a tuple"); + return std::nullopt; + } + const Py_ssize_t emitCount = PyTuple_Size(emits); + if (emitCount < 0) + return std::nullopt; + for (Py_ssize_t index = 0; index < emitCount; ++index) { + auto name = pythonString(PyTuple_GetItem(emits, index), "emit name"); + if (!name) + return std::nullopt; + auto kind = parseArtifactKind(*name); + if (!kind) { + PyErr_SetString(PyExc_ValueError, "unknown native compiler artifact"); + return std::nullopt; + } + request.emits.push_back(*kind); + } + + if (!parseOptions(PyDict_GetItemString(value, "options"), request)) + return std::nullopt; + return request; +} + +OwnedPy jsonToPython(const llvm::json::Value &value) { + switch (value.kind()) { + case llvm::json::Value::Null: + return pyNone(); + case llvm::json::Value::Boolean: + return OwnedPy(PyBool_FromLong(*value.getAsBoolean())); + case llvm::json::Value::Number: + if (auto integer = value.getAsInteger()) + return OwnedPy(PyLong_FromLongLong(*integer)); + return OwnedPy(PyFloat_FromDouble(*value.getAsNumber())); + case llvm::json::Value::String: + return pyString(*value.getAsString()); + case llvm::json::Value::Array: { + const llvm::json::Array &array = *value.getAsArray(); + OwnedPy result(PyList_New(array.size())); + if (!result) + return {}; + for (size_t index = 0; index < array.size(); ++index) { + OwnedPy item = jsonToPython(array[index]); + if (!item) + return {}; + PyList_SetItem(result.get(), index, item.release()); + } + return result; + } + case llvm::json::Value::Object: { + OwnedPy result(PyDict_New()); + if (!result) + return {}; + for (const auto &entry : *value.getAsObject()) + if (!setItem(result.get(), entry.first.str().c_str(), + jsonToPython(entry.second))) + return {}; + return result; + } + } + return {}; +} + +OwnedPy +sourceToPython(const std::optional &source) { + if (!source) + return pyNone(); + OwnedPy result(PyDict_New()); + if (!result || !setItem(result.get(), "file", pyString(source->file)) || + !setItem(result.get(), "line", + OwnedPy(PyLong_FromUnsignedLongLong(source->line))) || + !setItem(result.get(), "column", + OwnedPy(PyLong_FromUnsignedLongLong(source->column)))) + return {}; + return result; +} + +OwnedPy optionalString(const std::optional &value) { + return value ? pyString(*value) : pyNone(); +} + +OwnedPy diagnosticToPython(const CompilerDiagnostic &diagnostic) { + OwnedPy result(PyDict_New()); + if (!result || !setItem(result.get(), "stage", pyString(diagnostic.stage)) || + !setItem(result.get(), "code", pyString(diagnostic.code)) || + !setItem(result.get(), "severity", pyString(diagnostic.severity)) || + !setItem(result.get(), "message", pyString(diagnostic.message)) || + !setItem(result.get(), "source", sourceToPython(diagnostic.source)) || + !setItem(result.get(), "object_path", + optionalString(diagnostic.objectPath)) || + !setItem(result.get(), "expected", jsonToPython(diagnostic.expected)) || + !setItem(result.get(), "actual", jsonToPython(diagnostic.actual))) + return {}; + + OwnedPy related(PyTuple_New(diagnostic.related.size())); + if (!related) + return {}; + for (size_t index = 0; index < diagnostic.related.size(); ++index) { + const auto &item = diagnostic.related[index]; + OwnedPy entry(PyDict_New()); + if (!entry || !setItem(entry.get(), "message", pyString(item.message)) || + !setItem(entry.get(), "source", sourceToPython(item.source)) || + !setItem(entry.get(), "object_path", optionalString(item.objectPath))) + return {}; + PyTuple_SetItem(related.get(), index, entry.release()); + } + if (!setItem(result.get(), "related", std::move(related))) + return {}; + + OwnedPy fixits(PyTuple_New(diagnostic.fixits.size())); + if (!fixits) + return {}; + for (size_t index = 0; index < diagnostic.fixits.size(); ++index) { + OwnedPy entry(PyDict_New()); + if (!entry || !setItem(entry.get(), "message", + pyString(diagnostic.fixits[index].message))) + return {}; + PyTuple_SetItem(fixits.get(), index, entry.release()); + } + if (!setItem(result.get(), "fixits", std::move(fixits))) + return {}; + return result; +} + +OwnedPy artifactToPython(const CompilerArtifact &artifact) { + OwnedPy result(PyDict_New()); + if (!result || + !setItem(result.get(), "path", pyString(artifact.logicalPath)) || + !setItem(result.get(), "kind", + pyString(artifactKindName(artifact.kind))) || + !setItem(result.get(), "data", pyBytes(artifact.bytes)) || + !setItem(result.get(), "sha256", pyString(artifact.sha256))) + return {}; + return result; +} + +OwnedPy resultToPython(const CompilerResult &result) { + OwnedPy output(PyDict_New()); + OwnedPy artifacts(PyTuple_New(result.artifacts.size())); + OwnedPy diagnostics(PyTuple_New(result.diagnostics.size())); + if (!output || !artifacts || !diagnostics) + return {}; + for (size_t index = 0; index < result.artifacts.size(); ++index) { + OwnedPy item = artifactToPython(result.artifacts[index]); + if (!item) + return {}; + PyTuple_SetItem(artifacts.get(), index, item.release()); + } + for (size_t index = 0; index < result.diagnostics.size(); ++index) { + OwnedPy item = diagnosticToPython(result.diagnostics[index]); + if (!item) + return {}; + PyTuple_SetItem(diagnostics.get(), index, item.release()); + } + if (!setItem(output.get(), "artifacts", std::move(artifacts)) || + !setItem(output.get(), "diagnostics", std::move(diagnostics))) + return {}; + if (result.build) { + if (!setItem(output.get(), "build_directory", + pyString(result.build->buildDirectory)) || + !setItem(output.get(), "executable", + pyString(result.build->executable)) || + !setItem(output.get(), "build_fingerprint", + pyString(result.build->buildFingerprint)) || + !setItem(output.get(), "cache_hit", + OwnedPy(PyBool_FromLong(result.build->cacheHit)))) + return {}; + } else if (!setItem(output.get(), "build_directory", pyNone()) || + !setItem(output.get(), "executable", pyNone()) || + !setItem(output.get(), "build_fingerprint", pyNone()) || + !setItem(output.get(), "cache_hit", pyNone())) { + return {}; + } + return output; +} + +CompilerResult failedResult(llvm::Error error) { + CompilerResult result; + llvm::handleAllErrors( + std::move(error), + [&](const acir::compiler::CompilerError &failure) { + result.diagnostics = failure.diagnostics(); + }, + [&](const llvm::ErrorInfoBase &failure) { + result.diagnostics.push_back({.stage = "native", + .code = "ACLOWER-NATIVE-001", + .severity = "error", + .message = failure.message()}); + }); + return result; +} + +PyObject *runCompiler(PyObject *, PyObject *arguments) { + PyObject *requestObject = nullptr; + if (!PyArg_ParseTuple(arguments, "O:run_compiler", &requestObject)) + return nullptr; + auto request = parseRequest(requestObject); + if (!request) + return nullptr; + auto compiled = acir::compiler::runCompiler(*request); + OwnedPy result = compiled + ? resultToPython(*compiled) + : resultToPython(failedResult(compiled.takeError())); + return result.release(); +} + +PyObject *capabilities(PyObject *, PyObject *) { + OwnedPy result(PyDict_New()); + if (!result || + !setItem(result.get(), "compiler_build_id", + pyString("agentic-circuit-0.1.0+llvm-22.1.8")) || + !setItem(result.get(), "runtime_build_id", + pyString("gfsim-0.1.0+cxx20")) || + !setItem(result.get(), "items", OwnedPy(PyTuple_New(0)))) + return nullptr; + return result.release(); +} + +PyMethodDef methods[] = { + {"run_compiler", runCompiler, METH_VARARGS, + "Run the closed native compiler request."}, + {"capabilities", capabilities, METH_NOARGS, + "Return native compiler and runtime capabilities."}, + {nullptr, nullptr, 0, nullptr}, +}; + +PyModuleDef module = { + PyModuleDef_HEAD_INIT, + "_native", + "Private Agentic Circuit native compiler bridge.", + -1, + methods, +}; + +} // namespace + +PyMODINIT_FUNC PyInit__native() { return PyModule_Create(&module); } diff --git a/components/agentic-circuit/python/resource-package-init.py b/components/agentic-circuit/python/resource-package-init.py new file mode 100644 index 00000000..92a8f1cd --- /dev/null +++ b/components/agentic-circuit/python/resource-package-init.py @@ -0,0 +1 @@ +"""Agentic Circuit package resources.""" diff --git a/components/agentic-circuit/references/README.md b/components/agentic-circuit/references/README.md new file mode 100644 index 00000000..63490978 --- /dev/null +++ b/components/agentic-circuit/references/README.md @@ -0,0 +1,8 @@ +# External references + +This directory contains pinned upstream source snapshots used as executable +comparison oracles. References are not installed as Agentic Circuit product +code unless a build option explicitly enables them. + +Every reference must record its repository, commit, selected subtree, import +mode, checksums, and license status. diff --git a/components/agentic-circuit/references/davincioo-gfsim/CMakeLists.txt b/components/agentic-circuit/references/davincioo-gfsim/CMakeLists.txt new file mode 100644 index 00000000..33aacafa --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 3.20) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(DavinciOOGfsimReference LANGUAGES CXX) + include(CTest) +endif() + +set(DAVINCIOO_GFSIM_REFERENCE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/upstream") + +add_executable(davincioo-gfsim-reference + upstream/src/backend/cube.cpp + upstream/src/backend/dispatch.cpp + upstream/src/backend/engine.cpp + upstream/src/backend/issue_queue.cpp + upstream/src/backend/rename.cpp + upstream/src/backend/ready_table.cpp + upstream/src/backend/rob.cpp + upstream/src/backend/scalar.cpp + upstream/src/backend/tma.cpp + upstream/src/backend/vector.cpp + upstream/src/pto_inst.cpp + upstream/src/frontend/trace.cpp + upstream/src/frontend/trace_source_module.cpp + upstream/src/model_top/config.cpp + upstream/src/model_top/cli.cpp + upstream/src/model_top/core.cpp + upstream/src/model_top/core_system.cpp + upstream/src/model_top/kanata_pipeview.cpp + upstream/src/model_top/perfetto_trace.cpp + upstream/src/model_top/main.cpp +) + +target_compile_features(davincioo-gfsim-reference PRIVATE cxx_std_20) +set_target_properties(davincioo-gfsim-reference PROPERTIES CXX_EXTENSIONS OFF) +target_include_directories(davincioo-gfsim-reference PRIVATE + "${DAVINCIOO_GFSIM_REFERENCE_ROOT}/include" + "${DAVINCIOO_GFSIM_REFERENCE_ROOT}/src" +) + +if(BUILD_TESTING) + add_test( + NAME DavinciOOGfsimReferenceHelp + COMMAND $ --help + ) + add_test( + NAME DavinciOOGfsimReferenceSmoke + COMMAND $ + simulate + --trace + "${DAVINCIOO_GFSIM_REFERENCE_ROOT}/tests/fixtures/traces/examples_intermediate_softmax.pto.trace" + ) + set_tests_properties(DavinciOOGfsimReferenceHelp PROPERTIES + LABELS "reference;davincioo" + PASS_REGULAR_EXPRESSION "gfsim simulate" + TIMEOUT 10 + ) + set_tests_properties(DavinciOOGfsimReferenceSmoke PROPERTIES + LABELS "reference;davincioo" + PASS_REGULAR_EXPRESSION "simulated_cycles: 453" + TIMEOUT 10 + ) +endif() diff --git a/components/agentic-circuit/references/davincioo-gfsim/GENERATION_CONTRACT.md b/components/agentic-circuit/references/davincioo-gfsim/GENERATION_CONTRACT.md new file mode 100644 index 00000000..19125be3 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/GENERATION_CONTRACT.md @@ -0,0 +1,64 @@ +# ACIR-to-gfsim Reference Generation Contract + +## Purpose + +The DavinciOO snapshot is an executable target shape for future ACIR lowering. +It is not the implementation template itself. The generator should reproduce +the model's static topology and observable scheduling contracts using Agentic +Circuit's supported gfsim runtime rather than copying source text from this +directory. + +## Generation boundary + +| Reference area | Future ownership | Required ACIR information | +| --- | --- | --- | +| `model_top/core.*` | Generated topology and wiring | module instances, arrays, queue endpoints, capacities, latencies, stable paths | +| `model_top/core_system.*` | Generated system wrapper | root module, clock/time domain, reset, termination and observation policy | +| `frontend/trace_source_module.*` | Generated component instantiation around a reusable provider | one typed trace owner, output protocol and backpressure | +| `backend/dispatch.*`, `rename.*` | Reusable component providers selected by static bindings | typed ports, resource parameters, dependency/rename policy identity | +| `backend/issue_queue.*`, engine classes and `rob.*` | Reusable component providers selected by static bindings | queue/resource parameters, arbitration order, latency policy, activation edges | +| `include/davincioo/model/pto_inst.hpp` | Typed packet/value schema or provider-local ABI | opcode, operands, engine class, identity and committed timestamps | +| `model_top/cli.*`, trace parsing and trace exporters | Harness/tooling, not model topology | run manifest, canonical trace input and observation output contracts | +| `include/davincioo/model/framework.hpp` | Must not be generated | replaced by the repository `gfsim` runtime ABI | + +## Invariants to preserve + +Generated output must preserve these semantic properties rather than the +reference model's filenames or class spellings: + +1. The object graph, queue ownership, capacities, latencies and engine counts + are fully static after construction. +2. Trace records enter through exactly one typed trace-source owner. +3. Rename and readiness are committed before dependent issue becomes visible. +4. Each issue queue uses deterministic oldest-ready arbitration. +5. Scalar, vector, cube and TMA engines can execute independent instructions + concurrently while observing finite capacity. +6. Completion makes destinations ready; retirement remains ordered by the + architectural sequence contract. +7. Work, arbitration and Xfer state remain separated, and observations describe + committed state only. +8. Generated model identity, hierarchy, results, statistics and event ordering + are independent of pointers, allocation order and host scheduling. + +## Comparison strategy + +The first generator milestone lowers +[`examples/pipelines/davincioo_queue_model.py`](../../examples/pipelines/davincioo_queue_model.py) +through Queue/Var ACIR into normal Agentic Circuit generated C++. Its checked-in +projection consumes this snapshot's 15-record softmax trace and compares opcode +counts, out-of-order completion, in-order retirement, architectural values and +the 453-cycle completion contract. The same frozen ACIR also lowers through +PYC C++ and Verilog, where cycle-level equivalence is checked independently. + +The generated model now uses explicit `ac.dependency` predecessor tracking, +per-resource reservation, and execution countdown; dependency-wait time is not +folded into token latency. +The projection retains a fixed 5-cycle ingress and 4-cycle drain compensation +for the different Queue boundaries. It therefore proves contract-equivalent +committed behavior for this bounded trace, not internal per-unit issue/ROB +occupancy equivalence. Later milestones may add memory and credit blocks without +changing the trace or output contract. Byte-identical C++ is not +required; contract-equivalent committed output is the acceptance criterion. + +The imported source is frozen by [`UPSTREAM_FILES.sha256`](UPSTREAM_FILES.sha256), +so changes in the comparison oracle are explicit and reviewable. diff --git a/components/agentic-circuit/references/davincioo-gfsim/README.md b/components/agentic-circuit/references/davincioo-gfsim/README.md new file mode 100644 index 00000000..4b0ad09d --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/README.md @@ -0,0 +1,65 @@ +# DavinciOO gfsim Reference Model + +This directory preserves the DavinciOO out-of-order NPU C++ model as a fixed +reference for future ACIR-to-gfsim code generation. It is deliberately outside +the installed Agentic Circuit runtime and public schema catalog. + +## Snapshot layout + +| Path | Purpose | +| --- | --- | +| [`upstream/`](upstream/) | Selected upstream `model/` files, kept byte-for-byte unchanged | +| [`SOURCE.json`](SOURCE.json) | Repository, commit, subtree, selection, and license status | +| [`UPSTREAM_FILES.sha256`](UPSTREAM_FILES.sha256) | Exact imported-file inventory and SHA-256 lock | +| [`GENERATION_CONTRACT.md`](GENERATION_CONTRACT.md) | Boundary between IR-generated topology and reusable model policies | +| [`CMakeLists.txt`](CMakeLists.txt) | Non-installed wrapper that builds the snapshot without target-name collision | + +The selected snapshot includes one 15-record upstream softmax trace. Its smoke +test exercises decode, rename, issue, all required engines and ROB retirement, +and deterministically completes in 453 model cycles. + +The snapshot comes from DavinciOO commit +`a542b9cf705096288c615575be222b974b570a18`. Do not edit files under +`upstream/` in place. Refresh the snapshot as one reviewed import and update +both provenance files together. + +## Build + +The normal Agentic Circuit development build includes reference models because +`BUILD_TESTING` is enabled. It can be disabled explicitly: + +```sh +cmake --preset dev-llvm22 -DACIR_BUILD_REFERENCE_MODELS=OFF +``` + +The reference also builds independently and has no LLVM, MLIR, Python, or PTO +runtime dependency: + +```sh +cmake -S references/davincioo-gfsim \ + -B build/davincioo-gfsim-reference -G Ninja +cmake --build build/davincioo-gfsim-reference +build/davincioo-gfsim-reference/davincioo-gfsim-reference --help +``` + +The wrapper changes only the CMake target name. All selected C++, topology +documentation and the smoke trace remain byte-identical to the locked source +snapshot. The upstream model README is intentionally replaced by this snapshot +README because it documents scripts, tests and toolchain submodules that are +outside the reference boundary. + +Because `upstream/` is a verbatim external snapshot, repository formatting and +static-analysis gates exclude it. The wrapper, provenance, tests and generation +contract remain normal owned Agentic Circuit sources and are covered by the +repository gates. A path-specific Git whitespace attribute preserves one +upstream blank-line trailing-space occurrence without weakening checks for +owned files. + +## Licensing boundary + +The source DavinciOO repository currently contains a repository-license +placeholder stating that it does not publish a final blanket license grant. +Accordingly, this snapshot is recorded as `unresolved` in `SOURCE.json` and is +not represented as BSD-3-Clause code, installed, packaged, or linked into public +Agentic Circuit libraries. Resolve upstream licensing before distributing the +snapshot as part of a release artifact. diff --git a/components/agentic-circuit/references/davincioo-gfsim/SOURCE.json b/components/agentic-circuit/references/davincioo-gfsim/SOURCE.json new file mode 100644 index 00000000..f7c90970 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/SOURCE.json @@ -0,0 +1,26 @@ +{ + "schema": "agentic-circuit-reference-source@0.1", + "repository": "https://github.com/hengliao1972/DavinciOO.git", + "commit": "a542b9cf705096288c615575be222b974b570a18", + "subtree": "model", + "import_mode": "verbatim-selected-snapshot", + "included": [ + "CMakeLists.txt", + "docs/", + "include/", + "src/backend/", + "src/frontend/", + "src/model_top/", + "src/pto_inst.cpp", + "tests/fixtures/traces/examples_intermediate_softmax.pto.trace" + ], + "excluded": [ + "README.md", + "scripts/", + "src/davinci_ooo_model.py", + "tests/ except tests/fixtures/traces/examples_intermediate_softmax.pto.trace" + ], + "license_file": "LICENSE", + "license_status": "unresolved", + "license_note": "The upstream repository states that it does not yet publish a final repository-wide license grant." +} diff --git a/components/agentic-circuit/references/davincioo-gfsim/UPSTREAM_FILES.sha256 b/components/agentic-circuit/references/davincioo-gfsim/UPSTREAM_FILES.sha256 new file mode 100644 index 00000000..b8b0a6d3 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/UPSTREAM_FILES.sha256 @@ -0,0 +1,53 @@ +57290f369741daca4df89185b056d112f5527aed58d469d64e1c4a5120354198 upstream/CMakeLists.txt +73f0a9833ada284f4d9e8aa7392a1b4c35aa02d95695768a60acbefb23d31ff0 upstream/docs/diagrams/core_topology.dot +608ae0bca71ecddc699d4c05f74064382eb94be404217350221c328767b9bafc upstream/docs/diagrams/core_topology.svg +03d6e854e3ae5ff8352e9fbd729b16c6040d66c364db28ec07ebf1d8bb7647a7 upstream/include/davincioo/model/config.hpp +113914be9155b66573a68e4d2041cf61acef56a36fd65fa551c1e7be7b989941 upstream/include/davincioo/model/framework.hpp +151d01dfc71c4561dd7092f6e7f1b450cc7aab6f3d8c932552d26f7026d367b1 upstream/include/davincioo/model/pto_inst.hpp +b2d42d9fdcc06441da9a8fd0e9e549dc2d1f0f63efa1960ac349852178d98c98 upstream/include/davincioo/model/rob.hpp +3481840e8c246fcbaeaba1215de4c93be91bb840a61ab2c15a80bd5f48055163 upstream/src/backend/DISPATCH.md +e7ae1aefccc695b1ff752005a718ed9e327a9c46a6ecd191435d661fbc3b54c4 upstream/src/backend/ENGINE.md +f2a55dee6607c9383049a7c7f264e9a39b599359897506dab9ffc01c56f9e6cb upstream/src/backend/ISSUE_QUEUE.md +2a91b75b4525168b216b38e2f1a064f350adcf18cdccfd9d7cb451133872cd2e upstream/src/backend/READY_TABLE.md +fbe1222972b8cc346369da15f7edf030310ee763e1e3d832286d2d20e3873ee8 upstream/src/backend/RENAME.md +f2172e778a1596dda9aba606ba0786d8d3686fcec639a1146d1b740f6031ca42 upstream/src/backend/ROB.md +cda6dfba5965b8790829b4137a61c85db84c57ec8ae6f86be8b6980bb5c80663 upstream/src/backend/cube.cpp +892a813791da081550ea0dbfcf941db7bc10c8a42570bafb03308526483a6ba7 upstream/src/backend/cube.hpp +5e8a43c1494d51b99a185c37d4fbced77b1bb1270dcf14706b019c60dc99cc29 upstream/src/backend/dispatch.cpp +c14dbe83df74e9ead879e1e9c7b027b58d068c3ad71f48312b3a6b641dc0deb0 upstream/src/backend/dispatch.hpp +19a0642ffde14c11f1329d51e75bf6c4b593d1beb9948da97597137ba03f63c3 upstream/src/backend/engine.cpp +3c9c9476d729902049c9f6b58aa41a7cbec13e38c447c30dad9b93bbda4859a1 upstream/src/backend/engine.hpp +f39d1953cdb3cd751f2ef3c06713c222773ed9df9f992c3fcfb60cf0b8ad4cc4 upstream/src/backend/issue_queue.cpp +9fb264193ea1be03e477c2911b791edb4aa9969824b92a16bdfa7390b65a921e upstream/src/backend/issue_queue.hpp +26f926449297190db2d7177e40c014fc23441350599c767ca019316e08a6804f upstream/src/backend/ready_table.cpp +6a147c2b334b63ac66b207b38249fb35b6c34f9aaa4e3d77d5f456d0c44bf4af upstream/src/backend/ready_table.hpp +c765a8aef9b144cba590b755b4dc5bb4d3c8e7900ee090bf084628a2c18c0241 upstream/src/backend/rename.cpp +7404e26513c167fea36f1f7c7d6ce072dcd33f4e1b5fce26d4f01dbe5afacc50 upstream/src/backend/rename.hpp +66dd21f2bb115ffe1acb85a444f613cdc62c88fef880e953ffc5102a10cb79f0 upstream/src/backend/rob.cpp +87a22c925b6f3cd4391c077cc99c972f27f1823afb906dd5407914865707d8d6 upstream/src/backend/rob.hpp +1bcd875c862f68deec7d02c180ad8ee40df4e717e1732dd0c682d418c12251a3 upstream/src/backend/scalar.cpp +9a715cf093b36df3dfda92f321e9690d7fbd22d765293b00724840b4daa31d52 upstream/src/backend/scalar.hpp +202e2e53d2958e79ffdbf5a1dee2ba7fd29123eac057df723e53838a6e3b69f3 upstream/src/backend/tma.cpp +55ac1f86811c092ffda67390f4331b7db09e6b76ee8ae1cc0461d530fe1a676a upstream/src/backend/tma.hpp +b9feb0c8bc533576bcc1dff2b19466b024a4f9b6ff30cf4b30978ba61e69994f upstream/src/backend/vector.cpp +76824120404dd922b7b8883e577b8209ba5b48ce6f79dada0fac3505afd402ce upstream/src/backend/vector.hpp +279ff9cb41d17afbdcd56b55fdf0f9c68e0818d88f8ca827b136452e2a1d2838 upstream/src/frontend/trace.cpp +c411b1a815e58fa68ae327b1ab1373bcccd59c74f616fac6f298cb11bc33db69 upstream/src/frontend/trace.hpp +eaddef617bacf925b5a0b817ad0fb09a2d584ff9f9902a72b8e20d47ef16a7d9 upstream/src/frontend/trace_source_module.cpp +3f6b346b7981936c3a90985d7b9021ed719ebfb85d8bb671e23d7e5a9970d975 upstream/src/frontend/trace_source_module.hpp +3293f6de691aec9bcd01943b0c050f327255bc2415a5a2487e37d552f40109df upstream/src/model_top/CORE.md +f57845496871829b467547fb9d4d03428ab6c5c81f129a7e234fa255e8f2c7a5 upstream/src/model_top/CORE_SYSTEM.md +a18bba6f94170a707c229019bcc7cc69e37f20bf626b87674145f0eab1194fe0 upstream/src/model_top/cli.cpp +7b6579c6145b00f6490ddfe5b95e8a8258dfd4f0c5edf527a45105124dafbd74 upstream/src/model_top/cli.hpp +f035f61b908cd1b70735dbfe8f4e2c8b4dbda259f615c397c7dfcbc999b2c190 upstream/src/model_top/config.cpp +1329fa57d45af3222fb94ffaf126b142fa44246e05c214a24e9dda7b3280a765 upstream/src/model_top/core.cpp +fa465471424fb604d5f712a47d25ed0e8362d7683575fbb114a88f5f91b7832e upstream/src/model_top/core.hpp +793bf7fa063be7490cbc4f5772e3c0beab7f75230eebfcec21c7e719ed62801f upstream/src/model_top/core_system.cpp +1be29625a4ae16e5c7dba2084ef27063271c804effc62ccc81fa7ab3b5dfc352 upstream/src/model_top/core_system.hpp +6a090875cc2dd3781d4726e8ec36725707d36eed25bc8f232d1a63e19496f6df upstream/src/model_top/kanata_pipeview.cpp +5353d52db30cf9fcdecae0aacdec6ae8ae952c65087a116c0481a3febd2b9ec1 upstream/src/model_top/kanata_pipeview.hpp +c9d5b3c805eb34ee35f46e9520be79eb4b29b4c06a2bd3992897d1394c28ddbe upstream/src/model_top/main.cpp +fa5592a1a3488d1f8bd937ba4223680e05fd2d6835bb8b4a02371a0924bab557 upstream/src/model_top/perfetto_trace.cpp +2b2996dbdda8cfe80e62135962579cef55f7554d7c309b98c00e8cb70c44b6e5 upstream/src/model_top/perfetto_trace.hpp +39a0b39adffaa80c85ef600757c19eaf2e1dcd5e0ef8917e64511d5b53a9c767 upstream/src/pto_inst.cpp +4125323d00bd259abe992c1b207526ac7dd834a3fc42a1a03d7d1c894b350016 upstream/tests/fixtures/traces/examples_intermediate_softmax.pto.trace diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/CMakeLists.txt b/components/agentic-circuit/references/davincioo-gfsim/upstream/CMakeLists.txt new file mode 100644 index 00000000..8329616a --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.20) + +project(gfsim LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(gfsim + src/backend/cube.cpp + src/backend/dispatch.cpp + src/backend/engine.cpp + src/backend/issue_queue.cpp + src/backend/rename.cpp + src/backend/ready_table.cpp + src/backend/rob.cpp + src/backend/scalar.cpp + src/backend/tma.cpp + src/backend/vector.cpp + src/pto_inst.cpp + src/frontend/trace.cpp + src/frontend/trace_source_module.cpp + src/model_top/config.cpp + src/model_top/cli.cpp + src/model_top/core.cpp + src/model_top/core_system.cpp + src/model_top/kanata_pipeview.cpp + src/model_top/perfetto_trace.cpp + src/model_top/main.cpp +) + +target_compile_features(gfsim PRIVATE cxx_std_20) +target_include_directories(gfsim PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/src +) diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/docs/diagrams/core_topology.dot b/components/agentic-circuit/references/davincioo-gfsim/upstream/docs/diagrams/core_topology.dot new file mode 100644 index 00000000..9cb7f641 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/docs/diagrams/core_topology.dot @@ -0,0 +1,77 @@ +digraph gfsim_core_topology { + rankdir=LR; + graph [fontname="Helvetica", fontsize=11, labelloc="t", label="DavinciOO gfsim Core Topology"]; + node [fontname="Helvetica", fontsize=10]; + edge [fontname="Helvetica", fontsize=9]; + + node [shape=box, style="rounded,filled", fillcolor="#f7f7f7"]; + frontend [label="TraceSource"]; + rob [label="ROB"]; + rename [label="Rename"]; + dispatch [label="Dispatch"]; + readytable [label="ReadyTable"]; + scalar_iq [label="IssueQueue[SCALAR]"]; + vec_iq [label="IssueQueue[VEC]"]; + cube_iq [label="IssueQueue[CUBE]"]; + tma_iq [label="IssueQueue[TMA]"]; + scalar_eng [label="Engine[SCALAR]"]; + vec_eng [label="Engine[VEC]"]; + cube_eng [label="Engine[CUBE]"]; + tma_eng [label="Engine[TMA]"]; + + node [shape=ellipse, style="filled", fillcolor="#eef5ff"]; + rob_input_q [label="rob_input_q_"]; + rob_to_rename_q [label="rob_to_rename_q_"]; + rob_to_rename_retire_q [label="rob_to_rename_retire_q_"]; + rename_to_dispatch_q [label="rename_to_dispatch_q_"]; + dispatch_to_scalar_q [label="dispatch_to_issue_qs_[SCALAR]"]; + dispatch_to_vec_q [label="dispatch_to_issue_qs_[VEC]"]; + dispatch_to_cube_q [label="dispatch_to_issue_qs_[CUBE]"]; + dispatch_to_tma_q [label="dispatch_to_issue_qs_[TMA]"]; + ready_to_scalar_q [label="ready_to_issue_qs_[SCALAR]"]; + ready_to_vec_q [label="ready_to_issue_qs_[VEC]"]; + ready_to_cube_q [label="ready_to_issue_qs_[CUBE]"]; + ready_to_tma_q [label="ready_to_issue_qs_[TMA]"]; + wake_scalar_q [label="issue_wakeup_qs_[SCALAR]"]; + wake_vec_q [label="issue_wakeup_qs_[VEC]"]; + wake_cube_q [label="issue_wakeup_qs_[CUBE]"]; + wake_tma_q [label="issue_wakeup_qs_[TMA]"]; + issue_to_scalar_q [label="issue_to_engine_qs_[SCALAR]"]; + issue_to_vec_q [label="issue_to_engine_qs_[VEC]"]; + issue_to_cube_q [label="issue_to_engine_qs_[CUBE]"]; + issue_to_tma_q [label="issue_to_engine_qs_[TMA]"]; + wakeup_q [label="wakeup_q_"]; + rob_done_q [label="rob_done_q_"]; + + frontend -> rob_input_q -> rob; + rob -> rob_to_rename_q -> rename; + rob -> rob_to_rename_retire_q -> rename; + rename -> rename_to_dispatch_q -> dispatch; + + dispatch -> dispatch_to_scalar_q -> readytable; + dispatch -> dispatch_to_vec_q -> readytable; + dispatch -> dispatch_to_cube_q -> readytable; + dispatch -> dispatch_to_tma_q -> readytable; + + readytable -> ready_to_scalar_q -> scalar_iq; + readytable -> ready_to_vec_q -> vec_iq; + readytable -> ready_to_cube_q -> cube_iq; + readytable -> ready_to_tma_q -> tma_iq; + + readytable -> wake_scalar_q -> scalar_iq; + readytable -> wake_vec_q -> vec_iq; + readytable -> wake_cube_q -> cube_iq; + readytable -> wake_tma_q -> tma_iq; + + scalar_iq -> issue_to_scalar_q -> scalar_eng; + vec_iq -> issue_to_vec_q -> vec_eng; + cube_iq -> issue_to_cube_q -> cube_eng; + tma_iq -> issue_to_tma_q -> tma_eng; + + scalar_eng -> wakeup_q -> readytable; + vec_eng -> wakeup_q; + cube_eng -> wakeup_q; + tma_eng -> wakeup_q; + + readytable -> rob_done_q -> rob; +} diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/docs/diagrams/core_topology.svg b/components/agentic-circuit/references/davincioo-gfsim/upstream/docs/diagrams/core_topology.svg new file mode 100644 index 00000000..e2ea01b0 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/docs/diagrams/core_topology.svg @@ -0,0 +1,506 @@ + + + + + + +gfsim_core_topology + +DavinciOO gfsim Core Topology + + +frontend + +TraceSource + + + +rob_input_q + +rob_input_q_ + + + +frontend->rob_input_q + + + + + +rob + +ROB + + + +rob_to_rename_q + +rob_to_rename_q_ + + + +rob->rob_to_rename_q + + + + + +rob_to_rename_retire_q + +rob_to_rename_retire_q_ + + + +rob->rob_to_rename_retire_q + + + + + +rename + +Rename + + + +rename_to_dispatch_q + +rename_to_dispatch_q_ + + + +rename->rename_to_dispatch_q + + + + + +dispatch + +Dispatch + + + +dispatch_to_scalar_q + +dispatch_to_issue_qs_[SCALAR] + + + +dispatch->dispatch_to_scalar_q + + + + + +dispatch_to_vec_q + +dispatch_to_issue_qs_[VEC] + + + +dispatch->dispatch_to_vec_q + + + + + +dispatch_to_cube_q + +dispatch_to_issue_qs_[CUBE] + + + +dispatch->dispatch_to_cube_q + + + + + +dispatch_to_tma_q + +dispatch_to_issue_qs_[TMA] + + + +dispatch->dispatch_to_tma_q + + + + + +readytable + +ReadyTable + + + +ready_to_scalar_q + +ready_to_issue_qs_[SCALAR] + + + +readytable->ready_to_scalar_q + + + + + +ready_to_vec_q + +ready_to_issue_qs_[VEC] + + + +readytable->ready_to_vec_q + + + + + +ready_to_cube_q + +ready_to_issue_qs_[CUBE] + + + +readytable->ready_to_cube_q + + + + + +ready_to_tma_q + +ready_to_issue_qs_[TMA] + + + +readytable->ready_to_tma_q + + + + + +wake_scalar_q + +issue_wakeup_qs_[SCALAR] + + + +readytable->wake_scalar_q + + + + + +wake_vec_q + +issue_wakeup_qs_[VEC] + + + +readytable->wake_vec_q + + + + + +wake_cube_q + +issue_wakeup_qs_[CUBE] + + + +readytable->wake_cube_q + + + + + +wake_tma_q + +issue_wakeup_qs_[TMA] + + + +readytable->wake_tma_q + + + + + +rob_done_q + +rob_done_q_ + + + +readytable->rob_done_q + + + + + +scalar_iq + +IssueQueue[SCALAR] + + + +issue_to_scalar_q + +issue_to_engine_qs_[SCALAR] + + + +scalar_iq->issue_to_scalar_q + + + + + +vec_iq + +IssueQueue[VEC] + + + +issue_to_vec_q + +issue_to_engine_qs_[VEC] + + + +vec_iq->issue_to_vec_q + + + + + +cube_iq + +IssueQueue[CUBE] + + + +issue_to_cube_q + +issue_to_engine_qs_[CUBE] + + + +cube_iq->issue_to_cube_q + + + + + +tma_iq + +IssueQueue[TMA] + + + +issue_to_tma_q + +issue_to_engine_qs_[TMA] + + + +tma_iq->issue_to_tma_q + + + + + +scalar_eng + +Engine[SCALAR] + + + +wakeup_q + +wakeup_q_ + + + +scalar_eng->wakeup_q + + + + + +vec_eng + +Engine[VEC] + + + +vec_eng->wakeup_q + + + + + +cube_eng + +Engine[CUBE] + + + +cube_eng->wakeup_q + + + + + +tma_eng + +Engine[TMA] + + + +tma_eng->wakeup_q + + + + + +rob_input_q->rob + + + + + +rob_to_rename_q->rename + + + + + +rob_to_rename_retire_q->rename + + + + + +rename_to_dispatch_q->dispatch + + + + + +dispatch_to_scalar_q->readytable + + + + + +dispatch_to_vec_q->readytable + + + + + +dispatch_to_cube_q->readytable + + + + + +dispatch_to_tma_q->readytable + + + + + +ready_to_scalar_q->scalar_iq + + + + + +ready_to_vec_q->vec_iq + + + + + +ready_to_cube_q->cube_iq + + + + + +ready_to_tma_q->tma_iq + + + + + +wake_scalar_q->scalar_iq + + + + + +wake_vec_q->vec_iq + + + + + +wake_cube_q->cube_iq + + + + + +wake_tma_q->tma_iq + + + + + +issue_to_scalar_q->scalar_eng + + + + + +issue_to_vec_q->vec_eng + + + + + +issue_to_cube_q->cube_eng + + + + + +issue_to_tma_q->tma_eng + + + + + +wakeup_q->readytable + + + + + +rob_done_q->rob + + + + + diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/config.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/config.hpp new file mode 100644 index 00000000..8b570988 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/config.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include + +namespace davincioo { + +struct ROBConfig { + std::size_t entries = 64; +}; + +struct RenameConfig { + std::size_t tile_tags = 4096; +}; + +struct IssueQueueConfig { + std::size_t entries = 8; +}; + +struct ScalarCostConfig { + std::size_t default_latency = 1; + std::size_t tassign_latency = 1; + std::size_t unknown_latency = 1; +}; + +struct VectorCostConfig { + std::size_t fast_bandwidth_bytes_per_cycle = 512; + std::size_t slow_bandwidth_bytes_per_cycle = 256; + std::size_t elementwise_compute_cycles = 4; + std::size_t reduction_compute_cycles = 8; + std::size_t reduction_merge_cycles = 3; + std::size_t slow_compute_cycles = 12; + std::size_t move_compute_cycles = 4; + std::size_t unknown_latency = 12; +}; + +struct CubeCostConfig { + std::size_t input_bandwidth_bytes_per_cycle = 512; + std::size_t macs_per_cycle_fp32 = 4096; + std::size_t macs_per_cycle_fp16 = 4096; + std::size_t macs_per_cycle_bf16 = 4096; + std::size_t macs_per_cycle_fp8 = 8192; + std::size_t macs_per_cycle_int8 = 8192; + std::size_t macs_per_cycle_fp4 = 32768; + std::size_t accumulate_extra_cycles = 0; + bool overlap_mode = false; + std::size_t unknown_latency = 72; +}; + +struct TmaCostConfig { + std::size_t bandwidth_bytes_per_cycle = 512; + std::size_t load_overhead_cycles = 0; + std::size_t store_overhead_cycles = 0; + std::size_t move_overhead_cycles = 0; + std::size_t unknown_latency = 2; +}; + +struct EngineConfig { + std::size_t count = 1; +}; + +struct FrontendWidthConfig { + std::size_t fetch_width = 1; // TraceSourceModule -> rob_input_q + std::size_t rob_alloc_width = 1; // ROB::Allocate per cycle + std::size_t rob_issue_width = 1; // ROB -> rename per cycle + std::size_t rename_width = 1; // Rename body per cycle + std::size_t dispatch_width = 1; // Dispatch routing per cycle +}; + +struct CoreConfig { + ROBConfig rob; + RenameConfig rename; + IssueQueueConfig issue_queue; + FrontendWidthConfig frontend_width; + EngineConfig scalar; + EngineConfig vec; + EngineConfig cube; + EngineConfig tma; + ScalarCostConfig scalar_cost; + VectorCostConfig vec_cost; + CubeCostConfig cube_cost; + TmaCostConfig tma_cost; +}; + +CoreConfig ParseCoreTomlConfig(const std::filesystem::path& path); + +} // namespace davincioo diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/framework.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/framework.hpp new file mode 100644 index 00000000..c8d60c29 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/framework.hpp @@ -0,0 +1,429 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace davincioo { + +#define GFSIM_ASSERT(condition) \ + do { \ + if (!(condition)) { \ + throw std::runtime_error("gfsim assertion failed: " #condition); \ + } \ + } while (false) + +class SimObject { +public: + explicit SimObject(std::string name) : name_(std::move(name)) {} + virtual ~SimObject() = default; + + virtual void Build() {} + virtual void Reset() {} + virtual void Report() {} + virtual void Work() {} + virtual void Xfer() {} + + const std::string& Name() const noexcept { return name_; } + void SetCurrentCycle(std::uint64_t cycle) noexcept { current_cycle_ = cycle; } + std::uint64_t CurrentCycle() const noexcept { return current_cycle_; } + void MarkProgress(std::size_t amount = 1) noexcept { progress_count_ += amount; } + std::size_t ConsumeProgress() noexcept { + const std::size_t amount = progress_count_; + progress_count_ = 0; + return amount; + } + +private: + std::string name_; + std::uint64_t current_cycle_ = 0; + std::size_t progress_count_ = 0; +}; + +template +class SimQueue : public SimObject { +public: + using value_type = T; + using size_type = std::size_t; + + explicit SimQueue(size_type max_size = 1024, std::uint32_t latency = 1, std::string name = "sim_queue") + : SimObject(std::move(name)), max_size_(max_size), latency_(latency) { + GFSIM_ASSERT(max_size_ > 0); + } + + void Build() override {} + + void Work() override { + if (stalled_) { + return; + } + + bool moved = false; + for (auto& delay : delay_queue_) { + if (delay > 0) { + --delay; + moved = true; + } + } + while (!write_queue_.empty() && !delay_queue_.empty() && delay_queue_.front() == 0 && + read_queue_.size() < max_size_) { + read_queue_.push_back(std::move(write_queue_.front())); + write_queue_.pop_front(); + delay_queue_.pop_front(); + moved = true; + } + GFSIM_ASSERT(write_queue_.size() == delay_queue_.size()); + if (moved) { + MarkProgress(); + } + } + + void Xfer() override {} + + void Reset() override { + write_queue_.clear(); + read_queue_.clear(); + delay_queue_.clear(); + stalled_ = false; + ConsumeProgress(); + } + + void SetLatency(std::uint32_t latency) { latency_ = latency; } + std::uint32_t GetLatency() const noexcept { return latency_; } + + bool getStall() const noexcept { return stalled_; } + void setStall() noexcept { stalled_ = true; } + void setStall(bool stalled) noexcept { stalled_ = stalled; } + void unsetStall() noexcept { stalled_ = false; } + + template + T& Write(Args&&... args) { + GFSIM_ASSERT(!Full()); + write_queue_.emplace_back(std::forward(args)...); + delay_queue_.push_back(latency_); + GFSIM_ASSERT(write_queue_.size() == delay_queue_.size()); + MarkProgress(); + return write_queue_.back(); + } + + template + T& PrimeVisible(Args&&... args) { + GFSIM_ASSERT(!Full()); + read_queue_.emplace_back(std::forward(args)...); + MarkProgress(); + return read_queue_.back(); + } + + T Read() { + GFSIM_ASSERT(!read_queue_.empty()); + T value = std::move(read_queue_.front()); + read_queue_.pop_front(); + MarkProgress(); + return value; + } + + T& Front() { + GFSIM_ASSERT(!read_queue_.empty()); + return read_queue_.front(); + } + + const T& Front() const { + GFSIM_ASSERT(!read_queue_.empty()); + return read_queue_.front(); + } + + void Pop() { + GFSIM_ASSERT(!read_queue_.empty()); + read_queue_.pop_front(); + MarkProgress(); + } + + bool Empty() const noexcept { return read_queue_.empty(); } + bool EmptyW() const noexcept { return write_queue_.empty(); } + bool Full() const noexcept { return read_queue_.size() + write_queue_.size() >= max_size_; } + bool Full(std::uint32_t reserve) const noexcept { return read_queue_.size() + write_queue_.size() + reserve >= max_size_; } + bool ToBeFull(std::uint32_t reserve = 0) const noexcept { return Full(reserve); } + size_type Size() const noexcept { return read_queue_.size(); } + size_type SizeW() const noexcept { return write_queue_.size(); } + size_type Occupancy() const noexcept { return read_queue_.size() + write_queue_.size(); } + std::int32_t MaxSize() const noexcept { return static_cast(max_size_); } + + std::deque& GetRawWriteData() { return write_queue_; } + const std::deque& GetRawWriteData() const { return write_queue_; } + + std::deque& GetRawReadData() { return read_queue_; } + const std::deque& GetRawReadData() const { return read_queue_; } + + template + void FlushIf(Function&& match) { + for (size_type i = 0; i < write_queue_.size();) { + if (std::invoke(match, write_queue_[i])) { + write_queue_.erase(write_queue_.begin() + static_cast(i)); + delay_queue_.erase(delay_queue_.begin() + static_cast(i)); + MarkProgress(); + } else { + ++i; + } + } + + for (auto it = read_queue_.begin(); it != read_queue_.end();) { + if (std::invoke(match, *it)) { + it = read_queue_.erase(it); + MarkProgress(); + } else { + ++it; + } + } + GFSIM_ASSERT(write_queue_.size() == delay_queue_.size()); + } + + void InitMaxSize(std::uint32_t size) { + GFSIM_ASSERT(size > 0); + max_size_ = size; + } + +private: + std::deque write_queue_; + std::deque read_queue_; + std::deque delay_queue_; + size_type max_size_ = 0; + std::uint32_t latency_ = 1; + bool stalled_ = false; +}; + +template +using SimUniqueQueue = SimQueue>; + +template +class Module : public SimObject { +public: + using Queue = SimQueue; + using size_type = std::size_t; + + explicit Module(std::string name) : SimObject(std::move(name)) {} + ~Module() override = default; + + void Build() override { + BuildSelf(); + for (auto& submodule : submodules_) { + GFSIM_ASSERT(submodule != nullptr); + submodule->Build(); + } + built_ = true; + } + + void Reset() override { + ResetSelf(); + for (auto& submodule : submodules_) { + GFSIM_ASSERT(submodule != nullptr); + submodule->Reset(); + } + for (auto& queue : owned_queues_) { + GFSIM_ASSERT(queue != nullptr); + queue->Reset(); + } + } + + void Report() override { + ReportSelf(); + for (auto& submodule : submodules_) { + GFSIM_ASSERT(submodule != nullptr); + submodule->Report(); + } + } + + void Work() override { + WorkSelf(); + for (auto& submodule : submodules_) { + GFSIM_ASSERT(submodule != nullptr); + submodule->SetCurrentCycle(CurrentCycle()); + submodule->Work(); + MarkProgress(submodule->ConsumeProgress()); + } + for (auto& queue : owned_queues_) { + GFSIM_ASSERT(queue != nullptr); + queue->SetCurrentCycle(CurrentCycle()); + queue->Work(); + MarkProgress(queue->ConsumeProgress()); + } + } + + void Xfer() override { + XferSelf(); + for (auto& submodule : submodules_) { + GFSIM_ASSERT(submodule != nullptr); + submodule->Xfer(); + } + for (auto& queue : owned_queues_) { + GFSIM_ASSERT(queue != nullptr); + queue->Xfer(); + } + } + + void BindInput(size_type idx, Queue* queue) { BindPort(idx, queue, inputs_); } + void BindOutput(size_type idx, Queue* queue) { BindPort(idx, queue, outputs_); } + void BindInner(size_type idx, Queue* queue) { BindPort(idx, queue, inners_); } + + template + Queue* CreateOwnedQueue(Args&&... args) { + owned_queues_.push_back(std::make_unique(std::forward(args)...)); + return owned_queues_.back().get(); + } + + Derived& AddSubmodule(std::unique_ptr submodule) { + GFSIM_ASSERT(submodule != nullptr); + submodules_.push_back(std::move(submodule)); + return *submodules_.back(); + } + + void ConnectInput(Derived& child, size_type child_input_idx, Queue* queue) { child.BindInput(child_input_idx, queue); } + void ConnectOutput(Derived& child, size_type child_output_idx, Queue* queue) { child.BindOutput(child_output_idx, queue); } + void ConnectInner(Derived& child, size_type child_inner_idx, Queue* queue) { child.BindInner(child_inner_idx, queue); } + + Queue* Input(size_type idx) { + GFSIM_ASSERT(idx < inputs_.size()); + GFSIM_ASSERT(inputs_[idx] != nullptr); + return inputs_[idx]; + } + + Queue* Output(size_type idx) { + GFSIM_ASSERT(idx < outputs_.size()); + GFSIM_ASSERT(outputs_[idx] != nullptr); + return outputs_[idx]; + } + + Queue* Inner(size_type idx) { + GFSIM_ASSERT(idx < inners_.size()); + GFSIM_ASSERT(inners_[idx] != nullptr); + return inners_[idx]; + } + +protected: + virtual void BuildSelf() {} + virtual void ResetSelf() {} + virtual void ReportSelf() {} + virtual void WorkSelf() {} + virtual void XferSelf() {} + + std::vector inputs_; + std::vector outputs_; + std::vector inners_; + std::vector> submodules_; + std::vector> owned_queues_; + +private: + void BindPort(size_type idx, Queue* queue, std::vector& slots) { + GFSIM_ASSERT(queue != nullptr); + if (slots.size() <= idx) { + slots.resize(idx + 1, nullptr); + } + GFSIM_ASSERT(slots[idx] == nullptr || slots[idx] == queue); + slots[idx] = queue; + } + + bool built_ = false; +}; + +class SimSystem { +public: + using size_type = std::size_t; + + virtual ~SimSystem() = default; + + void Build() { + BuildSystem(); + for (auto* module : modules_) { + GFSIM_ASSERT(module != nullptr); + module->Build(); + } + built_ = true; + } + + void Reset() { + ResetSystem(); + cycle_ = 0; + for (auto* module : modules_) { + GFSIM_ASSERT(module != nullptr); + module->Reset(); + } + } + + void Report() { + ReportSystem(); + for (auto* module : modules_) { + GFSIM_ASSERT(module != nullptr); + module->Report(); + } + } + + void Step() { + for (auto* module : modules_) { + GFSIM_ASSERT(module != nullptr); + module->SetCurrentCycle(cycle_); + } + for (auto* module : modules_) { + GFSIM_ASSERT(module != nullptr); + module->Work(); + } + for (auto* module : modules_) { + GFSIM_ASSERT(module != nullptr); + module->Xfer(); + } + ++cycle_; + } + + template + ModuleT& EmplaceOwnedModule(Args&&... args) { + auto module = std::make_unique(std::forward(args)...); + ModuleT& ref = *module; + modules_.push_back(&ref); + owned_modules_.push_back(std::move(module)); + return ref; + } + + std::uint64_t Cycle() const noexcept { return cycle_; } + bool IsBuilt() const noexcept { return built_; } + +protected: + virtual void BuildSystem() {} + virtual void ResetSystem() {} + virtual void ReportSystem() {} + +private: + std::vector modules_; + std::vector> owned_modules_; + std::uint64_t cycle_ = 0; + bool built_ = false; +}; + +} // namespace davincioo + +#define GFSIM_INPUT(name, idx) \ + auto* name [[maybe_unused]] = this->Input(idx); \ + GFSIM_ASSERT((name) != nullptr) + +#define GFSIM_OUTPUT(name, idx) \ + auto* name [[maybe_unused]] = this->Output(idx); \ + GFSIM_ASSERT((name) != nullptr) + +#define GFSIM_INNER(name, idx) \ + auto* name [[maybe_unused]] = this->Inner(idx); \ + GFSIM_ASSERT((name) != nullptr) + +#ifndef INPUT +#define INPUT(name, idx) GFSIM_INPUT(name, idx) +#endif + +#ifndef OUTPUT +#define OUTPUT(name, idx) GFSIM_OUTPUT(name, idx) +#endif + +#ifndef INNER +#define INNER(name, idx) GFSIM_INNER(name, idx) +#endif diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/pto_inst.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/pto_inst.hpp new file mode 100644 index 00000000..804a81f1 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/pto_inst.hpp @@ -0,0 +1,591 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace davincioo { + +enum class PTOOpcode { + Unknown, + TLOAD, + TSTORE, + TSTORE_FP, + MGATHER, + MSCATTER, + TPUT, + TGET, + TPUT_ASYNC, + TGET_ASYNC, + TBROADCAST, + TREDUCE, + RECORD_EVENT, + WAIT_EVENT, + BARRIER, + TSYNC, + TALLOC, + TFREE, + TPUSH, + TPOP, + TNOTIFY, + TWAIT, + TTEST, + TADD, + TSUB, + TMUL, + TDIV, + TREM, + TAND, + TOR, + TXOR, + TSHL, + TSHR, + TMAX, + TMIN, + TPRELU, + TCMP, + TABS, + TEXP, + TLOG, + TSQRT, + TRSQRT, + TRECIP, + TNEG, + TNOT, + TRELU, + TCVT, + TADDC, + TSUBC, + TSEL, + TADDS, + TSUBS, + TMULS, + TDIVS, + TREMS, + TANDS, + TORS, + TXORS, + TSHLS, + TSHRS, + TMAXS, + TMINS, + TLRELU, + TAXPY, + TCMPS, + TADDSC, + TSUBSC, + TSELS, + TEXPANDS, + TROWSUM, + TROWPROD, + TROWMAX, + TROWMIN, + TROWARGMAX, + TROWARGMIN, + TROWEXPAND, + TCOLSUM, + TCOLPROD, + TCOLMAX, + TCOLMIN, + TCOLARGMAX, + TCOLARGMIN, + TCOLEXPAND, + TROWEXPANDDIV, + TROWEXPANDMUL, + TROWEXPANDSUB, + TROWEXPANDADD, + TROWEXPANDMAX, + TROWEXPANDMIN, + TROWEXPANDEXPDIF, + TCOLEXPANDDIV, + TCOLEXPANDMUL, + TCOLEXPANDSUB, + TCOLEXPANDADD, + TCOLEXPANDMAX, + TCOLEXPANDMIN, + TCOLEXPANDEXPDIF, + TFILLPAD, + TFILLPAD_INPLACE, + TFILLPAD_EXPAND, + TMATMUL, + TMATMUL_MX, + TMATMUL_ACC, + TMATMUL_BIAS, + TMATMUL_MX_ACC, + TMATMUL_MX_BIAS, + TGEMV, + TGEMV_ACC, + TGEMV_BIAS, + TGEMV_MX, + TMOV, + TMOV_FP, + TTRANS, + TEXTRACT, + TEXTRACT_FP, + TRESHAPE, + TASSIGN, + TCI, + TTRI, + TGATHER, + TGATHERB, + TSCATTER, + TSORT32, + TMRGSORT, + TPARTADD, + TPARTMUL, + TPARTMAX, + TPARTMIN, + TPARTARGMAX, + TPARTARGMIN, + TRANDOM, + TPRINT, + TPREFETCH, + TIMG2COL, + SETFMATRIX, + SET_IMG2COL_RPT, + SET_IMG2COL_PADDING, + TCONCAT, + TDEQUANT, + TINSERT, + TINSERT_FP, + TFMOD, + TFMODS, + THISTOGRAM, + TQUANT, + TGET_SCALE_ADDR, + TSUBVIEW, +}; + +enum class PTOTileType { + Unknown, + Vec, + Mat, + Left, + Right, + Acc, + Bias, + Scaling, + ScaleLeft, + ScaleRight, + Ctrl, + Global, +}; + +enum class PTODType { + kUnknown, + kBool, + kInt8, + kInt32, + kInt64, + kUInt64, + kFloat16, + kBFloat16, + kFloat32, + kFloat8, + kFloat4, +}; + +enum class PTOLayout { + kUnknown, + kND, + kDN, + kNZ, + kScale, + kMxAND, + kMxADN, + kMxAZZ, + kMxBND, + kMxBDN, + kMxBNN, + kNC1HWC0, + kNCHW, + kNHWC, + kNDC1HWC0, + kNCDHW, + kFractalZ, + kFractalZS16S8, + kFractalZ3D, +}; + +enum class PTOEngineKind { + Scalar, + Vec, + Cube, + Tma, +}; + +struct PTOOpcodeDescriptor { + std::string_view name; + PTOOpcode opcode; + PTOEngineKind engine_kind; +}; + +inline constexpr PTOOpcodeDescriptor kUnknownPTOOpcodeDescriptor{ + .name = "UNKNOWN", + .opcode = PTOOpcode::Unknown, + .engine_kind = PTOEngineKind::Scalar, +}; + +inline constexpr auto kPTOOpcodeDescriptors = std::to_array({ + {"TLOAD", PTOOpcode::TLOAD, PTOEngineKind::Tma}, + {"TSTORE", PTOOpcode::TSTORE, PTOEngineKind::Tma}, + {"TSTORE_FP", PTOOpcode::TSTORE_FP, PTOEngineKind::Tma}, + {"MGATHER", PTOOpcode::MGATHER, PTOEngineKind::Tma}, + {"MSCATTER", PTOOpcode::MSCATTER, PTOEngineKind::Tma}, + {"TPUT", PTOOpcode::TPUT, PTOEngineKind::Tma}, + {"TGET", PTOOpcode::TGET, PTOEngineKind::Tma}, + {"TPUT_ASYNC", PTOOpcode::TPUT_ASYNC, PTOEngineKind::Tma}, + {"TGET_ASYNC", PTOOpcode::TGET_ASYNC, PTOEngineKind::Tma}, + {"TBROADCAST", PTOOpcode::TBROADCAST, PTOEngineKind::Tma}, + {"TREDUCE", PTOOpcode::TREDUCE, PTOEngineKind::Tma}, + {"RECORD_EVENT", PTOOpcode::RECORD_EVENT, PTOEngineKind::Scalar}, + {"WAIT_EVENT", PTOOpcode::WAIT_EVENT, PTOEngineKind::Scalar}, + {"BARRIER", PTOOpcode::BARRIER, PTOEngineKind::Scalar}, + {"TSYNC", PTOOpcode::TSYNC, PTOEngineKind::Scalar}, + {"TALLOC", PTOOpcode::TALLOC, PTOEngineKind::Scalar}, + {"TFREE", PTOOpcode::TFREE, PTOEngineKind::Scalar}, + {"TPUSH", PTOOpcode::TPUSH, PTOEngineKind::Scalar}, + {"TPOP", PTOOpcode::TPOP, PTOEngineKind::Scalar}, + {"TNOTIFY", PTOOpcode::TNOTIFY, PTOEngineKind::Scalar}, + {"TWAIT", PTOOpcode::TWAIT, PTOEngineKind::Scalar}, + {"TTEST", PTOOpcode::TTEST, PTOEngineKind::Scalar}, + {"TADD", PTOOpcode::TADD, PTOEngineKind::Vec}, + {"TSUB", PTOOpcode::TSUB, PTOEngineKind::Vec}, + {"TMUL", PTOOpcode::TMUL, PTOEngineKind::Vec}, + {"TDIV", PTOOpcode::TDIV, PTOEngineKind::Vec}, + {"TREM", PTOOpcode::TREM, PTOEngineKind::Vec}, + {"TAND", PTOOpcode::TAND, PTOEngineKind::Vec}, + {"TOR", PTOOpcode::TOR, PTOEngineKind::Vec}, + {"TXOR", PTOOpcode::TXOR, PTOEngineKind::Vec}, + {"TSHL", PTOOpcode::TSHL, PTOEngineKind::Vec}, + {"TSHR", PTOOpcode::TSHR, PTOEngineKind::Vec}, + {"TMAX", PTOOpcode::TMAX, PTOEngineKind::Vec}, + {"TMIN", PTOOpcode::TMIN, PTOEngineKind::Vec}, + {"TPRELU", PTOOpcode::TPRELU, PTOEngineKind::Vec}, + {"TCMP", PTOOpcode::TCMP, PTOEngineKind::Vec}, + {"TABS", PTOOpcode::TABS, PTOEngineKind::Vec}, + {"TEXP", PTOOpcode::TEXP, PTOEngineKind::Vec}, + {"TLOG", PTOOpcode::TLOG, PTOEngineKind::Vec}, + {"TSQRT", PTOOpcode::TSQRT, PTOEngineKind::Vec}, + {"TRSQRT", PTOOpcode::TRSQRT, PTOEngineKind::Vec}, + {"TRECIP", PTOOpcode::TRECIP, PTOEngineKind::Vec}, + {"TNEG", PTOOpcode::TNEG, PTOEngineKind::Vec}, + {"TNOT", PTOOpcode::TNOT, PTOEngineKind::Vec}, + {"TRELU", PTOOpcode::TRELU, PTOEngineKind::Vec}, + {"TCVT", PTOOpcode::TCVT, PTOEngineKind::Vec}, + {"TADDC", PTOOpcode::TADDC, PTOEngineKind::Vec}, + {"TSUBC", PTOOpcode::TSUBC, PTOEngineKind::Vec}, + {"TSEL", PTOOpcode::TSEL, PTOEngineKind::Vec}, + {"TADDS", PTOOpcode::TADDS, PTOEngineKind::Vec}, + {"TSUBS", PTOOpcode::TSUBS, PTOEngineKind::Vec}, + {"TMULS", PTOOpcode::TMULS, PTOEngineKind::Vec}, + {"TDIVS", PTOOpcode::TDIVS, PTOEngineKind::Vec}, + {"TREMS", PTOOpcode::TREMS, PTOEngineKind::Vec}, + {"TANDS", PTOOpcode::TANDS, PTOEngineKind::Vec}, + {"TORS", PTOOpcode::TORS, PTOEngineKind::Vec}, + {"TXORS", PTOOpcode::TXORS, PTOEngineKind::Vec}, + {"TSHLS", PTOOpcode::TSHLS, PTOEngineKind::Vec}, + {"TSHRS", PTOOpcode::TSHRS, PTOEngineKind::Vec}, + {"TMAXS", PTOOpcode::TMAXS, PTOEngineKind::Vec}, + {"TMINS", PTOOpcode::TMINS, PTOEngineKind::Vec}, + {"TLRELU", PTOOpcode::TLRELU, PTOEngineKind::Vec}, + {"TAXPY", PTOOpcode::TAXPY, PTOEngineKind::Vec}, + {"TCMPS", PTOOpcode::TCMPS, PTOEngineKind::Vec}, + {"TADDSC", PTOOpcode::TADDSC, PTOEngineKind::Vec}, + {"TSUBSC", PTOOpcode::TSUBSC, PTOEngineKind::Vec}, + {"TSELS", PTOOpcode::TSELS, PTOEngineKind::Vec}, + {"TEXPANDS", PTOOpcode::TEXPANDS, PTOEngineKind::Vec}, + {"TROWSUM", PTOOpcode::TROWSUM, PTOEngineKind::Vec}, + {"TROWPROD", PTOOpcode::TROWPROD, PTOEngineKind::Vec}, + {"TROWMAX", PTOOpcode::TROWMAX, PTOEngineKind::Vec}, + {"TROWMIN", PTOOpcode::TROWMIN, PTOEngineKind::Vec}, + {"TROWARGMAX", PTOOpcode::TROWARGMAX, PTOEngineKind::Vec}, + {"TROWARGMIN", PTOOpcode::TROWARGMIN, PTOEngineKind::Vec}, + {"TROWEXPAND", PTOOpcode::TROWEXPAND, PTOEngineKind::Vec}, + {"TCOLSUM", PTOOpcode::TCOLSUM, PTOEngineKind::Vec}, + {"TCOLPROD", PTOOpcode::TCOLPROD, PTOEngineKind::Vec}, + {"TCOLMAX", PTOOpcode::TCOLMAX, PTOEngineKind::Vec}, + {"TCOLMIN", PTOOpcode::TCOLMIN, PTOEngineKind::Vec}, + {"TCOLARGMAX", PTOOpcode::TCOLARGMAX, PTOEngineKind::Vec}, + {"TCOLARGMIN", PTOOpcode::TCOLARGMIN, PTOEngineKind::Vec}, + {"TCOLEXPAND", PTOOpcode::TCOLEXPAND, PTOEngineKind::Vec}, + {"TROWEXPANDDIV", PTOOpcode::TROWEXPANDDIV, PTOEngineKind::Vec}, + {"TROWEXPANDMUL", PTOOpcode::TROWEXPANDMUL, PTOEngineKind::Vec}, + {"TROWEXPANDSUB", PTOOpcode::TROWEXPANDSUB, PTOEngineKind::Vec}, + {"TROWEXPANDADD", PTOOpcode::TROWEXPANDADD, PTOEngineKind::Vec}, + {"TROWEXPANDMAX", PTOOpcode::TROWEXPANDMAX, PTOEngineKind::Vec}, + {"TROWEXPANDMIN", PTOOpcode::TROWEXPANDMIN, PTOEngineKind::Vec}, + {"TROWEXPANDEXPDIF", PTOOpcode::TROWEXPANDEXPDIF, PTOEngineKind::Vec}, + {"TCOLEXPANDDIV", PTOOpcode::TCOLEXPANDDIV, PTOEngineKind::Vec}, + {"TCOLEXPANDMUL", PTOOpcode::TCOLEXPANDMUL, PTOEngineKind::Vec}, + {"TCOLEXPANDSUB", PTOOpcode::TCOLEXPANDSUB, PTOEngineKind::Vec}, + {"TCOLEXPANDADD", PTOOpcode::TCOLEXPANDADD, PTOEngineKind::Vec}, + {"TCOLEXPANDMAX", PTOOpcode::TCOLEXPANDMAX, PTOEngineKind::Vec}, + {"TCOLEXPANDMIN", PTOOpcode::TCOLEXPANDMIN, PTOEngineKind::Vec}, + {"TCOLEXPANDEXPDIF", PTOOpcode::TCOLEXPANDEXPDIF, PTOEngineKind::Vec}, + {"TFILLPAD", PTOOpcode::TFILLPAD, PTOEngineKind::Vec}, + {"TFILLPAD_INPLACE", PTOOpcode::TFILLPAD_INPLACE, PTOEngineKind::Vec}, + {"TFILLPAD_EXPAND", PTOOpcode::TFILLPAD_EXPAND, PTOEngineKind::Vec}, + {"TMATMUL", PTOOpcode::TMATMUL, PTOEngineKind::Cube}, + {"TMATMUL_MX", PTOOpcode::TMATMUL_MX, PTOEngineKind::Cube}, + {"TMATMUL_ACC", PTOOpcode::TMATMUL_ACC, PTOEngineKind::Cube}, + {"TMATMUL_BIAS", PTOOpcode::TMATMUL_BIAS, PTOEngineKind::Cube}, + {"TMATMUL_MX_ACC", PTOOpcode::TMATMUL_MX_ACC, PTOEngineKind::Cube}, + {"TMATMUL_MX_BIAS", PTOOpcode::TMATMUL_MX_BIAS, PTOEngineKind::Cube}, + {"TGEMV", PTOOpcode::TGEMV, PTOEngineKind::Cube}, + {"TGEMV_ACC", PTOOpcode::TGEMV_ACC, PTOEngineKind::Cube}, + {"TGEMV_BIAS", PTOOpcode::TGEMV_BIAS, PTOEngineKind::Cube}, + {"TGEMV_MX", PTOOpcode::TGEMV_MX, PTOEngineKind::Cube}, + {"TMOV", PTOOpcode::TMOV, PTOEngineKind::Vec}, + {"TMOV_FP", PTOOpcode::TMOV_FP, PTOEngineKind::Vec}, + {"TTRANS", PTOOpcode::TTRANS, PTOEngineKind::Vec}, + {"TEXTRACT", PTOOpcode::TEXTRACT, PTOEngineKind::Vec}, + {"TEXTRACT_FP", PTOOpcode::TEXTRACT_FP, PTOEngineKind::Vec}, + {"TRESHAPE", PTOOpcode::TRESHAPE, PTOEngineKind::Scalar}, + {"TASSIGN", PTOOpcode::TASSIGN, PTOEngineKind::Scalar}, + {"TCI", PTOOpcode::TCI, PTOEngineKind::Scalar}, + {"TTRI", PTOOpcode::TTRI, PTOEngineKind::Vec}, + {"TGATHER", PTOOpcode::TGATHER, PTOEngineKind::Vec}, + {"TGATHERB", PTOOpcode::TGATHERB, PTOEngineKind::Vec}, + {"TSCATTER", PTOOpcode::TSCATTER, PTOEngineKind::Vec}, + {"TSORT32", PTOOpcode::TSORT32, PTOEngineKind::Vec}, + {"TMRGSORT", PTOOpcode::TMRGSORT, PTOEngineKind::Vec}, + {"TPARTADD", PTOOpcode::TPARTADD, PTOEngineKind::Vec}, + {"TPARTMUL", PTOOpcode::TPARTMUL, PTOEngineKind::Vec}, + {"TPARTMAX", PTOOpcode::TPARTMAX, PTOEngineKind::Vec}, + {"TPARTMIN", PTOOpcode::TPARTMIN, PTOEngineKind::Vec}, + {"TPARTARGMAX", PTOOpcode::TPARTARGMAX, PTOEngineKind::Vec}, + {"TPARTARGMIN", PTOOpcode::TPARTARGMIN, PTOEngineKind::Vec}, + {"TRANDOM", PTOOpcode::TRANDOM, PTOEngineKind::Vec}, + {"TPRINT", PTOOpcode::TPRINT, PTOEngineKind::Scalar}, + {"TPREFETCH", PTOOpcode::TPREFETCH, PTOEngineKind::Tma}, + {"TIMG2COL", PTOOpcode::TIMG2COL, PTOEngineKind::Tma}, + {"SETFMATRIX", PTOOpcode::SETFMATRIX, PTOEngineKind::Scalar}, + {"SET_IMG2COL_RPT", PTOOpcode::SET_IMG2COL_RPT, PTOEngineKind::Scalar}, + {"SET_IMG2COL_PADDING", PTOOpcode::SET_IMG2COL_PADDING, PTOEngineKind::Scalar}, + {"TCONCAT", PTOOpcode::TCONCAT, PTOEngineKind::Vec}, + {"TDEQUANT", PTOOpcode::TDEQUANT, PTOEngineKind::Vec}, + {"TINSERT", PTOOpcode::TINSERT, PTOEngineKind::Vec}, + {"TINSERT_FP", PTOOpcode::TINSERT_FP, PTOEngineKind::Vec}, + {"TFMOD", PTOOpcode::TFMOD, PTOEngineKind::Vec}, + {"TFMODS", PTOOpcode::TFMODS, PTOEngineKind::Vec}, + {"THISTOGRAM", PTOOpcode::THISTOGRAM, PTOEngineKind::Vec}, + {"TQUANT", PTOOpcode::TQUANT, PTOEngineKind::Vec}, + {"TGET_SCALE_ADDR", PTOOpcode::TGET_SCALE_ADDR, PTOEngineKind::Scalar}, + {"TSUBVIEW", PTOOpcode::TSUBVIEW, PTOEngineKind::Vec}, +}); + +constexpr const PTOOpcodeDescriptor* FindPTOOpcodeDescriptor(std::string_view name) { + for (const auto& descriptor : kPTOOpcodeDescriptors) { + if (descriptor.name == name) { + return &descriptor; + } + } + return nullptr; +} + +using PTOScalarValue = std::variant; + +struct PTOTileReg { + PTOTileType tile_type = PTOTileType::Unknown; + std::uint64_t address = 0; + std::uint64_t tile_tag = std::numeric_limits::max(); + PTODType dtype = PTODType::kUnknown; + PTOLayout layout = PTOLayout::kUnknown; + bool rename_managed = false; + std::vector shape; +}; + +struct PTOScalarInput { + PTODType dtype = PTODType::kUnknown; + PTOScalarValue value; +}; + +constexpr std::uint64_t kUnsetCycleStamp = std::numeric_limits::max(); +constexpr std::uint64_t kInvalidTileTag = std::numeric_limits::max(); + +struct PTOInstTimestamps { + std::uint64_t rob_alloc_cycle = kUnsetCycleStamp; + std::uint64_t rename_cycle = kUnsetCycleStamp; + std::uint64_t dispatch_cycle = kUnsetCycleStamp; + std::uint64_t issue_cycle = kUnsetCycleStamp; + std::uint64_t engine_pop_cycle = kUnsetCycleStamp; + std::uint64_t engine_complete_cycle = kUnsetCycleStamp; + std::uint64_t rob_retire_cycle = kUnsetCycleStamp; +}; + +struct PTOInst { + PTOOpcode opcode = PTOOpcode::Unknown; + std::string raw_opcode; + PTOEngineKind engine_kind = PTOEngineKind::Scalar; + std::uint64_t block_idx = 0; + std::uint64_t sequence_id = 0; + std::uint64_t rob_id = 0; + std::uint64_t engine_id = std::numeric_limits::max(); + bool dispatched = false; + bool src_ready = false; + std::size_t runtime_latency = 0; + PTOInstTimestamps timestamps; + std::vector tile_input_valid; + std::vector tile_input_ready; + std::vector tile_reg_inputs; + std::vector scalar_inputs; + std::vector tile_reg_outputs; + std::vector output_replaced_tile_tags; +}; + +using PTOInstPtr = std::unique_ptr; +using PTOInstRef = std::shared_ptr; + +struct SimulationResult { + std::size_t record_count = 0; + std::size_t simulated_cycles = 0; + std::size_t rob_capacity = 0; + std::size_t rob_count = 0; + std::size_t scalar_engine_count = 0; + std::size_t vec_engine_count = 0; + std::size_t cube_engine_count = 0; + std::size_t tma_engine_count = 0; + std::vector processed; + std::map opcode_counts; +}; + +constexpr std::string_view ToString(PTOOpcode opcode) { + if (opcode == PTOOpcode::Unknown) { + return "UNKNOWN"; + } + for (const auto& descriptor : kPTOOpcodeDescriptors) { + if (descriptor.opcode == opcode) { + return descriptor.name; + } + } + return "UNKNOWN"; +} + +constexpr std::string_view ToString(PTODType dtype) { + switch (dtype) { + case PTODType::kBool: + return "bool"; + case PTODType::kInt8: + return "int8"; + case PTODType::kInt32: + return "int32"; + case PTODType::kInt64: + return "int64"; + case PTODType::kUInt64: + return "uint64"; + case PTODType::kFloat16: + return "float16"; + case PTODType::kBFloat16: + return "bfloat16"; + case PTODType::kFloat32: + return "float32"; + case PTODType::kFloat8: + return "float8"; + case PTODType::kFloat4: + return "float4"; + case PTODType::kUnknown: + default: + return "unknown"; + } +} + +constexpr std::string_view ToString(PTOTileType tile_type) { + switch (tile_type) { + case PTOTileType::Vec: + return "Vec"; + case PTOTileType::Mat: + return "Mat"; + case PTOTileType::Left: + return "Left"; + case PTOTileType::Right: + return "Right"; + case PTOTileType::Acc: + return "Acc"; + case PTOTileType::Bias: + return "Bias"; + case PTOTileType::Scaling: + return "Scaling"; + case PTOTileType::ScaleLeft: + return "ScaleLeft"; + case PTOTileType::ScaleRight: + return "ScaleRight"; + case PTOTileType::Ctrl: + return "Ctrl"; + case PTOTileType::Global: + return "Global"; + case PTOTileType::Unknown: + default: + return "Unknown"; + } +} + +constexpr std::string_view ToString(PTOLayout layout) { + switch (layout) { + case PTOLayout::kND: + return "ND"; + case PTOLayout::kDN: + return "DN"; + case PTOLayout::kNZ: + return "NZ"; + case PTOLayout::kScale: + return "SCALE"; + case PTOLayout::kMxAND: + return "MX_A_ND"; + case PTOLayout::kMxADN: + return "MX_A_DN"; + case PTOLayout::kMxAZZ: + return "MX_A_ZZ"; + case PTOLayout::kMxBND: + return "MX_B_ND"; + case PTOLayout::kMxBDN: + return "MX_B_DN"; + case PTOLayout::kMxBNN: + return "MX_B_NN"; + case PTOLayout::kNC1HWC0: + return "NC1HWC0"; + case PTOLayout::kNCHW: + return "NCHW"; + case PTOLayout::kNHWC: + return "NHWC"; + case PTOLayout::kNDC1HWC0: + return "NDC1HWC0"; + case PTOLayout::kNCDHW: + return "NCDHW"; + case PTOLayout::kFractalZ: + return "FRACTAL_Z"; + case PTOLayout::kFractalZS16S8: + return "FRACTAL_Z_S16S8"; + case PTOLayout::kFractalZ3D: + return "FRACTAL_Z_3D"; + case PTOLayout::kUnknown: + default: + return "UNKNOWN"; + } +} + +constexpr std::string_view ToString(PTOEngineKind kind) { + switch (kind) { + case PTOEngineKind::Scalar: + return "SCALAR"; + case PTOEngineKind::Vec: + return "VEC"; + case PTOEngineKind::Cube: + return "CUBE"; + case PTOEngineKind::Tma: + return "TMA"; + default: + return "SCALAR"; + } +} + +std::string OpcodeName(const PTOInst& inst); +std::string DumpPTOInst(const PTOInst& inst); + +} // namespace davincioo diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/rob.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/rob.hpp new file mode 100644 index 00000000..ce1f3a26 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/include/davincioo/model/rob.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "davincioo/model/config.hpp" +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo { + +enum class ROBEntryStatus { + Free, + Alloc, + Issued, + Completed, + Resolved, + Exception, +}; + +struct ROBEntry { + std::uint64_t rid = 0; + ROBEntryStatus status = ROBEntryStatus::Free; + PTOInstRef inst; +}; + +constexpr std::string_view ToString(ROBEntryStatus status) { + switch (status) { + case ROBEntryStatus::Free: + return "free"; + case ROBEntryStatus::Alloc: + return "alloc"; + case ROBEntryStatus::Issued: + return "issued"; + case ROBEntryStatus::Completed: + return "completed"; + case ROBEntryStatus::Resolved: + return "resolved"; + case ROBEntryStatus::Exception: + return "exception"; + default: + return "unknown"; + } +} + +} // namespace davincioo diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/DISPATCH.md b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/DISPATCH.md new file mode 100644 index 00000000..a27c515f --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/DISPATCH.md @@ -0,0 +1,93 @@ +# Dispatch + +This document describes the dispatch module implemented in: + +- [dispatch.hpp](./dispatch.hpp) +- [dispatch.cpp](./dispatch.cpp) + +## Purpose + +`Dispatch` routes backend-issued `PTOInst` objects to engine-specific input +queues based on the typed `engine_kind` field. + +## Inputs + +- `inst_in` + - type: `SimQueue` + - producer: `Rename` + - meaning: issued and renamed instruction stream leaving the rename stage + +## Outputs + +Boundary outputs are all `SimQueue` queues owned by the parent +`Core` topology and consumed by `ReadyTable`. + +- VEC routed queues +- CUBE routed queues +- TMA routed queues +- SCALAR routed queues + +The output index space is contiguous: + +1. SCALAR outputs +2. VEC outputs +3. CUBE outputs +4. TMA outputs + +## Inner State / Private State + +- round-robin cursor for VEC outputs +- round-robin cursor for CUBE outputs +- round-robin cursor for TMA outputs +- configured engine counts per family + +## Owned Storage + +This module owns no physical queue storage. + +Queue storage is owned by `Core`. + +## Features + +- consumes one instruction at a time from the dispatch input queue +- checks `PTOInst.engine_kind` +- routes `VEC` instructions to VEC engine queues +- routes `CUBE` instructions to CUBE engine queues +- routes `TMA` instructions to TMA engine queues +- routes fallback or scalar instructions to SCALAR engine queues + for later ready-table initialization and issue-queue residency +- uses round-robin selection across configured engines of the same kind + +## PTOInstRef Pop/Update/Push Rule + +Dispatch owns the routing-stage runtime transition. + +When dispatch moves an instruction: + +- pop `PTOInstRef` from `inst_in` +- inspect `engine_kind` +- set: + - `dispatched = true` + - source-readiness runtime state + - `timestamps.dispatch_cycle` +- preserve rename-resolved tile tags already written onto the instruction +- push the same `PTOInstRef` to the selected engine queue + +There is no `NONE` engine lane in the current topology. Fallback or +otherwise-unclassified ops route to the `SCALAR` engine family. + +## Cycle Behavior + +Per cycle: + +1. inspect the input queue head +2. select the target engine family +3. if the selected output queue has space, pop the input instruction +4. mark it dispatched +5. push it to the chosen engine queue + +## Limitations / Simplifications + +- only one instruction is dispatched per cycle +- dispatch does not model arbitration cost beyond queue fullness +- no fairness metrics or structural hazard reporting yet diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ENGINE.md b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ENGINE.md new file mode 100644 index 00000000..47c4e912 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ENGINE.md @@ -0,0 +1,96 @@ +# Engine + +This document describes the generic engine module implemented in: + +- [engine.hpp](./engine.hpp) +- [engine.cpp](./engine.cpp) + +## Purpose + +`Engine` is the current dummy execution resource for `SCALAR`, `VEC`, `CUBE`, and `TMA` +backend work. + +It models latency and completion, not detailed functional execution. + +## Inputs + +- `inst_in` + - type: `SimQueue` + - producer: `IssueQueue` + - meaning: ready instructions selected for this engine instance + +## Outputs + +- `done_out` + - type: `SimQueue` + - consumer: `ReadyTable` + - meaning: completed instruction stream used for wakeup broadcast and ROB completion + +## Inner State / Private State + +- `kind_` +- `config_` +- `execute_q_` + +`execute_q_` is a private `SimQueue` inside the engine. The queue +stores per-op latency on write and only exposes the instruction for completion +once that latency has counted down to zero. + +## Owned Storage + +The engine owns only its local in-flight queue state. + +It does not own boundary interconnect queues; those are owned by `Core`. + +## Features + +- one logical engine instance per configured execution lane +- per-engine per-op latency configuration from TOML +- one in-flight instruction at a time through a private latency queue +- emits a completion event when latency reaches zero +- emits a completed `PTOInstRef` when latency reaches zero + +## PTOInstRef Pop/Update/Push Rule + +Engine owns the execution-stage runtime transition. + +When engine accepts an instruction: + +- pop `PTOInstRef` from `inst_in` +- assign `engine_id` +- look up latency from the engine config based on opcode +- assign runtime latency +- stamp `timestamps.engine_pop_cycle` +- write the shared instruction into `execute_q_` with that latency + +When engine completes an instruction: + +- wait for the private latency queue to expose the completed instruction +- update the in-flight instruction runtime latency to zero +- stamp `timestamps.engine_complete_cycle` +- push the same `PTOInstRef` to `done_out` +- remove the instruction from the private latency queue + +The engine does not directly update ROB entry state. Completion is routed +through `ReadyTable`, which broadcasts wakeup and forwards the same +`PTOInstRef` to ROB completion. + +## Cycle Behavior + +Per cycle: + +1. advance the private latency queue +2. if a completed instruction is visible and output space exists, pop it and + push it to `done_out` +3. if the private latency queue has space and input is non-empty, pop one + instruction and enqueue it for execution with its configured latency + +## Limitations / Simplifications + +- no result data generation +- no bypass network +- the latency model is queue-based and per-op, but still not a full functional + execution model +- capacity is fixed to one in-flight instruction per engine instance +- completion is returned through a PTOInstRef wakeup queue +- deadlock avoidance is external; `CoreSystem` aborts after sustained no-progress cycles diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ISSUE_QUEUE.md b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ISSUE_QUEUE.md new file mode 100644 index 00000000..f3a24a36 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ISSUE_QUEUE.md @@ -0,0 +1,92 @@ +# IssueQueue + +This document describes the distributed issue-queue module implemented in: + +- [issue_queue.hpp](./issue_queue.hpp) +- [issue_queue.cpp](./issue_queue.cpp) + +## Purpose + +`IssueQueue` is the per-engine distributed issue buffer between `ReadyTable` +and `Engine`. + +It holds resident `PTOInstRef` entries, tracks per-input valid/ready state on +the instruction object itself, consumes wakeup broadcasts, and only issues a +ready instruction into the target engine. + +## Inputs + +- `enqueue_in` + - type: `SimQueue` + - producer: `ReadyTable` + - meaning: instructions already annotated with current ready-table state +- `wakeup_in` + - type: `SimQueue` + - producer: `ReadyTable` + - meaning: completed instructions whose output tile tags may wake resident entries + +## Outputs + +- `issued_out` + - type: `SimQueue` + - consumer: `Engine` + - meaning: ready instructions selected for execution + +## Inner State / Private State + +- resident `entries_` +- configured depth +- engine kind + +## Owned Storage + +This module owns only its internal resident entry deque. + +Boundary queue storage is owned by `Core`. + +## Features + +- one logical issue queue per engine lane +- resident `PTOInstRef` storage +- per-source valid/ready tracking through the instruction object +- wakeup by matching completed output tile tags against waiting input tile tags +- oldest-ready-first pick by `rob_id` + +## PTOInstRef Pop/Update/Push Rule + +IssueQueue owns the issue-stage runtime transition. + +When it consumes an enqueue: + +- pop `PTOInstRef` from `enqueue_in` +- preserve the ready state initialized by `ReadyTable` +- place the instruction into the resident IQ state + +When it consumes wakeup: + +- pop `PTOInstRef` from `wakeup_in` +- compare wakeup output tile tags against all resident waiting inputs +- mark matching `tile_input_ready` bits true +- recompute `src_ready` + +When it issues: + +- pick the oldest resident instruction whose valid inputs are all ready +- stamp `timestamps.issue_cycle` +- push the same `PTOInstRef` to `issued_out` +- remove the entry from resident IQ storage + +## Cycle Behavior + +Per cycle: + +1. consume all visible wakeup packets and update resident entries +2. accept one new enqueue if capacity exists +3. if a ready entry exists and engine output has space, pick and issue it + +## Limitations / Simplifications + +- single-width enqueue and single-width issue +- no separate `inflight` residency after pick; deallocation happens at issue +- oldest-ready-first uses `rob_id` only +- no replay, cancellation, or arbitration retry path yet diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/READY_TABLE.md b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/READY_TABLE.md new file mode 100644 index 00000000..20b1672c --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/READY_TABLE.md @@ -0,0 +1,105 @@ +# ReadyTable + +This document describes the ready-table and wakeup-broadcast module implemented in: + +- [ready_table.hpp](./ready_table.hpp) +- [ready_table.cpp](./ready_table.cpp) + +## Purpose + +`ReadyTable` records global tile-tag readiness for instructions that are not +currently resident inside an `IssueQueue`. + +It also owns the wakeup fanout path: + +- consume completed `PTOInstRef` from the global wakeup queue +- mark their produced tile tags ready +- broadcast the same `PTOInstRef` to all issue-queue wakeup queues +- forward the same `PTOInstRef` to ROB completion + +## Inputs + +- `dispatch_inputs_[i]` + - type: `SimQueue` + - producer: `Dispatch` + - meaning: per-engine routed instructions before issue-queue residency +- `wakeup_in` + - type: `SimQueue` + - producer: `Engine` + - meaning: completed instructions used for wakeup and ROB completion + +## Outputs + +- `issue_enqueue_outputs_[i]` + - type: `SimQueue` + - consumer: matching `IssueQueue` + - meaning: instructions with initial source-ready state set from the ready table +- `issue_wakeup_outputs_[i]` + - type: `SimQueue` + - consumer: every `IssueQueue` + - meaning: broadcast wakeup stream +- `rob_done_out` + - type: `SimQueue` + - consumer: `ROB` + - meaning: completed instruction stream for completion/retire ownership + +## Inner State / Private State + +- `ready_tags_` +- configured issue-queue count + +`ready_tags_` is the non-resident/global ready state for tile tags. + +## Owned Storage + +This module owns only the ready-tag set. + +Boundary queue storage is owned by `Core`. + +## Features + +- initialize `tile_input_valid` / `tile_input_ready` +- maintain global tile-tag readiness +- clear newly allocated output tags from the ready set +- mark completed output tags ready +- broadcast completed instructions to all issue queues and ROB + +## PTOInstRef Pop/Update/Push Rule + +ReadyTable owns the ready-table and wakeup-broadcast transition. + +For dispatch-side traffic: + +- pop `PTOInstRef` from the routed dispatch queue +- initialize `tile_input_valid` +- initialize `tile_input_ready` by querying `ready_tags_` +- recompute `src_ready` +- clear all rename-managed output tags from `ready_tags_` +- push the same `PTOInstRef` to the matching issue-queue enqueue output + +For wakeup-side traffic: + +- peek the completed instruction +- only pop when every issue-wakeup output queue and ROB-done queue can accept it +- pop `PTOInstRef` from `wakeup_in` +- mark produced output tile tags ready +- push the same `PTOInstRef` to every issue-wakeup output queue +- push the same `PTOInstRef` to `rob_done_out` + +## Cycle Behavior + +Per cycle: + +1. if a wakeup packet is visible and all fanout outputs can accept it: + - consume it + - update global ready tags + - broadcast it +2. for each routed dispatch input: + - if the paired output queue has space, consume one instruction + - initialize ready state and forward it to the issue queue + +## Limitations / Simplifications + +- the ready table is a simple set of ready tile tags +- no speculative/non-spec split yet +- no explicit wakeup priority or bandwidth model beyond queue fullness diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/RENAME.md b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/RENAME.md new file mode 100644 index 00000000..4b721928 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/RENAME.md @@ -0,0 +1,94 @@ +# Rename + +This document describes the rename module implemented in: + +- [rename.hpp](./rename.hpp) +- [rename.cpp](./rename.cpp) + +## Purpose + +`Rename` assigns tile tags to local PTO tile operands and maintains the current +speculative mapping from tile address to live tile tag. + +It sits between `ROB` and `Dispatch` and follows strict pop-update-push queue +ownership. + +## Inputs + +- `inst_in` + - type: `SimQueue` + - producer: `ROB` + - meaning: issued instructions that require rename before dispatch +- `retire_in` + - type: `SimQueue` + - producer: `ROB` + - meaning: retirement notifications used to recycle replaced tile tags + +## Outputs + +- `inst_out` + - type: `SimQueue` + - consumer: `Dispatch` + - meaning: renamed instructions ready for dispatch + +## Inner State / Private State + +- `free_tile_tags_` +- `smap_` +- `RenameConfig` + +`smap_` is the current speculative map from tile address to live tile tag. +`free_tile_tags_` is an ordered set-backed free list used for duplicate-safe +tag recycling. + +## Owned Storage + +This module owns no boundary queue storage. + +`Core` owns all physical queues and binds queue pointers into `Rename`. + +## Features + +- assigns tile tags for rename-managed local outputs +- resolves tile tags for rename-managed local inputs +- records replaced output mappings per output tile +- recycles replaced mappings after retirement +- stalls when insufficient tile tags are free for the head instruction + +## PTOInstRef Pop/Update/Push Rule + +Rename owns the rename-stage runtime transition. + +When rename consumes a retire notification: + +- pop `PTOInstRef` from `retire_in` +- recycle each valid `output_replaced_tile_tags` entry + +When rename processes an instruction: + +- pop `PTOInstRef` from `inst_in` +- stamp `timestamps.rename_cycle` +- look up SMAP for rename-managed inputs and write `tile_tag` +- allocate fresh tile tags for rename-managed outputs +- record overwritten SMAP tags in `output_replaced_tile_tags` +- update SMAP to the new output mappings +- push the same `PTOInstRef` to `inst_out` + +## Cycle Behavior + +Per cycle: + +1. consume all visible retire notifications and recycle replaced tags +2. inspect the head instruction +3. if output queue has space and the free-tag set can satisfy all managed outputs: + - pop the instruction + - rename it + - push it forward +4. otherwise stall the head instruction in place + +## Limitations / Simplifications + +- single-width rename +- associative SMAP with no explicit capacity model +- no checkpoint/flush rollback yet +- rename domain is heuristic and currently local-tile-only by opcode role diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ROB.md b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ROB.md new file mode 100644 index 00000000..6ee3d04f --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ROB.md @@ -0,0 +1,215 @@ +# ROB + +This document describes the current `gfsim` backend reorder buffer +implementation in: + +- [rob.hpp](./rob.hpp) +- [rob.cpp](./rob.cpp) + +## Purpose + +The ROB is the current backend owner for `PTOInst` lifecycle state. + +Its responsibilities are: + +- accept `PTOInst` payloads from the backend input queue +- allocate ROB slots in program order +- track per-entry status +- issue instructions to rename +- send retire notifications to rename +- observe completion through an explicit completion-event queue +- retire completed head entries in order +- preserve parent-owned queue wiring rules from the modeling framework + +## Input / Output Contract + +The ROB module consumes exactly one boundary input queue and drives one boundary +output queue pair: + +- `INPUT(inst_in, 0)` of type `SimQueue` +- `INPUT(done_in, 1)` of type `SimQueue` +- `OUTPUT(rename_out, 0)` of type `SimQueue` +- `OUTPUT(retire_out, 1)` of type `SimQueue` + +`rename_out` carries issued instructions toward rename. `retire_out` carries +the retiring instruction reference to rename so overwritten tile tags can be +recycled. The completion input queue carries completed `PTOInstRef` packets +back from the wakeup/ready-table path. + +The ROB also keeps a `processed_` log of retired instructions for the top-level +simulator to report. + +## Parent Ownership + +The ROB follows the queue ownership rules from the model framework: + +- parent modules own interconnect queue storage +- child modules receive only queue pointers for bound ports + +In the current topology, `Core` owns the queue that connects frontend to +backend, and `ROB` consumes that queue through its input binding. + +## Parameterization + +ROB sizing comes from TOML config through `ROBConfig`: + +```toml +[rob] +entries = 64 +``` + +Current rules: + +- `entries` must be greater than zero +- `entries` must be a power of two + +These rules match the wrap-friendly ring-buffer discipline used in LinxCore. + +## Internal State + +The ROB owns: + +- `std::vector entries_` +- `head_` +- `tail_` +- `count_` +- `next_rid_` + +This is a classic ring-buffer structure: + +- `head_` points to the oldest live entry +- `tail_` points to the next free allocation slot +- `count_` tracks occupancy +- `next_rid_` assigns a monotonically increasing ROB id + +## ROBEntry + +Each `ROBEntry` contains: + +- `rid` +- `status` +- `inst` + +`inst` is a `std::shared_ptr`, so the ROB and downstream execution +pipeline can observe the same logical instruction object while the ROB retains +retirement ownership. + +## PTOInstRef Pop/Update/Push Rule + +ROB is one of the stages that must update runtime state when it pops and pushes +`PTOInstRef`. + +Current ownership points: + +- on alloc: + - pop from `inst_in` + - assign `rob_id` + - clear runtime transport state such as `engine_id` + - clear stage-latency / source-readiness runtime state + - stamp `timestamps.rob_alloc_cycle` + - set ROB entry status to `Alloc` +- on issue: + - pop no queue; inspect owned entry + - for no-engine instructions, mark immediate terminal status locally + - otherwise push the same `PTOInstRef` to `rename_out` +- on completion: + - pop completed `PTOInstRef` from `done_in` + - update ROB entry state by `rob_id` + - consume the completion timestamp already written by engine +- on retire: + - stamp `timestamps.rob_retire_cycle` + - push the same `PTOInstRef` to `retire_out` + - copy final `PTOInst` data into `processed_` + - free the ROB slot + +## Status Model + +Current entry states are: + +- `Free` +- `Alloc` +- `Issued` +- `Completed` +- `Resolved` +- `Exception` + +Intended meaning: + +- `Free`: slot is unallocated +- `Alloc`: instruction has entered the ROB but has not been issued +- `Issued`: instruction has been accepted by the backend execution path +- `Completed`: execution/writeback finished +- `Resolved`: instruction is ready to retire normally +- `Exception`: instruction reached an exceptional terminal state + +## Current Cycle Behavior + +Each cycle, the ROB does the following in order: + +1. consume at most one engine completion from `done_in` +2. retire the head entry if its status is `Completed` or `Exception` +3. allocate one new `PTOInst` from the input queue if space is available +4. issue one alloc-state entry toward rename if rename output space exists + +This is the current minimal Davinci model-top behavior: fetch one instruction +from the trace side when the ROB has space, place it in the ROB, wait a dummy +latency, and retire it in order. + +## Current Transition Policy + +The current status progression is: + +- `Free -> Alloc` +- `Alloc -> Issued` +- `Issued -> Completed` +- `Completed -> Resolved -> Free` + +`Exception` replaces `Completed` for unsupported or unknown instruction cases. +In the current simplified backend, `Resolved` is a transient retirement state +that is immediately cleared back to `Free` after the instruction is copied into +the processed log. + +## Retirement + +`RetireCompletedHead()` retires only from the head in order. + +Retirement behavior: + +- stop if the head entry is not `Completed` or `Exception` +- copy the observed `PTOInst` into `processed_` +- mark the entry as `Resolved` +- clear the entry back to `Free` +- advance `head_` +- decrement `count_` + +This preserves in-order retirement semantics. + +## Relation To LinxCore + +This ROB borrows the high-level structure from LinxCore: + +- power-of-two depth +- explicit head / tail / count bookkeeping +- vector-backed entry storage +- explicit per-entry state +- in-order head retirement + +It is still much simpler than the LinxCore backend ROB because it does not yet +model: + +- multi-alloc / multi-retire width +- distinct issue / execute / complete owner modules +- a dedicated completion payload separate from `PTOInstRef` +- traps with detailed payload propagation +- checkpoint / redirect / flush interactions +- wakeup, scoreboard, or memory ordering interactions + +## Expected Future Evolution + +Likely next steps: + +- split issue / completion / retire into richer multi-width stages +- add richer completion event payloads beyond the current minimal event +- add flush / redirect behavior +- add exception payload fields to `ROBEntry` +- expose ROB occupancy / state probes for trace and debugging diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/cube.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/cube.cpp new file mode 100644 index 00000000..30f0492d --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/cube.cpp @@ -0,0 +1,114 @@ +#include "backend/cube.hpp" + +namespace davincioo::backend { + +namespace { + +bool IsAccumulateLike(const PTOInst& inst) { + const std::string opcode = OpcodeName(inst); + return opcode == "TMATMUL_ACC" || opcode == "TMATMUL_MX_ACC" || + opcode == "TGEMV_ACC"; +} + +bool IsMxLike(const PTOInst& inst) { + const std::string opcode = OpcodeName(inst); + return opcode == "TMATMUL_MX" || opcode == "TMATMUL_MX_ACC" || opcode == "TMATMUL_MX_BIAS" || + opcode == "TGEMV_MX"; +} + +std::size_t AInputIndex(const PTOInst& inst) { + if (IsAccumulateLike(inst)) { + return IsMxLike(inst) ? 1 : 1; + } + return 0; +} + +std::size_t BInputIndex(const PTOInst& inst) { + if (IsMxLike(inst)) { + return IsAccumulateLike(inst) ? 3 : 2; + } + return IsAccumulateLike(inst) ? 2 : 1; +} + +bool IsCubeFamily(const PTOInst& inst) { + return inst.engine_kind == PTOEngineKind::Cube; +} + +} // namespace + +Cube::Cube(EngineConfig config, CubeCostConfig cost_config, std::string name) + : Engine(PTOEngineKind::Cube, config.count, std::move(name)), + cost_config_(cost_config) {} + +std::size_t Cube::MatmulInputBytes(const PTOInst& inst, std::size_t input_index) const { + if (input_index >= inst.tile_reg_inputs.size()) { + return 0; + } + return TileByteSizeCeil(inst.tile_reg_inputs[input_index]); +} + +std::size_t Cube::MatmulMacs(const PTOInst& inst) const { + const std::size_t a_index = AInputIndex(inst); + const std::size_t b_index = BInputIndex(inst); + if (a_index >= inst.tile_reg_inputs.size() || b_index >= inst.tile_reg_inputs.size()) { + return 0; + } + const auto& a = inst.tile_reg_inputs[a_index]; + const auto& b = inst.tile_reg_inputs[b_index]; + if (a.shape.size() < 2 || b.shape.size() < 2) { + return 0; + } + const std::size_t m = static_cast(a.shape[0]); + const std::size_t k = static_cast(a.shape[1]); + const std::size_t n = static_cast(b.shape[1]); + return m * n * k; +} + +std::size_t Cube::MatmulMacsPerCycle(const PTOInst& inst) const { + const std::size_t a_index = AInputIndex(inst); + if (a_index >= inst.tile_reg_inputs.size()) { + return cost_config_.macs_per_cycle_fp32; + } + switch (inst.tile_reg_inputs[a_index].dtype) { + case PTODType::kFloat16: + return cost_config_.macs_per_cycle_fp16; + case PTODType::kBFloat16: + return cost_config_.macs_per_cycle_bf16; + case PTODType::kFloat8: + return cost_config_.macs_per_cycle_fp8; + case PTODType::kInt8: + return cost_config_.macs_per_cycle_int8; + case PTODType::kFloat4: + return cost_config_.macs_per_cycle_fp4; + case PTODType::kFloat32: + case PTODType::kUnknown: + default: + return cost_config_.macs_per_cycle_fp32; + } +} + +std::size_t Cube::LookupLatency(const PTOInst& inst) const { + if (IsCubeFamily(inst)) { + const std::size_t a_index = AInputIndex(inst); + const std::size_t b_index = BInputIndex(inst); + const std::size_t a_bytes = MatmulInputBytes(inst, a_index); + const std::size_t b_bytes = MatmulInputBytes(inst, b_index); + const std::size_t load_cycles = + CeilDiv(a_bytes + b_bytes, cost_config_.input_bandwidth_bytes_per_cycle); + const std::size_t compute_cycles = + CeilDiv(MatmulMacs(inst), MatmulMacsPerCycle(inst)); + const std::size_t base_cycles = + cost_config_.overlap_mode + ? CeilDiv(a_bytes, cost_config_.input_bandwidth_bytes_per_cycle) + + std::max(CeilDiv(b_bytes, cost_config_.input_bandwidth_bytes_per_cycle), compute_cycles) + : load_cycles + compute_cycles; + return base_cycles + (IsAccumulateLike(inst) ? cost_config_.accumulate_extra_cycles : 0); + } + switch (inst.opcode) { + case PTOOpcode::Unknown: + default: + return cost_config_.unknown_latency; + } +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/cube.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/cube.hpp new file mode 100644 index 00000000..f4739bff --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/cube.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "backend/engine.hpp" +#include "davincioo/model/config.hpp" + +namespace davincioo::backend { + +class Cube : public Engine { +public: + Cube(EngineConfig config, CubeCostConfig cost_config, std::string name); + +private: + std::size_t LookupLatency(const PTOInst& inst) const override; + std::size_t MatmulMacs(const PTOInst& inst) const; + std::size_t MatmulInputBytes(const PTOInst& inst, std::size_t input_index) const; + std::size_t MatmulMacsPerCycle(const PTOInst& inst) const; + + CubeCostConfig cost_config_{}; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/dispatch.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/dispatch.cpp new file mode 100644 index 00000000..d5d67c9f --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/dispatch.cpp @@ -0,0 +1,102 @@ +#include "backend/dispatch.hpp" + +namespace davincioo::backend { + +Dispatch::Dispatch(std::size_t scalar_count, std::size_t vec_count, std::size_t cube_count, std::size_t tma_count) + : Module("dispatch"), + scalar_count_(scalar_count), + vec_count_(vec_count), + cube_count_(cube_count), + tma_count_(tma_count) {} + +void Dispatch::BuildSelf() { + GFSIM_ASSERT(scalar_count_ + vec_count_ + cube_count_ + tma_count_ > 0); +} + +void Dispatch::ResetSelf() { + vec_rr_ = 0; + cube_rr_ = 0; + tma_rr_ = 0; +} + +std::size_t Dispatch::SelectOutputIndex(PTOEngineKind kind) { + switch (kind) { + case PTOEngineKind::Scalar: { + if (scalar_count_ == 0) { + return static_cast(-1); + } + return 0; + } + case PTOEngineKind::Vec: { + if (vec_count_ == 0) { + return static_cast(-1); + } + const std::size_t output = 1 + (vec_rr_ % vec_count_); + vec_rr_ = (vec_rr_ + 1) % vec_count_; + return output; + } + case PTOEngineKind::Cube: { + if (cube_count_ == 0) { + return static_cast(-1); + } + const std::size_t output = 1 + vec_count_ + (cube_rr_ % cube_count_); + cube_rr_ = (cube_rr_ + 1) % cube_count_; + return output; + } + case PTOEngineKind::Tma: { + if (tma_count_ == 0) { + return static_cast(-1); + } + const std::size_t output = 1 + vec_count_ + cube_count_ + (tma_rr_ % tma_count_); + tma_rr_ = (tma_rr_ + 1) % tma_count_; + return output; + } + default: + return 0; + } +} + +void Dispatch::SetDispatchWidth(std::size_t width) { + GFSIM_ASSERT(width > 0); + dispatch_width_ = width; +} + +bool Dispatch::DispatchOne() { + INPUT(inst_in, 0); + if (inst_in->Empty()) { + return false; + } + PTOInstRef inst = inst_in->Front(); + GFSIM_ASSERT(inst != nullptr); + const std::size_t output_index = SelectOutputIndex(inst->engine_kind); + if (output_index == static_cast(-1)) { + inst->dispatched = true; + inst->runtime_latency = 0; + inst->timestamps.dispatch_cycle = CurrentCycle(); + inst_in->Pop(); + MarkProgress(); + return true; + } + + OUTPUT(out0, output_index); + if (out0->Full()) { + return false; + } + + PTOInstRef dispatched = inst_in->Read(); + dispatched->dispatched = true; + dispatched->timestamps.dispatch_cycle = CurrentCycle(); + out0->Write(dispatched); + MarkProgress(); + return true; +} + +void Dispatch::WorkSelf() { + for (std::size_t i = 0; i < dispatch_width_; ++i) { + if (!DispatchOne()) { + break; + } + } +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/dispatch.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/dispatch.hpp new file mode 100644 index 00000000..bd5694ed --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/dispatch.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo::backend { + +class Dispatch : public Module { +public: + Dispatch(std::size_t scalar_count, std::size_t vec_count, std::size_t cube_count, std::size_t tma_count); + + void SetDispatchWidth(std::size_t width); + std::size_t DispatchWidth() const noexcept { return dispatch_width_; } + +protected: + void BuildSelf() override; + void ResetSelf() override; + void WorkSelf() override; + +private: + bool DispatchOne(); + std::size_t SelectOutputIndex(PTOEngineKind kind); + + std::size_t scalar_count_ = 0; + std::size_t vec_count_ = 0; + std::size_t cube_count_ = 0; + std::size_t tma_count_ = 0; + std::size_t vec_rr_ = 0; + std::size_t cube_rr_ = 0; + std::size_t tma_rr_ = 0; + std::size_t dispatch_width_ = 1; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/engine.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/engine.cpp new file mode 100644 index 00000000..2398371f --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/engine.cpp @@ -0,0 +1,151 @@ +#include "backend/engine.hpp" + +#include + +namespace davincioo::backend { + +Engine::Engine(PTOEngineKind kind, std::size_t engine_count, std::string name) + : SimObject(std::move(name)), + kind_(kind), + engine_count_(engine_count), + execute_q_(1, 1, Name() + "_execute_q") { + const auto pos = Name().find_last_not_of("0123456789"); + if (pos != std::string::npos && pos + 1 < Name().size()) { + engine_id_ = static_cast(std::stoull(Name().substr(pos + 1))); + } +} + +void Engine::BindIssuedInput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + issued_in_ = queue; +} + +void Engine::BindCompletedOutput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + completed_out_ = queue; +} + +PTOEngineKind Engine::Kind() const { + return kind_; +} + +std::size_t Engine::Capacity() const { + return 1; +} + +std::size_t Engine::Count() const { + return execute_q_.Occupancy(); +} + +std::size_t Engine::DTypeBits(PTODType dtype) { + switch (dtype) { + case PTODType::kBool: + return 8; + case PTODType::kInt8: + return 8; + case PTODType::kInt32: + return 32; + case PTODType::kInt64: + return 64; + case PTODType::kUInt64: + return 64; + case PTODType::kFloat16: + case PTODType::kBFloat16: + return 16; + case PTODType::kFloat32: + return 32; + case PTODType::kFloat8: + return 8; + case PTODType::kFloat4: + return 4; + case PTODType::kUnknown: + default: + return 32; + } +} + +std::size_t Engine::CeilDiv(std::size_t num, std::size_t den) { + GFSIM_ASSERT(den != 0); + return (num + den - 1) / den; +} + +std::size_t Engine::TileElementCount(const PTOTileReg& reg) { + if (reg.shape.empty()) { + return 0; + } + std::size_t elements = 1; + for (const std::int64_t dim : reg.shape) { + if (dim <= 0) { + return 0; + } + elements *= static_cast(dim); + } + return elements; +} + +std::size_t Engine::TileByteSizeCeil(const PTOTileReg& reg) { + const std::size_t elements = TileElementCount(reg); + const std::size_t bits = DTypeBits(reg.dtype); + return CeilDiv(elements * bits, 8); +} + +void Engine::Build() { + GFSIM_ASSERT(engine_count_ > 0); + GFSIM_ASSERT(issued_in_ != nullptr); + GFSIM_ASSERT(completed_out_ != nullptr); + execute_q_.Build(); +} + +void Engine::Reset() { + execute_q_.Reset(); +} + +void Engine::Work() { + GFSIM_ASSERT(issued_in_ != nullptr); + GFSIM_ASSERT(completed_out_ != nullptr); + execute_q_.SetCurrentCycle(this->CurrentCycle()); + execute_q_.Work(); + this->MarkProgress(execute_q_.ConsumeProgress()); + + if (!execute_q_.Empty()) { + if (completed_out_->Full()) { + return; + } + PTOInstRef completed = execute_q_.Read(); + GFSIM_ASSERT(completed != nullptr); + completed->runtime_latency = 0; + completed->timestamps.engine_complete_cycle = this->CurrentCycle(); + completed_out_->Write(completed); + this->MarkProgress(); + } + + if (issued_in_->Empty()) { + return; + } + + const PTOInstRef& head = issued_in_->Front(); + GFSIM_ASSERT(head != nullptr); + if (execute_q_.Full()) { + return; + } + + PTOInstRef inst = issued_in_->Read(); + GFSIM_ASSERT(inst != nullptr); + // Clamp zero-byte tile latencies (e.g. valid_row=0 after batch padding) + // to one cycle so the engine still progresses through the SimQueue. The + // cost-model returns 0 when bytes / bandwidth + overhead is 0; that is + // a legal pypto codegen pattern, not a config bug. + const std::size_t latency = std::max(LookupLatency(*inst), 1); + inst->engine_id = engine_id_; + inst->runtime_latency = latency; + inst->timestamps.engine_pop_cycle = this->CurrentCycle(); + execute_q_.SetLatency(static_cast(latency)); + execute_q_.Write(inst); + this->MarkProgress(); +} + +void Engine::Xfer() { + execute_q_.Xfer(); +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/engine.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/engine.hpp new file mode 100644 index 00000000..91319f98 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/engine.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" +namespace davincioo::backend { + +class Engine : public SimObject { +public: + Engine(PTOEngineKind kind, std::size_t engine_count, std::string name); + virtual ~Engine() = default; + + void BindIssuedInput(SimQueue* queue); + void BindCompletedOutput(SimQueue* queue); + + PTOEngineKind Kind() const; + std::size_t Capacity() const; + std::size_t Count() const; + + void Build() override; + void Reset() override; + void Work() override; + void Xfer() override; + +protected: + static std::size_t TileElementCount(const PTOTileReg& reg); + static std::size_t TileByteSizeCeil(const PTOTileReg& reg); + static std::size_t CeilDiv(std::size_t num, std::size_t den); + static std::size_t DTypeBits(PTODType dtype); + + virtual std::size_t LookupLatency(const PTOInst& inst) const = 0; + + PTOEngineKind kind_ = PTOEngineKind::Scalar; + std::size_t engine_count_ = 0; + std::uint64_t engine_id_ = 0; + SimQueue* issued_in_ = nullptr; + SimQueue* completed_out_ = nullptr; + SimQueue execute_q_; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/issue_queue.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/issue_queue.cpp new file mode 100644 index 00000000..3d67206a --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/issue_queue.cpp @@ -0,0 +1,181 @@ +#include "backend/issue_queue.hpp" + +#include + +namespace davincioo::backend { + +IssueQueue::IssueQueue(IssueQueueConfig config, PTOEngineKind kind, std::string name) + : SimObject(std::move(name)), + config_(config), + kind_(kind) {} + +void IssueQueue::BindEnqueueInput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + enqueue_in_ = queue; +} + +void IssueQueue::BindWakeupInput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + wakeup_in_ = queue; +} + +void IssueQueue::BindIssuedOutput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + issued_out_ = queue; +} + +void IssueQueue::BindReadyChecker(std::function checker) { + ready_checker_ = std::move(checker); +} + +std::size_t IssueQueue::Count() const { + return entries_.size(); +} + +void IssueQueue::Build() { + GFSIM_ASSERT(config_.entries > 0); + GFSIM_ASSERT(enqueue_in_ != nullptr); + GFSIM_ASSERT(wakeup_in_ != nullptr); + GFSIM_ASSERT(issued_out_ != nullptr); + GFSIM_ASSERT(static_cast(ready_checker_)); +} + +void IssueQueue::Reset() { + entries_.clear(); +} + +bool IssueQueue::AllSourcesReady(const PTOInst& inst) const { + for (std::size_t index = 0; index < inst.tile_input_valid.size(); ++index) { + if (inst.tile_input_valid[index] && !inst.tile_input_ready[index]) { + return false; + } + } + return true; +} + +void IssueQueue::RefreshSourceReady(PTOInst& inst) const { + for (std::size_t index = 0; index < inst.tile_input_valid.size(); ++index) { + if (!inst.tile_input_valid[index]) { + inst.tile_input_ready[index] = true; + continue; + } + const auto& input = inst.tile_reg_inputs[index]; + if (!input.rename_managed || input.tile_tag == kInvalidTileTag) { + inst.tile_input_ready[index] = true; + continue; + } + inst.tile_input_ready[index] = ready_checker_(input.tile_tag); + } + inst.src_ready = AllSourcesReady(inst); +} + +bool IssueQueue::MatchesWakeupTag(const PTOInst& waiting, const PTOInst& completed) const { + for (const auto& produced : completed.tile_reg_outputs) { + if (!produced.rename_managed || produced.tile_tag == kInvalidTileTag) { + continue; + } + for (std::size_t index = 0; index < waiting.tile_reg_inputs.size(); ++index) { + if (!waiting.tile_input_valid[index] || waiting.tile_input_ready[index]) { + continue; + } + const auto& input = waiting.tile_reg_inputs[index]; + if (input.rename_managed && input.tile_tag == produced.tile_tag) { + return true; + } + } + } + return false; +} + +void IssueQueue::ApplyWakeup(const PTOInstRef& completed) { + GFSIM_ASSERT(completed != nullptr); + for (auto& entry : entries_) { + GFSIM_ASSERT(entry != nullptr); + for (const auto& produced : completed->tile_reg_outputs) { + if (!produced.rename_managed || produced.tile_tag == kInvalidTileTag) { + continue; + } + for (std::size_t index = 0; index < entry->tile_reg_inputs.size(); ++index) { + if (!entry->tile_input_valid[index] || entry->tile_input_ready[index]) { + continue; + } + const auto& input = entry->tile_reg_inputs[index]; + if (input.rename_managed && input.tile_tag == produced.tile_tag) { + entry->tile_input_ready[index] = true; + this->MarkProgress(); + } + } + } + entry->src_ready = AllSourcesReady(*entry); + } +} + +std::size_t IssueQueue::PickReadyIndex() const { + std::size_t selected = entries_.size(); + std::uint64_t best_rob_id = std::numeric_limits::max(); + for (std::size_t index = 0; index < entries_.size(); ++index) { + const auto& entry = entries_[index]; + if (entry == nullptr || !entry->src_ready) { + continue; + } + if (entry->rob_id < best_rob_id) { + best_rob_id = entry->rob_id; + selected = index; + } + } + return selected; +} + +void IssueQueue::Work() { + GFSIM_ASSERT(enqueue_in_ != nullptr); + GFSIM_ASSERT(wakeup_in_ != nullptr); + GFSIM_ASSERT(issued_out_ != nullptr); + + while (!wakeup_in_->Empty()) { + PTOInstRef completed = wakeup_in_->Read(); + ApplyWakeup(completed); + } + + if (!enqueue_in_->Empty() && entries_.size() < config_.entries) { + PTOInstRef inst = enqueue_in_->Read(); + GFSIM_ASSERT(inst != nullptr); + RefreshSourceReady(*inst); + entries_.push_back(inst); + this->MarkProgress(); + } + + if (issued_out_->Full()) { + return; + } + + const std::size_t pick_index = PickReadyIndex(); + if (pick_index == entries_.size()) { + return; + } + + PTOInstRef issued = entries_[pick_index]; + GFSIM_ASSERT(issued != nullptr); + issued->timestamps.issue_cycle = this->CurrentCycle(); + issued_out_->Write(issued); + entries_.erase(entries_.begin() + static_cast(pick_index)); + this->MarkProgress(); +} + +void IssueQueue::Xfer() {} + +std::string IssueQueue::DumpState() const { + std::ostringstream stream; + stream << Name() << "(count=" << entries_.size() << ")"; + for (std::size_t index = 0; index < entries_.size(); ++index) { + const auto& entry = entries_[index]; + if (entry == nullptr) { + continue; + } + stream << "\n slot=" << index + << " src_ready=" << (entry->src_ready ? 1 : 0) + << " inst={" << DumpPTOInst(*entry) << "}"; + } + return stream.str(); +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/issue_queue.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/issue_queue.hpp new file mode 100644 index 00000000..3b76e90a --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/issue_queue.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include "davincioo/model/config.hpp" +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo::backend { + +class IssueQueue : public SimObject { +public: + IssueQueue(IssueQueueConfig config, PTOEngineKind kind, std::string name); + + void BindEnqueueInput(SimQueue* queue); + void BindWakeupInput(SimQueue* queue); + void BindIssuedOutput(SimQueue* queue); + void BindReadyChecker(std::function checker); + + std::size_t Count() const; + + void Build() override; + void Reset() override; + void Work() override; + void Xfer() override; + std::string DumpState() const; + +private: + bool AllSourcesReady(const PTOInst& inst) const; + void RefreshSourceReady(PTOInst& inst) const; + bool MatchesWakeupTag(const PTOInst& waiting, const PTOInst& completed) const; + void ApplyWakeup(const PTOInstRef& completed); + std::size_t PickReadyIndex() const; + + IssueQueueConfig config_{}; + PTOEngineKind kind_ = PTOEngineKind::Scalar; + SimQueue* enqueue_in_ = nullptr; + SimQueue* wakeup_in_ = nullptr; + SimQueue* issued_out_ = nullptr; + std::function ready_checker_; + std::deque entries_; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ready_table.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ready_table.cpp new file mode 100644 index 00000000..85363a01 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ready_table.cpp @@ -0,0 +1,169 @@ +#include "backend/ready_table.hpp" + +#include + +namespace davincioo::backend { + +namespace { + +bool AllTileInputsReady(const PTOInst& inst) { + for (std::size_t index = 0; index < inst.tile_input_valid.size(); ++index) { + if (inst.tile_input_valid[index] && !inst.tile_input_ready[index]) { + return false; + } + } + return true; +} + +} // namespace + +ReadyTable::ReadyTable(std::size_t issue_queue_count) + : SimObject("ready_table"), + issue_queue_count_(issue_queue_count), + dispatch_inputs_(issue_queue_count, nullptr), + issue_enqueue_outputs_(issue_queue_count, nullptr), + issue_wakeup_outputs_(issue_queue_count, nullptr) {} + +void ReadyTable::BindDispatchInput(std::size_t index, SimQueue* queue) { + GFSIM_ASSERT(index < dispatch_inputs_.size()); + GFSIM_ASSERT(queue != nullptr); + dispatch_inputs_[index] = queue; +} + +void ReadyTable::BindIssueEnqueueOutput(std::size_t index, SimQueue* queue) { + GFSIM_ASSERT(index < issue_enqueue_outputs_.size()); + GFSIM_ASSERT(queue != nullptr); + issue_enqueue_outputs_[index] = queue; +} + +void ReadyTable::BindIssueWakeupOutput(std::size_t index, SimQueue* queue) { + GFSIM_ASSERT(index < issue_wakeup_outputs_.size()); + GFSIM_ASSERT(queue != nullptr); + issue_wakeup_outputs_[index] = queue; +} + +void ReadyTable::BindWakeupInput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + wakeup_in_ = queue; +} + +void ReadyTable::BindRobCompletionOutput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + rob_done_out_ = queue; +} + +bool ReadyTable::IsTagReady(std::uint64_t tag) const { + return ready_tags_.contains(tag); +} + +void ReadyTable::Build() { + GFSIM_ASSERT(wakeup_in_ != nullptr); + GFSIM_ASSERT(rob_done_out_ != nullptr); + for (std::size_t index = 0; index < issue_queue_count_; ++index) { + GFSIM_ASSERT(dispatch_inputs_[index] != nullptr); + GFSIM_ASSERT(issue_enqueue_outputs_[index] != nullptr); + GFSIM_ASSERT(issue_wakeup_outputs_[index] != nullptr); + } +} + +void ReadyTable::Reset() { + ready_tags_.clear(); +} + +void ReadyTable::InitializeReadyState(PTOInst& inst) { + inst.tile_input_valid.assign(inst.tile_reg_inputs.size(), false); + inst.tile_input_ready.assign(inst.tile_reg_inputs.size(), true); + for (std::size_t index = 0; index < inst.tile_reg_inputs.size(); ++index) { + const auto& reg = inst.tile_reg_inputs[index]; + if (!reg.rename_managed || reg.tile_tag == kInvalidTileTag) { + inst.tile_input_valid[index] = false; + inst.tile_input_ready[index] = true; + continue; + } + inst.tile_input_valid[index] = true; + inst.tile_input_ready[index] = ready_tags_.contains(reg.tile_tag); + } + inst.src_ready = AllTileInputsReady(inst); +} + +void ReadyTable::MarkOutputsNotReady(const PTOInst& inst) { + for (const auto& reg : inst.tile_reg_outputs) { + if (!reg.rename_managed || reg.tile_tag == kInvalidTileTag) { + continue; + } + ready_tags_.erase(reg.tile_tag); + } +} + +void ReadyTable::MarkOutputsReady(const PTOInst& inst) { + for (const auto& reg : inst.tile_reg_outputs) { + if (!reg.rename_managed || reg.tile_tag == kInvalidTileTag) { + continue; + } + ready_tags_.insert(reg.tile_tag); + } +} + +bool ReadyTable::CanBroadcastWakeup() const { + if (rob_done_out_->Full()) { + return false; + } + for (auto* queue : issue_wakeup_outputs_) { + if (queue->Full()) { + return false; + } + } + return true; +} + +void ReadyTable::Work() { + if (!wakeup_in_->Empty()) { + PTOInstRef completed = wakeup_in_->Front(); + GFSIM_ASSERT(completed != nullptr); + if (!CanBroadcastWakeup()) { + return; + } + completed = wakeup_in_->Read(); + MarkOutputsReady(*completed); + for (auto* queue : issue_wakeup_outputs_) { + queue->Write(completed); + } + rob_done_out_->Write(completed); + this->MarkProgress(); + } + + for (std::size_t index = 0; index < issue_queue_count_; ++index) { + auto* dispatch_in = dispatch_inputs_[index]; + auto* enqueue_out = issue_enqueue_outputs_[index]; + if (dispatch_in->Empty() || enqueue_out->Full()) { + continue; + } + + PTOInstRef inst = dispatch_in->Front(); + GFSIM_ASSERT(inst != nullptr); + inst = dispatch_in->Read(); + InitializeReadyState(*inst); + MarkOutputsNotReady(*inst); + enqueue_out->Write(inst); + this->MarkProgress(); + } +} + +void ReadyTable::Xfer() {} + +std::string ReadyTable::DumpState() const { + std::ostringstream stream; + stream << "ready_table(tags={"; + bool first = true; + for (const std::uint64_t tag : ready_tags_) { + if (!first) { + stream << ","; + } + first = false; + stream << tag; + } + stream << "})"; + return stream.str(); +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ready_table.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ready_table.hpp new file mode 100644 index 00000000..f429645d --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/ready_table.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo::backend { + +class ReadyTable : public SimObject { +public: + explicit ReadyTable(std::size_t issue_queue_count); + + void BindDispatchInput(std::size_t index, SimQueue* queue); + void BindIssueEnqueueOutput(std::size_t index, SimQueue* queue); + void BindIssueWakeupOutput(std::size_t index, SimQueue* queue); + void BindWakeupInput(SimQueue* queue); + void BindRobCompletionOutput(SimQueue* queue); + bool IsTagReady(std::uint64_t tag) const; + + void Build() override; + void Reset() override; + void Work() override; + void Xfer() override; + std::string DumpState() const; + +private: + void InitializeReadyState(PTOInst& inst); + void MarkOutputsNotReady(const PTOInst& inst); + void MarkOutputsReady(const PTOInst& inst); + bool CanBroadcastWakeup() const; + + std::size_t issue_queue_count_ = 0; + std::vector*> dispatch_inputs_; + std::vector*> issue_enqueue_outputs_; + std::vector*> issue_wakeup_outputs_; + SimQueue* wakeup_in_ = nullptr; + SimQueue* rob_done_out_ = nullptr; + std::set ready_tags_; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rename.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rename.cpp new file mode 100644 index 00000000..bf3bc2a1 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rename.cpp @@ -0,0 +1,183 @@ +#include "backend/rename.hpp" + +namespace davincioo::backend { + +namespace { + +bool IsScalarOnlyOp(const PTOInst& inst) { + return inst.engine_kind == PTOEngineKind::Scalar; +} + +bool IsLoadLike(const PTOInst& inst) { + return inst.engine_kind == PTOEngineKind::Tma && inst.raw_opcode == "TLOAD"; +} + +bool IsStoreLike(const PTOInst& inst) { + return inst.engine_kind == PTOEngineKind::Tma && + (inst.raw_opcode == "TSTORE" || inst.raw_opcode == "TSTORE_FP"); +} + +} // namespace + +Rename::Rename(RenameConfig config) + : Module("rename"), + config_(config) {} + +void Rename::BuildSelf() { + GFSIM_ASSERT(config_.tile_tags > 0); + GFSIM_ASSERT(inputs_.size() > 1); + GFSIM_ASSERT(inputs_[0] != nullptr); + GFSIM_ASSERT(inputs_[1] != nullptr); + GFSIM_ASSERT(outputs_.size() > 0); + GFSIM_ASSERT(outputs_[0] != nullptr); +} + +void Rename::ResetSelf() { + free_tile_tags_.clear(); + smap_.clear(); + for (std::uint64_t tag = 0; tag < config_.tile_tags; ++tag) { + free_tile_tags_.insert(tag); + } +} + +bool Rename::IsManagedInput(const PTOInst& inst, std::size_t) const { + if (IsScalarOnlyOp(inst) || IsLoadLike(inst)) { + return false; + } + return true; +} + +bool Rename::IsManagedOutput(const PTOInst& inst, std::size_t) const { + if (IsScalarOnlyOp(inst) || IsStoreLike(inst)) { + return false; + } + return true; +} + +std::size_t Rename::RequiredOutputTags(const PTOInst& inst) const { + std::size_t count = 0; + for (std::size_t index = 0; index < inst.tile_reg_outputs.size(); ++index) { + if (IsManagedOutput(inst, index)) { + ++count; + } + } + return count; +} + +bool Rename::HasEnoughFreeTags(const PTOInst& inst) const { + return free_tile_tags_.size() >= RequiredOutputTags(inst); +} + +void Rename::RecycleRetiredInstruction(const PTOInstRef& inst) { + GFSIM_ASSERT(inst != nullptr); + for (const std::uint64_t tag : inst->output_replaced_tile_tags) { + if (tag == kInvalidTileTag) { + continue; + } + const auto [_, inserted] = free_tile_tags_.insert(tag); + GFSIM_ASSERT(inserted); + MarkProgress(); + } +} + +std::uint64_t Rename::AllocateTileTag() { + GFSIM_ASSERT(!free_tile_tags_.empty()); + const auto it = free_tile_tags_.begin(); + const std::uint64_t tag = *it; + free_tile_tags_.erase(it); + return tag; +} + +Rename::TileStorageKey Rename::MakeTileStorageKey(const PTOTileReg& reg) { + return TileStorageKey{ + .tile_type = reg.tile_type, + .address = reg.address, + }; +} + +void Rename::RenameInstruction(PTOInst& inst) { + inst.timestamps.rename_cycle = CurrentCycle(); + inst.output_replaced_tile_tags.assign(inst.tile_reg_outputs.size(), kInvalidTileTag); + inst.tile_input_valid.assign(inst.tile_reg_inputs.size(), false); + inst.tile_input_ready.assign(inst.tile_reg_inputs.size(), true); + inst.src_ready = false; + + for (std::size_t index = 0; index < inst.tile_reg_inputs.size(); ++index) { + PTOTileReg& reg = inst.tile_reg_inputs[index]; + reg.rename_managed = IsManagedInput(inst, index); + if (!reg.rename_managed) { + reg.tile_tag = kInvalidTileTag; + inst.tile_input_valid[index] = false; + inst.tile_input_ready[index] = true; + continue; + } + const auto it = smap_.find(MakeTileStorageKey(reg)); + if (it == smap_.end()) { + // No in-trace producer for this tile-storage key. This is legitimate + // for pypto-emitted kernels whose loop-carried tiles are first + // referenced as reads (e.g. RMSNorm broadcasts a row-reduced tile + // whose storage key differs subtly from the TASSIGN that allocated + // it). Rather than asserting on what is really "external input" + // semantics, mark the tile as already-ready and let the cycle model + // proceed. + reg.rename_managed = false; + reg.tile_tag = kInvalidTileTag; + inst.tile_input_valid[index] = false; + inst.tile_input_ready[index] = true; + continue; + } + reg.tile_tag = it->second; + inst.tile_input_valid[index] = true; + inst.tile_input_ready[index] = false; + } + + for (std::size_t index = 0; index < inst.tile_reg_outputs.size(); ++index) { + PTOTileReg& reg = inst.tile_reg_outputs[index]; + reg.rename_managed = IsManagedOutput(inst, index); + if (!reg.rename_managed) { + reg.tile_tag = kInvalidTileTag; + inst.output_replaced_tile_tags[index] = kInvalidTileTag; + continue; + } + + const TileStorageKey key = MakeTileStorageKey(reg); + const auto replaced = smap_.find(key); + inst.output_replaced_tile_tags[index] = replaced == smap_.end() ? kInvalidTileTag : replaced->second; + + const std::uint64_t new_tag = AllocateTileTag(); + reg.tile_tag = new_tag; + smap_[key] = new_tag; + MarkProgress(); + } +} + +void Rename::SetRenameWidth(std::size_t width) { + GFSIM_ASSERT(width > 0); + rename_width_ = width; +} + +void Rename::WorkSelf() { + INPUT(inst_in, 0); + INPUT(retire_in, 1); + OUTPUT(inst_out, 0); + + while (!retire_in->Empty()) { + PTOInstRef retired = retire_in->Read(); + RecycleRetiredInstruction(retired); + } + std::size_t renamed_count = 0; + while (renamed_count < rename_width_ && !inst_in->Empty() && !inst_out->Full()) { + PTOInstRef head = inst_in->Front(); + GFSIM_ASSERT(head != nullptr); + if (!HasEnoughFreeTags(*head)) { + break; + } + PTOInstRef renamed = inst_in->Read(); + RenameInstruction(*renamed); + inst_out->Write(renamed); + MarkProgress(); + ++renamed_count; + } +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rename.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rename.hpp new file mode 100644 index 00000000..924279f9 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rename.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include "davincioo/model/config.hpp" +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo::backend { + +class Rename : public Module { +public: + explicit Rename(RenameConfig config); + + void SetRenameWidth(std::size_t width); + std::size_t RenameWidth() const noexcept { return rename_width_; } + +protected: + void BuildSelf() override; + void ResetSelf() override; + void WorkSelf() override; + +private: + struct TileStorageKey { + PTOTileType tile_type = PTOTileType::Unknown; + std::uint64_t address = 0; + + bool operator==(const TileStorageKey& other) const noexcept { + return tile_type == other.tile_type && address == other.address; + } + }; + + struct TileStorageKeyHash { + std::size_t operator()(const TileStorageKey& key) const noexcept { + return (static_cast(key.address) << 4) ^ static_cast(key.tile_type); + } + }; + + bool IsManagedInput(const PTOInst& inst, std::size_t input_index) const; + bool IsManagedOutput(const PTOInst& inst, std::size_t output_index) const; + std::size_t RequiredOutputTags(const PTOInst& inst) const; + bool HasEnoughFreeTags(const PTOInst& inst) const; + void RecycleRetiredInstruction(const PTOInstRef& inst); + void RenameInstruction(PTOInst& inst); + std::uint64_t AllocateTileTag(); + static TileStorageKey MakeTileStorageKey(const PTOTileReg& reg); + + RenameConfig config_{}; + std::set free_tile_tags_; + std::unordered_map smap_; + std::size_t rename_width_ = 1; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rob.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rob.cpp new file mode 100644 index 00000000..cfea1892 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rob.cpp @@ -0,0 +1,238 @@ +#include "backend/rob.hpp" + +#include + +namespace davincioo::backend { + +namespace { + +bool ShouldRaiseException(const PTOInst& inst) { + return inst.opcode == PTOOpcode::Unknown; +} + +} // namespace + +ROB::ROB(ROBConfig config) + : SimObject("rob"), + config_(config) {} + +void ROB::BindInstInput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + inst_in_ = queue; +} + +void ROB::BindCompletionInput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + done_in_ = queue; +} + +void ROB::BindRenameOutput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + rename_out_ = queue; +} + +void ROB::BindRetireOutput(SimQueue* queue) { + GFSIM_ASSERT(queue != nullptr); + retire_out_ = queue; +} + +const std::vector& ROB::Processed() const { + return processed_; +} + +const std::vector& ROB::Entries() const { + return entries_; +} + +std::string ROB::DumpState() const { + std::ostringstream stream; + stream << "ROB(head=" << head_ + << ", tail=" << tail_ + << ", count=" << count_ + << ", capacity=" << entries_.size() << ")"; + for (std::size_t index = 0; index < entries_.size(); ++index) { + const auto& entry = entries_[index]; + if (entry.status == ROBEntryStatus::Free || entry.inst == nullptr) { + continue; + } + stream << "\n slot=" << index + << " rid=" << entry.rid + << " status=" << ToString(entry.status) + << " inst={" << DumpPTOInst(*entry.inst) << "}"; + } + return stream.str(); +} + +std::size_t ROB::Capacity() const { + return entries_.size(); +} + +std::size_t ROB::Count() const { + return count_; +} + +bool ROB::Full() const { + return count_ >= entries_.size(); +} + +bool ROB::Empty() const { + return count_ == 0; +} + +void ROB::Build() { + GFSIM_ASSERT(config_.entries > 0); + GFSIM_ASSERT((config_.entries & (config_.entries - 1)) == 0); + GFSIM_ASSERT(inst_in_ != nullptr); + GFSIM_ASSERT(done_in_ != nullptr); + GFSIM_ASSERT(rename_out_ != nullptr); + GFSIM_ASSERT(retire_out_ != nullptr); + entries_.assign(config_.entries, ROBEntry{}); +} + +void ROB::Reset() { + processed_.clear(); + for (ROBEntry& entry : entries_) { + entry.rid = 0; + entry.status = ROBEntryStatus::Free; + entry.inst.reset(); + } + head_ = 0; + tail_ = 0; + count_ = 0; + next_rid_ = 0; +} + +bool ROB::CanAlloc() const { + return count_ < entries_.size(); +} + +void ROB::Allocate(PTOInstRef inst) { + GFSIM_ASSERT(inst != nullptr); + GFSIM_ASSERT(CanAlloc()); + ROBEntry& entry = entries_[tail_]; + GFSIM_ASSERT(entry.status == ROBEntryStatus::Free); + + entry.rid = next_rid_++; + entry.status = ROBEntryStatus::Alloc; + entry.inst = std::move(inst); + entry.inst->rob_id = entry.rid; + entry.inst->engine_id = std::numeric_limits::max(); + entry.inst->dispatched = false; + entry.inst->src_ready = false; + entry.inst->runtime_latency = 0; + entry.inst->timestamps.rob_alloc_cycle = this->CurrentCycle(); + entry.inst->timestamps.rename_cycle = kUnsetCycleStamp; + entry.inst->timestamps.dispatch_cycle = kUnsetCycleStamp; + entry.inst->timestamps.engine_pop_cycle = kUnsetCycleStamp; + entry.inst->timestamps.engine_complete_cycle = kUnsetCycleStamp; + entry.inst->timestamps.rob_retire_cycle = kUnsetCycleStamp; + entry.inst->output_replaced_tile_tags.assign(entry.inst->tile_reg_outputs.size(), kInvalidTileTag); + for (auto& reg : entry.inst->tile_reg_inputs) { + reg.tile_tag = kInvalidTileTag; + reg.rename_managed = false; + } + for (auto& reg : entry.inst->tile_reg_outputs) { + reg.tile_tag = kInvalidTileTag; + reg.rename_managed = false; + } + + tail_ = (tail_ + 1) % entries_.size(); + ++count_; + this->MarkProgress(); +} + +void ROB::ConsumeCompletion(const PTOInstRef& inst) { + GFSIM_ASSERT(inst != nullptr); + for (ROBEntry& entry : entries_) { + if (entry.status != ROBEntryStatus::Issued || entry.inst == nullptr) { + continue; + } + if (entry.rid != inst->rob_id) { + continue; + } + entry.status = ShouldRaiseException(*inst) ? ROBEntryStatus::Exception : ROBEntryStatus::Completed; + entry.inst->runtime_latency = 0; + entry.inst->timestamps.engine_complete_cycle = inst->timestamps.engine_complete_cycle; + this->MarkProgress(); + return; + } +} + +void ROB::AllocateMultipleEntries(std::size_t max_count) { + std::size_t allocated = 0; + while (allocated < max_count && !inst_in_->Empty() && CanAlloc()) { + Allocate(inst_in_->Read()); + ++allocated; + } +} + +void ROB::IssueMultipleEntries(std::size_t max_count) { + std::size_t issued = 0; + for (std::size_t offset = 0; offset < entries_.size() && issued < max_count; ++offset) { + const std::size_t idx = (head_ + offset) % entries_.size(); + ROBEntry& entry = entries_[idx]; + if (entry.status != ROBEntryStatus::Alloc || entry.inst == nullptr) { + continue; + } + if (rename_out_->Full()) { + return; + } + + rename_out_->Write(entry.inst); + entry.status = ROBEntryStatus::Issued; + this->MarkProgress(); + ++issued; + } +} + +void ROB::SetAllocWidth(std::size_t width) { + GFSIM_ASSERT(width > 0); + alloc_width_ = width; +} + +void ROB::SetIssueWidth(std::size_t width) { + GFSIM_ASSERT(width > 0); + issue_width_ = width; +} + +void ROB::RetireHead() { + while (count_ > 0) { + ROBEntry& entry = entries_[head_]; + if (entry.status != ROBEntryStatus::Completed && entry.status != ROBEntryStatus::Exception) { + break; + } + if (entry.inst != nullptr && retire_out_->Full()) { + break; + } + + if (entry.inst != nullptr) { + entry.inst->timestamps.rob_retire_cycle = this->CurrentCycle(); + retire_out_->Write(entry.inst); + processed_.push_back(*entry.inst); + entry.inst.reset(); + } + entry.status = ROBEntryStatus::Resolved; + entry.status = ROBEntryStatus::Free; + head_ = (head_ + 1) % entries_.size(); + --count_; + this->MarkProgress(); + } +} + +void ROB::Work() { + GFSIM_ASSERT(inst_in_ != nullptr); + GFSIM_ASSERT(done_in_ != nullptr); + GFSIM_ASSERT(rename_out_ != nullptr); + GFSIM_ASSERT(retire_out_ != nullptr); + + if (!done_in_->Empty()) { + ConsumeCompletion(done_in_->Read()); + } + RetireHead(); + AllocateMultipleEntries(alloc_width_); + IssueMultipleEntries(issue_width_); +} + +void ROB::Xfer() {} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rob.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rob.hpp new file mode 100644 index 00000000..690d78c7 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/rob.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include + +#include "davincioo/model/config.hpp" +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" +#include "davincioo/model/rob.hpp" + +namespace davincioo::backend { + +class ROB : public SimObject { +public: + explicit ROB(ROBConfig config); + + void BindInstInput(SimQueue* queue); + void BindCompletionInput(SimQueue* queue); + void BindRenameOutput(SimQueue* queue); + void BindRetireOutput(SimQueue* queue); + + const std::vector& Processed() const; + const std::vector& Entries() const; + std::string DumpState() const; + std::size_t Capacity() const; + std::size_t Count() const; + bool Full() const; + bool Empty() const; + + void Build() override; + void Reset() override; + void Work() override; + void Xfer() override; + + void SetAllocWidth(std::size_t width); + void SetIssueWidth(std::size_t width); + std::size_t AllocWidth() const noexcept { return alloc_width_; } + std::size_t IssueWidth() const noexcept { return issue_width_; } + +private: + bool CanAlloc() const; + void Allocate(PTOInstRef inst); + void ConsumeCompletion(const PTOInstRef& inst); + void AllocateMultipleEntries(std::size_t max_count); + void IssueMultipleEntries(std::size_t max_count); + void RetireHead(); + + ROBConfig config_; + SimQueue* inst_in_ = nullptr; + SimQueue* done_in_ = nullptr; + SimQueue* rename_out_ = nullptr; + SimQueue* retire_out_ = nullptr; + std::vector entries_; + std::vector processed_; + std::size_t head_ = 0; + std::size_t tail_ = 0; + std::size_t count_ = 0; + std::uint64_t next_rid_ = 0; + std::size_t alloc_width_ = 1; + std::size_t issue_width_ = 1; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/scalar.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/scalar.cpp new file mode 100644 index 00000000..38d486cf --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/scalar.cpp @@ -0,0 +1,19 @@ +#include "backend/scalar.hpp" + +namespace davincioo::backend { + +Scalar::Scalar(EngineConfig config, ScalarCostConfig cost_config, std::string name) + : Engine(PTOEngineKind::Scalar, config.count, std::move(name)), + cost_config_(cost_config) {} + +std::size_t Scalar::LookupLatency(const PTOInst& inst) const { + switch (inst.opcode) { + case PTOOpcode::TASSIGN: + return cost_config_.tassign_latency; + case PTOOpcode::Unknown: + default: + return cost_config_.unknown_latency != 0 ? cost_config_.unknown_latency : cost_config_.default_latency; + } +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/scalar.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/scalar.hpp new file mode 100644 index 00000000..361058c0 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/scalar.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "backend/engine.hpp" +#include "davincioo/model/config.hpp" + +namespace davincioo::backend { + +class Scalar : public Engine { +public: + Scalar(EngineConfig config, ScalarCostConfig cost_config, std::string name); + +private: + std::size_t LookupLatency(const PTOInst& inst) const override; + + ScalarCostConfig cost_config_{}; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/tma.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/tma.cpp new file mode 100644 index 00000000..b206af9f --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/tma.cpp @@ -0,0 +1,29 @@ +#include "backend/tma.hpp" + +namespace davincioo::backend { + +Tma::Tma(EngineConfig config, TmaCostConfig cost_config, std::string name) + : Engine(PTOEngineKind::Tma, config.count, std::move(name)), + cost_config_(cost_config) {} + +std::size_t Tma::LookupLatency(const PTOInst& inst) const { + switch (inst.opcode) { + case PTOOpcode::TLOAD: { + const auto& source = inst.tile_reg_inputs.empty() ? PTOTileReg{} : inst.tile_reg_inputs.front(); + return CeilDiv(TileByteSizeCeil(source), cost_config_.bandwidth_bytes_per_cycle) + cost_config_.load_overhead_cycles; + } + case PTOOpcode::TSTORE: { + const auto& source = inst.tile_reg_inputs.empty() ? PTOTileReg{} : inst.tile_reg_inputs.front(); + return CeilDiv(TileByteSizeCeil(source), cost_config_.bandwidth_bytes_per_cycle) + cost_config_.store_overhead_cycles; + } + case PTOOpcode::TSTORE_FP: { + const auto& source = inst.tile_reg_inputs.empty() ? PTOTileReg{} : inst.tile_reg_inputs.front(); + return CeilDiv(TileByteSizeCeil(source), cost_config_.bandwidth_bytes_per_cycle) + cost_config_.store_overhead_cycles; + } + case PTOOpcode::Unknown: + default: + return cost_config_.unknown_latency; + } +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/tma.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/tma.hpp new file mode 100644 index 00000000..6486d5cc --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/tma.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "backend/engine.hpp" +#include "davincioo/model/config.hpp" + +namespace davincioo::backend { + +class Tma : public Engine { +public: + Tma(EngineConfig config, TmaCostConfig cost_config, std::string name); + +private: + std::size_t LookupLatency(const PTOInst& inst) const override; + + TmaCostConfig cost_config_{}; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/vector.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/vector.cpp new file mode 100644 index 00000000..5c011216 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/vector.cpp @@ -0,0 +1,84 @@ +#include "backend/vector.hpp" + +namespace davincioo::backend { + +namespace { + +bool IsSlowVectorOp(PTOOpcode opcode) { + switch (opcode) { + case PTOOpcode::Unknown: + return true; + default: + return false; + } +} + +bool IsReductionLike(const PTOInst& inst) { + const std::string opcode = OpcodeName(inst); + return opcode == "TROWMAX" || opcode == "TROWSUM" || opcode == "TROWMIN" || + opcode == "TCOLMAX" || opcode == "TCOLSUM" || opcode == "TCOLMIN"; +} + +bool IsMoveLike(const PTOInst& inst) { + const std::string opcode = OpcodeName(inst); + return opcode == "TMOV" || opcode == "TMOV_FP" || opcode == "TTRANS" || + opcode == "TEXTRACT" || opcode == "TRESHAPE"; +} + +} // namespace + +Vector::Vector(EngineConfig config, VectorCostConfig cost_config, std::string name) + : Engine(PTOEngineKind::Vec, config.count, std::move(name)), + cost_config_(cost_config) {} + +std::size_t Vector::LookupLatency(const PTOInst& inst) const { + switch (inst.opcode) { + case PTOOpcode::TADD: { + const auto& output = inst.tile_reg_outputs.empty() ? PTOTileReg{} : inst.tile_reg_outputs.front(); + const std::size_t bytes = TileByteSizeCeil(output); + return CeilDiv(bytes, cost_config_.fast_bandwidth_bytes_per_cycle) + cost_config_.elementwise_compute_cycles; + } + case PTOOpcode::TADDC: { + const auto& output = inst.tile_reg_outputs.empty() ? PTOTileReg{} : inst.tile_reg_outputs.front(); + const std::size_t bytes = TileByteSizeCeil(output); + return CeilDiv(bytes, cost_config_.fast_bandwidth_bytes_per_cycle) + cost_config_.elementwise_compute_cycles; + } + case PTOOpcode::TMOV: { + const auto& output = inst.tile_reg_outputs.empty() ? PTOTileReg{} : inst.tile_reg_outputs.front(); + const std::size_t bytes = TileByteSizeCeil(output); + return CeilDiv(bytes, cost_config_.fast_bandwidth_bytes_per_cycle) + cost_config_.move_compute_cycles; + } + case PTOOpcode::TMOV_FP: { + const auto& output = inst.tile_reg_outputs.empty() ? PTOTileReg{} : inst.tile_reg_outputs.front(); + const std::size_t bytes = TileByteSizeCeil(output); + return CeilDiv(bytes, cost_config_.fast_bandwidth_bytes_per_cycle) + cost_config_.move_compute_cycles; + } + case PTOOpcode::TROWMAX: { + const auto& input = inst.tile_reg_inputs.empty() ? PTOTileReg{} : inst.tile_reg_inputs.front(); + const std::size_t bytes = TileByteSizeCeil(input); + return CeilDiv(bytes, cost_config_.fast_bandwidth_bytes_per_cycle) + cost_config_.reduction_compute_cycles + + cost_config_.reduction_merge_cycles; + } + case PTOOpcode::Unknown: + default: { + if (cost_config_.unknown_latency != 0) { + if (!IsReductionLike(inst) && !IsMoveLike(inst)) { + return cost_config_.unknown_latency; + } + } + const auto& output = inst.tile_reg_outputs.empty() ? PTOTileReg{} : inst.tile_reg_outputs.front(); + const std::size_t bytes = TileByteSizeCeil(output); + const bool move_like = IsMoveLike(inst); + const bool reduction_like = IsReductionLike(inst); + const std::size_t bandwidth = IsSlowVectorOp(inst.opcode) ? cost_config_.slow_bandwidth_bytes_per_cycle + : cost_config_.fast_bandwidth_bytes_per_cycle; + const std::size_t compute = reduction_like ? cost_config_.reduction_compute_cycles + cost_config_.reduction_merge_cycles + : move_like ? cost_config_.move_compute_cycles + : IsSlowVectorOp(inst.opcode) ? cost_config_.slow_compute_cycles + : cost_config_.elementwise_compute_cycles; + return CeilDiv(bytes, bandwidth) + compute; + } + } +} + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/vector.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/vector.hpp new file mode 100644 index 00000000..48da5791 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/backend/vector.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "backend/engine.hpp" +#include "davincioo/model/config.hpp" + +namespace davincioo::backend { + +class Vector : public Engine { +public: + Vector(EngineConfig config, VectorCostConfig cost_config, std::string name); + +private: + std::size_t LookupLatency(const PTOInst& inst) const override; + + VectorCostConfig cost_config_{}; +}; + +} // namespace davincioo::backend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace.cpp new file mode 100644 index 00000000..4e9d8f1a --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace.cpp @@ -0,0 +1,405 @@ +#include "frontend/trace.hpp" + +#include +#include +#include +#include +#include + +namespace davincioo::frontend { + +namespace { + +std::optional ExtractJsonArraySegment(const std::string& line, const std::string& key) { + const std::string pattern = "\"" + key + "\":["; + std::size_t start = line.find(pattern); + if (start == std::string::npos) { + return std::nullopt; + } + start += pattern.size() - 1; + std::size_t end = start; + int depth = 0; + for (; end < line.size(); ++end) { + if (line[end] == '[') { + ++depth; + } else if (line[end] == ']') { + --depth; + if (depth == 0) { + return line.substr(start, end - start + 1); + } + } + } + return std::nullopt; +} + +std::vector SplitJsonObjects(const std::string& array_segment) { + std::vector objects; + std::size_t object_start = std::string::npos; + int depth = 0; + for (std::size_t index = 0; index < array_segment.size(); ++index) { + if (array_segment[index] == '{') { + if (depth == 0) { + object_start = index; + } + ++depth; + } else if (array_segment[index] == '}') { + --depth; + if (depth == 0 && object_start != std::string::npos) { + objects.push_back(array_segment.substr(object_start, index - object_start + 1)); + object_start = std::string::npos; + } + } + } + return objects; +} + +std::vector ParseJsonIntArray(const std::string& object, const std::string& key) { + std::vector values; + auto segment = ExtractJsonArraySegment(object, key); + if (!segment.has_value()) { + return values; + } + + std::string content = segment->substr(1, segment->size() - 2); + std::size_t start = 0; + while (start < content.size()) { + while (start < content.size() && (content[start] == ',' || content[start] == ' ')) { + ++start; + } + if (start >= content.size()) { + break; + } + std::size_t end = start; + if (content[end] == '-') { + ++end; + } + while (end < content.size() && content[end] >= '0' && content[end] <= '9') { + ++end; + } + values.push_back(static_cast(std::stoll(content.substr(start, end - start)))); + start = end + 1; + } + return values; +} + +std::uint64_t ParseHexUint64(const std::string& text) { + if (text.empty()) { + return 0; + } + return std::stoull(text, nullptr, 16); +} + +PTOOpcodeDescriptor ParseOpcodeDescriptor(const std::string& raw_opcode) { + if (const auto* descriptor = FindPTOOpcodeDescriptor(raw_opcode)) { + return *descriptor; + } + return kUnknownPTOOpcodeDescriptor; +} + +PTOOpcodeDescriptor RequireOpcodeDescriptor(const std::string& raw_opcode, std::size_t record_index) { + const PTOOpcodeDescriptor descriptor = ParseOpcodeDescriptor(raw_opcode); + if (descriptor.opcode == PTOOpcode::Unknown) { + throw std::runtime_error( + "unknown PTO opcode at trace record " + std::to_string(record_index) + ": " + raw_opcode); + } + return descriptor; +} + +PTODType ParseDType(const std::string& value) { + if (value == "bool") { + return PTODType::kBool; + } + if (value == "int8") { + return PTODType::kInt8; + } + if (value == "int32") { + return PTODType::kInt32; + } + if (value == "int64") { + return PTODType::kInt64; + } + if (value == "uint64") { + return PTODType::kUInt64; + } + if (value == "float16" || value == "fp16") { + return PTODType::kFloat16; + } + if (value == "bfloat16" || value == "bf16") { + return PTODType::kBFloat16; + } + if (value == "float32") { + return PTODType::kFloat32; + } + if (value == "float8" || value == "fp8") { + return PTODType::kFloat8; + } + if (value == "float4" || value == "fp4") { + return PTODType::kFloat4; + } + return PTODType::kUnknown; +} + +PTOTileType ParseTileType(const std::string& value) { + if (value == "Vec") { + return PTOTileType::Vec; + } + if (value == "Mat") { + return PTOTileType::Mat; + } + if (value == "Left") { + return PTOTileType::Left; + } + if (value == "Right") { + return PTOTileType::Right; + } + if (value == "Acc") { + return PTOTileType::Acc; + } + if (value == "Bias") { + return PTOTileType::Bias; + } + if (value == "Scaling") { + return PTOTileType::Scaling; + } + if (value == "ScaleLeft") { + return PTOTileType::ScaleLeft; + } + if (value == "ScaleRight") { + return PTOTileType::ScaleRight; + } + if (value == "Ctrl") { + return PTOTileType::Ctrl; + } + if (value == "Global") { + return PTOTileType::Global; + } + return PTOTileType::Unknown; +} + +PTOLayout ParseLayout(const std::string& value) { + if (value == "ND") { + return PTOLayout::kND; + } + if (value == "DN") { + return PTOLayout::kDN; + } + if (value == "NZ") { + return PTOLayout::kNZ; + } + if (value == "SCALE") { + return PTOLayout::kScale; + } + if (value == "MX_A_ND") { + return PTOLayout::kMxAND; + } + if (value == "MX_A_DN") { + return PTOLayout::kMxADN; + } + if (value == "MX_A_ZZ") { + return PTOLayout::kMxAZZ; + } + if (value == "MX_B_ND") { + return PTOLayout::kMxBND; + } + if (value == "MX_B_DN") { + return PTOLayout::kMxBDN; + } + if (value == "MX_B_NN") { + return PTOLayout::kMxBNN; + } + if (value == "NC1HWC0") { + return PTOLayout::kNC1HWC0; + } + if (value == "NCHW") { + return PTOLayout::kNCHW; + } + if (value == "NHWC") { + return PTOLayout::kNHWC; + } + if (value == "NDC1HWC0") { + return PTOLayout::kNDC1HWC0; + } + if (value == "NCDHW") { + return PTOLayout::kNCDHW; + } + if (value == "FRACTAL_Z") { + return PTOLayout::kFractalZ; + } + if (value == "FRACTAL_Z_S16S8") { + return PTOLayout::kFractalZS16S8; + } + if (value == "FRACTAL_Z_3D") { + return PTOLayout::kFractalZ3D; + } + return PTOLayout::kUnknown; +} + +PTOScalarValue ParseScalarValue(PTODType dtype, const std::string& value) { + switch (dtype) { + case PTODType::kBool: + return value == "true"; + case PTODType::kInt32: + case PTODType::kInt64: + return static_cast(std::stoll(value)); + case PTODType::kUInt64: + return static_cast(std::stoull(value)); + case PTODType::kFloat32: + return std::stod(value); + case PTODType::kUnknown: + default: + return std::monostate{}; + } +} + +PTOTileReg ParseTileReg(const std::string& object) { + const std::string address = ExtractJsonStringField(object, "address").value_or(""); + const std::string tile_type = ExtractJsonStringField(object, "tile_type").value_or(""); + const std::string dtype = ExtractJsonStringField(object, "dtype").value_or(""); + const std::string layout = ExtractJsonStringField(object, "layout").value_or(""); + return PTOTileReg{ + .tile_type = ParseTileType(tile_type), + .address = ParseHexUint64(address), + .dtype = ParseDType(dtype), + .layout = ParseLayout(layout), + .shape = ParseJsonIntArray(object, "shape"), + }; +} + +PTOScalarInput ParseScalarInput(const std::string& object) { + const std::string dtype = ExtractJsonStringField(object, "dtype").value_or(""); + const PTODType parsed_dtype = ParseDType(dtype); + const std::string value = ExtractJsonStringField(object, "value").value_or(""); + return PTOScalarInput{ + .dtype = parsed_dtype, + .value = ParseScalarValue(parsed_dtype, value), + }; +} + +std::vector ParseTileRegArray(const std::string& line, const std::string& key) { + std::vector regs; + auto segment = ExtractJsonArraySegment(line, key); + if (!segment.has_value()) { + return regs; + } + for (const std::string& object : SplitJsonObjects(*segment)) { + regs.push_back(ParseTileReg(object)); + } + return regs; +} + +std::vector ParseScalarInputArray(const std::string& line, const std::string& key) { + std::vector scalars; + auto segment = ExtractJsonArraySegment(line, key); + if (!segment.has_value()) { + return scalars; + } + for (const std::string& object : SplitJsonObjects(*segment)) { + scalars.push_back(ParseScalarInput(object)); + } + return scalars; +} + +} // namespace + +std::optional ExtractJsonStringField(const std::string& line, const std::string& key) { + const std::string pattern = "\"" + key + "\":\""; + std::size_t start = line.find(pattern); + if (start == std::string::npos) { + return std::nullopt; + } + start += pattern.size(); + std::size_t end = start; + while (end < line.size()) { + if (line[end] == '"' && line[end - 1] != '\\') { + break; + } + ++end; + } + if (end >= line.size()) { + return std::nullopt; + } + return line.substr(start, end - start); +} + +std::optional ExtractJsonUintField(const std::string& line, const std::string& key) { + const std::string pattern = "\"" + key + "\":"; + std::size_t start = line.find(pattern); + if (start == std::string::npos) { + return std::nullopt; + } + start += pattern.size(); + std::size_t end = start; + while (end < line.size() && line[end] >= '0' && line[end] <= '9') { + ++end; + } + if (end == start) { + return std::nullopt; + } + return static_cast(std::stoull(line.substr(start, end - start))); +} + +std::string JsonEscape(const std::string& input) { + std::ostringstream escaped; + for (char ch : input) { + switch (ch) { + case '\\': + escaped << "\\\\"; + break; + case '"': + escaped << "\\\""; + break; + case '\n': + escaped << "\\n"; + break; + case '\r': + escaped << "\\r"; + break; + case '\t': + escaped << "\\t"; + break; + default: + escaped << ch; + break; + } + } + return escaped.str(); +} + +std::vector ReadTraceLines(const std::filesystem::path& trace_path) { + std::ifstream trace_stream(trace_path); + if (!trace_stream) { + throw std::runtime_error("failed to open trace file: " + trace_path.string()); + } + + std::vector lines; + std::string line; + while (std::getline(trace_stream, line)) { + if (!line.empty()) { + lines.push_back(line); + } + } + return lines; +} + +std::vector ParsePTOInsts(const std::vector& trace_lines) { + std::vector insts; + insts.reserve(trace_lines.size()); + for (std::size_t index = 0; index < trace_lines.size(); ++index) { + const std::string& line = trace_lines[index]; + const std::string raw_opcode = ExtractJsonStringField(line, "opcode").value_or("UNKNOWN"); + const PTOOpcodeDescriptor descriptor = RequireOpcodeDescriptor(raw_opcode, index); + insts.push_back(PTOInst{ + .opcode = descriptor.opcode, + .raw_opcode = raw_opcode, + .engine_kind = descriptor.engine_kind, + .block_idx = ExtractJsonUintField(line, "block_idx").value_or(0), + .sequence_id = ExtractJsonUintField(line, "sequence_id").value_or(0), + .tile_reg_inputs = ParseTileRegArray(line, "input_tiles"), + .scalar_inputs = ParseScalarInputArray(line, "scalar_inputs"), + .tile_reg_outputs = ParseTileRegArray(line, "output_tiles"), + }); + } + return insts; +} + +} // namespace davincioo::frontend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace.hpp new file mode 100644 index 00000000..cea9d30e --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include + +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo::frontend { + +std::optional ExtractJsonStringField(const std::string& line, const std::string& key); +std::optional ExtractJsonUintField(const std::string& line, const std::string& key); +std::string JsonEscape(const std::string& input); +std::vector ReadTraceLines(const std::filesystem::path& trace_path); +std::vector ParsePTOInsts(const std::vector& trace_lines); + +} // namespace davincioo::frontend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace_source_module.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace_source_module.cpp new file mode 100644 index 00000000..6e09b2d5 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace_source_module.cpp @@ -0,0 +1,43 @@ +#include "frontend/trace_source_module.hpp" + +#include "davincioo/model/framework.hpp" + +namespace davincioo::frontend { + +TraceSourceModule::TraceSourceModule() : Module("trace_source") {} + +void TraceSourceModule::LoadTrace(std::vector insts) { + source_insts_ = std::move(insts); +} + +bool TraceSourceModule::Done() const { + return pending_insts_.empty(); +} + +std::size_t TraceSourceModule::SourceCount() const { + return source_insts_.size(); +} + +void TraceSourceModule::SetFetchWidth(std::size_t width) { + GFSIM_ASSERT(width > 0); + fetch_width_ = width; +} + +void TraceSourceModule::ResetSelf() { + pending_insts_.clear(); + for (const PTOInst& inst : source_insts_) { + pending_insts_.push_back(inst); + } +} + +void TraceSourceModule::WorkSelf() { + OUTPUT(out0, 0); + std::size_t emitted = 0; + while (emitted < fetch_width_ && !pending_insts_.empty() && !out0->Full()) { + out0->Write(std::make_shared(pending_insts_.front())); + pending_insts_.pop_front(); + ++emitted; + } +} + +} // namespace davincioo::frontend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace_source_module.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace_source_module.hpp new file mode 100644 index 00000000..56fa23a1 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/frontend/trace_source_module.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo::frontend { + +class TraceSourceModule : public Module { +public: + TraceSourceModule(); + + void LoadTrace(std::vector insts); + bool Done() const; + std::size_t SourceCount() const; + + void SetFetchWidth(std::size_t width); + std::size_t FetchWidth() const noexcept { return fetch_width_; } + +protected: + void ResetSelf() override; + void WorkSelf() override; + +private: + std::vector source_insts_; + std::deque pending_insts_; + std::size_t fetch_width_ = 1; +}; + +} // namespace davincioo::frontend diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/CORE.md b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/CORE.md new file mode 100644 index 00000000..6a7face3 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/CORE.md @@ -0,0 +1,135 @@ +# Core + +This document describes the top-level model topology implemented in: + +- [core.hpp](./core.hpp) +- [core.cpp](./core.cpp) + +## Schematic + +Source: +- [core_topology.dot](../../docs/diagrams/core_topology.dot) + +Rendered SVG: + +![Core topology](../../docs/diagrams/core_topology.svg) + +## Purpose + +`Core` is the parent topology owner for the current basic Davinci model top. + +It owns: + +- frontend trace source +- ROB +- rename +- dispatch +- ready table +- distributed issue queues +- engine modules +- all physical interconnect queue storage between those modules + +## Inputs + +`Core` has no boundary `SimQueue` inputs. + +Instead, the host loads the trace instruction vector through: + +- `LoadTrace(std::vector)` + +## Outputs + +`Core` has no boundary `SimQueue` outputs. + +Externally visible results are queried through: + +- processed instruction log +- ROB capacity / occupancy +- engine counts + +## Inner State / Private Queues + +Owned queues: + +- `rob_input_q_` +- `rob_to_rename_q_` +- `rob_to_rename_retire_q_` +- `rename_to_dispatch_q_` +- `dispatch_to_issue_qs_[]` +- `ready_to_issue_qs_[]` +- `issue_wakeup_qs_[]` +- `issue_to_engine_qs_[]` +- `wakeup_q_` +- `rob_done_q_` + +Owned child modules: + +- `TraceSourceModule frontend_` +- `ROB rob_` +- `Rename rename_` +- `Dispatch dispatch_` +- `ReadyTable ready_table_` +- `IssueQueue` arrays for `VEC`, `CUBE`, and `TMA` +- `IssueQueue` arrays for `SCALAR`, `VEC`, `CUBE`, and `TMA` +- `Engine` arrays for `SCALAR`, `VEC`, `CUBE`, and `TMA` + +## Owned Storage + +`Core` is the physical owner of all interconnect queue storage between child +modules. + +Children only receive queue pointers via binding. + +## Features + +- reads typed PTO instructions from the frontend source +- blocks frontend fetch when the ROB is full +- allocates ROB entries in order +- renames local tile operands through SMAP and a tile-tag free list +- routes issued instructions through dispatch +- initializes source readiness from the ready table +- holds non-ready instructions in distributed issue queues +- routes work to `SCALAR`, `VEC`, `CUBE`, and `TMA` engine families only when ready +- routes engine completions into a wakeup queue, then through ready-table + broadcast back to issue queues and ROB +- drains until frontend is empty, queues are empty, ROB is empty, and engines are idle + +## Cycle Behavior + +Per cycle: + +1. if ROB is not full, allow frontend to emit one instruction +2. advance frontend-to-ROB queue +3. run ROB +4. advance ROB-to-rename issue and retire queues +5. run rename +6. advance rename-to-dispatch queue +7. run dispatch +8. advance dispatch-routed queues +9. advance global wakeup queue from the previous cycle +10. run ready table / wakeup broadcast +11. advance ready-table outputs into distributed issue queues +12. run distributed issue queues +13. advance issue-to-engine queues +14. run all engines + +## PTOInstRef Pop/Update/Push Rule + +`Core` does not itself mutate per-instruction runtime state. Its job is to own +the queues and tick the stage owners in a stable order so those stage-local +updates happen at the correct place: + +- ROB owns alloc / retire-side updates +- Rename owns tile-tag / SMAP updates +- Dispatch owns dispatch-side updates +- ReadyTable owns global tile-tag readiness outside IQ residency +- IssueQueue owns resident per-input valid/ready state and pick +- Engine owns execution-side updates + +## Limitations / Simplifications + +- single-width frontend fetch into ROB +- single-width ROB issue to dispatch +- simple queue-order scheduling +- no flush / redirect / replay / recovery control +- ready state is still a simplified tile-tag model rather than a full LinxCore speculative scoreboard diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/CORE_SYSTEM.md b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/CORE_SYSTEM.md new file mode 100644 index 00000000..b7df3ee6 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/CORE_SYSTEM.md @@ -0,0 +1,83 @@ +# CoreSystem + +This document describes the wrapper system implemented in: + +- [core_system.hpp](./core_system.hpp) +- [core_system.cpp](./core_system.cpp) + +## Purpose + +`CoreSystem` is the top-level cycle driver around `Core`. + +It provides: + +- ownership through `SimSystem` +- reset/build/step lifecycle +- explicit run-loop helper methods +- summary extraction + +## Inputs + +- loaded trace instruction vector through: + - `LoadTrace(std::vector)` + +## Outputs + +- `SimulationResult` + +The result includes: + +- total record count +- total simulated cycles +- ROB capacity / final count +- processed instruction log +- opcode histogram + +## Inner State / Private State + +- owned `Core` module instance + +## Owned Storage + +`CoreSystem` owns the top-level `Core` instance through the `SimSystem` +container. + +`Core` owns the queue storage and child modules below that. + +## Features + +- build once +- reset before execution +- expose: + - `RunReference(...)` + - `step()` + - `getCycles()` + - `needTerminate()` + - `enableTrace(...)` + - `PrintPipeView(...)` +- step until `Core::Done()` +- abort if no module or queue reports progress for 2000 consecutive cycles +- collect final processed stream and counters + +## Cycle Behavior + +1. build the `Core` topology +2. reset the topology +3. run a cycle loop of: + - `RunReference(...)` + - `step()` + - consume the per-step progress signal + - optional `PrintPipeView(...)` + - `enableTrace(...)` + - `needTerminate()` +4. stop only when the core and all owned queues/modules are drained or a + stop-cycle limit is reached +5. raise a deadlock error if nothing moved for the configured watchdog window +6. collect final summary + +## Limitations / Simplifications + +- no external time source or wall-clock pacing +- no interactive stepping CLI yet +- no partial-run checkpoint/restore support +- deadlock detection is progress-based, not a semantic dependency analysis diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/cli.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/cli.cpp new file mode 100644 index 00000000..8428243f --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/cli.cpp @@ -0,0 +1,293 @@ +#include "model_top/cli.hpp" + +#include +#include +#include +#include +#include + +#include "frontend/trace.hpp" +#include "model_top/core_system.hpp" +#include "model_top/kanata_pipeview.hpp" +#include "model_top/perfetto_trace.hpp" + +namespace davincioo::model_top { + +void PrintUsage() { + std::cerr + << "usage:\n" + << " gfsim simulate --trace [--config ] [--summary-out ] [--pipeview-out ] [--perfetto-out ] [--dump-cycles]\n" + << " gfsim summary --trace [--config ] --summary-out [--pipeview-out ] [--perfetto-out ]\n" + << " gfsim dump-trace --trace [--limit N]\n" + << " gfsim stats --trace \n"; +} + +std::optional ParseArgs(int argc, char** argv) { + if (argc < 2) { + PrintUsage(); + return std::nullopt; + } + + Options options; + options.command = argv[1]; + if (options.command == "--help" || options.command == "-h" || options.command == "help") { + options.help_only = true; + PrintUsage(); + return options; + } + + for (int index = 2; index < argc; ++index) { + std::string arg = argv[index]; + if (arg == "--trace" && index + 1 < argc) { + options.trace_path = argv[++index]; + continue; + } + if (arg == "--summary-out" && index + 1 < argc) { + options.summary_out = argv[++index]; + continue; + } + if (arg == "--pipeview-out" && index + 1 < argc) { + options.pipeview_out = argv[++index]; + continue; + } + if (arg == "--perfetto-out" && index + 1 < argc) { + options.perfetto_out = argv[++index]; + continue; + } + if (arg == "--config" && index + 1 < argc) { + options.config_path = argv[++index]; + continue; + } + if (arg == "--limit" && index + 1 < argc) { + options.limit = static_cast(std::stoull(argv[++index])); + continue; + } + if (arg == "--dump-cycles") { + options.dump_cycles = true; + continue; + } + std::cerr << "unknown or incomplete argument: " << arg << "\n"; + PrintUsage(); + return std::nullopt; + } + + if (options.command != "simulate" && options.command != "summary" && + options.command != "dump-trace" && options.command != "stats") { + std::cerr << "unknown command: " << options.command << "\n"; + PrintUsage(); + return std::nullopt; + } + if (options.trace_path.empty()) { + std::cerr << "--trace is required\n"; + PrintUsage(); + return std::nullopt; + } + if ((options.command == "simulate" || options.command == "summary") && options.summary_out.empty() && + options.command == "summary") { + std::cerr << "--summary-out is required for summary\n"; + PrintUsage(); + return std::nullopt; + } + return options; +} + +namespace { + +std::string ScalarValueToString(const PTOScalarValue& value) { + return std::visit( + [](const auto& inner) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return "null"; + } else if constexpr (std::is_same_v) { + return inner ? "true" : "false"; + } else if constexpr (std::is_same_v || std::is_same_v) { + return std::to_string(inner); + } else { + return std::to_string(inner); + } + }, + value); +} + +void WriteSummaryJson(const Options& options, const SimulationResult& result) { + if (options.summary_out.empty()) { + return; + } + if (!options.summary_out.parent_path().empty()) { + std::filesystem::create_directories(options.summary_out.parent_path()); + } + + std::ofstream summary_stream(options.summary_out, std::ios::out | std::ios::trunc); + if (!summary_stream) { + throw std::runtime_error("failed to open summary output: " + options.summary_out.string()); + } + + std::string first_opcode = "UNKNOWN"; + if (!result.processed.empty()) { + first_opcode = OpcodeName(result.processed.front()); + } + + summary_stream << "{\n" + << " \"tool\": \"gfsim\",\n" + << " \"model\": \"davinci_ooo_model\",\n" + << " \"status\": \"basic_model_top\",\n" + << " \"message\": \"Basic core model executed trace through ROB, dispatch, and engines.\",\n" + << " \"trace_path\": \"" << frontend::JsonEscape(options.trace_path.string()) << "\",\n" + << " \"record_count\": " << result.record_count << ",\n" + << " \"simulated_cycles\": " << result.simulated_cycles << ",\n" + << " \"rob_capacity\": " << result.rob_capacity << ",\n" + << " \"rob_count\": " << result.rob_count << ",\n" + << " \"mode\": \"basic_core_model\",\n" + << " \"first_opcode\": \"" << frontend::JsonEscape(first_opcode) << "\",\n" + << " \"opcode_counts\": {\n"; + + bool first = true; + for (const auto& [opcode, count] : result.opcode_counts) { + if (!first) { + summary_stream << ",\n"; + } + first = false; + summary_stream << " \"" << frontend::JsonEscape(opcode) << "\": " << count; + } + summary_stream << "\n }\n}\n"; +} + +void WritePipeView(const Options& options, const SimulationResult& result) { + if (options.pipeview_out.empty()) { + return; + } + WriteKanataPipeView(options.pipeview_out, result); +} + +void WritePerfetto(const Options& options, const SimulationResult& result) { + if (options.perfetto_out.empty()) { + return; + } + WritePerfettoTrace(options.perfetto_out, result); +} + +void DumpCycleReplay(const SimulationResult& result) { + for (std::size_t index = 0; index < result.processed.size(); ++index) { + const PTOInst& inst = result.processed[index]; + std::cout << "retire_index=" << index + << " opcode=" << OpcodeName(inst) + << " engine=" << ToString(inst.engine_kind) + << " block_idx=" << inst.block_idx + << " sequence_id=" << inst.sequence_id + << " rob_id=" << inst.rob_id + << " engine_id=" << inst.engine_id + << " src_ready=" << (inst.src_ready ? 1 : 0) + << " runtime_latency=" << inst.runtime_latency + << " alloc_cycle=" << inst.timestamps.rob_alloc_cycle + << " rename_cycle=" << inst.timestamps.rename_cycle + << " dispatch_cycle=" << inst.timestamps.dispatch_cycle + << " issue_cycle=" << inst.timestamps.issue_cycle + << " engine_pop_cycle=" << inst.timestamps.engine_pop_cycle + << " engine_complete_cycle=" << inst.timestamps.engine_complete_cycle + << " retire_cycle=" << inst.timestamps.rob_retire_cycle + << " input_regs=" << inst.tile_reg_inputs.size() + << " scalar_inputs=" << inst.scalar_inputs.size() + << " output_regs=" << inst.tile_reg_outputs.size(); + if (!inst.tile_reg_inputs.empty()) { + std::cout << " in0=0x" << std::hex << inst.tile_reg_inputs.front().address << std::dec; + std::cout << " in0_tag=" << inst.tile_reg_inputs.front().tile_tag; + std::cout << " in0_rm=" << (inst.tile_reg_inputs.front().rename_managed ? 1 : 0); + if (!inst.tile_input_valid.empty()) { + std::cout << " in0_valid=" << (inst.tile_input_valid.front() ? 1 : 0); + } + if (!inst.tile_input_ready.empty()) { + std::cout << " in0_ready=" << (inst.tile_input_ready.front() ? 1 : 0); + } + } + if (!inst.tile_reg_outputs.empty()) { + std::cout << " out0=0x" << std::hex << inst.tile_reg_outputs.front().address << std::dec; + std::cout << " out0_tag=" << inst.tile_reg_outputs.front().tile_tag; + std::cout << " out0_rm=" << (inst.tile_reg_outputs.front().rename_managed ? 1 : 0); + if (!inst.output_replaced_tile_tags.empty()) { + std::cout << " out0_prev_tag=" << inst.output_replaced_tile_tags.front(); + } + } + if (!inst.scalar_inputs.empty()) { + std::cout << " scalar0=" << ScalarValueToString(inst.scalar_inputs.front().value); + } + std::cout << "\n"; + } +} + +CoreConfig ResolveConfig(const Options& options) { + if (!options.config_path.empty()) { + return ParseCoreTomlConfig(options.config_path); + } + return CoreConfig{}; +} + +int RunSimulationLikeCommand(const Options& options, const std::vector& trace_lines) { + CoreSystem system(ResolveConfig(options)); + system.LoadTrace(frontend::ParsePTOInsts(trace_lines)); + const SimulationResult result = system.RunToCompletion(); + + if (options.dump_cycles) { + DumpCycleReplay(result); + } + WriteSummaryJson(options, result); + WritePipeView(options, result); + WritePerfetto(options, result); + + std::cout << "trace: " << options.trace_path << "\n"; + std::cout << "records: " << result.record_count << "\n"; + std::cout << "simulated_cycles: " << result.simulated_cycles << "\n"; + std::cout << "rob_capacity: " << result.rob_capacity << "\n"; + std::cout << "rob_count: " << result.rob_count << "\n"; + if (!options.pipeview_out.empty()) { + std::cout << "pipeview: " << options.pipeview_out << "\n"; + } + if (!options.perfetto_out.empty()) { + std::cout << "perfetto: " << options.perfetto_out << "\n"; + } + std::cout << "mode: basic_core_model\n"; + return 0; +} + +int RunDumpTrace(const Options& options, const std::vector& trace_lines) { + std::size_t emitted = 0; + for (const std::string& line : trace_lines) { + if (options.limit != 0 && emitted >= options.limit) { + break; + } + std::cout << line << "\n"; + ++emitted; + } + return 0; +} + +int RunStats(const Options& options, const std::vector& trace_lines) { + CoreSystem system(ResolveConfig(options)); + system.LoadTrace(frontend::ParsePTOInsts(trace_lines)); + const SimulationResult result = system.RunToCompletion(); + + std::cout << "trace: " << options.trace_path << "\n"; + std::cout << "records: " << result.record_count << "\n"; + std::cout << "simulated_cycles: " << result.simulated_cycles << "\n"; + std::cout << "rob_capacity: " << result.rob_capacity << "\n"; + std::cout << "rob_count: " << result.rob_count << "\n"; + std::cout << "opcodes:\n"; + for (const auto& [opcode, count] : result.opcode_counts) { + std::cout << " " << std::setw(12) << std::left << opcode << " " << count << "\n"; + } + return 0; +} + +} // namespace + +int RunGfsimCommand(const Options& options, const std::vector& trace_lines) { + if (options.command == "simulate" || options.command == "summary") { + return RunSimulationLikeCommand(options, trace_lines); + } + if (options.command == "dump-trace") { + return RunDumpTrace(options, trace_lines); + } + return RunStats(options, trace_lines); +} + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/cli.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/cli.hpp new file mode 100644 index 00000000..40d16d90 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/cli.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + +namespace davincioo::model_top { + +struct Options { + std::string command; + std::filesystem::path trace_path{}; + std::filesystem::path config_path{}; + std::filesystem::path summary_out{}; + std::filesystem::path pipeview_out{}; + std::filesystem::path perfetto_out{}; + std::size_t limit = 0; + bool dump_cycles = false; + bool help_only = false; +}; + +void PrintUsage(); +std::optional ParseArgs(int argc, char** argv); +int RunGfsimCommand(const Options& options, const std::vector& trace_lines); + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/config.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/config.cpp new file mode 100644 index 00000000..79e0c8f6 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/config.cpp @@ -0,0 +1,211 @@ +#include "davincioo/model/config.hpp" + +#include +#include +#include + +namespace davincioo { + +namespace { + +std::string Trim(const std::string& input) { + std::size_t start = input.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) { + return ""; + } + std::size_t end = input.find_last_not_of(" \t\r\n"); + return input.substr(start, end - start + 1); +} + +bool IsPowerOfTwo(std::size_t value) { + return value != 0 && (value & (value - 1)) == 0; +} + +} // namespace + +CoreConfig ParseCoreTomlConfig(const std::filesystem::path& path) { + std::ifstream stream(path); + if (!stream) { + throw std::runtime_error("failed to open config: " + path.string()); + } + + CoreConfig config; + std::string section; + std::string line; + while (std::getline(stream, line)) { + const std::size_t comment = line.find('#'); + if (comment != std::string::npos) { + line = line.substr(0, comment); + } + line = Trim(line); + if (line.empty()) { + continue; + } + if (line.front() == '[' && line.back() == ']') { + section = Trim(line.substr(1, line.size() - 2)); + continue; + } + + const std::size_t eq = line.find('='); + if (eq == std::string::npos) { + continue; + } + const std::string key = Trim(line.substr(0, eq)); + const std::string value = Trim(line.substr(eq + 1)); + + if (section == "rob") { + if (key == "entries") { + config.rob.entries = static_cast(std::stoull(value)); + } + continue; + } + if (section == "rename") { + if (key == "tile_tags") { + config.rename.tile_tags = static_cast(std::stoull(value)); + } + continue; + } + if (section == "issue_queue") { + if (key == "entries") { + config.issue_queue.entries = static_cast(std::stoull(value)); + } + continue; + } + if (section == "frontend_width") { + const std::size_t v = static_cast(std::stoull(value)); + if (key == "width") { + config.frontend_width.fetch_width = v; + config.frontend_width.rob_alloc_width = v; + config.frontend_width.rob_issue_width = v; + config.frontend_width.rename_width = v; + config.frontend_width.dispatch_width = v; + } else if (key == "fetch_width") { + config.frontend_width.fetch_width = v; + } else if (key == "rob_alloc_width") { + config.frontend_width.rob_alloc_width = v; + } else if (key == "rob_issue_width") { + config.frontend_width.rob_issue_width = v; + } else if (key == "rename_width") { + config.frontend_width.rename_width = v; + } else if (key == "dispatch_width") { + config.frontend_width.dispatch_width = v; + } + continue; + } + if (section == "scalar") { + if (key == "count") { + config.scalar.count = static_cast(std::stoull(value)); + } + continue; + } + if (section == "vec") { + if (key == "count") { + config.vec.count = static_cast(std::stoull(value)); + } + continue; + } + if (section == "cube") { + if (key == "count") { + config.cube.count = static_cast(std::stoull(value)); + } + continue; + } + if (section == "tma") { + if (key == "count") { + config.tma.count = static_cast(std::stoull(value)); + } + continue; + } + if (section == "scalar_cost") { + if (key == "default_latency") { + config.scalar_cost.default_latency = static_cast(std::stoull(value)); + } else if (key == "tassign_latency") { + config.scalar_cost.tassign_latency = static_cast(std::stoull(value)); + } else if (key == "unknown_latency") { + config.scalar_cost.unknown_latency = static_cast(std::stoull(value)); + } + continue; + } + if (section == "vec_cost") { + if (key == "fast_bandwidth_bytes_per_cycle") { + config.vec_cost.fast_bandwidth_bytes_per_cycle = static_cast(std::stoull(value)); + } else if (key == "slow_bandwidth_bytes_per_cycle") { + config.vec_cost.slow_bandwidth_bytes_per_cycle = static_cast(std::stoull(value)); + } else if (key == "elementwise_compute_cycles") { + config.vec_cost.elementwise_compute_cycles = static_cast(std::stoull(value)); + } else if (key == "reduction_compute_cycles") { + config.vec_cost.reduction_compute_cycles = static_cast(std::stoull(value)); + } else if (key == "reduction_merge_cycles") { + config.vec_cost.reduction_merge_cycles = static_cast(std::stoull(value)); + } else if (key == "slow_compute_cycles") { + config.vec_cost.slow_compute_cycles = static_cast(std::stoull(value)); + } else if (key == "move_compute_cycles") { + config.vec_cost.move_compute_cycles = static_cast(std::stoull(value)); + } else if (key == "unknown_latency") { + config.vec_cost.unknown_latency = static_cast(std::stoull(value)); + } + continue; + } + if (section == "cube_cost") { + if (key == "input_bandwidth_bytes_per_cycle") { + config.cube_cost.input_bandwidth_bytes_per_cycle = static_cast(std::stoull(value)); + } else if (key == "macs_per_cycle_fp32") { + config.cube_cost.macs_per_cycle_fp32 = static_cast(std::stoull(value)); + } else if (key == "macs_per_cycle_fp16") { + config.cube_cost.macs_per_cycle_fp16 = static_cast(std::stoull(value)); + } else if (key == "macs_per_cycle_bf16") { + config.cube_cost.macs_per_cycle_bf16 = static_cast(std::stoull(value)); + } else if (key == "macs_per_cycle_fp8") { + config.cube_cost.macs_per_cycle_fp8 = static_cast(std::stoull(value)); + } else if (key == "macs_per_cycle_int8") { + config.cube_cost.macs_per_cycle_int8 = static_cast(std::stoull(value)); + } else if (key == "macs_per_cycle_fp4") { + config.cube_cost.macs_per_cycle_fp4 = static_cast(std::stoull(value)); + } else if (key == "accumulate_extra_cycles") { + config.cube_cost.accumulate_extra_cycles = static_cast(std::stoull(value)); + } else if (key == "overlap_mode") { + config.cube_cost.overlap_mode = (value == "true" || value == "1"); + } else if (key == "unknown_latency") { + config.cube_cost.unknown_latency = static_cast(std::stoull(value)); + } + continue; + } + if (section == "tma_cost") { + if (key == "bandwidth_bytes_per_cycle") { + config.tma_cost.bandwidth_bytes_per_cycle = static_cast(std::stoull(value)); + } else if (key == "load_overhead_cycles") { + config.tma_cost.load_overhead_cycles = static_cast(std::stoull(value)); + } else if (key == "store_overhead_cycles") { + config.tma_cost.store_overhead_cycles = static_cast(std::stoull(value)); + } else if (key == "move_overhead_cycles") { + config.tma_cost.move_overhead_cycles = static_cast(std::stoull(value)); + } else if (key == "unknown_latency") { + config.tma_cost.unknown_latency = static_cast(std::stoull(value)); + } + continue; + } + } + + if (!IsPowerOfTwo(config.rob.entries)) { + throw std::runtime_error("rob.entries must be a power of two and > 0"); + } + if (config.rename.tile_tags == 0) { + throw std::runtime_error("rename.tile_tags must be > 0"); + } + if (config.issue_queue.entries == 0) { + throw std::runtime_error("issue_queue.entries must be > 0"); + } + if (config.scalar.count == 0 || config.vec.count == 0 || config.cube.count == 0 || config.tma.count == 0) { + throw std::runtime_error("engine counts must be > 0"); + } + if (config.frontend_width.fetch_width == 0 || + config.frontend_width.rob_alloc_width == 0 || + config.frontend_width.rob_issue_width == 0 || + config.frontend_width.rename_width == 0 || + config.frontend_width.dispatch_width == 0) { + throw std::runtime_error("frontend_width.* must be > 0"); + } + return config; +} + +} // namespace davincioo diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core.cpp new file mode 100644 index 00000000..150df5b0 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core.cpp @@ -0,0 +1,478 @@ +#include "model_top/core.hpp" + +#include +#include + +namespace davincioo::model_top { + +namespace { + +std::size_t CountTotalEngines(const CoreConfig& config) { + return config.scalar.count + config.vec.count + config.cube.count + config.tma.count; +} + +template +void BuildEngineArray( + std::vector>& engines, + EngineConfig config, + CostConfigT cost_config, + const std::string& prefix) { + engines.clear(); + engines.reserve(config.count); + for (std::size_t index = 0; index < config.count; ++index) { + engines.push_back(std::make_unique(config, cost_config, prefix + std::to_string(index))); + } +} + +void BuildPerEngineQueues( + std::vector>& queues, + std::size_t count, + const std::string& prefix, + std::uint32_t latency = 0) { + queues.clear(); + queues.reserve(count); + for (std::size_t index = 0; index < count; ++index) { + queues.emplace_back(8, latency, prefix + std::to_string(index)); + } +} + +void AppendIssueQueues( + std::vector>& issue_queues, + IssueQueueConfig config, + std::size_t count, + PTOEngineKind kind, + const std::string& prefix) { + for (std::size_t index = 0; index < count; ++index) { + issue_queues.push_back(std::make_unique(config, kind, prefix + std::to_string(index))); + } +} + +void MergeChildProgress(SimObject& parent, SimObject& child) { + const std::size_t amount = child.ConsumeProgress(); + if (amount != 0) { + parent.MarkProgress(amount); + } +} + +} // namespace + +Core::Core(CoreConfig config) + : SimObject("core"), + frontend_(), + rob_(config.rob), + rename_(config.rename), + dispatch_(config.scalar.count, config.vec.count, config.cube.count, config.tma.count), + ready_table_(config.scalar.count + config.vec.count + config.cube.count + config.tma.count), + rob_input_q_(std::max(1, config.frontend_width.fetch_width), 0, "rob_input_q"), + rob_to_rename_q_(std::max(1, config.frontend_width.rob_issue_width), 0, "rob_to_rename_q"), + rob_to_rename_retire_q_(config.rob.entries, 0, "rob_to_rename_retire_q"), + rename_to_dispatch_q_(std::max(1, config.frontend_width.rename_width), 0, "rename_to_dispatch_q"), + wakeup_q_(CountTotalEngines(config), 0, "wakeup_q"), + rob_done_q_(CountTotalEngines(config), 0, "rob_done_q") { + frontend_.SetFetchWidth(config.frontend_width.fetch_width); + rob_.SetAllocWidth(config.frontend_width.rob_alloc_width); + rob_.SetIssueWidth(config.frontend_width.rob_issue_width); + rename_.SetRenameWidth(config.frontend_width.rename_width); + dispatch_.SetDispatchWidth(config.frontend_width.dispatch_width); + const std::size_t total_engines = CountTotalEngines(config); + BuildPerEngineQueues(dispatch_to_issue_qs_, total_engines, "dispatch_to_issue_q"); + BuildPerEngineQueues(ready_to_issue_qs_, total_engines, "ready_to_issue_q"); + BuildPerEngineQueues(issue_wakeup_qs_, total_engines, "issue_wakeup_q"); + BuildPerEngineQueues(issue_to_engine_qs_, total_engines, "issue_to_engine_q", 1); + BuildEngineArray(scalar_engines_, config.scalar, config.scalar_cost, "scalar_engine"); + BuildEngineArray(vec_engines_, config.vec, config.vec_cost, "vec_engine"); + BuildEngineArray(cube_engines_, config.cube, config.cube_cost, "cube_engine"); + BuildEngineArray(tma_engines_, config.tma, config.tma_cost, "tma_engine"); + issue_queues_.clear(); + issue_queues_.reserve(total_engines); + AppendIssueQueues(issue_queues_, config.issue_queue, config.scalar.count, PTOEngineKind::Scalar, "scalar_iq"); + AppendIssueQueues(issue_queues_, config.issue_queue, config.vec.count, PTOEngineKind::Vec, "vec_iq"); + AppendIssueQueues(issue_queues_, config.issue_queue, config.cube.count, PTOEngineKind::Cube, "cube_iq"); + AppendIssueQueues(issue_queues_, config.issue_queue, config.tma.count, PTOEngineKind::Tma, "tma_iq"); +} + +void Core::LoadTrace(std::vector insts) { + frontend_.LoadTrace(std::move(insts)); +} + +std::size_t Core::SourceCount() const { + return frontend_.SourceCount(); +} + +bool Core::Done() const { + const bool engines_empty = + std::all_of(scalar_engines_.begin(), scalar_engines_.end(), [](const auto& engine) { return engine->Count() == 0; }) && + std::all_of(vec_engines_.begin(), vec_engines_.end(), [](const auto& engine) { return engine->Count() == 0; }) && + std::all_of(cube_engines_.begin(), cube_engines_.end(), [](const auto& engine) { return engine->Count() == 0; }) && + std::all_of(tma_engines_.begin(), tma_engines_.end(), [](const auto& engine) { return engine->Count() == 0; }); + const bool issue_queues_empty = + std::all_of(issue_queues_.begin(), issue_queues_.end(), [](const auto& issue_queue) { return issue_queue->Count() == 0; }); + return frontend_.Done() && rob_input_q_.Empty() && rob_to_rename_q_.Empty() && rob_to_rename_retire_q_.Empty() && + rename_to_dispatch_q_.Empty() && wakeup_q_.Empty() && rob_done_q_.Empty() && + std::all_of(dispatch_to_issue_qs_.begin(), dispatch_to_issue_qs_.end(), [](const auto& q) { return q.Empty(); }) && + std::all_of(ready_to_issue_qs_.begin(), ready_to_issue_qs_.end(), [](const auto& q) { return q.Empty(); }) && + std::all_of(issue_wakeup_qs_.begin(), issue_wakeup_qs_.end(), [](const auto& q) { return q.Empty(); }) && + std::all_of(issue_to_engine_qs_.begin(), issue_to_engine_qs_.end(), [](const auto& q) { return q.Empty(); }) && + rob_.Empty() && issue_queues_empty && engines_empty; +} + +const std::vector& Core::Processed() const { + return rob_.Processed(); +} + +std::size_t Core::RobCapacity() const { + return rob_.Capacity(); +} + +std::size_t Core::RobCount() const { + return rob_.Count(); +} + +std::size_t Core::ScalarEngineCount() const { + return scalar_engines_.size(); +} + +std::size_t Core::VecEngineCount() const { + return vec_engines_.size(); +} + +std::size_t Core::CubeEngineCount() const { + return cube_engines_.size(); +} + +std::size_t Core::TmaEngineCount() const { + return tma_engines_.size(); +} + +std::string Core::DumpRobState() const { + return rob_.DumpState(); +} + +std::string Core::DumpSchedulerState() const { + std::ostringstream stream; + stream << ready_table_.DumpState(); + for (const auto& issue_queue : issue_queues_) { + stream << "\n" << issue_queue->DumpState(); + } + return stream.str(); +} + +void Core::Build() { + if (built_) { + return; + } + const std::size_t total_engines = + scalar_engines_.size() + vec_engines_.size() + cube_engines_.size() + tma_engines_.size(); + GFSIM_ASSERT(total_engines > 0); + GFSIM_ASSERT(dispatch_to_issue_qs_.size() == total_engines); + GFSIM_ASSERT(ready_to_issue_qs_.size() == total_engines); + GFSIM_ASSERT(issue_wakeup_qs_.size() == total_engines); + GFSIM_ASSERT(issue_to_engine_qs_.size() == total_engines); + GFSIM_ASSERT(issue_queues_.size() == total_engines); + + frontend_.BindOutput(0, &rob_input_q_); + rob_.BindInstInput(&rob_input_q_); + rob_.BindCompletionInput(&rob_done_q_); + rob_.BindRenameOutput(&rob_to_rename_q_); + rob_.BindRetireOutput(&rob_to_rename_retire_q_); + rename_.BindInput(0, &rob_to_rename_q_); + rename_.BindInput(1, &rob_to_rename_retire_q_); + rename_.BindOutput(0, &rename_to_dispatch_q_); + dispatch_.BindInput(0, &rename_to_dispatch_q_); + + std::size_t output_index = 0; + for (std::size_t index = 0; index < dispatch_to_issue_qs_.size(); ++index) { + dispatch_.BindOutput(output_index++, &dispatch_to_issue_qs_[index]); + ready_table_.BindDispatchInput(index, &dispatch_to_issue_qs_[index]); + ready_table_.BindIssueEnqueueOutput(index, &ready_to_issue_qs_[index]); + ready_table_.BindIssueWakeupOutput(index, &issue_wakeup_qs_[index]); + issue_queues_[index]->BindEnqueueInput(&ready_to_issue_qs_[index]); + issue_queues_[index]->BindWakeupInput(&issue_wakeup_qs_[index]); + issue_queues_[index]->BindIssuedOutput(&issue_to_engine_qs_[index]); + issue_queues_[index]->BindReadyChecker([this](std::uint64_t tag) { return ready_table_.IsTagReady(tag); }); + } + GFSIM_ASSERT(output_index == total_engines); + ready_table_.BindWakeupInput(&wakeup_q_); + ready_table_.BindRobCompletionOutput(&rob_done_q_); + + std::size_t engine_index = 0; + for (std::size_t index = 0; index < scalar_engines_.size(); ++index, ++engine_index) { + scalar_engines_[index]->BindIssuedInput(&issue_to_engine_qs_[engine_index]); + scalar_engines_[index]->BindCompletedOutput(&wakeup_q_); + } + for (std::size_t index = 0; index < vec_engines_.size(); ++index, ++engine_index) { + vec_engines_[index]->BindIssuedInput(&issue_to_engine_qs_[engine_index]); + vec_engines_[index]->BindCompletedOutput(&wakeup_q_); + } + for (std::size_t index = 0; index < cube_engines_.size(); ++index, ++engine_index) { + cube_engines_[index]->BindIssuedInput(&issue_to_engine_qs_[engine_index]); + cube_engines_[index]->BindCompletedOutput(&wakeup_q_); + } + for (std::size_t index = 0; index < tma_engines_.size(); ++index, ++engine_index) { + tma_engines_[index]->BindIssuedInput(&issue_to_engine_qs_[engine_index]); + tma_engines_[index]->BindCompletedOutput(&wakeup_q_); + } + GFSIM_ASSERT(engine_index == total_engines); + + frontend_.Build(); + rob_.Build(); + rename_.Build(); + dispatch_.Build(); + ready_table_.Build(); + for (auto& issue_queue : issue_queues_) { + issue_queue->Build(); + } + for (auto& engine : scalar_engines_) { + engine->Build(); + } + for (auto& engine : vec_engines_) { + engine->Build(); + } + for (auto& engine : cube_engines_) { + engine->Build(); + } + for (auto& engine : tma_engines_) { + engine->Build(); + } + built_ = true; +} + +void Core::Reset() { + rob_input_q_.Reset(); + rob_to_rename_q_.Reset(); + rob_to_rename_retire_q_.Reset(); + rename_to_dispatch_q_.Reset(); + wakeup_q_.Reset(); + rob_done_q_.Reset(); + for (auto& queue : dispatch_to_issue_qs_) { + queue.Reset(); + } + for (auto& queue : ready_to_issue_qs_) { + queue.Reset(); + } + for (auto& queue : issue_wakeup_qs_) { + queue.Reset(); + } + for (auto& queue : issue_to_engine_qs_) { + queue.Reset(); + } + frontend_.Reset(); + rob_.Reset(); + rename_.Reset(); + dispatch_.Reset(); + ready_table_.Reset(); + for (auto& issue_queue : issue_queues_) { + issue_queue->Reset(); + } + for (auto& engine : scalar_engines_) { + engine->Reset(); + } + for (auto& engine : vec_engines_) { + engine->Reset(); + } + for (auto& engine : cube_engines_) { + engine->Reset(); + } + for (auto& engine : tma_engines_) { + engine->Reset(); + } +} + +void Core::Report() { + frontend_.Report(); + rob_.Report(); + rename_.Report(); + dispatch_.Report(); + ready_table_.Report(); + rob_input_q_.Report(); + rob_to_rename_q_.Report(); + rob_to_rename_retire_q_.Report(); + rename_to_dispatch_q_.Report(); + wakeup_q_.Report(); + rob_done_q_.Report(); + for (auto& queue : dispatch_to_issue_qs_) { + queue.Report(); + } + for (auto& queue : ready_to_issue_qs_) { + queue.Report(); + } + for (auto& queue : issue_wakeup_qs_) { + queue.Report(); + } + for (auto& queue : issue_to_engine_qs_) { + queue.Report(); + } + for (auto& issue_queue : issue_queues_) { + issue_queue->Report(); + } + for (auto& engine : scalar_engines_) { + engine->Report(); + } + for (auto& engine : vec_engines_) { + engine->Report(); + } + for (auto& engine : cube_engines_) { + engine->Report(); + } + for (auto& engine : tma_engines_) { + engine->Report(); + } +} + +void Core::Work() { + frontend_.SetCurrentCycle(CurrentCycle()); + rob_.SetCurrentCycle(CurrentCycle()); + rename_.SetCurrentCycle(CurrentCycle()); + dispatch_.SetCurrentCycle(CurrentCycle()); + rob_input_q_.SetCurrentCycle(CurrentCycle()); + rob_to_rename_q_.SetCurrentCycle(CurrentCycle()); + rob_to_rename_retire_q_.SetCurrentCycle(CurrentCycle()); + rename_to_dispatch_q_.SetCurrentCycle(CurrentCycle()); + wakeup_q_.SetCurrentCycle(CurrentCycle()); + rob_done_q_.SetCurrentCycle(CurrentCycle()); + ready_table_.SetCurrentCycle(CurrentCycle()); + for (auto& queue : dispatch_to_issue_qs_) { + queue.SetCurrentCycle(CurrentCycle()); + } + for (auto& queue : ready_to_issue_qs_) { + queue.SetCurrentCycle(CurrentCycle()); + } + for (auto& queue : issue_wakeup_qs_) { + queue.SetCurrentCycle(CurrentCycle()); + } + for (auto& queue : issue_to_engine_qs_) { + queue.SetCurrentCycle(CurrentCycle()); + } + for (auto& issue_queue : issue_queues_) { + issue_queue->SetCurrentCycle(CurrentCycle()); + } + for (auto& engine : scalar_engines_) { + engine->SetCurrentCycle(CurrentCycle()); + } + for (auto& engine : vec_engines_) { + engine->SetCurrentCycle(CurrentCycle()); + } + for (auto& engine : cube_engines_) { + engine->SetCurrentCycle(CurrentCycle()); + } + for (auto& engine : tma_engines_) { + engine->SetCurrentCycle(CurrentCycle()); + } + + if (!rob_.Full()) { + frontend_.Work(); + MergeChildProgress(*this, frontend_); + } + rob_input_q_.Work(); + MergeChildProgress(*this, rob_input_q_); + rob_.Work(); + MergeChildProgress(*this, rob_); + rob_to_rename_q_.Work(); + MergeChildProgress(*this, rob_to_rename_q_); + rob_to_rename_retire_q_.Work(); + MergeChildProgress(*this, rob_to_rename_retire_q_); + rename_.Work(); + MergeChildProgress(*this, rename_); + rename_to_dispatch_q_.Work(); + MergeChildProgress(*this, rename_to_dispatch_q_); + dispatch_.Work(); + MergeChildProgress(*this, dispatch_); + for (auto& queue : dispatch_to_issue_qs_) { + queue.Work(); + MergeChildProgress(*this, queue); + } + wakeup_q_.Work(); + MergeChildProgress(*this, wakeup_q_); + ready_table_.Work(); + MergeChildProgress(*this, ready_table_); + for (auto& queue : ready_to_issue_qs_) { + queue.Work(); + MergeChildProgress(*this, queue); + } + for (auto& queue : issue_wakeup_qs_) { + queue.Work(); + MergeChildProgress(*this, queue); + } + rob_done_q_.Work(); + MergeChildProgress(*this, rob_done_q_); + for (auto& issue_queue : issue_queues_) { + issue_queue->Work(); + MergeChildProgress(*this, *issue_queue); + } + for (auto& queue : issue_to_engine_qs_) { + queue.Work(); + MergeChildProgress(*this, queue); + } + for (auto& engine : scalar_engines_) { + engine->Work(); + MergeChildProgress(*this, *engine); + } + for (auto& engine : vec_engines_) { + engine->Work(); + MergeChildProgress(*this, *engine); + } + for (auto& engine : cube_engines_) { + engine->Work(); + MergeChildProgress(*this, *engine); + } + for (auto& engine : tma_engines_) { + engine->Work(); + MergeChildProgress(*this, *engine); + } +} + +void Core::Xfer() { + frontend_.Xfer(); + rob_.Xfer(); + rename_.Xfer(); + dispatch_.Xfer(); + ready_table_.Xfer(); + rob_input_q_.Xfer(); + rob_to_rename_q_.Xfer(); + rob_to_rename_retire_q_.Xfer(); + rename_to_dispatch_q_.Xfer(); + wakeup_q_.Xfer(); + rob_done_q_.Xfer(); + for (auto& queue : dispatch_to_issue_qs_) { + queue.Xfer(); + } + for (auto& queue : ready_to_issue_qs_) { + queue.Xfer(); + } + for (auto& queue : issue_wakeup_qs_) { + queue.Xfer(); + } + for (auto& queue : issue_to_engine_qs_) { + queue.Xfer(); + } + for (auto& issue_queue : issue_queues_) { + issue_queue->Xfer(); + } + for (auto& engine : scalar_engines_) { + engine->Xfer(); + } + for (auto& engine : vec_engines_) { + engine->Xfer(); + } + for (auto& engine : cube_engines_) { + engine->Xfer(); + } + for (auto& engine : tma_engines_) { + engine->Xfer(); + } +} + +void Core::PrintPipeView(std::ostream& os) const { + os << "rob_count=" << rob_.Count() + << " rob_capacity=" << rob_.Capacity() + << " frontend_done=" << (frontend_.Done() ? 1 : 0) + << " q_rob_in=" << rob_input_q_.Occupancy() + << " q_rob_rename=" << rob_to_rename_q_.Occupancy() + << " q_rename_retire=" << rob_to_rename_retire_q_.Occupancy() + << " q_rename_dispatch=" << rename_to_dispatch_q_.Occupancy() + << " q_wakeup=" << wakeup_q_.Occupancy() + << " q_rob_done=" << rob_done_q_.Occupancy() + << "\n"; +} + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core.hpp new file mode 100644 index 00000000..2ba1f356 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include + +#include "backend/dispatch.hpp" +#include "backend/cube.hpp" +#include "backend/issue_queue.hpp" +#include "backend/ready_table.hpp" +#include "backend/rename.hpp" +#include "backend/rob.hpp" +#include "backend/scalar.hpp" +#include "backend/tma.hpp" +#include "backend/vector.hpp" +#include "davincioo/model/config.hpp" +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" +#include "frontend/trace_source_module.hpp" + +namespace davincioo::model_top { + +class Core : public SimObject { +public: + explicit Core(CoreConfig config); + + void LoadTrace(std::vector insts); + std::size_t SourceCount() const; + bool Done() const; + const std::vector& Processed() const; + std::size_t RobCapacity() const; + std::size_t RobCount() const; + std::size_t ScalarEngineCount() const; + std::size_t VecEngineCount() const; + std::size_t CubeEngineCount() const; + std::size_t TmaEngineCount() const; + std::string DumpRobState() const; + std::string DumpSchedulerState() const; + void PrintPipeView(std::ostream& os) const; + + void Build() override; + void Reset() override; + void Report() override; + void Work() override; + void Xfer() override; + +private: + frontend::TraceSourceModule frontend_; + backend::ROB rob_; + backend::Rename rename_; + backend::Dispatch dispatch_; + backend::ReadyTable ready_table_; + SimQueue rob_input_q_; + SimQueue rob_to_rename_q_; + SimQueue rob_to_rename_retire_q_; + SimQueue rename_to_dispatch_q_; + SimQueue wakeup_q_; + SimQueue rob_done_q_; + std::vector> dispatch_to_issue_qs_; + std::vector> ready_to_issue_qs_; + std::vector> issue_wakeup_qs_; + std::vector> issue_to_engine_qs_; + std::vector> issue_queues_; + std::vector> scalar_engines_; + std::vector> vec_engines_; + std::vector> cube_engines_; + std::vector> tma_engines_; + bool built_ = false; +}; + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core_system.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core_system.cpp new file mode 100644 index 00000000..18a63256 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core_system.cpp @@ -0,0 +1,150 @@ +#include "model_top/core_system.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace davincioo::model_top { + +CoreSystem::CoreSystem(CoreConfig config) + : config_(config) { +} + +void CoreSystem::LoadTrace(std::vector insts) { + EnsureCoresForBlocks(insts); + + std::map> insts_by_block; + for (PTOInst& inst : insts) { + insts_by_block[inst.block_idx].push_back(std::move(inst)); + } + for (std::size_t index = 0; index < cores_.size(); ++index) { + cores_[index]->LoadTrace(std::move(insts_by_block[core_block_indices_[index]])); + } +} + +void CoreSystem::RunReference(std::optional stop_pc) { + (void)stop_pc; +} + +void CoreSystem::step() { + Step(); +} + +std::uint64_t CoreSystem::getCycles() const { + return Cycle(); +} + +bool CoreSystem::needTerminate() const { + return std::all_of(cores_.begin(), cores_.end(), [](const Core* core) { return core->Done(); }); +} + +void CoreSystem::enableTrace(std::optional start_pc) { + (void)start_pc; + trace_enabled_ = true; +} + +void CoreSystem::PrintPipeView(std::ostream& os) const { + os << "cycle=" << Cycle(); + for (std::size_t index = 0; index < cores_.size(); ++index) { + os << " block=" << core_block_indices_[index] << " "; + cores_[index]->PrintPipeView(os); + } +} + +SimulationResult CoreSystem::RunToCompletion() { + return RunToCompletion(RunArgs{}); +} + +SimulationResult CoreSystem::RunToCompletion(const RunArgs& args) { + Build(); + Reset(); + bool terminate = false; + std::uint64_t idle_cycles = 0; + while (!terminate) { + RunReference(args.stop_pc); + step(); + std::size_t total_progress = 0; + for (Core* core : cores_) { + total_progress += core->ConsumeProgress(); + } + if (total_progress == 0) { + ++idle_cycles; + } else { + idle_cycles = 0; + } + if (args.print_pipeview) { + PrintPipeView(std::cout); + } + enableTrace(std::nullopt); + terminate = needTerminate(); + if (!terminate && idle_cycles >= args.deadlock_cycles) { + throw std::runtime_error( + "deadlock detected: no simulator progress for " + std::to_string(args.deadlock_cycles) + + " cycles\n" + DumpRobStates()); + } + if (getCycles() >= args.stop_cycles) { + break; + } + } + + SimulationResult result; + result.record_count = 0; + result.simulated_cycles = static_cast(Cycle()); + result.rob_capacity = 0; + result.rob_count = 0; + result.scalar_engine_count = cores_.empty() ? 0 : cores_.front()->ScalarEngineCount(); + result.vec_engine_count = cores_.empty() ? 0 : cores_.front()->VecEngineCount(); + result.cube_engine_count = cores_.empty() ? 0 : cores_.front()->CubeEngineCount(); + result.tma_engine_count = cores_.empty() ? 0 : cores_.front()->TmaEngineCount(); + + for (Core* core : cores_) { + result.record_count += core->SourceCount(); + result.rob_capacity += core->RobCapacity(); + result.rob_count += core->RobCount(); + const auto& processed = core->Processed(); + result.processed.insert(result.processed.end(), processed.begin(), processed.end()); + } + std::sort(result.processed.begin(), result.processed.end(), [](const PTOInst& lhs, const PTOInst& rhs) { + return std::tie(lhs.timestamps.rob_alloc_cycle, lhs.block_idx, lhs.sequence_id) < + std::tie(rhs.timestamps.rob_alloc_cycle, rhs.block_idx, rhs.sequence_id); + }); + for (const PTOInst& inst : result.processed) { + ++result.opcode_counts[OpcodeName(inst)]; + } + return result; +} + +void CoreSystem::EnsureCoresForBlocks(const std::vector& insts) { + std::set blocks; + for (const PTOInst& inst : insts) { + blocks.insert(inst.block_idx); + } + const std::vector desired_blocks(blocks.begin(), blocks.end()); + if (!cores_.empty()) { + GFSIM_ASSERT(core_block_indices_ == desired_blocks); + return; + } + core_block_indices_ = desired_blocks; + cores_.reserve(core_block_indices_.size()); + for (std::size_t index = 0; index < core_block_indices_.size(); ++index) { + cores_.push_back(&EmplaceOwnedModule(config_)); + } +} + +std::string CoreSystem::DumpRobStates() const { + std::ostringstream stream; + for (std::size_t index = 0; index < cores_.size(); ++index) { + if (index != 0) { + stream << "\n"; + } + stream << "block_idx=" << core_block_indices_[index] << "\n" << cores_[index]->DumpRobState(); + stream << "\n" << cores_[index]->DumpSchedulerState(); + } + return stream.str(); +} + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core_system.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core_system.hpp new file mode 100644 index 00000000..9bdb1ced --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/core_system.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "davincioo/model/config.hpp" +#include "davincioo/model/framework.hpp" +#include "davincioo/model/pto_inst.hpp" +#include "model_top/core.hpp" + +namespace davincioo::model_top { + +class CoreSystem : public SimSystem { +public: + explicit CoreSystem(CoreConfig config); + + struct RunArgs { + std::optional stop_pc; + std::uint64_t stop_cycles = std::numeric_limits::max(); + std::uint64_t deadlock_cycles = 2000; + bool print_pipeview = false; + }; + + void LoadTrace(std::vector insts); + SimulationResult RunToCompletion(); + SimulationResult RunToCompletion(const RunArgs& args); + void RunReference(std::optional stop_pc); + void step(); + std::uint64_t getCycles() const; + bool needTerminate() const; + void enableTrace(std::optional start_pc); + void PrintPipeView(std::ostream& os) const; + +private: + void EnsureCoresForBlocks(const std::vector& insts); + std::string DumpRobStates() const; + + CoreConfig config_{}; + std::vector cores_; + std::vector core_block_indices_; + bool trace_enabled_ = false; +}; + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/kanata_pipeview.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/kanata_pipeview.cpp new file mode 100644 index 00000000..5214b555 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/kanata_pipeview.cpp @@ -0,0 +1,148 @@ +#include "model_top/kanata_pipeview.hpp" + +#include +#include +#include +#include +#include +#include + +#include "davincioo/model/rob.hpp" + +namespace davincioo::model_top { + +namespace { + +struct TimedEvent { + std::uint64_t cycle = 0; + int order = 0; + std::uint64_t inst_id = 0; + std::string line; +}; + +bool HasCycleStamp(std::uint64_t cycle) { + return cycle != kUnsetCycleStamp; +} + +std::string EscapeKanataText(const std::string& text) { + std::string escaped; + escaped.reserve(text.size()); + for (const char ch : text) { + if (ch == '\t' || ch == '\n' || ch == '\r') { + escaped.push_back(' '); + } else { + escaped.push_back(ch); + } + } + return escaped; +} + +std::string MakeTimestampNote(const PTOInst& inst) { + std::ostringstream stream; + stream << "alloc=" << inst.timestamps.rob_alloc_cycle + << " rename=" << inst.timestamps.rename_cycle + << " dispatch=" << inst.timestamps.dispatch_cycle + << " issue=" << inst.timestamps.issue_cycle + << " engine_pop=" << inst.timestamps.engine_pop_cycle + << " engine_complete=" << inst.timestamps.engine_complete_cycle + << " retire=" << inst.timestamps.rob_retire_cycle; + return stream.str(); +} + +std::string MakeExecStageName(const PTOInst& inst) { + std::ostringstream stream; + stream << ToString(inst.engine_kind) << inst.engine_id; + return stream.str(); +} + +void AddEvent( + std::vector& events, + std::uint64_t cycle, + int order, + std::uint64_t inst_id, + std::string line) { + if (!HasCycleStamp(cycle)) { + return; + } + events.push_back(TimedEvent{ + .cycle = cycle, + .order = order, + .inst_id = inst_id, + .line = std::move(line), + }); +} + +} // namespace + +void WriteKanataPipeView(const std::filesystem::path& path, const SimulationResult& result) { + if (!path.parent_path().empty()) { + std::filesystem::create_directories(path.parent_path()); + } + + std::ofstream stream(path, std::ios::out | std::ios::trunc); + if (!stream) { + throw std::runtime_error("failed to open Kanata pipeview output: " + path.string()); + } + + stream << "Kanata\t0004\n"; + if (result.processed.empty()) { + stream << "C=\t0\n"; + return; + } + + std::vector events; + events.reserve(result.processed.size() * 10); + + std::uint64_t retire_id = 0; + for (std::size_t index = 0; index < result.processed.size(); ++index) { + const PTOInst& inst = result.processed[index]; + const std::uint64_t inst_id = static_cast(index); + const std::uint64_t sim_id = inst.sequence_id; + const std::uint64_t thread_id = inst.block_idx; + + AddEvent(events, inst.timestamps.rob_alloc_cycle, 0, inst_id, + "I\t" + std::to_string(inst_id) + "\t" + std::to_string(sim_id) + "\t" + std::to_string(thread_id)); + AddEvent(events, inst.timestamps.rob_alloc_cycle, 1, inst_id, + "L\t" + std::to_string(inst_id) + "\t0\t" + EscapeKanataText(DumpPTOInst(inst))); + AddEvent(events, inst.timestamps.rob_alloc_cycle, 2, inst_id, + "L\t" + std::to_string(inst_id) + "\t1\t" + EscapeKanataText(MakeTimestampNote(inst))); + AddEvent(events, inst.timestamps.rob_alloc_cycle, 3, inst_id, + "S\t" + std::to_string(inst_id) + "\t0\tROB"); + AddEvent(events, inst.timestamps.rename_cycle, 4, inst_id, + "S\t" + std::to_string(inst_id) + "\t0\tRENAME"); + AddEvent(events, inst.timestamps.dispatch_cycle, 5, inst_id, + "S\t" + std::to_string(inst_id) + "\t0\tDISP"); + AddEvent(events, inst.timestamps.issue_cycle, 6, inst_id, + "S\t" + std::to_string(inst_id) + "\t0\tISSQ"); + AddEvent(events, inst.timestamps.engine_pop_cycle, 7, inst_id, + "S\t" + std::to_string(inst_id) + "\t0\t" + MakeExecStageName(inst)); + AddEvent(events, inst.timestamps.engine_complete_cycle, 8, inst_id, + "S\t" + std::to_string(inst_id) + "\t0\tWB"); + AddEvent(events, inst.timestamps.rob_retire_cycle, 9, inst_id, + "S\t" + std::to_string(inst_id) + "\t0\tRET"); + AddEvent(events, inst.timestamps.rob_retire_cycle, 10, inst_id, + "R\t" + std::to_string(inst_id) + "\t" + std::to_string(retire_id++) + "\t0"); + } + + std::sort(events.begin(), events.end(), [](const TimedEvent& lhs, const TimedEvent& rhs) { + if (lhs.cycle != rhs.cycle) { + return lhs.cycle < rhs.cycle; + } + if (lhs.order != rhs.order) { + return lhs.order < rhs.order; + } + return lhs.inst_id < rhs.inst_id; + }); + + std::uint64_t current_cycle = events.front().cycle; + stream << "C=\t" << current_cycle << "\n"; + for (const TimedEvent& event : events) { + if (event.cycle > current_cycle) { + stream << "C\t" << (event.cycle - current_cycle) << "\n"; + current_cycle = event.cycle; + } + stream << event.line << "\n"; + } +} + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/kanata_pipeview.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/kanata_pipeview.hpp new file mode 100644 index 00000000..8d8124be --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/kanata_pipeview.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo::model_top { + +void WriteKanataPipeView(const std::filesystem::path& path, const SimulationResult& result); + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/main.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/main.cpp new file mode 100644 index 00000000..eaaabce1 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/main.cpp @@ -0,0 +1,32 @@ +#include +#include + +#include "frontend/trace.hpp" +#include "model_top/cli.hpp" + +int main(int argc, char** argv) { + auto parsed = davincioo::model_top::ParseArgs(argc, argv); + if (!parsed.has_value()) { + return 2; + } + + const davincioo::model_top::Options& options = *parsed; + if (options.help_only) { + return 0; + } + + std::vector trace_lines; + try { + trace_lines = davincioo::frontend::ReadTraceLines(options.trace_path); + } catch (const std::runtime_error& error) { + std::cerr << error.what() << "\n"; + return 1; + } + + try { + return davincioo::model_top::RunGfsimCommand(options, trace_lines); + } catch (const std::runtime_error& error) { + std::cerr << error.what() << "\n"; + return 1; + } +} diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/perfetto_trace.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/perfetto_trace.cpp new file mode 100644 index 00000000..79eadc09 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/perfetto_trace.cpp @@ -0,0 +1,388 @@ +#include "model_top/perfetto_trace.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "frontend/trace.hpp" + +namespace davincioo::model_top { + +namespace { + +struct TrackRef { + int pid = 0; + int tid = 0; + std::string process_name; + std::string thread_name; + int sort_index = 0; +}; + +struct RobDelta { + std::uint64_t cycle = 0; + int order = 0; + int delta = 0; +}; + +constexpr int kRobPid = 1; +constexpr int kRobTid = 1; +constexpr int kCorePidBase = 1000; +constexpr int kScalarTidBase = 100; +constexpr int kVecTidBase = 200; +constexpr int kCubeTidBase = 300; +constexpr int kTmaTidBase = 400; + +std::uint64_t DurationFromCycles(std::uint64_t start_cycle, std::uint64_t end_cycle) { + if (start_cycle == kUnsetCycleStamp || end_cycle == kUnsetCycleStamp || end_cycle < start_cycle) { + return 0; + } + return end_cycle - start_cycle; +} + +TrackRef MakeEngineTrack(PTOEngineKind kind, std::uint64_t engine_id, std::uint64_t block_idx) { + const int pid = kCorePidBase + static_cast(block_idx); + switch (kind) { + case PTOEngineKind::Scalar: + return TrackRef{ + .pid = pid, + .tid = static_cast(kScalarTidBase + engine_id), + .process_name = "CORE " + std::to_string(block_idx), + .thread_name = "SCALAR" + std::to_string(engine_id), + .sort_index = static_cast(block_idx * 1000 + kScalarTidBase + engine_id), + }; + case PTOEngineKind::Vec: + return TrackRef{ + .pid = pid, + .tid = static_cast(kVecTidBase + engine_id), + .process_name = "CORE " + std::to_string(block_idx), + .thread_name = "VEC" + std::to_string(engine_id), + .sort_index = static_cast(block_idx * 1000 + kVecTidBase + engine_id), + }; + case PTOEngineKind::Cube: + return TrackRef{ + .pid = pid, + .tid = static_cast(kCubeTidBase + engine_id), + .process_name = "CORE " + std::to_string(block_idx), + .thread_name = "CUBE" + std::to_string(engine_id), + .sort_index = static_cast(block_idx * 1000 + kCubeTidBase + engine_id), + }; + case PTOEngineKind::Tma: + return TrackRef{ + .pid = pid, + .tid = static_cast(kTmaTidBase + engine_id), + .process_name = "CORE " + std::to_string(block_idx), + .thread_name = "TMA" + std::to_string(engine_id), + .sort_index = static_cast(block_idx * 1000 + kTmaTidBase + engine_id), + }; + default: + return TrackRef{}; + } +} + +std::string MakeEngineSliceName(const PTOInst& inst) { + std::ostringstream stream; + stream << OpcodeName(inst) + << " seq=" << inst.sequence_id + << " c" << inst.timestamps.engine_pop_cycle + << "->" << inst.timestamps.engine_complete_cycle; + return stream.str(); +} + +std::string MakeProcessNameEvent(int pid, const std::string& name) { + std::ostringstream stream; + stream << "{\"ph\":\"M\",\"pid\":" << pid << ",\"tid\":0," + << "\"name\":\"process_name\",\"args\":{\"name\":\"" << frontend::JsonEscape(name) << "\"}}"; + return stream.str(); +} + +std::string MakeProcessSortEvent(int pid, int sort_index) { + std::ostringstream stream; + stream << "{\"ph\":\"M\",\"pid\":" << pid << ",\"tid\":0," + << "\"name\":\"process_sort_index\",\"args\":{\"sort_index\":" << sort_index << "}}"; + return stream.str(); +} + +std::string MakeThreadNameEvent(const TrackRef& track) { + std::ostringstream stream; + stream << "{\"ph\":\"M\",\"pid\":" << track.pid << ",\"tid\":" << track.tid << "," + << "\"name\":\"thread_name\",\"args\":{\"name\":\"" << frontend::JsonEscape(track.thread_name) << "\"}}"; + return stream.str(); +} + +std::string MakeThreadSortEvent(const TrackRef& track) { + std::ostringstream stream; + stream << "{\"ph\":\"M\",\"pid\":" << track.pid << ",\"tid\":" << track.tid << "," + << "\"name\":\"thread_sort_index\",\"args\":{\"sort_index\":" << track.sort_index << "}}"; + return stream.str(); +} + +std::string MakeCounterEvent( + std::uint64_t cycle, + std::size_t occupancy, + std::size_t capacity) { + std::ostringstream stream; + stream << "{\"name\":\"ROB occupancy\",\"cat\":\"rob\",\"ph\":\"C\"," + << "\"pid\":" << kRobPid << ",\"tid\":" << kRobTid << ",\"ts\":" << cycle << "," + << "\"args\":{\"occupancy\":" << occupancy << ",\"capacity\":" << capacity << "}}"; + return stream.str(); +} + +std::string MakeInstantEvent( + std::uint64_t cycle, + const TrackRef& track, + const std::string& name, + const std::string& args_json) { + std::ostringstream stream; + stream << "{\"name\":\"" << frontend::JsonEscape(name) << "\",\"ph\":\"i\",\"s\":\"t\"," + << "\"pid\":" << track.pid << ",\"tid\":" << track.tid << ",\"ts\":" << cycle + << ",\"args\":" << args_json << "}"; + return stream.str(); +} + +std::string MakeSliceEvent(const PTOInst& inst, const TrackRef& track) { + std::ostringstream args; + args << "{\"opcode\":\"" << frontend::JsonEscape(OpcodeName(inst)) << "\"," + << "\"engine_kind\":\"" << frontend::JsonEscape(std::string(ToString(inst.engine_kind))) << "\"," + << "\"engine_id\":" << inst.engine_id << "," + << "\"block_idx\":" << inst.block_idx << "," + << "\"sequence_id\":" << inst.sequence_id << "," + << "\"rob_id\":" << inst.rob_id << "," + << "\"start_cycle\":" << inst.timestamps.engine_pop_cycle << "," + << "\"end_cycle\":" << inst.timestamps.engine_complete_cycle << "," + << "\"duration_cycles\":" << DurationFromCycles(inst.timestamps.engine_pop_cycle, inst.timestamps.engine_complete_cycle) + << "}"; + + std::ostringstream stream; + stream << "{\"name\":\"" << frontend::JsonEscape(MakeEngineSliceName(inst)) << "\"," + << "\"cat\":\"engine\",\"ph\":\"X\"," + << "\"pid\":" << track.pid << ",\"tid\":" << track.tid << "," + << "\"ts\":" << inst.timestamps.engine_pop_cycle << "," + << "\"dur\":" << DurationFromCycles(inst.timestamps.engine_pop_cycle, inst.timestamps.engine_complete_cycle) << "," + << "\"args\":" << args.str() << "}"; + return stream.str(); +} + +std::string MakeFlowEvent( + const char phase, + std::uint64_t flow_id, + std::uint64_t cycle, + const TrackRef& track, + std::uint64_t producer_sequence, + std::uint64_t consumer_sequence, + std::uint64_t tile_tag, + bool bind_to_enclosing) { + std::ostringstream args; + args << "{\"producer_sequence_id\":" << producer_sequence << "," + << "\"consumer_sequence_id\":" << consumer_sequence << "," + << "\"tile_tag\":" << tile_tag << "}"; + + std::ostringstream stream; + stream << "{\"name\":\"wake tag=" << tile_tag << "\",\"cat\":\"dependency\",\"ph\":\"" << phase << "\"," + << "\"pid\":" << track.pid << ",\"tid\":" << track.tid << ",\"ts\":" << cycle << "," + << "\"id\":" << flow_id; + if (bind_to_enclosing) { + stream << ",\"bp\":\"e\""; + } + stream << ",\"args\":" << args.str() << "}"; + return stream.str(); +} + +std::uint64_t BindInsideSlice(std::uint64_t start_cycle, std::uint64_t end_cycle) { + if (start_cycle == kUnsetCycleStamp || end_cycle == kUnsetCycleStamp || end_cycle <= start_cycle) { + return start_cycle; + } + return end_cycle - 1; +} + +void AppendTrackMetadata( + std::vector& events, + const TrackRef& track, + std::set& emitted_processes, + std::set>& emitted_threads) { + if (emitted_processes.insert(track.pid).second) { + events.push_back(MakeProcessNameEvent(track.pid, track.process_name)); + events.push_back(MakeProcessSortEvent(track.pid, track.sort_index / 100)); + } + if (emitted_threads.insert({track.pid, track.tid}).second) { + events.push_back(MakeThreadNameEvent(track)); + events.push_back(MakeThreadSortEvent(track)); + } +} + +void AppendEngineTrackMetadata( + std::vector& events, + std::uint64_t block_idx, + PTOEngineKind kind, + std::size_t engine_count, + std::set& emitted_processes, + std::set>& emitted_threads) { + for (std::size_t engine_index = 0; engine_index < engine_count; ++engine_index) { + AppendTrackMetadata( + events, + MakeEngineTrack(kind, engine_index, block_idx), + emitted_processes, + emitted_threads); + } +} + +} // namespace + +void WritePerfettoTrace(const std::filesystem::path& path, const SimulationResult& result) { + if (!path.parent_path().empty()) { + std::filesystem::create_directories(path.parent_path()); + } + + std::ofstream stream(path, std::ios::out | std::ios::trunc); + if (!stream) { + throw std::runtime_error("failed to open Perfetto trace output: " + path.string()); + } + + std::vector events; + events.reserve(result.processed.size() * 6 + 32); + + std::set emitted_processes; + std::set> emitted_threads; + const TrackRef rob_track{ + .pid = kRobPid, + .tid = kRobTid, + .process_name = "ROB", + .thread_name = "ROB occupancy / cap=" + std::to_string(result.rob_capacity), + .sort_index = 0, + }; + AppendTrackMetadata(events, rob_track, emitted_processes, emitted_threads); + std::set block_indices; + for (const PTOInst& inst : result.processed) { + block_indices.insert(inst.block_idx); + } + for (const std::uint64_t block_idx : block_indices) { + AppendEngineTrackMetadata(events, block_idx, PTOEngineKind::Scalar, result.scalar_engine_count, emitted_processes, emitted_threads); + AppendEngineTrackMetadata(events, block_idx, PTOEngineKind::Vec, result.vec_engine_count, emitted_processes, emitted_threads); + AppendEngineTrackMetadata(events, block_idx, PTOEngineKind::Cube, result.cube_engine_count, emitted_processes, emitted_threads); + AppendEngineTrackMetadata(events, block_idx, PTOEngineKind::Tma, result.tma_engine_count, emitted_processes, emitted_threads); + } + + std::vector rob_deltas; + rob_deltas.reserve(result.processed.size() * 2); + for (const PTOInst& inst : result.processed) { + if (inst.timestamps.rob_alloc_cycle != kUnsetCycleStamp) { + rob_deltas.push_back(RobDelta{ + .cycle = inst.timestamps.rob_alloc_cycle, + .order = 1, + .delta = 1, + }); + } + if (inst.timestamps.rob_retire_cycle != kUnsetCycleStamp) { + rob_deltas.push_back(RobDelta{ + .cycle = inst.timestamps.rob_retire_cycle, + .order = 0, + .delta = -1, + }); + } + } + std::sort(rob_deltas.begin(), rob_deltas.end(), [](const RobDelta& lhs, const RobDelta& rhs) { + return std::tie(lhs.cycle, lhs.order, lhs.delta) < std::tie(rhs.cycle, rhs.order, rhs.delta); + }); + std::size_t occupancy = 0; + events.push_back(MakeCounterEvent(0, occupancy, result.rob_capacity)); + for (const RobDelta& delta : rob_deltas) { + occupancy = static_cast(static_cast(occupancy) + delta.delta); + events.push_back(MakeCounterEvent(delta.cycle, occupancy, result.rob_capacity)); + } + events.push_back(MakeInstantEvent( + 0, + rob_track, + "ROB summary", + "{\"capacity\":" + std::to_string(result.rob_capacity) + + ",\"retired\":" + std::to_string(result.processed.size()) + + ",\"simulated_cycles\":" + std::to_string(result.simulated_cycles) + "}")); + + for (const PTOInst& inst : result.processed) { + if (inst.engine_id == std::numeric_limits::max() || + inst.timestamps.engine_pop_cycle == kUnsetCycleStamp || + inst.timestamps.engine_complete_cycle == kUnsetCycleStamp) { + continue; + } + const TrackRef track = MakeEngineTrack(inst.engine_kind, inst.engine_id, inst.block_idx); + events.push_back(MakeSliceEvent(inst, track)); + } + + std::map, const PTOInst*> tag_producers; + std::uint64_t next_flow_id = 1; + for (const PTOInst& inst : result.processed) { + std::set> seen_edges; + for (const auto& input : inst.tile_reg_inputs) { + if (!input.rename_managed || input.tile_tag == kInvalidTileTag) { + continue; + } + const auto producer_it = tag_producers.find({inst.block_idx, input.tile_tag}); + if (producer_it == tag_producers.end()) { + continue; + } + const PTOInst& producer = *producer_it->second; + if (producer.engine_id == std::numeric_limits::max() || + producer.timestamps.engine_complete_cycle == kUnsetCycleStamp || + inst.timestamps.issue_cycle == kUnsetCycleStamp) { + continue; + } + const auto edge_key = std::make_tuple(producer.sequence_id, inst.sequence_id, input.tile_tag); + if (!seen_edges.insert(edge_key).second) { + continue; + } + const TrackRef producer_track = MakeEngineTrack(producer.engine_kind, producer.engine_id, producer.block_idx); + const TrackRef consumer_track = MakeEngineTrack(inst.engine_kind, inst.engine_id, inst.block_idx); + events.push_back(MakeFlowEvent( + 's', + next_flow_id, + BindInsideSlice(producer.timestamps.engine_pop_cycle, producer.timestamps.engine_complete_cycle), + producer_track, + producer.sequence_id, + inst.sequence_id, + input.tile_tag, + false)); + events.push_back(MakeFlowEvent( + 'f', + next_flow_id, + inst.timestamps.issue_cycle, + consumer_track, + producer.sequence_id, + inst.sequence_id, + input.tile_tag, + true)); + ++next_flow_id; + } + for (const auto& output : inst.tile_reg_outputs) { + if (!output.rename_managed || output.tile_tag == kInvalidTileTag) { + continue; + } + tag_producers[{inst.block_idx, output.tile_tag}] = &inst; + } + } + + stream << "{\n" + << " \"displayTimeUnit\": \"ns\",\n" + << " \"otherData\": {\n" + << " \"tool\": \"gfsim\",\n" + << " \"record_count\": " << result.record_count << ",\n" + << " \"simulated_cycles\": " << result.simulated_cycles << ",\n" + << " \"rob_capacity\": " << result.rob_capacity << "\n" + << " },\n" + << " \"traceEvents\": [\n"; + for (std::size_t index = 0; index < events.size(); ++index) { + stream << " " << events[index]; + if (index + 1 != events.size()) { + stream << ","; + } + stream << "\n"; + } + stream << " ]\n" + << "}\n"; +} + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/perfetto_trace.hpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/perfetto_trace.hpp new file mode 100644 index 00000000..248118de --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/model_top/perfetto_trace.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "davincioo/model/pto_inst.hpp" + +namespace davincioo::model_top { + +void WritePerfettoTrace(const std::filesystem::path& path, const SimulationResult& result); + +} // namespace davincioo::model_top diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/src/pto_inst.cpp b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/pto_inst.cpp new file mode 100644 index 00000000..fbafa19a --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/src/pto_inst.cpp @@ -0,0 +1,118 @@ +#include "davincioo/model/pto_inst.hpp" + +#include +#include + +namespace davincioo { + +namespace { + +std::string DumpScalarValue(const PTOScalarValue& value) { + return std::visit( + [](const auto& inner) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return "null"; + } else if constexpr (std::is_same_v) { + return inner ? "true" : "false"; + } else { + return std::to_string(inner); + } + }, + value); +} + +void AppendShape(std::ostringstream& stream, const std::vector& shape) { + stream << "["; + for (std::size_t index = 0; index < shape.size(); ++index) { + if (index != 0) { + stream << ","; + } + stream << shape[index]; + } + stream << "]"; +} + +void AppendTileReg(std::ostringstream& stream, const PTOTileReg& reg) { + stream << "0x" << std::hex << reg.address << std::dec + << ":tag="; + if (reg.tile_tag == kInvalidTileTag) { + stream << "-"; + } else { + stream << reg.tile_tag; + } + stream << ":rm=" << (reg.rename_managed ? 1 : 0) + << ":" << ToString(reg.tile_type) + << ":" << ToString(reg.dtype) + << ":" << ToString(reg.layout) + << ":"; + AppendShape(stream, reg.shape); +} + +void AppendTileInputState( + std::ostringstream& stream, + const PTOInst& inst, + std::size_t index) { + if (index >= inst.tile_input_valid.size() || index >= inst.tile_input_ready.size()) { + return; + } + stream << ":v=" << (inst.tile_input_valid[index] ? 1 : 0) + << ":r=" << (inst.tile_input_ready[index] ? 1 : 0); +} + +void AppendTileRegVector( + std::ostringstream& stream, + std::string_view label, + const PTOInst& inst, + const std::vector& regs, + bool annotate_inputs) { + stream << " " << label << "=["; + for (std::size_t index = 0; index < regs.size(); ++index) { + if (index != 0) { + stream << ", "; + } + AppendTileReg(stream, regs[index]); + if (annotate_inputs) { + AppendTileInputState(stream, inst, index); + } + } + stream << "]"; +} + +void AppendScalarVector( + std::ostringstream& stream, + const std::vector& scalars) { + stream << " scalars=["; + for (std::size_t index = 0; index < scalars.size(); ++index) { + if (index != 0) { + stream << ", "; + } + stream << ToString(scalars[index].dtype) << ":" << DumpScalarValue(scalars[index].value); + } + stream << "]"; +} + +} // namespace + +std::string OpcodeName(const PTOInst& inst) { + if (!inst.raw_opcode.empty()) { + return inst.raw_opcode; + } + return std::string(ToString(inst.opcode)); +} + +std::string DumpPTOInst(const PTOInst& inst) { + std::ostringstream stream; + stream << "opcode=" << OpcodeName(inst) + << " engine=" << ToString(inst.engine_kind) + << " block=" << inst.block_idx + << " seq=" << inst.sequence_id + << " rob=" << inst.rob_id + << " eng_id=" << inst.engine_id; + AppendTileRegVector(stream, "in", inst, inst.tile_reg_inputs, true); + AppendScalarVector(stream, inst.scalar_inputs); + AppendTileRegVector(stream, "out", inst, inst.tile_reg_outputs, false); + return stream.str(); +} + +} // namespace davincioo diff --git a/components/agentic-circuit/references/davincioo-gfsim/upstream/tests/fixtures/traces/examples_intermediate_softmax.pto.trace b/components/agentic-circuit/references/davincioo-gfsim/upstream/tests/fixtures/traces/examples_intermediate_softmax.pto.trace new file mode 100644 index 00000000..08715682 --- /dev/null +++ b/components/agentic-circuit/references/davincioo-gfsim/upstream/tests/fixtures/traces/examples_intermediate_softmax.pto.trace @@ -0,0 +1,15 @@ +{"block_idx":0,"sequence_id":0,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"0"}],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":1,"opcode":"TLOAD","input_tiles":[{"address":"0xfffdb9b8d000","shape":[1,1,1,64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":2,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"65536"}],"output_tiles":[{"address":"0x10000","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":3,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"131072"}],"output_tiles":[{"address":"0x20000","shape":[64,1],"layout":"DN","dtype":"float32"}]} +{"block_idx":0,"sequence_id":4,"opcode":"TROWMAX","input_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"},{"address":"0x10000","shape":[64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x20000","shape":[64,1],"layout":"DN","dtype":"float32"}]} +{"block_idx":0,"sequence_id":5,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"0"}],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":6,"opcode":"TROWEXPANDSUB","input_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"},{"address":"0x20000","shape":[64,1],"layout":"DN","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":7,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"0"}],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":8,"opcode":"TEXP","input_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":9,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"65536"}],"output_tiles":[{"address":"0x10000","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":10,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"131072"}],"output_tiles":[{"address":"0x20000","shape":[64,1],"layout":"DN","dtype":"float32"}]} +{"block_idx":0,"sequence_id":11,"opcode":"TROWSUM","input_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"},{"address":"0x10000","shape":[64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x20000","shape":[64,1],"layout":"DN","dtype":"float32"}]} +{"block_idx":0,"sequence_id":12,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"0"}],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":13,"opcode":"TROWEXPANDDIV","input_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"},{"address":"0x20000","shape":[64,1],"layout":"DN","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}]} +{"block_idx":0,"sequence_id":14,"opcode":"TSTORE","input_tiles":[{"address":"0x0","shape":[64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0xfffeb9b8d000","shape":[1,1,1,64,256],"layout":"ND","dtype":"float32"}]} diff --git a/components/agentic-circuit/requirements-dev.lock b/components/agentic-circuit/requirements-dev.lock new file mode 100644 index 00000000..2701ae86 --- /dev/null +++ b/components/agentic-circuit/requirements-dev.lock @@ -0,0 +1,9 @@ +attrs==25.4.0 +clang-format==21.1.8 +jsonschema==4.25.1 +jsonschema-specifications==2025.9.1 +lit==18.1.8 +pyyaml==6.0.3 +referencing==0.36.2 +rpds-py==0.27.1 +typing-extensions==4.15.0 diff --git a/components/agentic-circuit/resources/diagnostics.json b/components/agentic-circuit/resources/diagnostics.json new file mode 100644 index 00000000..f2b2c7c4 --- /dev/null +++ b/components/agentic-circuit/resources/diagnostics.json @@ -0,0 +1,63 @@ +{ + "schema": "agentic-circuit-diagnostic-catalog", + "version": "0.1", + "contract_epoch": "0.3", + "entries": [ + { + "code": "ACBUILD-COMPILE-001", + "title": "Generated source compilation failed", + "rule": "Every generated translation unit must compile with the recorded C++20 toolchain contract.", + "causes": ["The selected compiler or standard-library mode differs from the build request."], + "examples": ["A libc++ build was invoked with libstdc++-specific flags."], + "repairs": ["Use one consistent compiler, standard library, ABI mode, and contract flag set."] + }, + { + "code": "ACIR-PROTOCOL-004", + "title": "Interface roles are incompatible", + "rule": "Connected endpoints must use complementary roles admitted by the selected protocol.", + "causes": ["Two initiators or two targets were connected directly."], + "examples": ["A request initiator was bound to another request initiator."], + "repairs": ["Bind the endpoint to the complementary protocol role or insert an explicit adapter."] + }, + { + "code": "ACLOWER-BINDING-MISSING", + "title": "No runtime binding is available", + "rule": "Every lowered component requires one exact provider binding at the current contract epoch.", + "causes": ["The component is declared but unavailable in the installed provider set."], + "examples": ["A declared_unavailable standard-library component was instantiated."], + "repairs": ["Install a compatible provider or select an available component implementation."] + }, + { + "code": "ACPY-CONFIG-001", + "title": "Workspace configuration was not found", + "rule": "Workspace commands require one agentic-circuit.toml found explicitly or by upward discovery.", + "causes": ["The command ran outside a workspace and no --project path was supplied."], + "examples": ["agentic-circuit check was invoked from an unrelated directory."], + "repairs": ["Run inside the workspace or pass --project with the exact manifest path."] + }, + { + "code": "ACPY-SCHEMA-001", + "title": "Schema identity is unknown", + "rule": "Discovery commands resolve only exact packaged schema names at contract epoch 0.3.", + "causes": ["The requested component, protocol, interface, packet, or diagnostic is not cataloged."], + "examples": ["schema component Missing requested a name absent from the standard-library catalog."], + "repairs": ["List the selected schema kind and use one of the returned exact identities."] + }, + { + "code": "ACRUN-DEADLOCK-001", + "title": "Simulation deadlock was detected", + "rule": "A nonterminal model must make declared progress within its configured deadlock window.", + "causes": ["All activations are blocked and no future event can wake the model."], + "examples": ["A cyclic protocol dependency left every queue waiting for input."], + "repairs": ["Inspect blocked activations and repair the protocol or resource dependency cycle."] + }, + { + "code": "ACTRACE-SCHEMA-001", + "title": "Trace input does not match pto-trace@0.1", + "rule": "Runtime traces must use the closed pto-trace@0.1 schema and exact contract epoch.", + "causes": ["The trace contains an unknown field or a different schema version."], + "examples": ["A trace produced for another epoch was supplied to the runtime."], + "repairs": ["Regenerate or convert the trace to the exact pto-trace@0.1 contract."] + } + ] +} diff --git a/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_fifo.v b/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_fifo.v new file mode 100644 index 00000000..d9282424 --- /dev/null +++ b/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_fifo.v @@ -0,0 +1,81 @@ +// PYC ready/valid FIFO migrated from pyCircuit's cycle-level primitive set. +module pyc_fifo #( + parameter integer WIDTH = 1, + parameter integer DEPTH = 2 +) ( + input wire clk, + input wire rst, + input wire in_valid, + output wire in_ready, + input wire [WIDTH-1:0] in_data, + output wire out_valid, + input wire out_ready, + output wire [WIDTH-1:0] out_data +); + function integer pyc_clog2; + input integer value; + integer i; + begin + pyc_clog2 = 0; + for (i = value - 1; i > 0; i = i >> 1) + pyc_clog2 = pyc_clog2 + 1; + end + endfunction + + localparam integer PTR_W = (DEPTH <= 1) ? 1 : pyc_clog2(DEPTH); + // Keep comparisons in the pointer/count widths. Unsized integer + // parameters otherwise trigger width expansion warnings in Verilator and + // obscure truncation bugs when this primitive is parameterized. + localparam [PTR_W:0] DEPTH_LIMIT = (PTR_W + 1)'(DEPTH); + localparam [PTR_W-1:0] LAST_PTR = PTR_W'(DEPTH - 1); + reg [WIDTH-1:0] storage [0:DEPTH-1]; + reg [PTR_W-1:0] rd_ptr; + reg [PTR_W-1:0] wr_ptr; + reg [PTR_W:0] count; + + assign in_ready = (count < DEPTH_LIMIT) || (out_ready && out_valid); + assign out_valid = (count != 0); + assign out_data = out_valid ? storage[rd_ptr] : {WIDTH{1'b0}}; + + wire do_pop = out_valid && out_ready; + wire do_push = in_valid && in_ready; + + function [PTR_W-1:0] bump_ptr; + input [PTR_W-1:0] p; + begin + if (DEPTH <= 1) + bump_ptr = {PTR_W{1'b0}}; + else if (p == LAST_PTR) + bump_ptr = {PTR_W{1'b0}}; + else + bump_ptr = p + 1'b1; + end + endfunction + + always @(posedge clk) begin + if (rst) begin + rd_ptr <= {PTR_W{1'b0}}; + wr_ptr <= {PTR_W{1'b0}}; + count <= {(PTR_W + 1){1'b0}}; + end else begin + case ({do_push, do_pop}) + 2'b01: begin + rd_ptr <= bump_ptr(rd_ptr); + count <= count - 1'b1; + end + 2'b10: begin + storage[wr_ptr] <= in_data; + wr_ptr <= bump_ptr(wr_ptr); + count <= count + 1'b1; + end + 2'b11: begin + storage[wr_ptr] <= in_data; + rd_ptr <= bump_ptr(rd_ptr); + wr_ptr <= bump_ptr(wr_ptr); + end + default: begin + end + endcase + end + end +endmodule diff --git a/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_popcount.v b/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_popcount.v new file mode 100644 index 00000000..725bcff4 --- /dev/null +++ b/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_popcount.v @@ -0,0 +1,20 @@ +// Qualified, parameterized population-count primitive. +module pyc_popcount #( + parameter integer IN_WIDTH = 8, + parameter integer OUT_WIDTH = 4 +) ( + input wire [IN_WIDTH-1:0] in, + output wire [OUT_WIDTH-1:0] out +); + integer i; + reg [OUT_WIDTH-1:0] count; + + always @* begin + count = {OUT_WIDTH{1'b0}}; + for (i = 0; i < IN_WIDTH; i = i + 1) + if (in[i]) + count = count + {{(OUT_WIDTH-1){1'b0}}, 1'b1}; + end + + assign out = count; +endmodule diff --git a/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_reg.v b/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_reg.v new file mode 100644 index 00000000..6b6baef8 --- /dev/null +++ b/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_reg.v @@ -0,0 +1,18 @@ +// PYC runtime register migrated from pyCircuit's cycle-level primitive set. +module pyc_reg #( + parameter integer WIDTH = 1 +) ( + input wire clk, + input wire rst, + input wire en, + input wire [WIDTH-1:0] d, + input wire [WIDTH-1:0] init, + output reg [WIDTH-1:0] q +); + always @(posedge clk) begin + if (rst) + q <= init; + else if (en) + q <= d; + end +endmodule diff --git a/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_rr_arbiter.v b/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_rr_arbiter.v new file mode 100644 index 00000000..7223919c --- /dev/null +++ b/components/agentic-circuit/resources/pyc_runtime/verilog/pyc_rr_arbiter.v @@ -0,0 +1,36 @@ +// Parameterized round-robin one-hot arbiter. +// Cursor/state ownership remains in the surrounding PYC graph. +module pyc_rr_arbiter #( + parameter integer NUM_INPUTS = 2, + parameter integer POINTER_WIDTH = 1 +) ( + input wire [NUM_INPUTS-1:0] req, + input wire [POINTER_WIDTH-1:0] cursor, + output wire [NUM_INPUTS-1:0] grant +); + integer offset; + integer index; + reg found; + reg [NUM_INPUTS-1:0] grant_r; + + always @* begin + grant_r = {NUM_INPUTS{1'b0}}; + found = 1'b0; + for (offset = 0; offset < NUM_INPUTS; offset = offset + 1) begin + index = integer'(cursor) + offset; + // Two subtracts cover every legal cursor/offset pair without a + // variable-condition loop. This keeps the primitive accepted by + // Yosys, which only permits while-loops in constant functions. + if (index >= NUM_INPUTS) + index = index - NUM_INPUTS; + if (index >= NUM_INPUTS) + index = index - NUM_INPUTS; + if (!found && req[index]) begin + grant_r[index] = 1'b1; + found = 1'b1; + end + end + end + + assign grant = grant_r; +endmodule diff --git a/components/agentic-circuit/resources/templates/agentic-circuit.toml b/components/agentic-circuit/resources/templates/agentic-circuit.toml new file mode 100644 index 00000000..41747dfc --- /dev/null +++ b/components/agentic-circuit/resources/templates/agentic-circuit.toml @@ -0,0 +1,26 @@ +contract_epoch = "0.3" + +[project] +name = "example" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["traces"] +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/resources/templates/architecture.py b/components/agentic-circuit/resources/templates/architecture.py new file mode 100644 index 00000000..9637a2c5 --- /dev/null +++ b/components/agentic-circuit/resources/templates/architecture.py @@ -0,0 +1,13 @@ +"""Agentic Circuit architecture entry point.""" + +from agentic_circuit import module, system + + +@module +def Top() -> None: + pass + + +@system +def main() -> None: + Top() diff --git a/components/agentic-circuit/schemas/acir-process-state-plan.schema.json b/components/agentic-circuit/schemas/acir-process-state-plan.schema.json new file mode 100644 index 00000000..359e8c62 --- /dev/null +++ b/components/agentic-circuit/schemas/acir-process-state-plan.schema.json @@ -0,0 +1,281 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agentic-circuit.dev/schemas/acir-process-state-plan.schema.json", + "title": "ACIR process-state plan v0.1", + "type": "object", + "additionalProperties": false, + "required": ["callees", "contract_epoch", "processes", "schema", "value_types"], + "properties": { + "callees": {"type": "array", "items": {"$ref": "#/$defs/callee"}}, + "contract_epoch": {"const": "0.3"}, + "processes": {"type": "array", "items": {"$ref": "#/$defs/process"}}, + "schema": {"const": "acir-process-state-plan-0.1"}, + "value_types": {"type": "array", "items": {"$ref": "#/$defs/value_type"}} + }, + "$defs": { + "ordinal": {"type": "integer", "minimum": 0, "maximum": 9007199254740991}, + "string_array": {"type": "array", "items": {"type": "string"}}, + "ordinal_array": {"type": "array", "items": {"$ref": "#/$defs/ordinal"}}, + "call_site": { + "type": "object", "additionalProperties": false, + "required": ["iteration_vector", "operation_path"], + "properties": {"iteration_vector": {"$ref": "#/$defs/ordinal_array"}, "operation_path": {"type": "string"}} + }, + "occurrence_original": { + "type": "object", "additionalProperties": false, + "required": ["call_sites", "iteration_vector", "kind", "operation_path"], + "properties": {"call_sites": {"type": "array", "items": {"$ref": "#/$defs/call_site"}}, "iteration_vector": {"$ref": "#/$defs/ordinal_array"}, "kind": {"const": "original"}, "operation_path": {"type": "string"}} + }, + "occurrence_loop": { + "type": "object", "additionalProperties": false, + "required": ["anchor", "kind", "phase"], + "properties": {"anchor": {"$ref": "#/$defs/occurrence"}, "kind": {"const": "synthetic"}, "phase": {"enum": ["initialize", "condition", "increment"]}} + }, + "occurrence_wrapper": { + "type": "object", "additionalProperties": false, + "required": ["anchor", "direction", "kind", "slot", "transition"], + "properties": {"anchor": {"$ref": "#/$defs/occurrence"}, "direction": {"enum": ["wrap", "unwrap"]}, "kind": {"const": "synthetic"}, "slot": {"$ref": "#/$defs/ordinal"}, "transition": {"$ref": "#/$defs/ordinal"}} + }, + "occurrence_constant": { + "type": "object", "additionalProperties": false, + "required": ["anchor", "constant", "kind"], + "properties": {"anchor": {"$ref": "#/$defs/occurrence"}, "constant": {"$ref": "#/$defs/ordinal"}, "kind": {"const": "synthetic"}} + }, + "occurrence": {"oneOf": [{"$ref": "#/$defs/occurrence_original"}, {"$ref": "#/$defs/occurrence_loop"}, {"$ref": "#/$defs/occurrence_wrapper"}, {"$ref": "#/$defs/occurrence_constant"}]}, + "coordinate": { + "type": "object", "additionalProperties": false, + "required": ["index", "kind", "owner_path"], + "properties": {"index": {"$ref": "#/$defs/ordinal"}, "kind": {"enum": ["result", "block_argument"]}, "owner_path": {"type": "string"}} + }, + "planned_original": { + "type": "object", "additionalProperties": false, + "required": ["coordinate", "kind", "occurrence", "path", "type"], + "properties": {"coordinate": {"$ref": "#/$defs/coordinate"}, "kind": {"const": "original"}, "occurrence": {"$ref": "#/$defs/occurrence"}, "path": {"type": "string"}, "type": {"type": "string"}} + }, + "planned_capture": { + "type": "object", "additionalProperties": false, + "required": ["capture", "kind", "type"], + "properties": {"capture": {"$ref": "#/$defs/ordinal"}, "kind": {"const": "capture"}, "type": {"type": "string"}} + }, + "planned_live_slot": { + "type": "object", "additionalProperties": false, + "required": ["kind", "slot", "type"], + "properties": {"kind": {"const": "live_slot"}, "slot": {"$ref": "#/$defs/ordinal"}, "type": {"type": "string"}} + }, + "planned_synthetic": { + "type": "object", "additionalProperties": false, + "required": ["coordinate", "kind", "occurrence", "type"], + "properties": {"coordinate": {"$ref": "#/$defs/coordinate"}, "kind": {"const": "synthetic"}, "occurrence": {"$ref": "#/$defs/occurrence"}, "type": {"type": "string"}} + }, + "planned_constant": { + "type": "object", "additionalProperties": false, + "required": ["kind", "type", "value"], + "properties": {"kind": {"const": "constant"}, "type": {"type": "string"}, "value": {"type": "string"}} + }, + "planned_value": {"oneOf": [{"$ref": "#/$defs/planned_original"}, {"$ref": "#/$defs/planned_capture"}, {"$ref": "#/$defs/planned_live_slot"}, {"$ref": "#/$defs/planned_synthetic"}, {"$ref": "#/$defs/planned_constant"}]}, + "binding": { + "type": "object", "additionalProperties": false, + "required": ["from", "to"], + "properties": {"from": {"$ref": "#/$defs/planned_value"}, "to": {"$ref": "#/$defs/planned_value"}} + }, + "scalar_attribute": { + "type": "object", "additionalProperties": false, + "required": ["name", "value"], "properties": {"name": {"type": "string"}, "value": {"type": "string"}} + }, + "scalar_op": { + "type": "object", "additionalProperties": false, + "required": ["attributes", "name", "properties"], + "properties": {"attributes": {"type": "array", "items": {"$ref": "#/$defs/scalar_attribute"}}, "name": {"type": "string"}, "properties": {"type": "string"}} + }, + "capture": { + "type": "object", "additionalProperties": false, + "required": ["argument_path", "name", "operand_path", "ordinal", "type"], + "properties": {"argument_path": {"type": "string"}, "name": {"type": "string"}, "operand_path": {"type": "string"}, "ordinal": {"$ref": "#/$defs/ordinal"}, "type": {"type": "string"}} + }, + "action_base": { + "type": "object", "additionalProperties": false, + "required": ["cost", "emission", "iteration_vector", "kind", "occurrence", "operands", "ordinal", "result_types", "results"], + "properties": { + "callee": {"$ref": "#/$defs/ordinal"}, "cost": {"enum": [0, 1]}, + "emission": {"enum": ["copy_scalar", "inline", "invoke", "wrap", "unwrap", "forward_only"]}, + "iteration_vector": {"$ref": "#/$defs/ordinal_array"}, + "kind": {"enum": ["original", "constant", "for_initialize", "for_condition", "for_increment", "scalar_wrap", "scalar_unwrap"]}, + "occurrence": {"$ref": "#/$defs/occurrence"}, "operands": {"type": "array", "items": {"$ref": "#/$defs/planned_value"}}, + "ordinal": {"$ref": "#/$defs/ordinal"}, "result_types": {"$ref": "#/$defs/string_array"}, "results": {"type": "array", "items": {"$ref": "#/$defs/planned_value"}}, + "scalar_op": {"$ref": "#/$defs/scalar_op"} + } + }, + "action": { + "oneOf": [ + {"allOf": [{"$ref": "#/$defs/action_base"}, {"properties": {"emission": {"const": "copy_scalar"}}, "required": ["scalar_op"], "not": {"required": ["callee"]}}]}, + {"allOf": [{"$ref": "#/$defs/action_base"}, {"properties": {"emission": {"const": "inline"}}, "required": ["callee"], "not": {"required": ["scalar_op"]}}]}, + {"allOf": [{"$ref": "#/$defs/action_base"}, {"properties": {"emission": {"const": "invoke"}}, "required": ["callee"], "not": {"required": ["scalar_op"]}}]}, + {"allOf": [{"$ref": "#/$defs/action_base"}, {"properties": {"emission": {"const": "wrap"}}, "required": ["callee"], "not": {"required": ["scalar_op"]}}]}, + {"allOf": [{"$ref": "#/$defs/action_base"}, {"properties": {"emission": {"const": "unwrap"}}, "required": ["callee"], "not": {"required": ["scalar_op"]}}]}, + {"allOf": [{"$ref": "#/$defs/action_base"}, {"properties": {"emission": {"const": "forward_only"}}, "not": {"anyOf": [{"required": ["callee"]}, {"required": ["scalar_op"]}]}}]} + ] + }, + "load": { + "type": "object", "additionalProperties": false, "required": ["replacements", "slot"], + "properties": {"replacements": {"type": "array", "items": {"$ref": "#/$defs/planned_value"}}, "slot": {"$ref": "#/$defs/ordinal"}} + }, + "store": { + "type": "object", "additionalProperties": false, "required": ["slot", "source"], + "properties": {"slot": {"$ref": "#/$defs/ordinal"}, "source": {"$ref": "#/$defs/planned_value"}} + }, + "frame_base": { + "type": "object", "additionalProperties": false, "required": ["bindings", "kind", "operation_path", "phase"], + "properties": {"bindings": {"type": "array", "items": {"$ref": "#/$defs/binding"}}, "kind": {"enum": ["entry", "scf.if", "scf.for", "scf.while"]}, "operation_path": {"type": "string"}, "phase": {"enum": ["entry", "then", "else", "merge", "header", "body", "before", "after", "exit"]}} + }, + "frame": {"oneOf": [ + {"allOf": [{"$ref": "#/$defs/frame_base"}, {"properties": {"kind": {"const": "entry"}, "phase": {"const": "entry"}}}]}, + {"allOf": [{"$ref": "#/$defs/frame_base"}, {"properties": {"kind": {"const": "scf.if"}, "phase": {"enum": ["then", "else", "merge"]}}}]}, + {"allOf": [{"$ref": "#/$defs/frame_base"}, {"properties": {"kind": {"const": "scf.for"}, "phase": {"enum": ["header", "body", "exit"]}}}]}, + {"allOf": [{"$ref": "#/$defs/frame_base"}, {"properties": {"kind": {"const": "scf.while"}, "phase": {"enum": ["before", "after", "exit"]}}}]} + ]}, + "edge_branch": { + "type": "object", "additionalProperties": false, + "required": ["condition", "false_bindings", "false_block", "kind", "true_bindings", "true_block"], + "properties": {"condition": {"$ref": "#/$defs/planned_value"}, "false_bindings": {"type": "array", "items": {"$ref": "#/$defs/binding"}}, "false_block": {"$ref": "#/$defs/ordinal"}, "kind": {"const": "branch"}, "true_bindings": {"type": "array", "items": {"$ref": "#/$defs/binding"}}, "true_block": {"$ref": "#/$defs/ordinal"}} + }, + "edge_local_continue": { + "type": "object", "additionalProperties": false, "required": ["bindings", "kind", "target_block"], + "properties": {"bindings": {"type": "array", "items": {"$ref": "#/$defs/binding"}}, "kind": {"const": "local_continue"}, "target_block": {"$ref": "#/$defs/ordinal"}} + }, + "edge_suspend": { + "type": "object", "additionalProperties": false, "required": ["kind", "transition"], + "properties": {"kind": {"const": "suspend"}, "transition": {"$ref": "#/$defs/ordinal"}} + }, + "edge_terminate": { + "type": "object", "additionalProperties": false, "required": ["kind", "status"], + "properties": {"kind": {"const": "terminate"}, "status": {"enum": ["success", "failure"]}} + }, + "edge": {"oneOf": [{"$ref": "#/$defs/edge_branch"}, {"$ref": "#/$defs/edge_local_continue"}, {"$ref": "#/$defs/edge_suspend"}, {"$ref": "#/$defs/edge_terminate"}]}, + "block": { + "type": "object", "additionalProperties": false, + "required": ["actions", "cost", "edge", "frames", "loads", "ordinal", "path", "pc"], + "properties": {"actions": {"type": "array", "items": {"$ref": "#/$defs/action"}}, "cost": {"$ref": "#/$defs/ordinal"}, "edge": {"$ref": "#/$defs/edge"}, "frames": {"type": "array", "items": {"$ref": "#/$defs/frame"}}, "loads": {"type": "array", "items": {"$ref": "#/$defs/load"}}, "ordinal": {"$ref": "#/$defs/ordinal"}, "path": {"type": "string"}, "pc": {"$ref": "#/$defs/ordinal"}} + }, + "pc": { + "type": "object", "additionalProperties": false, "required": ["blocks", "entry_path", "name", "ordinal"], + "properties": {"blocks": {"$ref": "#/$defs/ordinal_array"}, "entry_path": {"type": "string"}, "name": {"type": "string"}, "ordinal": {"$ref": "#/$defs/ordinal"}} + }, + "live_slot_base": { + "type": "object", "additionalProperties": false, + "required": ["member_values", "name", "ordinal", "storage_type", "type"], + "properties": {"member_values": {"type": "array", "items": {"$ref": "#/$defs/planned_value"}}, "name": {"type": "string"}, "ordinal": {"$ref": "#/$defs/ordinal"}, "storage_type": {"$ref": "#/$defs/ordinal"}, "type": {"type": "string"}, "unwrap_callee": {"$ref": "#/$defs/ordinal"}, "wrap_callee": {"$ref": "#/$defs/ordinal"}} + }, + "live_slot": { + "oneOf": [ + {"allOf": [{"$ref": "#/$defs/live_slot_base"}, {"required": ["unwrap_callee", "wrap_callee"]}]}, + {"allOf": [{"$ref": "#/$defs/live_slot_base"}, {"not": {"anyOf": [{"required": ["unwrap_callee"]}, {"required": ["wrap_callee"]}]}}]} + ] + }, + "source_capture": { + "type": "object", "additionalProperties": false, "required": ["capture", "kind", "path"], + "properties": {"capture": {"$ref": "#/$defs/ordinal"}, "kind": {"const": "capture"}, "path": {"type": "string"}} + }, + "source_value": { + "type": "object", "additionalProperties": false, "required": ["kind", "path"], + "properties": {"kind": {"const": "value"}, "owner_path": {"type": "string"}, "path": {"type": "string"}} + }, + "source_symbol": { + "type": "object", "additionalProperties": false, "required": ["kind", "path", "symbol"], + "properties": {"kind": {"const": "symbol"}, "owner_path": {"type": "string"}, "path": {"type": "string"}, "symbol": {"type": "string"}} + }, + "source": {"oneOf": [{"$ref": "#/$defs/source_capture"}, {"$ref": "#/$defs/source_value"}, {"$ref": "#/$defs/source_symbol"}]}, + "wake": { + "type": "object", "additionalProperties": false, + "required": ["callee", "iteration_vector", "kind", "occurrence", "operation_path", "ordinal", "sources", "target", "type_key"], + "properties": {"callee": {"$ref": "#/$defs/ordinal"}, "iteration_vector": {"$ref": "#/$defs/ordinal_array"}, "kind": {"enum": ["condition", "resource", "event_queue", "next_delta"]}, "occurrence": {"$ref": "#/$defs/occurrence"}, "operation_path": {"type": "string"}, "ordinal": {"$ref": "#/$defs/ordinal"}, "sources": {"type": "array", "items": {"$ref": "#/$defs/source"}}, "target": {"type": "string"}, "type_key": {"type": "string"}}, + "allOf": [ + {"if": {"properties": {"kind": {"const": "condition"}}}, "then": {"properties": {"type_key": {"const": "@acir_wake_condition"}}}}, + {"if": {"properties": {"kind": {"const": "resource"}}}, "then": {"properties": {"type_key": {"const": "@acir_wake_resource"}}}}, + {"if": {"properties": {"kind": {"const": "event_queue"}}}, "then": {"properties": {"type_key": {"const": "@acir_wake_event_queue"}}}}, + {"if": {"properties": {"kind": {"const": "next_delta"}}}, "then": {"properties": {"target": {"const": ""}, "type_key": {"const": "@acir_wake_next_delta"}}}} + ] + }, + "transition": { + "type": "object", "additionalProperties": false, + "required": ["iteration_vector", "loads", "ordinal", "source_pc", "stores", "target_pc", "wake"], + "properties": {"iteration_vector": {"$ref": "#/$defs/ordinal_array"}, "loads": {"type": "array", "items": {"$ref": "#/$defs/load"}}, "ordinal": {"$ref": "#/$defs/ordinal"}, "source_pc": {"$ref": "#/$defs/ordinal"}, "stores": {"type": "array", "items": {"$ref": "#/$defs/store"}}, "target_pc": {"$ref": "#/$defs/ordinal"}, "wake": {"$ref": "#/$defs/ordinal"}} + }, + "process": { + "type": "object", "additionalProperties": false, + "required": ["blocks", "captures", "definition_key", "entry_pc", "fairness_work", "live_slots", "pc_bit_width", "pcs", "transitions", "wakes"], + "properties": {"blocks": {"type": "array", "items": {"$ref": "#/$defs/block"}}, "captures": {"type": "array", "items": {"$ref": "#/$defs/capture"}}, "definition_key": {"type": "string", "pattern": "^@[^:]+::@.+$"}, "entry_pc": {"$ref": "#/$defs/ordinal"}, "fairness_work": {"type": "integer", "minimum": 1, "maximum": 9007199254740991}, "live_slots": {"type": "array", "items": {"$ref": "#/$defs/live_slot"}}, "pc_bit_width": {"type": "integer", "minimum": 1, "maximum": 32}, "pcs": {"type": "array", "items": {"$ref": "#/$defs/pc"}}, "transitions": {"type": "array", "items": {"$ref": "#/$defs/transition"}}, "wakes": {"type": "array", "items": {"$ref": "#/$defs/wake"}}} + }, + "record_field": { + "type": "object", "additionalProperties": false, "required": ["name", "type_key"], + "properties": {"name": {"type": "string"}, "type_key": {"type": "string"}} + }, + "payload_record_create": {"type": "object", "additionalProperties": false, "required": ["fields", "record_type"], "properties": {"fields": {"type": "array", "items": {"$ref": "#/$defs/record_field"}}, "record_type": {"type": "string"}}}, + "payload_record_get": {"type": "object", "additionalProperties": false, "required": ["field", "record", "result"], "properties": {"field": {"type": "string"}, "record": {"type": "string"}, "result": {"type": "string"}}}, + "payload_record_with": {"type": "object", "additionalProperties": false, "required": ["field", "record", "value"], "properties": {"field": {"type": "string"}, "record": {"type": "string"}, "value": {"type": "string"}}}, + "payload_packet": {"type": "object", "additionalProperties": false, "required": ["bytes", "packet", "packet_type"], "properties": {"bytes": {"$ref": "#/$defs/ordinal"}, "packet": {"type": "string"}, "packet_type": {"type": "string"}}}, + "payload_trace_decode": {"type": "object", "additionalProperties": false, "required": ["entry", "result", "source"], "properties": {"entry": {"type": "string"}, "result": {"type": "string"}, "source": {"type": "string"}}}, + "payload_queue": {"type": "object", "additionalProperties": false, "required": ["element", "queue"], "properties": {"element": {"type": "string"}, "queue": {"type": "string"}}}, + "payload_event": {"type": "object", "additionalProperties": false, "required": ["delay", "target", "value"], "properties": {"delay": {"type": "string"}, "target": {"type": "string"}, "value": {"type": "string"}}}, + "payload_source": {"type": "object", "additionalProperties": false, "required": ["source"], "properties": {"source": {"type": "string"}}}, + "payload_trace_next": {"type": "object", "additionalProperties": false, "required": ["entry", "source"], "properties": {"entry": {"type": "string"}, "source": {"type": "string"}}}, + "payload_message": {"type": "object", "additionalProperties": false, "required": ["message"], "properties": {"message": {"type": "string"}}}, + "payload_probe": {"type": "object", "additionalProperties": false, "required": ["kind", "result", "target"], "properties": {"kind": {"type": "string"}, "result": {"type": "string"}, "target": {"type": "string"}}}, + "payload_stat": {"type": "object", "additionalProperties": false, "required": ["stat", "value_type"], "properties": {"stat": {"type": "string"}, "value_type": {"type": "string"}}}, + "payload_wake": {"type": "object", "additionalProperties": false, "required": ["wake_kind", "wake_type"], "properties": {"wake_kind": {"enum": ["condition", "resource", "event_queue", "next_delta"]}, "wake_type": {"type": "string"}}}, + "payload_scalar": {"type": "object", "additionalProperties": false, "required": ["direction", "scalar", "value_type"], "properties": {"direction": {"enum": ["wrap", "unwrap"]}, "scalar": {"type": "string"}, "value_type": {"type": "string"}}}, + "callee_payload": {"oneOf": [{"$ref": "#/$defs/payload_record_create"}, {"$ref": "#/$defs/payload_record_get"}, {"$ref": "#/$defs/payload_record_with"}, {"$ref": "#/$defs/payload_packet"}, {"$ref": "#/$defs/payload_trace_decode"}, {"$ref": "#/$defs/payload_queue"}, {"$ref": "#/$defs/payload_event"}, {"$ref": "#/$defs/payload_source"}, {"$ref": "#/$defs/payload_trace_next"}, {"$ref": "#/$defs/payload_message"}, {"$ref": "#/$defs/payload_probe"}, {"$ref": "#/$defs/payload_stat"}, {"$ref": "#/$defs/payload_wake"}, {"$ref": "#/$defs/payload_scalar"}]}, + "callee_base": { + "type": "object", "additionalProperties": false, + "required": ["cpp", "effect", "fingerprint", "inputs", "kind", "ordinal", "payload", "results", "role", "source_paths", "symbol"], + "properties": {"cpp": {"type": "string"}, "effect": {"enum": ["pure", "stateful"]}, "fingerprint": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, "inputs": {"$ref": "#/$defs/string_array"}, "kind": {"const": "implementation"}, "ordinal": {"$ref": "#/$defs/ordinal"}, "payload": {"$ref": "#/$defs/callee_payload"}, "results": {"$ref": "#/$defs/string_array"}, "role": {"enum": ["record_create", "record_get", "record_with", "packet_serialize", "packet_deserialize", "trace_decode", "queue_try_send", "queue_try_recv", "event_schedule", "trace_open", "trace_next", "trace_eof", "trace_position", "contract_require", "contract_ensure", "contract_assert", "probe", "stat_add", "wake_condition", "wake_resource", "wake_event_queue", "wake_next_delta", "scalar_wrap", "scalar_unwrap"]}, "source_paths": {"$ref": "#/$defs/string_array"}, "symbol": {"type": "string"}} + }, + "callee": { + "oneOf": [ + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "record_create"}, "effect": {"const": "pure"}, "payload": {"$ref": "#/$defs/payload_record_create"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "record_get"}, "effect": {"const": "pure"}, "payload": {"$ref": "#/$defs/payload_record_get"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "record_with"}, "effect": {"const": "pure"}, "payload": {"$ref": "#/$defs/payload_record_with"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "packet_serialize"}, "effect": {"const": "pure"}, "payload": {"$ref": "#/$defs/payload_packet"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "packet_deserialize"}, "effect": {"const": "pure"}, "payload": {"$ref": "#/$defs/payload_packet"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "trace_decode"}, "effect": {"const": "pure"}, "payload": {"$ref": "#/$defs/payload_trace_decode"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "queue_try_send"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_queue"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "queue_try_recv"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_queue"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "event_schedule"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_event"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "trace_open"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_source"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "trace_next"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_trace_next"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "trace_eof"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_source"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "trace_position"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_source"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "contract_require"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_message"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "contract_ensure"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_message"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "contract_assert"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_message"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "probe"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_probe"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "stat_add"}, "effect": {"const": "stateful"}, "payload": {"$ref": "#/$defs/payload_stat"}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "wake_condition"}, "effect": {"const": "stateful"}, "payload": {"allOf": [{"$ref": "#/$defs/payload_wake"}, {"properties": {"wake_kind": {"const": "condition"}, "wake_type": {"const": "@acir_wake_condition"}}}]}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "wake_resource"}, "effect": {"const": "stateful"}, "payload": {"allOf": [{"$ref": "#/$defs/payload_wake"}, {"properties": {"wake_kind": {"const": "resource"}, "wake_type": {"const": "@acir_wake_resource"}}}]}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "wake_event_queue"}, "effect": {"const": "stateful"}, "payload": {"allOf": [{"$ref": "#/$defs/payload_wake"}, {"properties": {"wake_kind": {"const": "event_queue"}, "wake_type": {"const": "@acir_wake_event_queue"}}}]}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "wake_next_delta"}, "effect": {"const": "stateful"}, "inputs": {"maxItems": 0}, "payload": {"allOf": [{"$ref": "#/$defs/payload_wake"}, {"properties": {"wake_kind": {"const": "next_delta"}, "wake_type": {"const": "@acir_wake_next_delta"}}}]}, "results": {"prefixItems": [{"const": "@acir_wake_next_delta"}], "items": false, "minItems": 1}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "scalar_wrap"}, "effect": {"const": "pure"}, "payload": {"allOf": [{"$ref": "#/$defs/payload_scalar"}, {"properties": {"direction": {"const": "wrap"}}}]}}}]}, + {"allOf": [{"$ref": "#/$defs/callee_base"}, {"properties": {"role": {"const": "scalar_unwrap"}, "effect": {"const": "pure"}, "payload": {"allOf": [{"$ref": "#/$defs/payload_scalar"}, {"properties": {"direction": {"const": "unwrap"}}}]}}}]} + ] + }, + "value_member_field": { + "type": "object", "additionalProperties": false, "required": ["encoding", "kind", "name", "offset_bits", "type_key", "width_bits"], + "properties": {"encoding": {"type": "string"}, "kind": {"const": "field"}, "name": {"type": "string", "minLength": 1}, "offset_bits": {"$ref": "#/$defs/ordinal"}, "signedness": {"enum": ["signless", "signed", "unsigned"]}, "type_key": {"type": "string"}, "width_bits": {"$ref": "#/$defs/ordinal"}} + }, + "value_member_element": { + "type": "object", "additionalProperties": false, "required": ["encoding", "index", "kind", "offset_bits", "type_key", "width_bits"], + "properties": {"encoding": {"type": "string"}, "index": {"$ref": "#/$defs/ordinal"}, "kind": {"const": "element"}, "offset_bits": {"$ref": "#/$defs/ordinal"}, "signedness": {"enum": ["signless", "signed", "unsigned"]}, "type_key": {"type": "string"}, "width_bits": {"$ref": "#/$defs/ordinal"}} + }, + "value_member": {"oneOf": [{"$ref": "#/$defs/value_member_field"}, {"$ref": "#/$defs/value_member_element"}]}, + "value_payload": {"type": "object", "additionalProperties": false, "required": ["encoding", "members", "width_bits"], "properties": {"encoding": {"type": "string"}, "members": {"type": "array", "items": {"$ref": "#/$defs/value_member"}}, "width_bits": {"$ref": "#/$defs/ordinal"}}}, + "packet_payload": {"type": "object", "additionalProperties": false, "required": ["bytes", "encoding", "members", "width_bits"], "properties": {"bytes": {"$ref": "#/$defs/ordinal"}, "encoding": {"type": "string"}, "members": {"type": "array", "items": {"$ref": "#/$defs/value_member"}}, "width_bits": {"$ref": "#/$defs/ordinal"}}}, + "value_type_base": { + "type": "object", "additionalProperties": false, + "required": ["acir_type", "cpp", "fingerprint", "kind", "ordinal", "payload", "symbol"], + "properties": {"acir_type": {"type": "string"}, "cpp": {"type": "string"}, "fingerprint": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, "kind": {"enum": ["value", "packet"]}, "ordinal": {"$ref": "#/$defs/ordinal"}, "payload": {"oneOf": [{"$ref": "#/$defs/value_payload"}, {"$ref": "#/$defs/packet_payload"}]}, "symbol": {"type": "string"}} + }, + "value_type": {"oneOf": [ + {"allOf": [{"$ref": "#/$defs/value_type_base"}, {"properties": {"kind": {"const": "value"}, "payload": {"$ref": "#/$defs/value_payload"}}}]}, + {"allOf": [{"$ref": "#/$defs/value_type_base"}, {"properties": {"kind": {"const": "packet"}, "payload": {"$ref": "#/$defs/packet_payload"}}}]} + ]} + } +} diff --git a/components/agentic-circuit/schemas/acpy.schema.json b/components/agentic-circuit/schemas/acpy.schema.json new file mode 100644 index 00000000..a4d14bf9 --- /dev/null +++ b/components/agentic-circuit/schemas/acpy.schema.json @@ -0,0 +1,84 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:acpy:0.1", + "title": "Agentic Circuit ACPy 0.1", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "entry", "sources", "entities"], + "properties": { + "schema": {"const": "agentic-circuit-acpy"}, + "version": {"const": "0.1"}, + "contract_epoch": {"const": "0.3"}, + "entry": {"$ref": "#/$defs/entityId"}, + "sources": {"type": "array", "items": {"$ref": "#/$defs/sourceFile"}}, + "entities": {"type": "array", "items": {"$ref": "#/$defs/entity"}} + }, + "$defs": { + "sha256": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "entityId": {"type": "string", "pattern": "^e[0-9]+$"}, + "sourceFile": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "sha256": {"$ref": "#/$defs/sha256"} + } + }, + "sourceSpan": { + "type": "object", + "additionalProperties": false, + "required": ["file", "start_line", "start_column", "end_line", "end_column"], + "properties": { + "file": {"type": "string", "minLength": 1}, + "start_line": {"type": "integer", "minimum": 1}, + "start_column": {"type": "integer", "minimum": 1}, + "end_line": {"type": "integer", "minimum": 1}, + "end_column": {"type": "integer", "minimum": 1} + } + }, + "schemaRef": { + "type": "object", + "additionalProperties": false, + "required": ["identity", "fingerprint"], + "properties": { + "identity": {"type": "string", "minLength": 1}, + "fingerprint": {"$ref": "#/$defs/sha256"} + } + }, + "property": { + "type": "object", + "additionalProperties": false, + "required": ["name", "value"], + "properties": { + "name": {"type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"}, + "value": {"$ref": "#/$defs/jsonValue"} + } + }, + "entity": { + "type": "object", + "additionalProperties": false, + "required": ["id", "kind", "source", "parent", "scope", "type", "definition", "uses", "schema_ref", "properties"], + "properties": { + "id": {"$ref": "#/$defs/entityId"}, + "kind": {"enum": ["system", "module", "scope", "arg", "call", "result", "get_result", "bind", "static_if", "static_for", "collection", "get_static", "return", "capture", "escape", "process"]}, + "source": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/sourceSpan"}]}, + "parent": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/entityId"}]}, + "scope": {"type": "string", "minLength": 1}, + "type": {"type": ["string", "null"]}, + "definition": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/entityId"}]}, + "uses": {"type": "array", "items": {"$ref": "#/$defs/entityId"}, "uniqueItems": true}, + "schema_ref": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/schemaRef"}]}, + "properties": {"type": "array", "items": {"$ref": "#/$defs/property"}} + } + }, + "jsonScalar": {"type": ["string", "number", "boolean", "null"]}, + "jsonValue": { + "oneOf": [ + {"$ref": "#/$defs/jsonScalar"}, + {"type": "array", "items": {"$ref": "#/$defs/jsonValue"}}, + {"type": "object", "additionalProperties": {"$ref": "#/$defs/jsonValue"}} + ] + } + } +} diff --git a/components/agentic-circuit/schemas/acsim-binding.schema.json b/components/agentic-circuit/schemas/acsim-binding.schema.json new file mode 100644 index 00000000..28a63713 --- /dev/null +++ b/components/agentic-circuit/schemas/acsim-binding.schema.json @@ -0,0 +1,269 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:acsim-binding:0.1", + "title": "Agentic Circuit ACSim Binding Record 0.1", + "type": "object", + "additionalProperties": false, + "required": [ + "activation_sources", + "availability", + "binding", + "binding_schema", + "component_schema", + "component_schema_fingerprint", + "construction", + "contract_epoch", + "cpp", + "cpp_type", + "effect", + "fingerprint", + "implementation", + "ownership", + "parameters", + "ports", + "provider", + "provider_implementation_fingerprint", + "resources", + "results" + ], + "properties": { + "activation_sources": { + "type": "array", + "items": {"$ref": "#/$defs/activationSource"}, + "uniqueItems": true, + "$comment": "BindingRecord semantic validation additionally requires unique activation-source names, which JSON Schema cannot express." + }, + "availability": {"const": "available"}, + "binding": {"$ref": "#/$defs/name"}, + "binding_schema": {"const": "acsim-binding-0.1"}, + "component_schema": {"$ref": "#/$defs/identity"}, + "component_schema_fingerprint": {"$ref": "#/$defs/sha256"}, + "construction": {"$ref": "#/$defs/construction"}, + "contract_epoch": {"const": "0.3"}, + "cpp": {"$ref": "#/$defs/cpp"}, + "cpp_type": {"$ref": "#/$defs/identity"}, + "effect": {"enum": ["pure", "stateful"]}, + "fingerprint": {"$ref": "#/$defs/sha256"}, + "implementation": {"$ref": "#/$defs/identity"}, + "ownership": {"$ref": "#/$defs/ownership"}, + "parameters": { + "type": "array", + "items": {"$ref": "#/$defs/parameter"} + }, + "ports": { + "type": "array", + "items": {"$ref": "#/$defs/port"} + }, + "provider": {"$ref": "#/$defs/identity"}, + "provider_implementation_fingerprint": {"$ref": "#/$defs/sha256"}, + "resources": { + "type": "array", + "items": {"$ref": "#/$defs/resource"} + }, + "results": { + "type": "array", + "items": {"$ref": "#/$defs/result"} + } + }, + "allOf": [ + { + "if": {"properties": {"effect": {"const": "pure"}}}, + "then": { + "properties": { + "activation_sources": {"maxItems": 0}, + "ownership": { + "properties": { + "kind": {"const": "none"}, + "placement": {"const": "inline"} + } + }, + "cpp": { + "properties": { + "entry_points": { + "properties": { + "pure": {"$ref": "#/$defs/cppSymbol"}, + "reset": {"const": ""}, + "validate": {"const": ""}, + "work": {"const": ""}, + "xfer": {"const": ""} + } + } + } + } + } + }, + "else": { + "properties": { + "ownership": { + "properties": { + "kind": {"const": "unique"}, + "placement": {"enum": ["member_or_array", "root_or_process"]} + } + }, + "cpp": { + "properties": { + "entry_points": { + "properties": { + "pure": {"const": ""}, + "work": {"$ref": "#/$defs/cppSymbol"}, + "xfer": {"$ref": "#/$defs/cppSymbol"} + } + } + } + } + } + } + } + ], + "$defs": { + "name": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + }, + "identity": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_.]*$" + }, + "cppSymbol": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$" + }, + "sha256": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "typeString": { + "type": "string", + "minLength": 1, + "pattern": "^[^;{}()=%#`\\r\\n]+$" + }, + "staticValue": { + "oneOf": [ + {"type": "null"}, + {"type": "boolean"}, + {"type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991}, + {"type": "number", "not": {"type": "integer"}}, + {"type": "string"}, + {"type": "array", "items": {"$ref": "#/$defs/staticValue"}}, + { + "type": "object", + "patternProperties": { + "^[A-Za-z_][A-Za-z0-9_]*$": {"$ref": "#/$defs/staticValue"} + }, + "additionalProperties": false + } + ] + }, + "entryPoints": { + "type": "object", + "additionalProperties": false, + "required": ["pure", "reset", "validate", "work", "xfer"], + "properties": { + "pure": {"type": "string"}, + "reset": {"type": "string"}, + "validate": {"type": "string"}, + "work": {"type": "string"}, + "xfer": {"type": "string"} + } + }, + "cpp": { + "type": "object", + "additionalProperties": false, + "required": ["concept", "entry_points", "header", "symbol", "target"], + "properties": { + "concept": {"$ref": "#/$defs/cppSymbol"}, + "entry_points": {"$ref": "#/$defs/entryPoints"}, + "header": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*\\\\)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" + }, + "symbol": {"$ref": "#/$defs/cppSymbol"}, + "target": {"$ref": "#/$defs/name"} + } + }, + "construction": { + "type": "object", + "additionalProperties": false, + "required": ["arguments", "kind"], + "properties": { + "arguments": { + "type": "array", + "items": {"$ref": "#/$defs/staticValue"} + }, + "kind": {"const": "constructor"} + } + }, + "ownership": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "placement"], + "properties": { + "kind": {"enum": ["none", "unique"]}, + "placement": {"enum": ["inline", "member_or_array", "root_or_process"]} + } + }, + "parameter": { + "type": "object", + "additionalProperties": false, + "required": ["acir_type", "cpp_type", "mapping", "name", "ordinal", "value"], + "properties": { + "acir_type": {"$ref": "#/$defs/typeString"}, + "cpp_type": {"$ref": "#/$defs/typeString"}, + "mapping": {"enum": ["template_argument", "constexpr_argument", "constructor_constant"]}, + "name": {"$ref": "#/$defs/name"}, + "ordinal": {"type": "integer", "minimum": 0}, + "value": {"$ref": "#/$defs/staticValue"} + } + }, + "port": { + "type": "object", + "additionalProperties": false, + "required": ["accessor", "cardinality", "delegation", "direction", "interface", "ownership", "payload", "protocol", "role", "time_domain"], + "properties": { + "accessor": {"$ref": "#/$defs/name"}, + "cardinality": {"enum": ["exclusive", "shared"]}, + "delegation": {"enum": ["forbidden", "allowed", "required"]}, + "direction": {"enum": ["input", "output"]}, + "interface": {"$ref": "#/$defs/identity"}, + "ownership": {"enum": ["owned", "borrowed", "shared"]}, + "payload": {"$ref": "#/$defs/identity"}, + "protocol": {"$ref": "#/$defs/identity"}, + "role": {"$ref": "#/$defs/identity"}, + "time_domain": {"$ref": "#/$defs/identity"} + } + }, + "resource": { + "type": "object", + "additionalProperties": false, + "required": ["accessor", "delegation", "mode", "ownership", "resource", "role", "time_domain"], + "properties": { + "accessor": {"$ref": "#/$defs/name"}, + "delegation": {"enum": ["forbidden", "allowed", "required"]}, + "mode": {"enum": ["initiator", "target"]}, + "ownership": {"enum": ["owned", "borrowed", "shared"]}, + "resource": {"$ref": "#/$defs/identity"}, + "role": {"$ref": "#/$defs/identity"}, + "time_domain": {"$ref": "#/$defs/identity"} + } + }, + "result": { + "type": "object", + "additionalProperties": false, + "required": ["cpp_type", "name"], + "properties": { + "cpp_type": {"$ref": "#/$defs/identity"}, + "name": {"$ref": "#/$defs/name"} + } + }, + "activationSource": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "name"], + "properties": { + "kind": {"$ref": "#/$defs/identity"}, + "name": {"$ref": "#/$defs/name"} + } + } + } +} diff --git a/components/agentic-circuit/schemas/block-spec.schema.json b/components/agentic-circuit/schemas/block-spec.schema.json new file mode 100644 index 00000000..89710bde --- /dev/null +++ b/components/agentic-circuit/schemas/block-spec.schema.json @@ -0,0 +1,178 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:block-spec:0.3", + "title": "Agentic Circuit High-Level BlockSpec 0.3", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "blocks"], + "properties": { + "schema": {"const": "agentic-circuit-block-spec"}, + "version": {"const": "0.3"}, + "contract_epoch": {"const": "0.3"}, + "blocks": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/block"} + } + }, + "$defs": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "operation": { + "type": "string", + "pattern": "^ac\\.[a-z][a-z0-9_]*(?:\\.[a-z][a-z0-9_]*)*$" + }, + "arity": { + "type": "object", + "additionalProperties": false, + "required": ["min", "max"], + "properties": { + "min": {"type": "integer", "minimum": 0}, + "max": { + "oneOf": [ + {"type": "integer", "minimum": 0}, + {"type": "null"} + ] + } + } + }, + "portGroup": { + "type": "object", + "additionalProperties": false, + "required": ["name", "direction", "arity", "payload", "ownership"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "direction": {"enum": ["in", "out"]}, + "arity": {"$ref": "#/$defs/arity"}, + "payload": { + "enum": [ + "input", + "output", + "all_equal", + "region_yield" + ] + }, + "ownership": {"const": "borrowed_simqueue"} + } + }, + "parameter": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "required", + "default", + "cpp_mapping", + "verilog_mapping" + ], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "kind": { + "enum": ["type", "structural", "policy", "instance", "field"] + }, + "required": {"type": "boolean"}, + "default": true, + "cpp_mapping": { + "enum": [ + "inferred_template_type", + "template_argument", + "constexpr_argument", + "constructor_constant", + "typed_accessor" + ] + }, + "verilog_mapping": { + "enum": [ + "inferred_packed_type", + "parameter", + "localparam", + "generate_policy", + "packed_field_slice", + "requires_lane_lowering" + ] + } + } + }, + "cppProvider": { + "type": "object", + "additionalProperties": false, + "required": ["header", "symbol", "optimized"], + "properties": { + "header": {"type": "string", "minLength": 1}, + "symbol": {"type": "string", "minLength": 1}, + "optimized": {"const": true} + } + }, + "verilogProvider": { + "type": "object", + "additionalProperties": false, + "required": ["realization", "optimized"], + "properties": { + "realization": {"type": "string", "minLength": 1}, + "optimized": {"const": true} + } + }, + "providers": { + "type": "object", + "additionalProperties": false, + "required": ["cpp", "verilog"], + "properties": { + "cpp": {"$ref": "#/$defs/cppProvider"}, + "verilog": {"$ref": "#/$defs/verilogProvider"} + } + }, + "block": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "operation", + "lowers_to", + "category", + "lambda_regions", + "ports", + "parameters", + "providers", + "refinement_observations" + ], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "operation": {"$ref": "#/$defs/operation"}, + "lowers_to": { + "oneOf": [ + {"$ref": "#/$defs/operation"}, + { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/operation"}, + "uniqueItems": true + } + ] + }, + "category": { + "enum": ["compute", "transport", "scheduling", "execution", "storage"] + }, + "lambda_regions": {"type": "integer", "minimum": 0, "maximum": 1}, + "ports": { + "type": "array", + "minItems": 2, + "items": {"$ref": "#/$defs/portGroup"} + }, + "parameters": { + "type": "array", + "items": {"$ref": "#/$defs/parameter"} + }, + "providers": {"$ref": "#/$defs/providers"}, + "refinement_observations": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/name"}, + "uniqueItems": true + } + } + } + } +} diff --git a/components/agentic-circuit/schemas/blocks.json b/components/agentic-circuit/schemas/blocks.json new file mode 100644 index 00000000..953b1109 --- /dev/null +++ b/components/agentic-circuit/schemas/blocks.json @@ -0,0 +1,816 @@ +{ + "schema": "agentic-circuit-block-spec", + "version": "0.3", + "contract_epoch": "0.3", + "blocks": [ + { + "name": "barrier", + "operation": "ac.barrier", + "lowers_to": "ac.barrier", + "category": "scheduling", + "lambda_regions": 0, + "ports": [ + { + "name": "inputs", + "direction": "in", + "arity": { + "min": 2, + "max": null + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "outputs", + "direction": "out", + "arity": { + "min": 2, + "max": null + }, + "payload": "input", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::QueueBarrier", + "optimized": true + }, + "verilog": { + "realization": "all_input_valid_and_all_output_ready_atomic_handshake", + "optimized": true + } + }, + "refinement_observations": [ + "input_transactions", + "output_transactions", + "barrier_firing" + ] + }, + { + "name": "compute", + "operation": "ac.compute", + "lowers_to": "ac.transform", + "category": "compute", + "lambda_regions": 1, + "ports": [ + { + "name": "inputs", + "direction": "in", + "arity": { + "min": 1, + "max": null + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "outputs", + "direction": "out", + "arity": { + "min": 1, + "max": null + }, + "payload": "region_yield", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "payload", + "kind": "type", + "required": true, + "default": null, + "cpp_mapping": "inferred_template_type", + "verilog_mapping": "inferred_packed_type" + }, + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "rate", + "kind": "structural", + "required": false, + "default": 1, + "cpp_mapping": "template_argument", + "verilog_mapping": "requires_lane_lowering" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::Compute", + "optimized": true + }, + "verilog": { + "realization": "pure_comb_logic_plus_atomic_ready_valid_storage", + "optimized": true + } + }, + "refinement_observations": [ + "accepted_transaction", + "output_transaction" + ] + }, + { + "name": "engine", + "operation": "ac.engine", + "lowers_to": "ac.credit", + "category": "execution", + "lambda_regions": 0, + "ports": [ + { + "name": "input", + "direction": "in", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "output", + "direction": "out", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "cost", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "lanes", + "kind": "structural", + "required": false, + "default": 1, + "cpp_mapping": "template_argument", + "verilog_mapping": "parameter" + }, + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::Engine", + "optimized": true + }, + "verilog": { + "realization": "fixed_slot_register_bank_and_countdown", + "optimized": true + } + }, + "refinement_observations": [ + "accepted_transaction", + "completed_transaction", + "active_lanes" + ] + }, + { + "name": "fork", + "operation": "ac.fork", + "lowers_to": "ac.fork", + "category": "transport", + "lambda_regions": 0, + "ports": [ + { + "name": "input", + "direction": "in", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "outputs", + "direction": "out", + "arity": { + "min": 2, + "max": null + }, + "payload": "input", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::QueueFork", + "optimized": true + }, + "verilog": { + "realization": "per_output_delivered_registers", + "optimized": true + } + }, + "refinement_observations": [ + "input_transaction", + "output_transactions" + ] + }, + { + "name": "merge", + "operation": "ac.merge", + "lowers_to": "ac.merge", + "category": "transport", + "lambda_regions": 0, + "ports": [ + { + "name": "inputs", + "direction": "in", + "arity": { + "min": 2, + "max": null + }, + "payload": "all_equal", + "ownership": "borrowed_simqueue" + }, + { + "name": "output", + "direction": "out", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "all_equal", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "policy", + "kind": "policy", + "required": false, + "default": "priority", + "cpp_mapping": "template_argument", + "verilog_mapping": "generate_policy" + }, + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::QueueMerge", + "optimized": true + }, + "verilog": { + "realization": "priority_or_round_robin_ready_valid_arbiter", + "optimized": true + } + }, + "refinement_observations": [ + "selected_input_transaction", + "output_transaction" + ] + }, + { + "name": "pipeline", + "operation": "ac.pipeline", + "lowers_to": "ac.transform", + "category": "compute", + "lambda_regions": 0, + "ports": [ + { + "name": "input", + "direction": "in", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "output", + "direction": "out", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "stages", + "kind": "structural", + "required": false, + "default": 1, + "cpp_mapping": "template_argument", + "verilog_mapping": "parameter" + }, + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "rate", + "kind": "structural", + "required": false, + "default": 1, + "cpp_mapping": "template_argument", + "verilog_mapping": "requires_lane_lowering" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::Pipeline", + "optimized": true + }, + "verilog": { + "realization": "fixed_ready_valid_pipeline_stages", + "optimized": true + } + }, + "refinement_observations": [ + "input_transaction", + "output_transaction" + ] + }, + { + "name": "reorder", + "operation": "ac.reorder", + "lowers_to": "ac.reorder", + "category": "scheduling", + "lambda_regions": 0, + "ports": [ + { + "name": "input", + "direction": "in", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "output", + "direction": "out", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "by", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "entries", + "kind": "structural", + "required": false, + "default": 16, + "cpp_mapping": "template_argument", + "verilog_mapping": "parameter" + }, + { + "name": "start", + "kind": "instance", + "required": false, + "default": 0, + "cpp_mapping": "constexpr_argument", + "verilog_mapping": "localparam" + }, + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::Reorder", + "optimized": true + }, + "verilog": { + "realization": "tagged_register_bank_and_expected_key", + "optimized": true + } + }, + "refinement_observations": [ + "accepted_transaction", + "retired_transaction", + "sequence_order" + ] + }, + { + "name": "route", + "operation": "ac.route", + "lowers_to": "ac.route", + "category": "transport", + "lambda_regions": 0, + "ports": [ + { + "name": "input", + "direction": "in", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "outputs", + "direction": "out", + "arity": { + "min": 2, + "max": null + }, + "payload": "input", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "by", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::QueueRoute", + "optimized": true + }, + "verilog": { + "realization": "selector_decoder_valid_demux_and_ready_mux", + "optimized": true + } + }, + "refinement_observations": [ + "input_transaction", + "selected_output_transaction" + ] + }, + { + "name": "schedule", + "operation": "ac.schedule", + "lowers_to": "ac.dependency", + "category": "scheduling", + "lambda_regions": 0, + "ports": [ + { + "name": "enqueue", + "direction": "in", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "completed", + "direction": "out", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "by", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "waits_for", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "resource", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "cost", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "entries", + "kind": "structural", + "required": false, + "default": 16, + "cpp_mapping": "template_argument", + "verilog_mapping": "parameter" + }, + { + "name": "resources", + "kind": "structural", + "required": false, + "default": 1, + "cpp_mapping": "template_argument", + "verilog_mapping": "parameter" + }, + { + "name": "no_dependency", + "kind": "instance", + "required": false, + "default": 0, + "cpp_mapping": "constexpr_argument", + "verilog_mapping": "localparam" + }, + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::Schedule", + "optimized": true + }, + "verilog": { + "realization": "packed_dependency_window_resource_reservation_and_countdown", + "optimized": true + } + }, + "refinement_observations": [ + "accepted_transaction", + "issued_transaction", + "completed_transaction" + ] + }, + { + "name": "table", + "operation": "ac.table", + "lowers_to": ["ac.memory.instance", "ac.memory.request"], + "category": "storage", + "lambda_regions": 0, + "ports": [ + { + "name": "request", + "direction": "in", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + }, + { + "name": "response", + "direction": "out", + "arity": { + "min": 1, + "max": 1 + }, + "payload": "input", + "ownership": "borrowed_simqueue" + } + ], + "parameters": [ + { + "name": "address", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "write", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "data", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "result", + "kind": "field", + "required": true, + "default": null, + "cpp_mapping": "typed_accessor", + "verilog_mapping": "packed_field_slice" + }, + { + "name": "entries", + "kind": "structural", + "required": false, + "default": 16, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "init", + "kind": "instance", + "required": false, + "default": 0, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "localparam" + }, + { + "name": "depth", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + }, + { + "name": "latency", + "kind": "instance", + "required": false, + "default": 1, + "cpp_mapping": "constructor_constant", + "verilog_mapping": "parameter" + } + ], + "providers": { + "cpp": { + "header": "gfsim/queue_blocks.h", + "symbol": "gfsim::QueueMemoryArbiter", + "optimized": true + }, + "verilog": { + "realization": "pyc_sync_mem_with_aligned_request_state", + "optimized": true + } + }, + "refinement_observations": [ + "request_transaction", + "response_transaction", + "memory_write" + ] + } + ] +} diff --git a/components/agentic-circuit/schemas/build-manifest.schema.json b/components/agentic-circuit/schemas/build-manifest.schema.json new file mode 100644 index 00000000..d51e2877 --- /dev/null +++ b/components/agentic-circuit/schemas/build-manifest.schema.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:build-manifest:0.1", + "title": "Agentic Circuit Build Manifest 0.1", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "project", "system", "source_files", "normalized_acir_sha256", "compiler", "pass_pipeline", "providers", "component_specializations", "protocol_identities", "artifacts", "validation_gates", "build_profile", "instrumentation_layers", "specialization_inputs", "build_fingerprint"], + "properties": { + "schema": {"const": "agentic-circuit-build-manifest"}, + "version": {"const": "0.1"}, + "contract_epoch": {"const": "0.3"}, + "project": {"$ref": "#/$defs/identity"}, + "system": {"$ref": "#/$defs/identity"}, + "source_files": {"type": "array", "items": {"$ref": "#/$defs/fileHash"}}, + "normalized_acir_sha256": {"$ref": "#/$defs/sha256"}, + "compiler": {"$ref": "#/$defs/compiler"}, + "pass_pipeline": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "providers": {"type": "array", "items": {"$ref": "#/$defs/provider"}}, + "component_specializations": {"type": "array", "items": {"$ref": "#/$defs/specialization"}}, + "protocol_identities": {"type": "array", "items": {"$ref": "#/$defs/namedFingerprint"}}, + "artifacts": {"type": "array", "items": {"$ref": "#/$defs/artifact"}}, + "validation_gates": {"type": "array", "items": {"$ref": "#/$defs/validationGate"}}, + "build_profile": {"enum": ["fast", "validated", "custom"]}, + "instrumentation_layers": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}, + "specialization_inputs": {"type": "array", "items": {"$ref": "#/$defs/specializationInput"}}, + "build_fingerprint": {"$ref": "#/$defs/sha256"} + }, + "$defs": { + "sha256": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "identity": { + "type": "object", + "additionalProperties": false, + "required": ["name", "identity"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "identity": {"type": "string", "minLength": 1} + } + }, + "fileHash": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "sha256": {"$ref": "#/$defs/sha256"} + } + }, + "compiler": { + "type": "object", + "additionalProperties": false, + "required": ["name", "build_id", "toolchain_target"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "build_id": {"type": "string", "minLength": 1}, + "toolchain_target": {"type": "string", "minLength": 1} + } + }, + "provider": { + "type": "object", + "additionalProperties": false, + "required": ["namespace", "schema_fingerprint", "implementation_fingerprint"], + "properties": { + "namespace": {"type": "string", "minLength": 1}, + "schema_fingerprint": {"$ref": "#/$defs/sha256"}, + "implementation_fingerprint": {"$ref": "#/$defs/sha256"} + } + }, + "specialization": { + "type": "object", + "additionalProperties": false, + "required": ["canonical_name", "schema_fingerprint", "specialization_fingerprint"], + "properties": { + "canonical_name": {"type": "string", "minLength": 1}, + "schema_fingerprint": {"$ref": "#/$defs/sha256"}, + "specialization_fingerprint": {"$ref": "#/$defs/sha256"} + } + }, + "namedFingerprint": { + "type": "object", + "additionalProperties": false, + "required": ["name", "fingerprint"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "fingerprint": {"$ref": "#/$defs/sha256"} + } + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": ["path", "kind", "sha256"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "kind": {"enum": ["acpy", "acir", "acsim", "cpp_source", "cpp_header", "executable", "report"]}, + "sha256": {"$ref": "#/$defs/sha256"} + } + }, + "validationGate": { + "type": "object", + "additionalProperties": false, + "required": ["name", "status", "report_sha256"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "status": {"enum": ["passed", "failed"]}, + "report_sha256": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/sha256"}]} + } + }, + "specializationInput": { + "type": "object", + "additionalProperties": false, + "required": ["name", "acir_type", "canonical_value"], + "properties": { + "name": {"type": "string", "minLength": 1}, + "acir_type": {"type": "string", "minLength": 1}, + "canonical_value": true + } + } + } +} diff --git a/components/agentic-circuit/schemas/capabilities.schema.json b/components/agentic-circuit/schemas/capabilities.schema.json new file mode 100644 index 00000000..3095a00b --- /dev/null +++ b/components/agentic-circuit/schemas/capabilities.schema.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:capabilities:0.1", + "title": "Agentic Circuit Capabilities 0.1", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "contract_identities", "items", "compiler_build_id", "runtime_build_id"], + "properties": { + "schema": {"const": "agentic-circuit-capabilities"}, + "version": {"const": "0.1"}, + "contract_epoch": {"const": "0.3"}, + "contract_identities": { + "type": "object", + "additionalProperties": false, + "required": ["acpy", "acir", "acsim", "cli", "component_schema", "opcode_catalog", "block_spec", "cxx_source_contract", "pto_trace", "diagnostic", "build_manifest", "run_manifest", "run_result"], + "properties": { + "acpy": {"const": "acpy@0.1"}, + "acir": {"const": "acir@0.1"}, + "acsim": {"const": "acsim@0.1"}, + "cli": {"const": "agentic-circuit-cli@0.1"}, + "component_schema": {"const": "agentic-circuit-component@0.1"}, + "opcode_catalog": {"const": "agentic-circuit-opcode-catalog@0.2"}, + "block_spec": {"const": "agentic-circuit-block-spec@0.3"}, + "cxx_source_contract": {"const": "gfsim-cxx20@0.1"}, + "pto_trace": {"const": "pto-trace@0.1"}, + "diagnostic": {"const": "agentic-circuit-diagnostic@0.1"}, + "build_manifest": {"const": "agentic-circuit-build-manifest@0.1"}, + "run_manifest": {"const": "agentic-circuit-run-manifest@0.1"}, + "run_result": {"const": "agentic-circuit-run-result@0.1"} + } + }, + "items": {"type": "array", "items": {"$ref": "#/$defs/item"}}, + "compiler_build_id": {"type": "string", "minLength": 1}, + "runtime_build_id": {"type": "string", "minLength": 1} + }, + "$defs": { + "sha256": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "item": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "name", "availability", "schema_fingerprint", "implementation_fingerprint"], + "properties": { + "kind": {"enum": ["provider", "component", "block", "opcode", "policy", "interface", "protocol", "output_format"]}, + "name": {"type": "string", "minLength": 1}, + "availability": {"enum": ["available", "declared_unavailable"]}, + "schema_fingerprint": {"$ref": "#/$defs/sha256"}, + "implementation_fingerprint": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/sha256"}]} + } + } + } +} diff --git a/components/agentic-circuit/schemas/component.schema.json b/components/agentic-circuit/schemas/component.schema.json new file mode 100644 index 00000000..c75bb7b8 --- /dev/null +++ b/components/agentic-circuit/schemas/component.schema.json @@ -0,0 +1,257 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:component:0.1", + "title": "Agentic Circuit ComponentSchema 0.1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_kind", + "schema_version", + "contract_epoch", + "canonical_name", + "family", + "provider_namespace", + "stability", + "cpp_binding", + "static_parameters", + "bindings", + "results", + "resources", + "address_behavior", + "protocol_contracts", + "effect", + "activation", + "observation", + "schema_fingerprint" + ], + "properties": { + "schema_kind": {"const": "agentic-circuit-component"}, + "schema_version": {"const": "0.1"}, + "contract_epoch": {"const": "0.3"}, + "canonical_name": {"$ref": "#/$defs/qualifiedName"}, + "family": {"$ref": "#/$defs/name"}, + "provider_namespace": {"$ref": "#/$defs/namespaceName"}, + "stability": {"enum": ["experimental", "provisional", "stable"]}, + "cpp_binding": { + "oneOf": [ + {"type": "null"}, + {"$ref": "#/$defs/cppBinding"} + ] + }, + "static_parameters": { + "type": "array", + "items": {"$ref": "#/$defs/staticParameter"} + }, + "bindings": { + "type": "array", + "items": {"$ref": "#/$defs/binding"} + }, + "results": { + "type": "array", + "items": {"$ref": "#/$defs/result"} + }, + "resources": { + "type": "array", + "items": {"$ref": "#/$defs/resource"} + }, + "address_behavior": { + "oneOf": [ + {"type": "null"}, + {"$ref": "#/$defs/addressBehavior"} + ] + }, + "protocol_contracts": { + "type": "array", + "items": {"$ref": "#/$defs/protocolContract"} + }, + "effect": {"$ref": "#/$defs/effect"}, + "activation": { + "oneOf": [ + {"type": "null"}, + {"$ref": "#/$defs/activation"} + ] + }, + "observation": {"$ref": "#/$defs/observation"}, + "schema_fingerprint": {"$ref": "#/$defs/sha256"} + }, + "allOf": [ + { + "if": { + "properties": {"effect": {"properties": {"kind": {"const": "pure"}}}} + }, + "then": {"properties": {"activation": {"type": "null"}}}, + "else": {"properties": {"activation": {"$ref": "#/$defs/activation"}}} + } + ], + "$defs": { + "name": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + }, + "qualifiedName": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)+$" + }, + "namespaceName": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$" + }, + "sha256": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "stringList": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + "cppBinding": { + "type": "object", + "additionalProperties": false, + "required": ["header", "symbol", "language", "concept", "toolchain_target", "functional_policy"], + "properties": { + "header": {"type": "string", "minLength": 1}, + "symbol": {"type": "string", "minLength": 1}, + "language": {"const": "c++20"}, + "concept": {"type": "string", "minLength": 1}, + "toolchain_target": {"type": "string", "minLength": 1}, + "functional_policy": {"enum": ["required", "optional", "none"]} + } + }, + "staticParameter": { + "type": "object", + "additionalProperties": false, + "required": ["name", "acir_type", "required", "default", "constraint", "cpp_mapping"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "acir_type": {"type": "string", "minLength": 1}, + "required": {"type": "boolean"}, + "default": true, + "constraint": {"type": ["string", "null"]}, + "cpp_mapping": {"enum": ["template_argument", "constexpr_argument", "constructor_constant"]} + } + }, + "binding": { + "type": "object", + "additionalProperties": false, + "required": ["name", "binding_kind", "acir_type", "direction", "role", "cardinality", "linearity", "ownership", "delegation", "result_mapping"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "binding_kind": {"enum": ["flow", "endpoint", "resource_ref"]}, + "acir_type": {"type": "string", "minLength": 1}, + "direction": {"enum": ["in", "out", "inout"]}, + "role": {"type": "string", "minLength": 1}, + "cardinality": { + "oneOf": [ + {"type": "integer", "minimum": 0}, + {"type": "string", "minLength": 1} + ] + }, + "linearity": {"enum": ["linear", "affine", "unrestricted"]}, + "ownership": {"enum": ["owned", "borrowed", "shared"]}, + "delegation": {"enum": ["forbidden", "allowed", "required"]}, + "result_mapping": { + "oneOf": [ + {"type": "null"}, + {"$ref": "#/$defs/name"} + ] + } + } + }, + "result": { + "type": "object", + "additionalProperties": false, + "required": ["name", "acir_type", "source_binding", "linearity", "ownership"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "acir_type": {"type": "string", "minLength": 1}, + "source_binding": { + "oneOf": [ + {"type": "null"}, + {"$ref": "#/$defs/name"} + ] + }, + "linearity": {"enum": ["linear", "affine", "unrestricted"]}, + "ownership": {"enum": ["owned", "borrowed", "shared"]} + } + }, + "resource": { + "type": "object", + "additionalProperties": false, + "required": ["name", "class", "capacity", "lanes", "issue_width", "initiation_interval", "latency_policy", "reservation_owner", "transaction_classes", "statistics"], + "properties": { + "name": {"$ref": "#/$defs/name"}, + "class": {"type": "string", "minLength": 1}, + "capacity": {"oneOf": [{"type": "integer", "minimum": 0}, {"type": "string", "minLength": 1}]}, + "lanes": {"type": "integer", "minimum": 1}, + "issue_width": {"type": "integer", "minimum": 1}, + "initiation_interval": {"type": "integer", "minimum": 1}, + "latency_policy": {"type": "string", "minLength": 1}, + "reservation_owner": {"type": "string", "minLength": 1}, + "transaction_classes": {"$ref": "#/$defs/stringList"}, + "statistics": {"$ref": "#/$defs/stringList"} + } + }, + "addressBehavior": { + "type": "object", + "additionalProperties": false, + "required": ["consumes", "produces", "ranges", "translation", "routing", "unmapped"], + "properties": { + "consumes": {"$ref": "#/$defs/stringList"}, + "produces": {"$ref": "#/$defs/stringList"}, + "ranges": {"$ref": "#/$defs/stringList"}, + "translation": {"type": "string", "minLength": 1}, + "routing": {"type": "string", "minLength": 1}, + "unmapped": {"type": "string", "minLength": 1} + } + }, + "protocolContract": { + "type": "object", + "additionalProperties": false, + "required": ["protocol", "role", "ordering", "delivery", "correlation", "completion", "failure"], + "properties": { + "protocol": {"type": "string", "minLength": 1}, + "role": {"type": "string", "minLength": 1}, + "ordering": {"type": "string", "minLength": 1}, + "delivery": {"type": "string", "minLength": 1}, + "correlation": {"type": "string", "minLength": 1}, + "completion": {"type": "string", "minLength": 1}, + "failure": {"type": "string", "minLength": 1} + } + }, + "effect": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "requirements", "guarantees", "observable_effects", "failure_behavior"], + "properties": { + "kind": {"enum": ["pure", "stateful"]}, + "requirements": {"$ref": "#/$defs/stringList"}, + "guarantees": {"$ref": "#/$defs/stringList"}, + "observable_effects": {"$ref": "#/$defs/stringList"}, + "failure_behavior": {"type": "string", "minLength": 1} + } + }, + "activation": { + "type": "object", + "additionalProperties": false, + "required": ["sources", "predicate", "wakeup_contract", "quiescence"], + "properties": { + "sources": {"$ref": "#/$defs/stringList"}, + "predicate": {"type": "string", "minLength": 1}, + "wakeup_contract": {"$ref": "#/$defs/stringList"}, + "quiescence": {"type": "string", "minLength": 1} + } + }, + "observation": { + "type": "object", + "additionalProperties": false, + "required": ["probes", "counters", "gauges", "histograms"], + "properties": { + "probes": {"$ref": "#/$defs/stringList"}, + "counters": {"$ref": "#/$defs/stringList"}, + "gauges": {"$ref": "#/$defs/stringList"}, + "histograms": {"$ref": "#/$defs/stringList"} + } + } + } +} diff --git a/components/agentic-circuit/schemas/diagnostic.schema.json b/components/agentic-circuit/schemas/diagnostic.schema.json new file mode 100644 index 00000000..8947f801 --- /dev/null +++ b/components/agentic-circuit/schemas/diagnostic.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:diagnostic:0.1", + "title": "Agentic Circuit Diagnostic 0.1", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "code", "stage", "severity", "source", "object_path", "message", "expected", "actual", "related", "fixits"], + "properties": { + "schema": {"const": "agentic-circuit-diagnostic"}, + "version": {"const": "0.1"}, + "contract_epoch": {"const": "0.3"}, + "code": {"pattern": "^AC(PY|ELAB|IR-[A-Z]+|LOWER|BUILD|TRACE|RUN)-[A-Z0-9-]+$", "type": "string"}, + "stage": {"type": "string", "minLength": 1}, + "severity": {"enum": ["note", "warning", "error"]}, + "source": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/source"}]}, + "object_path": {"type": ["string", "null"]}, + "message": {"type": "string", "minLength": 1}, + "expected": true, + "actual": true, + "related": {"type": "array", "items": {"$ref": "#/$defs/related"}}, + "fixits": {"type": "array", "items": {"$ref": "#/$defs/fixit"}} + }, + "$defs": { + "source": { + "type": "object", + "additionalProperties": false, + "required": ["file", "line", "column"], + "properties": { + "file": {"type": "string", "minLength": 1}, + "line": {"type": "integer", "minimum": 1}, + "column": {"type": "integer", "minimum": 1} + } + }, + "related": { + "type": "object", + "additionalProperties": false, + "required": ["message", "source", "object_path"], + "properties": { + "message": {"type": "string", "minLength": 1}, + "source": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/source"}]}, + "object_path": {"type": ["string", "null"]} + } + }, + "fixit": { + "type": "object", + "additionalProperties": false, + "required": ["message"], + "properties": { + "message": {"type": "string", "minLength": 1} + } + } + } +} diff --git a/components/agentic-circuit/schemas/opcode-catalog.schema.json b/components/agentic-circuit/schemas/opcode-catalog.schema.json new file mode 100644 index 00000000..d87807fa --- /dev/null +++ b/components/agentic-circuit/schemas/opcode-catalog.schema.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:opcode-catalog:0.3", + "title": "Agentic Circuit Official Opcode Catalog 0.3", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "entries"], + "properties": { + "schema": {"const": "agentic-circuit-opcode-catalog"}, + "version": {"const": "0.3"}, + "contract_epoch": {"const": "0.3"}, + "entries": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/entry"} + } + }, + "$defs": { + "arity": { + "type": "object", + "additionalProperties": false, + "required": ["min", "max"], + "properties": { + "min": {"type": "integer", "minimum": 0}, + "max": { + "oneOf": [ + {"type": "integer", "minimum": 0}, + {"type": "null"} + ] + } + } + }, + "backend": { + "type": "object", + "additionalProperties": false, + "required": ["available", "realization"], + "properties": { + "available": {"type": "boolean"}, + "realization": {"type": "string", "minLength": 1} + } + }, + "nameList": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + }, + "uniqueItems": true + }, + "entry": { + "type": "object", + "additionalProperties": false, + "required": [ + "category", + "constants", + "effect", + "gfsim", + "inputs", + "kind", + "operation", + "outputs", + "payload_relation", + "pyc", + "refinement_observations", + "role" + ], + "properties": { + "category": { + "enum": ["boundary", "combinational", "scheduling", "state", "structural", "transport"] + }, + "constants": {"$ref": "#/$defs/nameList"}, + "effect": { + "enum": ["atomic", "observation", "stateful", "structural", "verification"] + }, + "gfsim": {"$ref": "#/$defs/backend"}, + "inputs": {"$ref": "#/$defs/arity"}, + "kind": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "operation": { + "type": "string", + "pattern": "^ac\\.[a-z][a-z0-9_]*(?:\\.[a-z][a-z0-9_]*)*$" + }, + "outputs": {"$ref": "#/$defs/arity"}, + "payload_relation": { + "enum": [ + "all_payloads_equal", + "consume_input_payload", + "declared_storage_owner", + "declared_output_payload", + "input_output_payload_equal", + "observe_input_payload", + "positionally_equal_boundary_payloads", + "positionally_equal_input_output_payloads", + "region_yields_match_output_payloads", + "selected_data_payload_matches_output" + ] + }, + "pyc": {"$ref": "#/$defs/backend"}, + "refinement_observations": {"$ref": "#/$defs/nameList"}, + "role": {"enum": ["design", "observation", "verification"]} + } + } + } +} diff --git a/components/agentic-circuit/schemas/opcodes.json b/components/agentic-circuit/schemas/opcodes.json new file mode 100644 index 00000000..7d9fafb0 --- /dev/null +++ b/components/agentic-circuit/schemas/opcodes.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","entries":[{"category":"scheduling","constants":["output_depths","output_latencies"],"effect":"atomic","gfsim":{"available":true,"realization":"gfsim::QueueBarrier>"},"inputs":{"max":null,"min":2},"kind":"barrier","operation":"ac.barrier","outputs":{"max":null,"min":2},"payload_relation":"positionally_equal_input_output_payloads","pyc":{"available":true,"realization":"all_input_valid_and_all_output_ready_atomic_handshake"},"refinement_observations":["input_transactions","output_transactions","barrier_firing"],"role":"design"},{"category":"transport","constants":["output_depths","output_latencies"],"effect":"atomic","gfsim":{"available":true,"realization":"gfsim::QueueBroadcast"},"inputs":{"max":1,"min":1},"kind":"broadcast","operation":"ac.broadcast","outputs":{"max":null,"min":2},"payload_relation":"all_payloads_equal","pyc":{"available":true,"realization":"all_output_ready_conjunction"},"refinement_observations":["input_transaction","output_transactions"],"role":"design"},{"category":"scheduling","constants":["credits","depth","latency"],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueCredit"},"inputs":{"max":1,"min":1},"kind":"credit","operation":"ac.credit","outputs":{"max":1,"min":1},"payload_relation":"input_output_payload_equal","pyc":{"available":true,"realization":"fixed_credit_register_window_and_countdown"},"refinement_observations":["accepted_transaction","completed_transaction","active_credits"],"role":"design"},{"category":"scheduling","constants":["capacity","resources","no_dependency","depth","latency"],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueDependency"},"inputs":{"max":1,"min":1},"kind":"dependency","operation":"ac.dependency","outputs":{"max":1,"min":1},"payload_relation":"input_output_payload_equal","pyc":{"available":true,"realization":"packed_dependency_window_resource_reservation_and_countdown"},"refinement_observations":["accepted_transaction","issued_transaction","completed_transaction"],"role":"design"},{"category":"boundary","constants":["message"],"effect":"verification","gfsim":{"available":true,"realization":"gfsim::QueueExpect"},"inputs":{"max":1,"min":1},"kind":"expect","operation":"ac.expect","outputs":{"max":0,"min":0},"payload_relation":"observe_input_payload","pyc":{"available":false,"realization":"pyc_testbench_boundary_only"},"refinement_observations":["predicate_input","assertion_outcome"],"role":"verification"},{"category":"state","constants":["depth","latency","max_iterations"],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueFeedback"},"inputs":{"max":1,"min":1},"kind":"feedback","operation":"ac.feedback","outputs":{"max":1,"min":1},"payload_relation":"input_output_payload_equal","pyc":{"available":true,"realization":"valid_data_iteration_registers"},"refinement_observations":["input_transaction","output_transaction","iteration_limit"],"role":"design"},{"category":"transport","constants":["output_depths","output_latencies"],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueFork"},"inputs":{"max":1,"min":1},"kind":"fork","operation":"ac.fork","outputs":{"max":null,"min":2},"payload_relation":"all_payloads_equal","pyc":{"available":true,"realization":"per_output_delivered_registers"},"refinement_observations":["input_transaction","output_transactions"],"role":"design"},{"category":"state","constants":["data_type","entries","init","latency","stable_id","owner"],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueMemoryArbiter storage owner"},"inputs":{"max":0,"min":0},"kind":"memory_instance","operation":"ac.memory.instance","outputs":{"max":0,"min":0},"payload_relation":"declared_storage_owner","pyc":{"available":true,"realization":"one_pyc.sync_mem_per_instance"},"refinement_observations":["memory_write","busy","selected_endpoint"],"role":"design"},{"category":"state","constants":["instance","ordinal","result_field","depth"],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueMemoryArbiter"},"inputs":{"max":1,"min":1},"kind":"memory_request","operation":"ac.memory.request","outputs":{"max":1,"min":1},"payload_relation":"input_output_payload_equal","pyc":{"available":true,"realization":"pyc.sync_mem_with_aligned_request_state"},"refinement_observations":["request_transaction","response_transaction","memory_write"],"role":"design"},{"category":"transport","constants":["policy","depth","latency"],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueMerge"},"inputs":{"max":null,"min":2},"kind":"merge","operation":"ac.merge","outputs":{"max":1,"min":1},"payload_relation":"all_payloads_equal","pyc":{"available":true,"realization":"priority_or_round_robin_arbiter"},"refinement_observations":["selected_input_transaction","output_transaction"],"role":"design"},{"category":"boundary","constants":["name"],"effect":"observation","gfsim":{"available":true,"realization":"gfsim::QueueObserve"},"inputs":{"max":1,"min":1},"kind":"observe","operation":"ac.observe","outputs":{"max":0,"min":0},"payload_relation":"observe_input_payload","pyc":{"available":true,"realization":"non_backpressuring_alias_probe"},"refinement_observations":["probe_value"],"role":"observation"},{"category":"scheduling","constants":["capacity","start","depth","latency"],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueReorder"},"inputs":{"max":1,"min":1},"kind":"reorder","operation":"ac.reorder","outputs":{"max":1,"min":1},"payload_relation":"input_output_payload_equal","pyc":{"available":true,"realization":"tagged_register_bank_and_expected_key"},"refinement_observations":["accepted_transaction","retired_transaction","sequence_order"],"role":"design"},{"category":"transport","constants":["output_depths","output_latencies"],"effect":"atomic","gfsim":{"available":true,"realization":"gfsim::QueueRoute"},"inputs":{"max":1,"min":1},"kind":"route","operation":"ac.route","outputs":{"max":null,"min":2},"payload_relation":"all_payloads_equal","pyc":{"available":true,"realization":"selector_decoder_and_ready_mux"},"refinement_observations":["input_transaction","selected_output_transaction"],"role":"design"},{"category":"structural","constants":["sym_name"],"effect":"structural","gfsim":{"available":true,"realization":"gfsim::Module_hierarchy"},"inputs":{"max":null,"min":0},"kind":"scope","operation":"ac.scope","outputs":{"max":null,"min":0},"payload_relation":"positionally_equal_boundary_payloads","pyc":{"available":true,"realization":"elaboration_time_scope_flattening"},"refinement_observations":["boundary_transactions"],"role":"design"},{"category":"transport","constants":["depth","latency"],"effect":"atomic","gfsim":{"available":true,"realization":"gfsim::QueueSelect"},"inputs":{"max":null,"min":3},"kind":"select","operation":"ac.select","outputs":{"max":1,"min":1},"payload_relation":"selected_data_payload_matches_output","pyc":{"available":true,"realization":"selector_mux_and_selected_ready_handshake"},"refinement_observations":["control_transaction","selected_input_transaction","output_transaction"],"role":"design"},{"category":"boundary","constants":[],"effect":"stateful","gfsim":{"available":true,"realization":"gfsim::QueueSink"},"inputs":{"max":1,"min":1},"kind":"sink","operation":"ac.sink","outputs":{"max":0,"min":0},"payload_relation":"consume_input_payload","pyc":{"available":true,"realization":"ready_valid_output_boundary"},"refinement_observations":["output_transaction"],"role":"design"},{"category":"boundary","constants":["depth","latency"],"effect":"stateful","gfsim":{"available":true,"realization":"typed_source_boundary"},"inputs":{"max":0,"min":0},"kind":"source","operation":"ac.source","outputs":{"max":1,"min":1},"payload_relation":"declared_output_payload","pyc":{"available":true,"realization":"ready_valid_input_boundary"},"refinement_observations":["input_transaction"],"role":"design"},{"category":"combinational","constants":["output_depths","output_latencies"],"effect":"atomic","gfsim":{"available":true,"realization":"gfsim::QueueTransform/QueueAtomicTransform"},"inputs":{"max":null,"min":1},"kind":"transform","operation":"ac.transform","outputs":{"max":null,"min":1},"payload_relation":"region_yields_match_output_payloads","pyc":{"available":true,"realization":"pure_logic_plus_ready_valid_fifos"},"refinement_observations":["accepted_transaction","output_transaction"],"role":"design"}],"schema":"agentic-circuit-opcode-catalog","version":"0.3"} diff --git a/components/agentic-circuit/schemas/pto-trace.schema.json b/components/agentic-circuit/schemas/pto-trace.schema.json new file mode 100644 index 00000000..ed9dbebd --- /dev/null +++ b/components/agentic-circuit/schemas/pto-trace.schema.json @@ -0,0 +1,137 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:pto-trace:0.1", + "title": "PTO Trace 0.1", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "metadata", "records"], + "properties": { + "schema": {"const": "pto-trace"}, + "version": {"const": "0.1"}, + "contract_epoch": {"const": "0.3"}, + "metadata": {"$ref": "#/$defs/metadata"}, + "records": { + "type": "array", + "items": {"$ref": "#/$defs/record"} + } + }, + "$defs": { + "uint64": { + "type": "integer", + "minimum": 0, + "maximum": 18446744073709551615 + }, + "sha256": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "metadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "producer": {"type": "string", "minLength": 1}, + "pto_identity": {"type": "string", "minLength": 1}, + "source_program": {"type": "string", "minLength": 1}, + "address_spaces": { + "type": "array", + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + }, + "data_layout": {"type": "string", "minLength": 1}, + "record_count": {"$ref": "#/$defs/uint64"}, + "content_hash": {"$ref": "#/$defs/sha256"} + } + }, + "record": { + "type": "object", + "additionalProperties": false, + "required": ["sequence_id", "opcode", "operands", "dependencies", "attributes"], + "properties": { + "sequence_id": {"$ref": "#/$defs/uint64"}, + "opcode": {"type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_.]*$"}, + "operands": { + "type": "array", + "items": {"$ref": "#/$defs/operand"} + }, + "dependencies": { + "type": "array", + "items": {"$ref": "#/$defs/uint64"}, + "uniqueItems": true + }, + "attributes": { + "type": "object", + "additionalProperties": {"$ref": "#/$defs/jsonValue"} + }, + "issue_time": {"$ref": "#/$defs/uint64"}, + "source": {"$ref": "#/$defs/source"} + } + }, + "operand": { + "oneOf": [ + {"$ref": "#/$defs/immediateOperand"}, + {"$ref": "#/$defs/idOperand"}, + {"$ref": "#/$defs/addressOperand"}, + {"$ref": "#/$defs/recordResultOperand"} + ] + }, + "immediateOperand": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "type", "value"], + "properties": { + "kind": {"const": "immediate"}, + "type": {"type": "string", "minLength": 1}, + "value": {"$ref": "#/$defs/jsonScalar"} + } + }, + "idOperand": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "id"], + "properties": { + "kind": {"enum": ["buffer", "tile", "symbol"]}, + "id": {"type": "string", "minLength": 1} + } + }, + "addressOperand": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "space", "value"], + "properties": { + "kind": {"const": "address"}, + "space": {"type": "string", "minLength": 1}, + "value": {"$ref": "#/$defs/uint64"} + } + }, + "recordResultOperand": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "sequence_id", "result_index"], + "properties": { + "kind": {"const": "record_result"}, + "sequence_id": {"$ref": "#/$defs/uint64"}, + "result_index": {"type": "integer", "minimum": 0} + } + }, + "source": { + "type": "object", + "additionalProperties": false, + "required": ["file", "line", "column"], + "properties": { + "file": {"type": "string", "minLength": 1}, + "line": {"type": "integer", "minimum": 1}, + "column": {"type": "integer", "minimum": 1} + } + }, + "jsonScalar": { + "type": ["string", "number", "integer", "boolean", "null"] + }, + "jsonValue": { + "oneOf": [ + {"$ref": "#/$defs/jsonScalar"}, + {"type": "array", "items": {"$ref": "#/$defs/jsonValue"}}, + {"type": "object", "additionalProperties": {"$ref": "#/$defs/jsonValue"}} + ] + } + } +} diff --git a/components/agentic-circuit/schemas/run-manifest.schema.json b/components/agentic-circuit/schemas/run-manifest.schema.json new file mode 100644 index 00000000..5dc5278d --- /dev/null +++ b/components/agentic-circuit/schemas/run-manifest.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:run-manifest:0.1", + "title": "Agentic Circuit Run Manifest 0.1", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "build_manifest", "trace", "seed", "output_directory", "deadlock_window", "max_ticks", "max_domain_cycles", "stats_format", "event_log", "termination_expectation"], + "properties": { + "schema": {"const": "agentic-circuit-run-manifest"}, + "version": {"const": "0.1"}, + "contract_epoch": {"const": "0.3"}, + "build_manifest": {"$ref": "#/$defs/fileHash"}, + "trace": {"$ref": "#/$defs/trace"}, + "seed": {"$ref": "#/$defs/uint64"}, + "output_directory": {"type": "string", "minLength": 1}, + "deadlock_window": {"oneOf": [{"type": "null"}, {"type": "integer", "minimum": 1}]}, + "max_ticks": {"oneOf": [{"type": "null"}, {"type": "integer", "minimum": 1}]}, + "max_domain_cycles": { + "type": "object", + "propertyNames": {"pattern": "^[A-Za-z_][A-Za-z0-9_.-]*$"}, + "additionalProperties": {"type": "integer", "minimum": 1} + }, + "stats_format": {"const": "json"}, + "event_log": {"enum": ["disabled", "jsonl"]}, + "termination_expectation": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "reason"], + "properties": { + "kind": {"enum": ["complete", "incomplete", "any"]}, + "reason": { + "oneOf": [ + {"type": "null"}, + {"enum": ["trace_drained", "max_ticks", "max_domain_cycles", "declared_model_termination"]} + ] + } + } + } + }, + "$defs": { + "uint64": {"type": "integer", "minimum": 0, "maximum": 18446744073709551615}, + "sha256": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "fileHash": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "sha256": {"$ref": "#/$defs/sha256"} + } + }, + "trace": { + "type": "object", + "additionalProperties": false, + "required": ["path", "schema", "version", "sha256"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "schema": {"const": "pto-trace"}, + "version": {"const": "0.1"}, + "sha256": {"$ref": "#/$defs/sha256"} + } + } + } +} diff --git a/components/agentic-circuit/schemas/run-result.schema.json b/components/agentic-circuit/schemas/run-result.schema.json new file mode 100644 index 00000000..27918d0f --- /dev/null +++ b/components/agentic-circuit/schemas/run-result.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:pto-isa:agentic-circuit:schema:run-result:0.1", + "title": "Agentic Circuit Run Result 0.1", + "type": "object", + "additionalProperties": false, + "required": ["schema", "version", "contract_epoch", "run_manifest", "status", "termination_reason", "simulated_ticks", "domain_cycles", "event_count", "trace_position", "outputs", "validation"], + "properties": { + "schema": {"const": "agentic-circuit-run-result"}, + "version": {"const": "0.1"}, + "contract_epoch": {"const": "0.3"}, + "run_manifest": {"$ref": "#/$defs/fileHash"}, + "status": {"enum": ["completed", "incomplete", "failed"]}, + "termination_reason": {"enum": ["trace_drained", "declared_model_termination", "max_ticks", "max_domain_cycles", "max_events", "max_deltas_per_tick", "max_trace_records", "max_validation_work", "deadlock", "invariant_violation", "trace_error", "runtime_error", "interrupted"]}, + "simulated_ticks": {"$ref": "#/$defs/uint64"}, + "domain_cycles": { + "type": "object", + "propertyNames": {"pattern": "^[A-Za-z_][A-Za-z0-9_.-]*$"}, + "additionalProperties": {"$ref": "#/$defs/uint64"} + }, + "event_count": {"$ref": "#/$defs/uint64"}, + "trace_position": { + "type": "object", + "additionalProperties": false, + "required": ["next_record_index", "last_committed_sequence_id"], + "properties": { + "next_record_index": {"$ref": "#/$defs/uint64"}, + "last_committed_sequence_id": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/uint64"}]} + } + }, + "outputs": {"type": "array", "items": {"$ref": "#/$defs/fileHash"}}, + "validation": { + "type": "object", + "additionalProperties": false, + "required": ["status", "report_sha256"], + "properties": { + "status": {"enum": ["passed", "failed", "not_run"]}, + "report_sha256": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/sha256"}]} + } + } + }, + "allOf": [ + { + "if": {"properties": {"status": {"const": "completed"}}}, + "then": {"properties": {"termination_reason": {"enum": ["trace_drained", "declared_model_termination"]}}} + }, + { + "if": {"properties": {"status": {"const": "incomplete"}}}, + "then": {"properties": {"termination_reason": {"enum": ["max_ticks", "max_domain_cycles", "max_events", "max_deltas_per_tick", "max_trace_records", "max_validation_work", "interrupted"]}}} + }, + { + "if": {"properties": {"status": {"const": "failed"}}}, + "then": {"properties": {"termination_reason": {"enum": ["deadlock", "invariant_violation", "trace_error", "runtime_error"]}}} + } + ], + "$defs": { + "uint64": {"type": "integer", "minimum": 0, "maximum": 18446744073709551615}, + "sha256": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "fileHash": { + "type": "object", + "additionalProperties": false, + "required": ["path", "sha256"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "sha256": {"$ref": "#/$defs/sha256"} + } + } + } +} diff --git a/components/agentic-circuit/schemas/stdlib/AddressTranslator.json b/components/agentic-circuit/schemas/stdlib/AddressTranslator.json new file mode 100644 index 00000000..08e9c6ef --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/AddressTranslator.json @@ -0,0 +1,123 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.AddressTranslator", + "family": "adaptation", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::AddressTranslator", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:9b600a098eb40576afba407026f3ccd205563339da4b93eb644926d8999c537c" +} diff --git a/components/agentic-circuit/schemas/stdlib/Arbiter.json b/components/agentic-circuit/schemas/stdlib/Arbiter.json new file mode 100644 index 00000000..045a140c --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Arbiter.json @@ -0,0 +1,119 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Arbiter", + "family": "control", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Arbiter", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:700f89d9d9df1facc03b30065b0654ad42fb1171ed52ebbb6320b3100298e8d7" +} diff --git a/components/agentic-circuit/schemas/stdlib/Barrier.json b/components/agentic-circuit/schemas/stdlib/Barrier.json new file mode 100644 index 00000000..abbbd811 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Barrier.json @@ -0,0 +1,127 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Barrier", + "family": "synchronization", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Barrier", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "ports", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "inputs", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": "fanin", + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "inputs.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "inputs.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:a8b0478453025d638045b7a889416aab17a82d32b352019ce60dd5816e2d5ca6" +} diff --git a/components/agentic-circuit/schemas/stdlib/Broadcast.json b/components/agentic-circuit/schemas/stdlib/Broadcast.json new file mode 100644 index 00000000..1dd83ec5 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Broadcast.json @@ -0,0 +1,127 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Broadcast", + "family": "synchronization", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Broadcast", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "ports", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "outputs", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": "fanout", + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "outputs.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "outputs.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:f06690f86032ba4be16aad63759b060b2f74d69a5a4c52d2e5715f796759e760" +} diff --git a/components/agentic-circuit/schemas/stdlib/Bus.json b/components/agentic-circuit/schemas/stdlib/Bus.json new file mode 100644 index 00000000..29ab4673 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Bus.json @@ -0,0 +1,119 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Bus", + "family": "interconnect", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Bus", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:52650a3dce32c6870da36e35d159e17bf1050cb4ba28fa77082b6065037e09b2" +} diff --git a/components/agentic-circuit/schemas/stdlib/Cache.json b/components/agentic-circuit/schemas/stdlib/Cache.json new file mode 100644 index 00000000..252bb607 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Cache.json @@ -0,0 +1,153 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Cache", + "family": "storage", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Cache", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "storage", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:9f58fa33c9beda1118ba7c226339b37fcfc9d1ec5575c2c75416691ee06d686b" +} diff --git a/components/agentic-circuit/schemas/stdlib/Compute.json b/components/agentic-circuit/schemas/stdlib/Compute.json new file mode 100644 index 00000000..2654789d --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Compute.json @@ -0,0 +1,133 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Compute", + "family": "compute", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Compute", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "optional" + }, + "static_parameters": [ + { + "name": "Input", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "Output", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "FunctionalPolicy", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.FunctionalPolicy", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "completed_transactions" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "completed_transactions" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:5871dd46fa5dc7f79661655acabee3bd7579a80af87f243653a01ca572574faa" +} diff --git a/components/agentic-circuit/schemas/stdlib/Crossbar.json b/components/agentic-circuit/schemas/stdlib/Crossbar.json new file mode 100644 index 00000000..170fca8b --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Crossbar.json @@ -0,0 +1,119 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Crossbar", + "family": "interconnect", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Crossbar", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:c91f38bec2800f7af8d146f20f1115842e677acf46cb7a31880552fd60920991" +} diff --git a/components/agentic-circuit/schemas/stdlib/DependencyTracker.json b/components/agentic-circuit/schemas/stdlib/DependencyTracker.json new file mode 100644 index 00000000..d70d5317 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/DependencyTracker.json @@ -0,0 +1,149 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.DependencyTracker", + "family": "control", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::DependencyTracker", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "control", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:3b7d380f363ff2a96db3860b36dfab088b093c0a9c53fcf9a8439bc1cd79d537" +} diff --git a/components/agentic-circuit/schemas/stdlib/Dispatcher.json b/components/agentic-circuit/schemas/stdlib/Dispatcher.json new file mode 100644 index 00000000..22c8bcfd --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Dispatcher.json @@ -0,0 +1,119 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Dispatcher", + "family": "control", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Dispatcher", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:a0467931c4d93f5dd93b7ef5e0867626c3130049fa71273a41b37ea128db7a0d" +} diff --git a/components/agentic-circuit/schemas/stdlib/Dma.json b/components/agentic-circuit/schemas/stdlib/Dma.json new file mode 100644 index 00000000..bc00db4e --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Dma.json @@ -0,0 +1,153 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Dma", + "family": "transport", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Dma", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "transport", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:9a4c600c2f4fe074dfe3c13b3c4d83d9f8449fdb6f7a1136951e642d8287f77e" +} diff --git a/components/agentic-circuit/schemas/stdlib/Fork.json b/components/agentic-circuit/schemas/stdlib/Fork.json new file mode 100644 index 00000000..0fdae65c --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Fork.json @@ -0,0 +1,127 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Fork", + "family": "synchronization", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Fork", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "ports", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "outputs", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": "fanout", + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "outputs.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "outputs.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:7a5ab27f3d82b1eb5a4177b3c2761cb05a371c28f4550ef102187b5cc96ed3f1" +} diff --git a/components/agentic-circuit/schemas/stdlib/Join.json b/components/agentic-circuit/schemas/stdlib/Join.json new file mode 100644 index 00000000..f40c5760 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Join.json @@ -0,0 +1,127 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Join", + "family": "synchronization", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Join", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "ports", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "inputs", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": "fanin", + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "inputs.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "inputs.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:5ae5c46325a4d1dc628c0da87845f1758991ed63fa7e4c1c9c9ba7650c85873a" +} diff --git a/components/agentic-circuit/schemas/stdlib/Link.json b/components/agentic-circuit/schemas/stdlib/Link.json new file mode 100644 index 00000000..80281378 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Link.json @@ -0,0 +1,117 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Link", + "family": "interconnect", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Link", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "T", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "completed_transactions" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "completed_transactions" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:ff1cab1105429617bbded07d22a3c013a03970e17657b97f9d8f89ee6adb3727" +} diff --git a/components/agentic-circuit/schemas/stdlib/LoadStore.json b/components/agentic-circuit/schemas/stdlib/LoadStore.json new file mode 100644 index 00000000..801f6ea8 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/LoadStore.json @@ -0,0 +1,153 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.LoadStore", + "family": "transport", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::LoadStore", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "transport", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:e213f2629732f381e679e3aa3176ff897308738d0e718fa4a56224ed5a8e0514" +} diff --git a/components/agentic-circuit/schemas/stdlib/Memory.json b/components/agentic-circuit/schemas/stdlib/Memory.json new file mode 100644 index 00000000..1fae2411 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Memory.json @@ -0,0 +1,146 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Memory", + "family": "storage", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Memory", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "T", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "storage", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:8533abc8fb5ca68c714177debb25ccf5e2320d9552a1d8a662fc7f2e78046a60" +} diff --git a/components/agentic-circuit/schemas/stdlib/MemoryController.json b/components/agentic-circuit/schemas/stdlib/MemoryController.json new file mode 100644 index 00000000..4897b3fb --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/MemoryController.json @@ -0,0 +1,153 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.MemoryController", + "family": "storage", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::MemoryController", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "storage", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:f3799084548341d23721abc3bf5910eb8cdf95af7f30b9becb081b1003a0aa51" +} diff --git a/components/agentic-circuit/schemas/stdlib/MemoryManagement.json b/components/agentic-circuit/schemas/stdlib/MemoryManagement.json new file mode 100644 index 00000000..2df5420e --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/MemoryManagement.json @@ -0,0 +1,123 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.MemoryManagement", + "family": "adaptation", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::MemoryManagement", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:4bcf3d516863d99b8067a034444fbce664450f16f1dc14d7e5542015aa9d6d31" +} diff --git a/components/agentic-circuit/schemas/stdlib/Packetizer.json b/components/agentic-circuit/schemas/stdlib/Packetizer.json new file mode 100644 index 00000000..41493962 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Packetizer.json @@ -0,0 +1,123 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Packetizer", + "family": "transport", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Packetizer", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:7d3166e0adac88facf31f4e383eec92c4e15d0a741a9444f5e5a7e7d5f8bd705" +} diff --git a/components/agentic-circuit/schemas/stdlib/ProtocolAdapter.json b/components/agentic-circuit/schemas/stdlib/ProtocolAdapter.json new file mode 100644 index 00000000..9c3b6f7b --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/ProtocolAdapter.json @@ -0,0 +1,123 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.ProtocolAdapter", + "family": "adaptation", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::ProtocolAdapter", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:425e78494c5931c71f6808a3b3508bff753c56d7c39c0f62562a1aef17be7267" +} diff --git a/components/agentic-circuit/schemas/stdlib/Queue.json b/components/agentic-circuit/schemas/stdlib/Queue.json new file mode 100644 index 00000000..e7efd10b --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Queue.json @@ -0,0 +1,164 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Queue", + "family": "transport", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/queue.h", + "symbol": "gfsim::Queue", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "T", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + }, + { + "name": "byteCapacity", + "acir_type": "index", + "required": false, + "default": "unbounded", + "constraint": "value >= 1 or value == unbounded", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "queue", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "completed_transactions", + "queue_occupancy", + "queue_occupancy_peak" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "completed_transactions", + "queue_occupancy", + "queue_occupancy_peak" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "completed_transactions" + ], + "gauges": [ + "queue_occupancy", + "queue_occupancy_peak" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:6ca4b73e0292a3ab122a059ffe927080fd039f7e3d390c63ac17fdebc67d64ef" +} diff --git a/components/agentic-circuit/schemas/stdlib/Reassembler.json b/components/agentic-circuit/schemas/stdlib/Reassembler.json new file mode 100644 index 00000000..d747c299 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Reassembler.json @@ -0,0 +1,123 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Reassembler", + "family": "transport", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Reassembler", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:c660b63d8533f91eca4862e789e68e0451dac8bb8b4235fc1677513057cc3a16" +} diff --git a/components/agentic-circuit/schemas/stdlib/RegisterFile.json b/components/agentic-circuit/schemas/stdlib/RegisterFile.json new file mode 100644 index 00000000..ad35caa4 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/RegisterFile.json @@ -0,0 +1,153 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.RegisterFile", + "family": "storage", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::RegisterFile", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "storage", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:af20286039fb652d9d166c903d022c114c3a14daeb876c4b6e81d45c6f9aafa8" +} diff --git a/components/agentic-circuit/schemas/stdlib/Router.json b/components/agentic-circuit/schemas/stdlib/Router.json new file mode 100644 index 00000000..a761c925 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Router.json @@ -0,0 +1,119 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Router", + "family": "interconnect", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Router", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:dfbb97591c0aa3050a5829286017dc263ce224b8af155ff6cbb794989c0af72f" +} diff --git a/components/agentic-circuit/schemas/stdlib/Scheduler.json b/components/agentic-circuit/schemas/stdlib/Scheduler.json new file mode 100644 index 00000000..3a68a136 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Scheduler.json @@ -0,0 +1,152 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Scheduler", + "family": "control", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Scheduler", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "T", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "queue", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "completed_transactions", + "queue_occupancy", + "queue_occupancy_peak" + ] + } + ], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "completed_transactions", + "queue_occupancy", + "queue_occupancy_peak" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "completed_transactions" + ], + "gauges": [ + "queue_occupancy", + "queue_occupancy_peak" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:e8cb1f1d2c1765c0ac8efe14cdd88a5b78d44ca019a9145d5f10c1df63581de0" +} diff --git a/components/agentic-circuit/schemas/stdlib/Scoreboard.json b/components/agentic-circuit/schemas/stdlib/Scoreboard.json new file mode 100644 index 00000000..85eba9d6 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Scoreboard.json @@ -0,0 +1,149 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Scoreboard", + "family": "control", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Scoreboard", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "control", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:2204843d83c1385cd52e0aca40542bfb768a834f8237520ebf82b8ec222269e3" +} diff --git a/components/agentic-circuit/schemas/stdlib/Scratchpad.json b/components/agentic-circuit/schemas/stdlib/Scratchpad.json new file mode 100644 index 00000000..0388a0e4 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Scratchpad.json @@ -0,0 +1,153 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Scratchpad", + "family": "storage", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Scratchpad", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "storage", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:bd5459cc59ca0ddd058786951624ebedac89113bc6d718d3d979c2af96fe5bf1" +} diff --git a/components/agentic-circuit/schemas/stdlib/Sink.json b/components/agentic-circuit/schemas/stdlib/Sink.json new file mode 100644 index 00000000..d561b34d --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Sink.json @@ -0,0 +1,94 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Sink", + "family": "workload", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Sink", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "T", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:242a92a05db9e045edbb57b19b5182341c2ae7c50fb99dd5ddb92aff65b66c69" +} diff --git a/components/agentic-circuit/schemas/stdlib/Switch.json b/components/agentic-circuit/schemas/stdlib/Switch.json new file mode 100644 index 00000000..ef31f28e --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Switch.json @@ -0,0 +1,119 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Switch", + "family": "interconnect", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Switch", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:b17edbd26dc9bd856095287c3d1445baf7fe8d015e5cadf4ce3a18fa3b04a669" +} diff --git a/components/agentic-circuit/schemas/stdlib/TimeDomainBridge.json b/components/agentic-circuit/schemas/stdlib/TimeDomainBridge.json new file mode 100644 index 00000000..4679b89b --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/TimeDomainBridge.json @@ -0,0 +1,123 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.TimeDomainBridge", + "family": "adaptation", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::TimeDomainBridge", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:77f73e3950a3c34f20a93f72c831404975b3c69e39407b09dfd381cf23f86cce" +} diff --git a/components/agentic-circuit/schemas/stdlib/Tlb.json b/components/agentic-circuit/schemas/stdlib/Tlb.json new file mode 100644 index 00000000..888a54a7 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/Tlb.json @@ -0,0 +1,153 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Tlb", + "family": "storage", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::Tlb", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [ + { + "name": "capacity", + "class": "storage", + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": [ + "ac.Transaction" + ], + "statistics": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ] + } + ], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles", + "queue_occupancy" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [ + "queue_occupancy" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:727f72c7d2e1becd27206d9a8116805c709f62478839c6c6a7407ac4311706a0" +} diff --git a/components/agentic-circuit/schemas/stdlib/TraceSource.json b/components/agentic-circuit/schemas/stdlib/TraceSource.json new file mode 100644 index 00000000..b4ebfb41 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/TraceSource.json @@ -0,0 +1,105 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.TraceSource", + "family": "workload", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/trace.h", + "symbol": "gfsim::TraceSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "Decoder", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "gfsim::TraceDecoder", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "trace or generated transactions are schema-valid" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "trace_position" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "output.transfer" + ], + "quiescence": "source is exhausted and no output offer is pending" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions" + ], + "gauges": [ + "trace_position" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:dd46cf0b83710a1425ea997a91efd413688998efeff38c94ca15050b7fe49a07" +} diff --git a/components/agentic-circuit/schemas/stdlib/TrafficSource.json b/components/agentic-circuit/schemas/stdlib/TrafficSource.json new file mode 100644 index 00000000..19c60cd6 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/TrafficSource.json @@ -0,0 +1,96 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.TrafficSource", + "family": "workload", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::TrafficSource", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "trace or generated transactions are schema-valid" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "output.transfer" + ], + "quiescence": "source is exhausted and no output offer is pending" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:b5c165bd89a51654bec35a50970491045a459cfda1a1e239ef11e6c6e678d387" +} diff --git a/components/agentic-circuit/schemas/stdlib/WidthAdapter.json b/components/agentic-circuit/schemas/stdlib/WidthAdapter.json new file mode 100644 index 00000000..6f1b5252 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/WidthAdapter.json @@ -0,0 +1,123 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.WidthAdapter", + "family": "adaptation", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::WidthAdapter", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Transaction", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [ + "ac.system" + ], + "produces": [ + "ac.system" + ], + "ranges": [], + "translation": "static", + "routing": "address_range", + "unmapped": "diagnostic" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "accepted_transactions", + "stalled_cycles" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "accepted_transactions", + "stalled_cycles" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:de472d25f8f1da54fc3c8794fa1e9b54ec8f9667e306f5a1740b2153ebeb84c7" +} diff --git a/components/agentic-circuit/schemas/stdlib/catalog.json b/components/agentic-circuit/schemas/stdlib/catalog.json new file mode 100644 index 00000000..0347baa6 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/catalog.json @@ -0,0 +1,223 @@ +{ + "catalog": "ac", + "version": "0.1", + "contract_epoch": "0.3", + "entries": [ + { + "canonical_name": "ac.AddressTranslator", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/AddressTranslator.json", + "schema_fingerprint": "sha256:9b600a098eb40576afba407026f3ccd205563339da4b93eb644926d8999c537c" + }, + { + "canonical_name": "ac.Arbiter", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Arbiter.json", + "schema_fingerprint": "sha256:700f89d9d9df1facc03b30065b0654ad42fb1171ed52ebbb6320b3100298e8d7" + }, + { + "canonical_name": "ac.Barrier", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Barrier.json", + "schema_fingerprint": "sha256:a8b0478453025d638045b7a889416aab17a82d32b352019ce60dd5816e2d5ca6" + }, + { + "canonical_name": "ac.Broadcast", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Broadcast.json", + "schema_fingerprint": "sha256:f06690f86032ba4be16aad63759b060b2f74d69a5a4c52d2e5715f796759e760" + }, + { + "canonical_name": "ac.Bus", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Bus.json", + "schema_fingerprint": "sha256:52650a3dce32c6870da36e35d159e17bf1050cb4ba28fa77082b6065037e09b2" + }, + { + "canonical_name": "ac.Cache", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Cache.json", + "schema_fingerprint": "sha256:9f58fa33c9beda1118ba7c226339b37fcfc9d1ec5575c2c75416691ee06d686b" + }, + { + "canonical_name": "ac.Compute", + "availability": "available", + "schema_path": "schemas/stdlib/Compute.json", + "schema_fingerprint": "sha256:5871dd46fa5dc7f79661655acabee3bd7579a80af87f243653a01ca572574faa" + }, + { + "canonical_name": "ac.Crossbar", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Crossbar.json", + "schema_fingerprint": "sha256:c91f38bec2800f7af8d146f20f1115842e677acf46cb7a31880552fd60920991" + }, + { + "canonical_name": "ac.DependencyTracker", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/DependencyTracker.json", + "schema_fingerprint": "sha256:3b7d380f363ff2a96db3860b36dfab088b093c0a9c53fcf9a8439bc1cd79d537" + }, + { + "canonical_name": "ac.Dispatcher", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Dispatcher.json", + "schema_fingerprint": "sha256:a0467931c4d93f5dd93b7ef5e0867626c3130049fa71273a41b37ea128db7a0d" + }, + { + "canonical_name": "ac.Dma", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Dma.json", + "schema_fingerprint": "sha256:9a4c600c2f4fe074dfe3c13b3c4d83d9f8449fdb6f7a1136951e642d8287f77e" + }, + { + "canonical_name": "ac.Fork", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Fork.json", + "schema_fingerprint": "sha256:7a5ab27f3d82b1eb5a4177b3c2761cb05a371c28f4550ef102187b5cc96ed3f1" + }, + { + "canonical_name": "ac.Join", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Join.json", + "schema_fingerprint": "sha256:5ae5c46325a4d1dc628c0da87845f1758991ed63fa7e4c1c9c9ba7650c85873a" + }, + { + "canonical_name": "ac.Link", + "availability": "available", + "schema_path": "schemas/stdlib/Link.json", + "schema_fingerprint": "sha256:ff1cab1105429617bbded07d22a3c013a03970e17657b97f9d8f89ee6adb3727" + }, + { + "canonical_name": "ac.LoadStore", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/LoadStore.json", + "schema_fingerprint": "sha256:e213f2629732f381e679e3aa3176ff897308738d0e718fa4a56224ed5a8e0514" + }, + { + "canonical_name": "ac.Memory", + "availability": "available", + "schema_path": "schemas/stdlib/Memory.json", + "schema_fingerprint": "sha256:8533abc8fb5ca68c714177debb25ccf5e2320d9552a1d8a662fc7f2e78046a60" + }, + { + "canonical_name": "ac.MemoryController", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/MemoryController.json", + "schema_fingerprint": "sha256:f3799084548341d23721abc3bf5910eb8cdf95af7f30b9becb081b1003a0aa51" + }, + { + "canonical_name": "ac.MemoryManagement", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/MemoryManagement.json", + "schema_fingerprint": "sha256:4bcf3d516863d99b8067a034444fbce664450f16f1dc14d7e5542015aa9d6d31" + }, + { + "canonical_name": "ac.Packetizer", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Packetizer.json", + "schema_fingerprint": "sha256:7d3166e0adac88facf31f4e383eec92c4e15d0a741a9444f5e5a7e7d5f8bd705" + }, + { + "canonical_name": "ac.ProtocolAdapter", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/ProtocolAdapter.json", + "schema_fingerprint": "sha256:425e78494c5931c71f6808a3b3508bff753c56d7c39c0f62562a1aef17be7267" + }, + { + "canonical_name": "ac.Queue", + "availability": "available", + "schema_path": "schemas/stdlib/Queue.json", + "schema_fingerprint": "sha256:6ca4b73e0292a3ab122a059ffe927080fd039f7e3d390c63ac17fdebc67d64ef" + }, + { + "canonical_name": "ac.Reassembler", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Reassembler.json", + "schema_fingerprint": "sha256:c660b63d8533f91eca4862e789e68e0451dac8bb8b4235fc1677513057cc3a16" + }, + { + "canonical_name": "ac.RegisterFile", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/RegisterFile.json", + "schema_fingerprint": "sha256:af20286039fb652d9d166c903d022c114c3a14daeb876c4b6e81d45c6f9aafa8" + }, + { + "canonical_name": "ac.Router", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Router.json", + "schema_fingerprint": "sha256:dfbb97591c0aa3050a5829286017dc263ce224b8af155ff6cbb794989c0af72f" + }, + { + "canonical_name": "ac.Scheduler", + "availability": "available", + "schema_path": "schemas/stdlib/Scheduler.json", + "schema_fingerprint": "sha256:e8cb1f1d2c1765c0ac8efe14cdd88a5b78d44ca019a9145d5f10c1df63581de0" + }, + { + "canonical_name": "ac.Scoreboard", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Scoreboard.json", + "schema_fingerprint": "sha256:2204843d83c1385cd52e0aca40542bfb768a834f8237520ebf82b8ec222269e3" + }, + { + "canonical_name": "ac.Scratchpad", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Scratchpad.json", + "schema_fingerprint": "sha256:bd5459cc59ca0ddd058786951624ebedac89113bc6d718d3d979c2af96fe5bf1" + }, + { + "canonical_name": "ac.Sink", + "availability": "available", + "schema_path": "schemas/stdlib/Sink.json", + "schema_fingerprint": "sha256:242a92a05db9e045edbb57b19b5182341c2ae7c50fb99dd5ddb92aff65b66c69" + }, + { + "canonical_name": "ac.Switch", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Switch.json", + "schema_fingerprint": "sha256:b17edbd26dc9bd856095287c3d1445baf7fe8d015e5cadf4ce3a18fa3b04a669" + }, + { + "canonical_name": "ac.TimeDomainBridge", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/TimeDomainBridge.json", + "schema_fingerprint": "sha256:77f73e3950a3c34f20a93f72c831404975b3c69e39407b09dfd381cf23f86cce" + }, + { + "canonical_name": "ac.Tlb", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/Tlb.json", + "schema_fingerprint": "sha256:727f72c7d2e1becd27206d9a8116805c709f62478839c6c6a7407ac4311706a0" + }, + { + "canonical_name": "ac.TraceSource", + "availability": "available", + "schema_path": "schemas/stdlib/TraceSource.json", + "schema_fingerprint": "sha256:dd46cf0b83710a1425ea997a91efd413688998efeff38c94ca15050b7fe49a07" + }, + { + "canonical_name": "ac.TrafficSource", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/TrafficSource.json", + "schema_fingerprint": "sha256:b5c165bd89a51654bec35a50970491045a459cfda1a1e239ef11e6c6e678d387" + }, + { + "canonical_name": "ac.WidthAdapter", + "availability": "declared_unavailable", + "schema_path": "schemas/stdlib/WidthAdapter.json", + "schema_fingerprint": "sha256:de472d25f8f1da54fc3c8794fa1e9b54ec8f9667e306f5a1740b2153ebeb84c7" + }, + { + "canonical_name": "ac.ready_valid", + "availability": "available", + "schema_path": "schemas/stdlib/ready_valid.json", + "schema_fingerprint": "sha256:de509897e393cdb7cace1789747fcdb93af7180ad7838c1522f2e44c302f11c6" + }, + { + "canonical_name": "ac.request_response", + "availability": "available", + "schema_path": "schemas/stdlib/request_response.json", + "schema_fingerprint": "sha256:e111345757ee022bf9f25c613a6146edf9b7ec7326097374b8515e070ebcc2d4" + } + ] +} diff --git a/components/agentic-circuit/schemas/stdlib/ready_valid.json b/components/agentic-circuit/schemas/stdlib/ready_valid.json new file mode 100644 index 00000000..4ab86099 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/ready_valid.json @@ -0,0 +1,117 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.ready_valid", + "family": "protocol", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::ReadyValid", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "T", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + } + ], + "bindings": [ + { + "name": "input", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,consumer>", + "direction": "in", + "role": "consumer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "output", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,producer>", + "direction": "out", + "role": "producer", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "consumer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "producer", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "none", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "completed_transactions" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "input.transfer", + "output.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "input.transfer", + "output.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "completed_transactions" + ], + "gauges": [], + "histograms": [] + }, + "schema_fingerprint": "sha256:de509897e393cdb7cace1789747fcdb93af7180ad7838c1522f2e44c302f11c6" +} diff --git a/components/agentic-circuit/schemas/stdlib/request_response.json b/components/agentic-circuit/schemas/stdlib/request_response.json new file mode 100644 index 00000000..4ff60900 --- /dev/null +++ b/components/agentic-circuit/schemas/stdlib/request_response.json @@ -0,0 +1,136 @@ +{ + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.request_response", + "family": "protocol", + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": "gfsim/components.h", + "symbol": "gfsim::RequestResponse", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "none" + }, + "static_parameters": [ + { + "name": "Req", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "Resp", + "acir_type": "!ac.static_type", + "required": true, + "default": null, + "constraint": "ac.Packet", + "cpp_mapping": "template_argument" + }, + { + "name": "capacity", + "acir_type": "index", + "required": true, + "default": null, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant" + } + ], + "bindings": [ + { + "name": "request", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "in", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + }, + { + "name": "response", + "binding_kind": "endpoint", + "acir_type": "!ac.endpoint,ac.ready_valid>>,responder>", + "direction": "out", + "role": "responder", + "cardinality": 1, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": null + } + ], + "results": [], + "resources": [], + "address_behavior": { + "consumes": [], + "produces": [], + "ranges": [], + "translation": "none", + "routing": "none", + "unmapped": "not_applicable" + }, + "protocol_contracts": [ + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + }, + { + "protocol": "ac.ready_valid", + "role": "responder", + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": "required", + "completion": "transfer", + "failure": "protocol_violation" + } + ], + "effect": { + "kind": "stateful", + "requirements": [ + "input offers remain immutable until committed transfer" + ], + "guarantees": [ + "accepted transactions commit exactly once in stable order" + ], + "observable_effects": [ + "completed_transactions", + "active_correlations" + ], + "failure_behavior": "terminate_with_diagnostic" + }, + "activation": { + "sources": [ + "request.transfer", + "response.transfer" + ], + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": [ + "request.transfer", + "response.transfer" + ], + "quiescence": "all inputs are closed and all committed internal state is empty" + }, + "observation": { + "probes": [], + "counters": [ + "completed_transactions" + ], + "gauges": [ + "active_correlations" + ], + "histograms": [] + }, + "schema_fingerprint": "sha256:e111345757ee022bf9f25c613a6146edf9b7ec7326097374b8515e070ebcc2d4" +} diff --git a/components/agentic-circuit/scripts/audit-op-coverage.py b/components/agentic-circuit/scripts/audit-op-coverage.py new file mode 100755 index 00000000..b4b44f1a --- /dev/null +++ b/components/agentic-circuit/scripts/audit-op-coverage.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Audit per-operation valid+invalid test coverage for ACIR and ACSim dialects. + +Reads .td definitions and scans lit test files to report which operations +have valid (positive) and invalid (negative) test coverage. +Exits non-zero if any public op lacks both valid and invalid tests. +""" + +import re +import sys +from pathlib import Path +from collections import defaultdict + +REPO = Path(__file__).resolve().parent.parent + +def extract_ops(td_path: Path) -> dict[str, str]: + """Extract {CppName: assemblyName} from a .td file.""" + ops = {} + text = td_path.read_text() + # Match: def ACIR_FooOp : ACIR_Op<"ac.foo", ...> + for m in re.finditer(r'def\s+(ACIR_\w+Op|ACSim_\w+Op)\s*:\s*\w+_Op<"([^"]+)"', text): + ops[m.group(1)] = m.group(2) + return ops + +def find_op_in_file(op_asm: str, filepath: Path) -> bool: + """Check if the assembly name appears in the file.""" + try: + return op_asm in filepath.read_text() + except Exception: + return False + +def main(): + acir_ops = extract_ops(REPO / "include/acir/Dialect/ACIR/ACIROps.td") + acsim_ops = extract_ops(REPO / "include/acir/Dialect/ACSim/ACSimOps.td") + all_ops = {**acir_ops, **acsim_ops} + + # Find test files + test_dir = REPO / "test" + valid_files = [f for f in test_dir.rglob("*.mlir") if "invalid" not in f.name] + invalid_files = [f for f in test_dir.rglob("*.mlir") if "invalid" in f.name] + + coverage = defaultdict(lambda: {"valid": False, "invalid": False}) + + for cpp_name, asm_name in all_ops.items(): + for f in valid_files: + if find_op_in_file(asm_name, f): + coverage[cpp_name]["valid"] = True + break + for f in invalid_files: + if find_op_in_file(asm_name, f): + coverage[cpp_name]["invalid"] = True + break + + missing_valid = [] + missing_invalid = [] + missing_both = [] + + for cpp_name in sorted(all_ops): + v = coverage[cpp_name]["valid"] + i = coverage[cpp_name]["invalid"] + if not v and not i: + missing_both.append(cpp_name) + elif not v: + missing_valid.append(cpp_name) + elif not i: + missing_invalid.append(cpp_name) + + total = len(all_ops) + covered_both = total - len(missing_valid) - len(missing_invalid) - len(missing_both) + covered_valid = total - len(missing_valid) - len(missing_both) + covered_invalid = total - len(missing_invalid) - len(missing_both) + + print(f"=== ACIR/ACSim Op Test Coverage Audit ===") + print(f"Total ops: {total}") + print(f" Both valid+invalid: {covered_both} ({100*covered_both//total}%)") + print(f" Valid only: {len(missing_invalid)}") + print(f" Invalid only: {len(missing_valid)}") + print(f" Missing both: {len(missing_both)}") + print() + + if missing_both: + print(f"--- Missing both valid and invalid tests ({len(missing_both)}) ---") + for name in missing_both: + print(f" {name} ({all_ops[name]})") + print() + + if missing_valid: + print(f"--- Missing valid tests ({len(missing_valid)}) ---") + for name in missing_valid: + print(f" {name} ({all_ops[name]})") + print() + + if missing_invalid: + print(f"--- Missing invalid tests ({len(missing_invalid)}) ---") + for name in missing_invalid: + print(f" {name} ({all_ops[name]})") + print() + + missing_any = len(missing_both) + len(missing_valid) + len(missing_invalid) + if missing_any == 0: + print("PASS: Every public operation has both valid and invalid test coverage.") + else: + print(f"FAIL: {missing_any} operations have incomplete coverage " + f"({len(missing_both)} missing-both, " + f"{len(missing_valid)} missing-valid, " + f"{len(missing_invalid)} missing-invalid).") + + sys.exit(0 if missing_any == 0 else 1) + +if __name__ == "__main__": + main() diff --git a/components/agentic-circuit/scripts/audit-workspace-determinism.sh b/components/agentic-circuit/scripts/audit-workspace-determinism.sh new file mode 100755 index 00000000..64e28384 --- /dev/null +++ b/components/agentic-circuit/scripts/audit-workspace-determinism.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Repeat the canonical workspace NPU pipeline under unrelated roots and require +# byte-identical trace, result, statistics, event, replay, and Perfetto output. +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd -P) +BUILD_DIR=${BUILD_DIR:-"$ROOT/build/dev-llvm22"} +RUNS=${RUNS:-3} +if [[ ! "$RUNS" =~ ^[2-9][0-9]*$ ]]; then + echo "RUNS must be an integer of at least 2" >&2 + exit 2 +fi +if [[ -x "$ROOT/.venv/bin/python" ]]; then + PYTHON_BIN=${PYTHON_BIN:-"$ROOT/.venv/bin/python"} +else + PYTHON_BIN=${PYTHON_BIN:-python3} +fi +export PYTHONPATH="$ROOT/src:$BUILD_DIR/python${PYTHONPATH:+:$PYTHONPATH}" + +EXAMPLE="$ROOT/examples/workspaces/npu" +GFSIM_TESTS="$BUILD_DIR/bin/GfsimTests" +if [[ ! -x "$GFSIM_TESTS" ]]; then + echo "missing GfsimTests at $GFSIM_TESTS" >&2 + exit 2 +fi + +TEMP_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/agentic-circuit-workspace.XXXXXX") +trap 'rm -rf "$TEMP_ROOT"' EXIT + +copy_workspace() { + "$PYTHON_BIN" - "$EXAMPLE" "$1" <<'PY' +import shutil +import sys +from pathlib import Path + +source, destination = map(Path, sys.argv[1:]) +shutil.copytree( + source, + destination, + ignore=shutil.ignore_patterns("build", "__pycache__", "*.pyc"), +) +PY +} + +hash_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +compare_outputs() { + local baseline=$1 candidate=$2 + for relative in \ + traces/pto-trace.json \ + artifacts/run/run-result.json \ + artifacts/run/stats.json \ + artifacts/run/events.jsonl \ + artifacts/perfetto.json; do + cmp "$baseline/$relative" "$candidate/$relative" + done +} + +"$GFSIM_TESTS" \ + --gtest_filter='ShowcaseTest.LegalWorkOrdersHaveByteIdenticalCommittedResults:NpuDependencyTrackerTest.WorkPermutationPreservesQueuesStatisticsAndObservations:NpuTraceSourceTest.RejectsUnsupportedOpcodeWithoutCommit' + +baseline= +for ((iteration = 1; iteration <= RUNS; ++iteration)); do + workspace="$TEMP_ROOT/root-$iteration/unrelated/npu" + mkdir -p "$(dirname "$workspace")" + copy_workspace "$workspace" + + generated_trace="$TEMP_ROOT/generated-trace-$iteration.json" + "$PYTHON_BIN" "$ROOT/tools/import-davincioo-pto-trace.py" \ + "$workspace/traces/davincioo.jsonl" "$generated_trace" \ + --source-program examples/workspaces/npu + cmp "$workspace/traces/pto-trace.json" "$generated_trace" + + ( + cd "$workspace" + PYTHONHASHSEED=$((iteration * 37)) "$PYTHON_BIN" \ + -m agentic_circuit._cli run architecture.py \ + --project agentic-circuit.toml --trace traces/pto-trace.json \ + --stats-format json --event-log jsonl --expect-termination \ + --output-dir artifacts/run --json + ) >"$TEMP_ROOT/run-$iteration.json" + "$PYTHON_BIN" "$ROOT/tools/pack-perfetto-trace.py" \ + "$workspace/artifacts/run/events.jsonl" \ + "$workspace/artifacts/perfetto.json" + ( + cd "$workspace" + PYTHONHASHSEED=$((iteration * 53)) "$PYTHON_BIN" \ + -m agentic_circuit._cli run \ + --replay-manifest artifacts/run/run-manifest.json \ + --output-dir artifacts/replay --json + ) >"$TEMP_ROOT/replay-$iteration.json" + for relative in run-result.json stats.json events.jsonl; do + cmp "$workspace/artifacts/run/$relative" \ + "$workspace/artifacts/replay/$relative" + done + + model=$(find "$workspace/build/main/builds" -type f -path '*/bin/model' -print -quit) + if [[ -z "$model" || ! -x "$model" ]]; then + echo "generated model executable is missing" >&2 + exit 1 + fi + dependencies="$TEMP_ROOT/dependencies-$iteration.txt" + if command -v otool >/dev/null 2>&1; then + otool -L "$model" >"$dependencies" + elif command -v ldd >/dev/null 2>&1; then + ldd "$model" >"$dependencies" + else + echo "neither otool nor ldd is available for dependency scanning" >&2 + exit 2 + fi + if grep -Eiq 'python|pybind|mlir|plugin' "$dependencies"; then + echo "generated model has a forbidden runtime dependency" >&2 + cat "$dependencies" >&2 + exit 1 + fi + if grep -R -Eiq \ + 'Python(\.h)?|pybind|importlib|mlir/|dl(open|sym)|runtime_factory|plugin_(loader|registry)' \ + "$workspace/build/main/builds"/*/src/generated; then + echo "generated source contains a forbidden dynamic dependency" >&2 + exit 1 + fi + + if [[ -z "$baseline" ]]; then + baseline=$workspace + else + compare_outputs "$baseline" "$workspace" + fi +done + +for relative in \ + traces/pto-trace.json \ + artifacts/run/run-result.json \ + artifacts/run/stats.json \ + artifacts/run/events.jsonl \ + artifacts/perfetto.json; do + printf '%s %s\n' "$(hash_file "$baseline/$relative")" "$relative" +done +echo "workspace determinism audit: PASS ($RUNS runs)" diff --git a/components/agentic-circuit/scripts/audit5-determinism.sh b/components/agentic-circuit/scripts/audit5-determinism.sh new file mode 100755 index 00000000..d34b0d7f --- /dev/null +++ b/components/agentic-circuit/scripts/audit5-determinism.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Audit 5 helper: run the project canonicalization/freeze pipeline repeatedly +# and compare text/bytecode/digest hashes. One-off audit evidence generator, +# not part of the repo gate. +set -uo pipefail +cd "$(dirname "$0")/.." +OPT=${OPT:-build/dev-llvm22/bin/acir-opt-internal} +CANON='builtin.module(ac-canonicalize-model)' +FREEZE='builtin.module(ac-canonicalize-model,ac-freeze-topology)' +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +# Extract freezable sections from split-file tests (split on `//--- name`). +extract() { # file section-name outfile + awk -v want="$2" ' + /^\/\/---/ { cur=$2; next } + NR==1 && !/^\/\// { cur="" } + { if (cur==want) print } + ' "$1" > "$3" +} +extract test/Transforms/freeze-topology.mlir valid.mlir "$TMP/freeze-valid.mlir" +extract test/Transforms/deterministic-canonicalization.mlir a.mlir "$TMP/detcanon-a.mlir" +extract test/Transforms/deterministic-canonicalization.mlir b.mlir "$TMP/detcanon-b.mlir" + +# Group A: full canonicalize+freeze (text + bytecode + topology digest) +GROUP_A=( + test/ACIR/hierarchy-valid.mlir + "$TMP/freeze-valid.mlir" + "$TMP/detcanon-a.mlir" + "$TMP/detcanon-b.mlir" +) +# Group B: canonicalize only (text + bytecode) +GROUP_B=( + test/ACIR/collections-valid.mlir + test/ACIR/resources-valid.mlir + test/ACIR/address-time-valid.mlir + test/ACIR/contracts-valid.mlir + test/ACIR/trace-valid.mlir + test/ACSim/ops-valid.mlir + test/ACSim/reusable-modules.mlir +) + +fail=0 +run_group() { # pipeline with-digest label files... + local pipe="$1" want_digest="$2" label="$3"; shift 3 + for f in "$@"; do + local name; name=$(basename "$f" .mlir) + local text_hashes=() bc_hashes=() digests=() + for i in 1 2 3 4 5; do + local out rc + out=$("$OPT" --verify-each=false --pass-pipeline="$pipe" "$f" 2>"$TMP/err") + rc=$? + if [ $rc -ne 0 ]; then + echo "$name: pipeline failed (rc=$rc): $(head -1 "$TMP/err")" + fail=1; continue 2 + fi + text_hashes+=("$(printf '%s' "$out" | shasum -a 256 | cut -d' ' -f1)") + if [ "$want_digest" = yes ]; then + digests+=("$(printf '%s' "$out" | grep -o 'ac.topology_digest = "[^"]*"' | sort -u | tr '\n' ';')") + fi + "$OPT" --verify-each=false --pass-pipeline="$pipe" --emit-bytecode \ + -o "$TMP/$label-$name.$i.bc" "$f" 2>/dev/null + bc_hashes+=("$(shasum -a 256 "$TMP/$label-$name.$i.bc" | cut -d' ' -f1)") + done + local u_text u_bc u_dig status=OK + u_text=$(printf '%s\n' "${text_hashes[@]}" | sort -u | wc -l | tr -d ' ') + u_bc=$(printf '%s\n' "${bc_hashes[@]}" | sort -u | wc -l | tr -d ' ') + u_dig=1 + if [ "$want_digest" = yes ]; then + u_dig=$(printf '%s\n' "${digests[@]}" | sort -u | wc -l | tr -d ' ') + if [ -z "${digests[0]}" ]; then u_dig=0; fi + fi + if [ "$u_text" != 1 ] || [ "$u_bc" != 1 ] || [ "$u_dig" != 1 ]; then + status=FAIL; fail=1 + fi + printf '%-22s [%s] text:%s bc:%s digest:%s %s\n' \ + "$name" "$label" "$u_text" "$u_bc" "$u_dig" "$status" + done +} + +run_group "$FREEZE" yes freeze "${GROUP_A[@]}" +run_group "$CANON" no canon "${GROUP_B[@]}" +exit $fail diff --git a/components/agentic-circuit/scripts/bootstrap-dev.sh b/components/agentic-circuit/scripts/bootstrap-dev.sh new file mode 100755 index 00000000..9ef9a8cd --- /dev/null +++ b/components/agentic-circuit/scripts/bootstrap-dev.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +llvm_prefix=${LLVM_PREFIX:-/opt/homebrew/opt/llvm} +python_bin=${PYTHON:-python3} + +if [[ ! -x "${llvm_prefix}/bin/llvm-config" ]]; then + echo "LLVM 22.1.8 was not found at ${llvm_prefix}" >&2 + exit 1 +fi + +actual_version=$("${llvm_prefix}/bin/llvm-config" --version) +if [[ "${actual_version}" != "22.1.8" ]]; then + echo "LLVM 22.1.8 is required; found ${actual_version}" >&2 + exit 1 +fi + +"${python_bin}" -m venv "${repo_root}/.venv" +"${repo_root}/.venv/bin/python" -m pip install \ + --disable-pip-version-check \ + -r "${repo_root}/requirements-dev.lock" + +echo "Development environment ready." +echo "Run: source ${repo_root}/.venv/bin/activate" diff --git a/components/agentic-circuit/scripts/check-contracts.py b/components/agentic-circuit/scripts/check-contracts.py new file mode 100755 index 00000000..7e92f32d --- /dev/null +++ b/components/agentic-circuit/scripts/check-contracts.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +import importlib.util +import hashlib +import json +import re +import subprocess +import sys +from pathlib import Path +from urllib.parse import unquote + + +ROOT = Path(__file__).resolve().parents[1] +EPOCH = "0.3" +GOVERNANCE_FILES = ( + "LICENSE", + "CONTRIBUTING.md", + "CODE_OF_CONDUCT.md", + "SECURITY.md", + "SUPPORT.md", + "CHANGELOG.md", +) +EXPECTED_LLVM = { + "release": "22.1.8", + "upstream_commit": "ca7933e47d3a3451d81e72ac174dcb5aa28b59d1", + "source_url": ( + "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/" + "llvm-project-22.1.8.src.tar.xz" + ), + "source_sha256": "922f1817a0df7b1489272d18134ee0087a8b068828f87ac63b9861b1a9965888", + "local_prefix": "/opt/homebrew/opt/llvm", + "supported_host_triples": ["arm64-apple-darwin", "x86_64-linux-gnu"], + "package_version_policy": "exact", +} + +AVAILABLE_STDLIB_COMPONENTS = { + "ac.TraceSource", + "ac.Queue", + "ac.Scheduler", + "ac.Compute", + "ac.Link", + "ac.Memory", + "ac.Sink", + "ac.ready_valid", + "ac.request_response", +} +AVAILABLE_STDLIB_BINDINGS = { + "ac.TraceSource": ("gfsim/trace.h", "gfsim::TraceSource"), + "ac.Queue": ("gfsim/queue.h", "gfsim::Queue"), + "ac.Scheduler": ("gfsim/components.h", "gfsim::Scheduler"), + "ac.Compute": ("gfsim/components.h", "gfsim::Compute"), + "ac.Link": ("gfsim/components.h", "gfsim::Link"), + "ac.Memory": ("gfsim/components.h", "gfsim::Memory"), + "ac.Sink": ("gfsim/components.h", "gfsim::Sink"), + "ac.ready_valid": ("gfsim/components.h", "gfsim::ReadyValid"), + "ac.request_response": ( + "gfsim/components.h", + "gfsim::RequestResponse", + ), +} +UNAVAILABLE_STDLIB_COMPONENTS = { + "ac.Arbiter", + "ac.Dispatcher", + "ac.Scoreboard", + "ac.DependencyTracker", + "ac.Bus", + "ac.Crossbar", + "ac.Router", + "ac.Switch", + "ac.Dma", + "ac.Packetizer", + "ac.Reassembler", + "ac.LoadStore", + "ac.RegisterFile", + "ac.Scratchpad", + "ac.Cache", + "ac.Tlb", + "ac.MemoryController", + "ac.Fork", + "ac.Join", + "ac.Broadcast", + "ac.Barrier", + "ac.ProtocolAdapter", + "ac.WidthAdapter", + "ac.TimeDomainBridge", + "ac.AddressTranslator", + "ac.MemoryManagement", + "ac.TrafficSource", +} + + +def check_governance(errors): + for name in GOVERNANCE_FILES: + path = ROOT / name + if not path.is_file() or not path.read_text().strip(): + errors.append(f"missing or empty governance file: {name}") + + +def check_epochs(errors): + schemas = sorted((ROOT / "schemas").glob("*.schema.json")) + if len(schemas) != 12: + errors.append(f"expected 12 JSON schemas, found {len(schemas)}") + for path in schemas: + document = json.loads(path.read_text()) + actual = document.get("properties", {}).get("contract_epoch", {}).get("const") + if actual != EPOCH: + errors.append(f"{path.relative_to(ROOT)} declares epoch {actual!r}") + pyproject = (ROOT / "pyproject.toml").read_text() + match = re.search(r'^contract-epoch\s*=\s*"([^"]+)"\s*$', pyproject, re.MULTILINE) + if match is None or match.group(1) != EPOCH: + errors.append('pyproject.toml must declare contract-epoch = "0.3"') + + +def check_schemas(errors): + if importlib.util.find_spec("jsonschema") is None: + errors.append("jsonschema is unavailable; install requirements-dev.lock") + return + from jsonschema.exceptions import SchemaError + from jsonschema.validators import Draft202012Validator + + for path in sorted((ROOT / "schemas").glob("*.schema.json")): + document = json.loads(path.read_text()) + if document.get("$schema") != "https://json-schema.org/draft/2020-12/schema": + errors.append(f"{path.relative_to(ROOT)} is not draft 2020-12") + continue + try: + Draft202012Validator.check_schema(document) + except SchemaError as exc: + errors.append(f"{path.relative_to(ROOT)} does not compile: {exc.message}") + + +def check_stdlib_catalog(errors): + if importlib.util.find_spec("jsonschema") is None: + return + from jsonschema.validators import Draft202012Validator + + catalog_root = ROOT / "schemas" / "stdlib" + catalog_path = catalog_root / "catalog.json" + if not catalog_path.is_file(): + errors.append("missing standard-library catalog: schemas/stdlib/catalog.json") + return + + catalog = json.loads(catalog_path.read_text()) + expected_names = AVAILABLE_STDLIB_COMPONENTS | UNAVAILABLE_STDLIB_COMPONENTS + expected_availability = { + **{name: "available" for name in AVAILABLE_STDLIB_COMPONENTS}, + **{name: "declared_unavailable" for name in UNAVAILABLE_STDLIB_COMPONENTS}, + } + if set(catalog) != {"catalog", "version", "contract_epoch", "entries"}: + errors.append("standard-library catalog has unknown or missing fields") + return + if ( + catalog["catalog"] != "ac" + or catalog["version"] != "0.1" + or catalog["contract_epoch"] != EPOCH + or not isinstance(catalog["entries"], list) + ): + errors.append("standard-library catalog identity is invalid") + return + + entries = catalog["entries"] + names = [entry.get("canonical_name") for entry in entries] + if names != sorted(expected_names): + errors.append("standard-library catalog names are incomplete or non-canonical") + return + + component_schema = json.loads((ROOT / "schemas/component.schema.json").read_text()) + validator = Draft202012Validator(component_schema) + seen_paths = set() + for entry in entries: + if set(entry) != { + "canonical_name", + "availability", + "schema_path", + "schema_fingerprint", + }: + errors.append(f"catalog entry has invalid fields: {entry!r}") + continue + name = entry["canonical_name"] + if entry["availability"] != expected_availability[name]: + errors.append(f"catalog availability mismatch for {name}") + schema_path = ROOT / entry["schema_path"] + try: + schema_path.relative_to(catalog_root) + except ValueError: + errors.append(f"catalog schema escapes schemas/stdlib: {name}") + continue + if not schema_path.is_file(): + errors.append(f"missing catalog component schema: {entry['schema_path']}") + continue + seen_paths.add(schema_path.resolve()) + record = json.loads(schema_path.read_text()) + record_errors = sorted( + validator.iter_errors(record), key=lambda error: list(error.path) + ) + if record_errors: + errors.append( + f"invalid component schema {name}: {record_errors[0].message}" + ) + continue + if record["canonical_name"] != name: + errors.append(f"component schema canonical name mismatch for {name}") + if record["cpp_binding"] is None: + errors.append(f"component schema omits frozen C++ binding for {name}") + elif entry["availability"] == "available": + binding = record["cpp_binding"] + expected_header, expected_symbol = AVAILABLE_STDLIB_BINDINGS[name] + if ( + binding["header"] != expected_header + or binding["symbol"] != expected_symbol + or binding["concept"] != "gfsim::Component" + ): + errors.append(f"available component binding mismatch for {name}") + header = ROOT / "include" / binding["header"] + if not header.is_file(): + errors.append(f"available component header is missing for {name}") + digest_input = dict(record) + digest_input.pop("schema_fingerprint", None) + canonical = json.dumps( + digest_input, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ).encode() + fingerprint = "sha256:" + hashlib.sha256(canonical).hexdigest() + if record["schema_fingerprint"] != fingerprint: + errors.append(f"component schema fingerprint mismatch for {name}") + if entry["schema_fingerprint"] != fingerprint: + errors.append(f"catalog fingerprint mismatch for {name}") + + extra_paths = { + path.resolve() + for path in catalog_root.glob("*.json") + if path.name != "catalog.json" + } - seen_paths + if extra_paths: + errors.append("unlisted standard-library component schema files are present") + + generation = subprocess.run( + [sys.executable, ROOT / "scripts/generate-stdlib-catalog.py", "--check"], + cwd=ROOT, + text=True, + capture_output=True, + ) + if generation.returncode: + errors.append(generation.stderr.strip() or "standard-library catalog is stale") + + +def tracked_markdown_files(): + result = subprocess.run( + ["git", "ls-files", "-z", "--", "*.md"], + cwd=ROOT, + check=True, + capture_output=True, + ) + return [ROOT / path.decode() for path in result.stdout.split(b"\0") if path] + + +def markdown_destination(raw_target): + target = raw_target.strip() + if target.startswith("<") and ">" in target: + return target[1 : target.index(">")] + return target.split(maxsplit=1)[0] + + +def markdown_prose_lines(path): + prose = [] + fence_character = None + fence_length = 0 + for line in path.read_text().splitlines(): + if fence_character is not None: + closing_fence = ( + rf"^ {{0,3}}{re.escape(fence_character)}{{{fence_length},}}\s*$" + ) + if re.match(closing_fence, line): + fence_character = None + fence_length = 0 + prose.append("") + continue + + opening_fence = re.match(r"^ {0,3}(`{3,}|~{3,})", line) + if opening_fence: + fence = opening_fence.group(1) + fence_character = fence[0] + fence_length = len(fence) + prose.append("") + elif line.startswith(" ") or line.startswith("\t"): + prose.append("") + else: + prose.append(line) + return prose + + +def github_heading_anchors(path): + anchors = set() + counts = {} + lines = markdown_prose_lines(path) + headings = [] + for index, line in enumerate(lines): + atx = re.match(r"^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$", line) + if atx: + headings.append(atx.group(1)) + elif index and re.match(r"^ {0,3}(?:=+|-+)\s*$", line): + headings.append(lines[index - 1].strip()) + + for explicit in re.findall( + r"<(?:a\s+[^>]*name|[a-z][a-z0-9-]*\s+[^>]*id)=[\"']([^\"']+)[\"']", + line, + re.IGNORECASE, + ): + anchors.add(explicit) + + for heading in headings: + rendered = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", heading) + rendered = re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", rendered) + rendered = re.sub(r"<[^>]+>", "", rendered) + rendered = re.sub(r"[`*_~]", "", rendered).lower() + slug = re.sub(r"[^\w\- ]", "", rendered, flags=re.UNICODE).replace(" ", "-") + count = counts.get(slug, 0) + counts[slug] = count + 1 + anchors.add(slug if count == 0 else f"{slug}-{count}") + return anchors + + +def check_links(errors): + pattern = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)") + anchor_cache = {} + for path in tracked_markdown_files(): + prose = "\n".join(markdown_prose_lines(path)) + for raw_target in pattern.findall(prose): + target = markdown_destination(raw_target) + if ( + not target + or target.startswith("//") + or re.match(r"^[a-z][a-z0-9+.-]*:", target, re.IGNORECASE) + ): + continue + destination, separator, fragment = target.partition("#") + target_path = ( + path if not destination else path.parent / unquote(destination) + ) + target_path = target_path.resolve() + if not target_path.exists(): + errors.append(f"broken link: {path.relative_to(ROOT)} -> {target}") + continue + if separator and target_path.suffix.lower() == ".md": + anchors = anchor_cache.setdefault( + target_path, github_heading_anchors(target_path) + ) + decoded_fragment = unquote(fragment) + if decoded_fragment not in anchors: + errors.append( + f"broken fragment: {path.relative_to(ROOT)} -> {target}" + ) + + +def check_placeholders(errors): + marker = re.compile(r"\b(?:TODO|TBD|FIXME|XXX)\b") + for name in ("README.md", *GOVERNANCE_FILES): + path = ROOT / name + if path.is_file() and marker.search(path.read_text()): + errors.append(f"placeholder marker in {name}") + readme = (ROOT / "README.md").read_text() + if ( + "proposed Python" in readme + or "No implementation contract is approved yet" in readme + ): + errors.append("README.md still describes a specification-only placeholder") + + +def check_llvm_lock(errors): + lock = json.loads((ROOT / "toolchains/llvm.lock.json").read_text()) + if lock.get("lock_version") != 1: + errors.append("LLVM lock_version must equal 1") + if lock.get("llvm") != EXPECTED_LLVM: + errors.append("LLVM lock does not match the approved 22.1.8 toolchain") + + +def check_release_layout(errors): + for script in ("check-release-layout.py", "check-ndf.py"): + completed = subprocess.run( + [sys.executable, ROOT / "scripts" / script], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if completed.returncode: + errors.append(completed.stderr.strip() or f"{script} failed") + + +def main(): + errors = [] + check_governance(errors) + check_epochs(errors) + check_schemas(errors) + check_stdlib_catalog(errors) + check_links(errors) + check_placeholders(errors) + check_llvm_lock(errors) + check_release_layout(errors) + if errors: + for error in errors: + print(f"error: {error}", file=sys.stderr) + return 1 + print( + "repository contracts: OK " + "(12 public schemas, 36 stdlib components, epoch 0.3, LLVM 22.1.8)" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/scripts/check-ir-coverage.py b/components/agentic-circuit/scripts/check-ir-coverage.py new file mode 100644 index 00000000..8f5a7ccd --- /dev/null +++ b/components/agentic-circuit/scripts/check-ir-coverage.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 +"""Enforce complete public IR coverage for the current ACIR contract. + +The contracts/*.yaml manifests are the normative source of truth for the +public IR surface. This checker requires that + +- the ODS definitions match the manifests exactly (any added, renamed, + removed, or implementation-only public operation or type must be reflected + in the manifest in the same commit), +- both manifests declare the current contract epoch, +- every manifest entry resolves to a source symbol in the ODS definitions, +- every manifest entry has observable positive and negative lit coverage, +- the dialect registration tables register the generated op/type lists, and +- docs/spec/50-verification/ir-coverage.md is the up-to-date generated ledger. + +Run with --write-ledger to regenerate the ledger after a deliberate surface +change. The default mode is read-only and exits non-zero on any gap. +""" + +import argparse +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +LEDGER_PATH = "docs/spec/50-verification/ir-coverage.md" + +DIALECTS = { + "acir": { + "manifest": "contracts/acir.yaml", + "contract_epoch": "0.3", + "ops_td": "include/acir/Dialect/ACIR/ACIROps.td", + "types_td": "include/acir/Dialect/ACIR/ACIRTypes.td", + "registration": "lib/Dialect/ACIR/ACIRTypes.cpp", + "registration_ops_inc": "acir/Dialect/ACIR/ACIROps.cpp.inc", + "registration_types_inc": "acir/Dialect/ACIR/ACIRTypes.cpp.inc", + "op_prefix": "ac.", + "type_prefix": "!ac.", + }, + "acsim": { + "manifest": "contracts/acsim.yaml", + "contract_epoch": "0.3", + "ops_td": "include/acir/Dialect/ACSim/ACSimOps.td", + "types_td": "include/acir/Dialect/ACSim/ACSimTypes.td", + "registration": "lib/Dialect/ACSim/ACSimTypes.cpp", + "registration_ops_inc": "acir/Dialect/ACSim/ACSimOps.cpp.inc", + "registration_types_inc": "acir/Dialect/ACSim/ACSimTypes.cpp.inc", + "op_prefix": "acsim.", + "type_prefix": "!acsim.", + }, +} + +MANIFEST_SCHEMA = "acir-ir-inventory" + +OP_DEF_PATTERN = re.compile( + r"def\s+(\w+Op)\s*:\s*\w+<\"([^\"]+)\"", re.MULTILINE +) +TYPE_DEF_PATTERN = re.compile( + r"def\s+(\w+)\s*:\s*\w+<\"[^\"]+\",\s*\"([^\"]+)\"", re.MULTILINE +) + + +def load_manifest(root, dialect, errors): + """Parse and validate one normative inventory manifest.""" + spec = DIALECTS[dialect] + path = root / spec["manifest"] + if not path.is_file(): + errors.append(f"missing normative inventory manifest: {spec['manifest']}") + return None + try: + import yaml + except ImportError: + errors.append("pyyaml is unavailable; install requirements-dev.lock") + return None + try: + document = yaml.safe_load(path.read_text()) + except yaml.YAMLError as exc: + errors.append(f"{spec['manifest']} does not parse: {exc}") + return None + if not isinstance(document, dict): + errors.append(f"{spec['manifest']} must be a mapping") + return None + if document.get("schema") != MANIFEST_SCHEMA: + errors.append( + f"{spec['manifest']} must declare schema: {MANIFEST_SCHEMA}" + ) + epoch = document.get("contract_epoch") + expected_epoch = spec["contract_epoch"] + if epoch != expected_epoch: + errors.append( + f"{spec['manifest']} declares stale contract_epoch {epoch!r}; " + f"expected {expected_epoch!r}" + ) + if document.get("dialect") != dialect: + errors.append( + f"{spec['manifest']} declares dialect {document.get('dialect')!r}; " + f"expected {dialect!r}" + ) + result = {} + for field in ("operations", "types"): + entries = document.get(field) + if not isinstance(entries, list) or not all( + isinstance(entry, str) for entry in entries + ): + errors.append(f"{spec['manifest']} field {field} must be a list of strings") + return None + if len(entries) != len(set(entries)): + errors.append(f"{spec['manifest']} field {field} contains duplicates") + result[field] = entries + return result + + +def extract_ops(td_path): + """Extract {mnemonic: def name} for every op definition in a .td file.""" + if not td_path.is_file(): + return None + text = td_path.read_text() + return {match.group(2): match.group(1) for match in OP_DEF_PATTERN.finditer(text)} + + +def extract_types(td_path): + """Extract {mnemonic: def name} for every type definition in a .td file.""" + if not td_path.is_file(): + return None + text = td_path.read_text() + return { + match.group(2): match.group(1) + for match in TYPE_DEF_PATTERN.finditer(text) + if match.group(1).endswith("Type") + } + + +def check_ods_surface(root, errors): + """Require exact set equality between manifests and ODS definitions.""" + surface = {} + for dialect, spec in DIALECTS.items(): + manifest = load_manifest(root, dialect, errors) + ops = extract_ops(root / spec["ops_td"]) + types = extract_types(root / spec["types_td"]) + if ops is None: + errors.append(f"missing ODS file: {spec['ops_td']}") + ops = {} + if types is None: + errors.append(f"missing ODS file: {spec['types_td']}") + types = {} + if manifest is None: + continue + + op_prefix = spec["op_prefix"] + manifest_ops = { + name[len(op_prefix):] + for name in manifest["operations"] + if name.startswith(op_prefix) + } + malformed = [ + name for name in manifest["operations"] if not name.startswith(op_prefix) + ] + for name in malformed: + errors.append( + f"{spec['manifest']} operation {name!r} lacks the {op_prefix!r} prefix" + ) + missing_ops = sorted(manifest_ops - set(ops)) + extra_ops = sorted(set(ops) - manifest_ops) + for name in missing_ops: + errors.append( + f"{dialect} operation {op_prefix}{name} has no source symbol " + f"in {spec['ops_td']}" + ) + for name in extra_ops: + errors.append( + f"{dialect} ODS operation {ops[name]} ({op_prefix}{name}) is an " + f"implementation-only public alias missing from {spec['manifest']}" + ) + + manifest_types = set(manifest["types"]) + missing_types = sorted(manifest_types - set(types)) + extra_types = sorted(set(types) - manifest_types) + for name in missing_types: + errors.append( + f"{dialect} type {spec['type_prefix']}{name} has no source " + f"symbol in {spec['types_td']}" + ) + for name in extra_types: + errors.append( + f"{dialect} ODS type {types[name]} ({spec['type_prefix']}{name}) " + f"is an implementation-only public alias missing from " + f"{spec['manifest']}" + ) + + surface[dialect] = { + "manifest": manifest, + "ops": ops, + "types": types, + } + return surface + + +def check_registration_tables(root, errors): + """Require the dialect initialize() tables to register generated lists.""" + for dialect, spec in DIALECTS.items(): + path = root / spec["registration"] + if not path.is_file(): + errors.append(f"missing registration table: {spec['registration']}") + continue + text = path.read_text() + for inc, kind in ( + (spec["registration_ops_inc"], "operations"), + (spec["registration_types_inc"], "types"), + ): + if inc not in text: + errors.append( + f"{spec['registration']} does not register the generated " + f"{kind} list ({inc})" + ) + + +def lit_test_files(root): + test_dir = root / "test" + positive = [] + negative = [] + if test_dir.is_dir(): + for path in sorted(test_dir.rglob("*.mlir")): + relative = path.relative_to(root).as_posix() + if "invalid" in path.name: + negative.append((relative, path)) + else: + positive.append((relative, path)) + return positive, negative + + +def coverage_pattern(needle): + return re.compile(rf"(?", + "", + ] + for dialect, spec in DIALECTS.items(): + lines.append(f"{dialect}_contract_epoch: {spec['contract_epoch']}") + lines.append("") + for dialect, entries in coverage.items(): + operations = [entry for entry in entries if entry["kind"] == "operation"] + types = [entry for entry in entries if entry["kind"] == "type"] + lines.append(f"## {dialect} operations ({len(operations)})") + lines.append("") + lines.append("| operation | source symbol | positive coverage | negative coverage |") + lines.append("| --- | --- | --- | --- |") + for entry in operations: + lines.append(_ledger_row(entry)) + lines.append("") + lines.append(f"## {dialect} types ({len(types)})") + lines.append("") + lines.append("| type | source symbol | positive coverage | negative coverage |") + lines.append("| --- | --- | --- | --- |") + for entry in types: + lines.append(_ledger_row(entry)) + lines.append("") + return "\n".join(lines) + + +def _ledger_row(entry): + positive = "
".join(entry["positive"]) if entry["positive"] else "(none)" + negative = "
".join(entry["negative"]) if entry["negative"] else "(none)" + return ( + f"| {entry['name']} | {entry['symbol']} | {positive} | {negative} |" + ) + + +def check_ledger(root, surface, errors): + """Require the committed ledger to match the freshly generated one.""" + path = root / LEDGER_PATH + if not path.is_file(): + errors.append( + f"missing coverage ledger: {LEDGER_PATH} " + f"(run scripts/check-ir-coverage.py --write-ledger)" + ) + return + expected = render_ledger(root, surface) + if path.read_text() != expected: + errors.append( + f"{LEDGER_PATH} is stale; regenerate with " + f"scripts/check-ir-coverage.py --write-ledger" + ) + + +def run_checks(root): + errors = [] + surface = check_ods_surface(root, errors) + check_registration_tables(root, errors) + collect_coverage(root, surface, errors) + check_ledger(root, surface, errors) + return errors + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--write-ledger", + action="store_true", + help=f"regenerate {LEDGER_PATH} instead of only verifying it", + ) + args = parser.parse_args(argv) + + if args.write_ledger: + errors = [] + surface = check_ods_surface(ROOT, errors) + if errors: + for error in errors: + print(f"error: {error}", file=sys.stderr) + return 1 + path = ROOT / LEDGER_PATH + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(render_ledger(ROOT, surface)) + print(f"wrote {LEDGER_PATH}") + return 0 + + errors = run_checks(ROOT) + if errors: + for error in errors: + print(f"error: {error}", file=sys.stderr) + return 1 + print( + "IR coverage: OK (ACIR and ACSim manifests match ODS, " + "positive+negative lit coverage complete, ledger current)" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/scripts/check-ndf.py b/components/agentic-circuit/scripts/check-ndf.py new file mode 100755 index 00000000..4157ba06 --- /dev/null +++ b/components/agentic-circuit/scripts/check-ndf.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Validate the repository's restricted NDF documentation profile.""" + +from __future__ import annotations + +import argparse +import re +import shlex +import sys +from dataclasses import dataclass, field +from pathlib import Path + + +CLAUSE_RE = re.compile(r"^#{2,6}\s+.+?\s+\{#([A-Z][A-Z0-9-]*)\}\s*$") +METADATA_RE = re.compile(r"") +CROSS_REFERENCE_RE = re.compile(r"\[\[([A-Z][A-Z0-9-]*)\]\]") +HEX40_RE = re.compile(r"^[0-9a-f]{40}$") +REQUIRED_METADATA = ("kind", "level", "layer", "status") +EDGE_KEYS = ( + "refines", + "depends-on", + "conflicts-with", + "couples-with", + "verifies", + "affects", + "blocks", +) + + +@dataclass +class Clause: + identifier: str + path: Path + line: int + metadata: dict[str, str] = field(default_factory=dict) + text: list[str] = field(default_factory=list) + + +def _targets(value: str) -> list[str]: + return [item for item in re.split(r"[,;]", value) if item] + + +def _load(root: Path) -> tuple[list[Clause], list[str]]: + clauses: list[Clause] = [] + errors: list[str] = [] + for document in sorted(root.rglob("*.md")): + current: Clause | None = None + for line_number, line in enumerate( + document.read_text(encoding="utf-8").splitlines(), start=1 + ): + heading = CLAUSE_RE.match(line) + if heading: + current = Clause(heading.group(1), document, line_number) + clauses.append(current) + continue + if current is None: + continue + current.text.append(line) + for raw in METADATA_RE.findall(line): + try: + tokens = shlex.split(raw) + except ValueError as error: + errors.append(f"{document}:{line_number}: {error}") + continue + for token in tokens: + if "=" not in token: + errors.append( + f"{document}:{line_number}: invalid metadata {token!r}" + ) + continue + key, value = token.split("=", 1) + current.metadata[key] = value + return clauses, errors + + +def validate(root: Path) -> tuple[list[Clause], list[str]]: + clauses, errors = _load(root) + by_identifier: dict[str, Clause] = {} + for clause in clauses: + if clause.identifier in by_identifier: + errors.append( + f"{clause.path}:{clause.line}: duplicate clause {clause.identifier}" + ) + by_identifier[clause.identifier] = clause + for key in REQUIRED_METADATA: + if key not in clause.metadata: + errors.append( + f"{clause.path}:{clause.line}: {clause.identifier} missing {key}" + ) + + for clause in clauses: + related = { + target + for key in EDGE_KEYS + for target in _targets(clause.metadata.get(key, "")) + } + related.update( + reference + for line in clause.text + for reference in CROSS_REFERENCE_RE.findall(line) + ) + for target in sorted(related): + if target not in by_identifier: + errors.append( + f"{clause.path}:{clause.line}: dangling reference {target}" + ) + if clause.identifier.startswith("REF-"): + if clause.metadata.get("origin-kind") != "git": + errors.append( + f"{clause.path}:{clause.line}: {clause.identifier} requires git origin" + ) + if not HEX40_RE.fullmatch(clause.metadata.get("revision", "")): + errors.append( + f"{clause.path}:{clause.line}: {clause.identifier} has invalid revision" + ) + if clause.metadata.get("origin-status") not in { + "verbatim", + "paraphrase", + "interpretation", + }: + errors.append( + f"{clause.path}:{clause.line}: {clause.identifier} has invalid origin status" + ) + + verified = { + target + for clause in clauses + if clause.metadata.get("kind") == "verif" + for target in _targets(clause.metadata.get("verifies", "")) + } + for clause in clauses: + if ( + clause.metadata.get("kind") == "req" + and clause.metadata.get("level") == "must" + and clause.metadata.get("layer") == "L1" + and clause.identifier not in verified + ): + errors.append( + f"{clause.path}:{clause.line}: {clause.identifier} lacks verification" + ) + return clauses, errors + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("root", nargs="?", type=Path, default=Path("docs/spec")) + arguments = parser.parse_args() + if not (arguments.root / "ndf.yaml").is_file(): + print(f"{arguments.root}: missing ndf.yaml", file=sys.stderr) + return 1 + clauses, errors = validate(arguments.root) + if errors: + print("\n".join(errors), file=sys.stderr) + return 1 + print(f"NDF profile: OK ({len(clauses)} clauses)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/scripts/check-release-layout.py b/components/agentic-circuit/scripts/check-release-layout.py new file mode 100755 index 00000000..bc70415d --- /dev/null +++ b/components/agentic-circuit/scripts/check-release-layout.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Reject product-version and implementation-phase tokens in tracked paths.""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + + +FORBIDDEN = re.compile( + r"(?:^|[-_./])(?:v0?\d+(?:[._]\d+)*|phase[-_]?\d+[a-z]?)" + r"(?:[-_./]|$)", + re.IGNORECASE, +) +REQUIRED_ROOTS = ( + Path("docs/spec"), + Path("examples/pipelines"), + Path("examples/memory"), + Path("examples/blocks"), + Path("examples/architecture"), + Path("examples/workspaces"), + Path("references"), + Path("tests/goldens"), +) + + +def tracked_paths(root: Path) -> list[str]: + completed = subprocess.run( + ("git", "ls-files"), + cwd=root, + text=True, + capture_output=True, + check=True, + ) + return completed.stdout.splitlines() + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + errors = [ + f"forbidden release/phase token in tracked path: {file_path}" + for file_path in tracked_paths(root) + if FORBIDDEN.search(file_path) + ] + errors.extend( + f"required semantic root is missing: {required}" + for required in REQUIRED_ROOTS + if not (root / required).is_dir() + ) + if errors: + print("\n".join(errors), file=sys.stderr) + return 1 + print("release layout: OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/scripts/ci-build-llvm.sh b/components/agentic-circuit/scripts/ci-build-llvm.sh new file mode 100755 index 00000000..cc909a80 --- /dev/null +++ b/components/agentic-circuit/scripts/ci-build-llvm.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${LLVM_RELEASE:?LLVM_RELEASE must be set}" + +cmake -S ".cache/llvm-project-${LLVM_RELEASE}.src/llvm" \ + -B .cache/llvm-build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_ENABLE_PROJECTS=mlir \ + -DLLVM_TARGETS_TO_BUILD=Native \ + -DLLVM_INCLUDE_TESTS=OFF \ + -DLLVM_INCLUDE_BENCHMARKS=OFF \ + -DLLVM_INCLUDE_EXAMPLES=OFF +# mlir-opt drives the MLIR package verification; FileCheck, not, split-file, +# and count are the lit substitutions used by test/lit.cfg.py, so they must +# exist in the cached build outputs as well. +cmake --build .cache/llvm-build --target mlir-opt FileCheck not split-file count diff --git a/components/agentic-circuit/scripts/generate-stdlib-catalog.py b/components/agentic-circuit/scripts/generate-stdlib-catalog.py new file mode 100755 index 00000000..6ee09c98 --- /dev/null +++ b/components/agentic-circuit/scripts/generate-stdlib-catalog.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +"""Generate the frozen ac v0.1 component catalog deterministically.""" + +import argparse +import hashlib +import json +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "schemas" / "stdlib" + +AVAILABLE = { + "TraceSource": ("workload", "source", "gfsim/trace.h"), + "Queue": ("transport", "duplex", "gfsim/queue.h"), + "Scheduler": ("control", "duplex", "gfsim/components.h"), + "Compute": ("compute", "duplex", "gfsim/components.h"), + "Link": ("interconnect", "duplex", "gfsim/components.h"), + "Memory": ("storage", "request_response", "gfsim/components.h"), + "Sink": ("workload", "sink", "gfsim/components.h"), + "ready_valid": ("protocol", "duplex", "gfsim/components.h"), + "request_response": ( + "protocol", + "request_response", + "gfsim/components.h", + ), +} + +UNAVAILABLE = { + "Arbiter": ("control", "duplex"), + "Dispatcher": ("control", "duplex"), + "Scoreboard": ("control", "request_response"), + "DependencyTracker": ("control", "request_response"), + "Bus": ("interconnect", "duplex"), + "Crossbar": ("interconnect", "duplex"), + "Router": ("interconnect", "duplex"), + "Switch": ("interconnect", "duplex"), + "Dma": ("transport", "request_response"), + "Packetizer": ("transport", "duplex"), + "Reassembler": ("transport", "duplex"), + "LoadStore": ("transport", "request_response"), + "RegisterFile": ("storage", "request_response"), + "Scratchpad": ("storage", "request_response"), + "Cache": ("storage", "request_response"), + "Tlb": ("storage", "request_response"), + "MemoryController": ("storage", "request_response"), + "Fork": ("synchronization", "fanout"), + "Join": ("synchronization", "fanin"), + "Broadcast": ("synchronization", "fanout"), + "Barrier": ("synchronization", "fanin"), + "ProtocolAdapter": ("adaptation", "duplex"), + "WidthAdapter": ("adaptation", "duplex"), + "TimeDomainBridge": ("adaptation", "duplex"), + "AddressTranslator": ("adaptation", "request_response"), + "MemoryManagement": ("adaptation", "request_response"), + "TrafficSource": ("workload", "source"), +} + + +def endpoint(name, direction, role, cardinality=1): + return { + "name": name, + "binding_kind": "endpoint", + "acir_type": ( + "!ac.endpoint,ac.ready_valid>>," + role + ">" + ), + "direction": direction, + "role": role, + "cardinality": cardinality, + "linearity": "linear", + "ownership": "borrowed", + "delegation": "forbidden", + "result_mapping": None, + } + + +def bindings_for(shape): + if shape == "source": + return [endpoint("output", "out", "producer")] + if shape == "sink": + return [endpoint("input", "in", "consumer")] + if shape == "request_response": + return [ + endpoint("request", "in", "responder"), + endpoint("response", "out", "responder"), + ] + if shape == "fanout": + return [ + endpoint("input", "in", "consumer"), + endpoint("outputs", "out", "producer", "fanout"), + ] + if shape == "fanin": + return [ + endpoint("inputs", "in", "consumer", "fanin"), + endpoint("output", "out", "producer"), + ] + return [ + endpoint("input", "in", "consumer"), + endpoint("output", "out", "producer"), + ] + + +def static_parameters_for(name, shape): + type_names = { + "TraceSource": ["Transaction", "Decoder"], + "Queue": ["T"], + "Scheduler": ["T"], + "Compute": ["Input", "Output", "FunctionalPolicy"], + "Link": ["T"], + "Memory": ["T"], + "Sink": ["T"], + "ready_valid": ["T"], + "request_response": ["Req", "Resp"], + }.get(name, ["Transaction"]) + parameters = [ + { + "name": type_name, + "acir_type": f"!ac.static_type", + "required": True, + "default": None, + "constraint": ( + "ac.FunctionalPolicy" + if type_name == "FunctionalPolicy" + else ( + "gfsim::TraceDecoder" + if name == "TraceSource" and type_name == "Decoder" + else "ac.Packet" + ) + ), + "cpp_mapping": "template_argument", + } + for type_name in type_names + ] + if name in { + "Queue", + "Scheduler", + "Memory", + "request_response", + "Scoreboard", + "DependencyTracker", + "Dma", + "LoadStore", + "RegisterFile", + "Scratchpad", + "Cache", + "Tlb", + "MemoryController", + }: + parameters.append( + { + "name": "capacity", + "acir_type": "index", + "required": True, + "default": None, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant", + } + ) + if name == "Queue": + parameters.append( + { + "name": "byteCapacity", + "acir_type": "index", + "required": False, + "default": "unbounded", + "constraint": "value >= 1 or value == unbounded", + "cpp_mapping": "constructor_constant", + } + ) + if shape in {"fanout", "fanin"}: + parameters.append( + { + "name": "ports", + "acir_type": "index", + "required": True, + "default": None, + "constraint": "value >= 1", + "cpp_mapping": "constructor_constant", + } + ) + return parameters + + +def resources_for(name, family): + if name not in { + "Queue", + "Scheduler", + "Memory", + "Scoreboard", + "DependencyTracker", + "Dma", + "LoadStore", + "RegisterFile", + "Scratchpad", + "Cache", + "Tlb", + "MemoryController", + }: + return [] + resource_class = "queue" if name in {"Queue", "Scheduler"} else family + return [ + { + "name": "capacity", + "class": resource_class, + "capacity": "capacity", + "lanes": 1, + "issue_width": 1, + "initiation_interval": 1, + "latency_policy": "ac.FixedLatency<0>", + "reservation_owner": "self", + "transaction_classes": ["ac.Transaction"], + "statistics": ["accepted_transactions", "queue_occupancy"], + } + ] + + +def protocol_contracts_for(bindings): + contracts = [] + for binding in bindings: + contracts.append( + { + "protocol": "ac.ready_valid", + "role": binding["role"], + "ordering": "fifo", + "delivery": "exactly_once_on_transfer", + "correlation": ( + "required" if binding["name"] in {"request", "response"} else "none" + ), + "completion": "transfer", + "failure": "protocol_violation", + } + ) + return contracts + + +def address_behavior(family): + address_aware = family in {"storage", "adaptation", "transport"} + return { + "consumes": ["ac.system"] if address_aware else [], + "produces": ["ac.system"] if address_aware else [], + "ranges": [], + "translation": "static" if address_aware else "none", + "routing": "address_range" if address_aware else "none", + "unmapped": "diagnostic" if address_aware else "not_applicable", + } + + +def observations_for(name, has_resources): + available = { + "TraceSource": (["accepted_transactions"], ["trace_position"]), + "Queue": ( + ["accepted_transactions", "completed_transactions"], + ["queue_occupancy", "queue_occupancy_peak"], + ), + "Scheduler": ( + ["accepted_transactions", "completed_transactions"], + ["queue_occupancy", "queue_occupancy_peak"], + ), + "Compute": (["completed_transactions"], []), + "Link": (["completed_transactions"], []), + "Memory": (["accepted_transactions"], []), + "Sink": (["accepted_transactions"], []), + "ready_valid": (["completed_transactions"], []), + "request_response": ( + ["completed_transactions"], + ["active_correlations"], + ), + } + if name in available: + counters, gauges = available[name] + else: + counters = ["accepted_transactions", "stalled_cycles"] + gauges = ["queue_occupancy"] if has_resources else [] + return { + "probes": [], + "counters": counters, + "gauges": gauges, + "histograms": [], + } + + +def component_record(name, family, shape, header): + bindings = bindings_for(shape) + resources = resources_for(name, family) + observation = observations_for(name, bool(resources)) + for resource in resources: + resource["statistics"] = observation["counters"] + observation["gauges"] + sources = [f"{binding['name']}.transfer" for binding in bindings] + inputs = [binding["name"] for binding in bindings if binding["direction"] == "in"] + symbol_names = { + "ready_valid": "ReadyValid", + "request_response": "RequestResponse", + } + record = { + "schema_kind": "agentic-circuit-component", + "schema_version": "0.1", + "contract_epoch": "0.3", + "canonical_name": f"ac.{name}", + "family": family, + "provider_namespace": "ac", + "stability": "provisional", + "cpp_binding": { + "header": header, + "symbol": f"gfsim::{symbol_names.get(name, name)}", + "language": "c++20", + "concept": "gfsim::Component", + "toolchain_target": "ac-gfsim-cxx20-v0.1", + "functional_policy": "optional" if name == "Compute" else "none", + }, + "static_parameters": static_parameters_for(name, shape), + "bindings": bindings, + "results": [], + "resources": resources, + "address_behavior": address_behavior(family), + "protocol_contracts": protocol_contracts_for(bindings), + "effect": { + "kind": "stateful", + "requirements": ["input offers remain immutable until committed transfer"] + if inputs + else ["trace or generated transactions are schema-valid"], + "guarantees": ["accepted transactions commit exactly once in stable order"], + "observable_effects": observation["counters"] + observation["gauges"], + "failure_behavior": "terminate_with_diagnostic", + }, + "activation": { + "sources": sources, + "predicate": "an input is transferable or an output can accept", + "wakeup_contract": sources, + "quiescence": ( + "all inputs are closed and all committed internal state is empty" + if inputs + else "source is exhausted and no output offer is pending" + ), + }, + "observation": observation, + } + canonical = json.dumps( + record, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ).encode() + record["schema_fingerprint"] = "sha256:" + hashlib.sha256(canonical).hexdigest() + return record + + +def rendered_files(): + records = {} + catalog_entries = [] + definitions = { + **AVAILABLE, + **{ + name: (family, shape, "gfsim/components.h") + for name, (family, shape) in UNAVAILABLE.items() + }, + } + for name in sorted(definitions): + family, shape, header = definitions[name] + record = component_record(name, family, shape, header) + path = OUTPUT / f"{name}.json" + records[path] = json.dumps(record, indent=2, ensure_ascii=False) + "\n" + catalog_entries.append( + { + "canonical_name": record["canonical_name"], + "availability": ( + "available" if name in AVAILABLE else "declared_unavailable" + ), + "schema_path": f"schemas/stdlib/{name}.json", + "schema_fingerprint": record["schema_fingerprint"], + } + ) + catalog_entries.sort(key=lambda entry: entry["canonical_name"]) + catalog = { + "catalog": "ac", + "version": "0.1", + "contract_epoch": "0.3", + "entries": catalog_entries, + } + records[OUTPUT / "catalog.json"] = ( + json.dumps(catalog, indent=2, ensure_ascii=False) + "\n" + ) + return records + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + expected = rendered_files() + if args.check: + failures = [] + for path, content in expected.items(): + if not path.is_file() or path.read_text() != content: + failures.append(path.relative_to(ROOT)) + extras = set(OUTPUT.glob("*.json")) - set(expected) + failures.extend(sorted(path.relative_to(ROOT) for path in extras)) + if failures: + for path in failures: + print(f"error: generated catalog is stale: {path}", file=sys.stderr) + return 1 + print("standard-library catalog generation: OK (36 component schemas)") + return 0 + + OUTPUT.mkdir(parents=True, exist_ok=True) + for path, content in expected.items(): + path.write_text(content) + print("generated 36 standard-library component schemas and catalog") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/src/agentic_circuit/__init__.py b/components/agentic-circuit/src/agentic_circuit/__init__.py new file mode 100644 index 00000000..5ce0f859 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/__init__.py @@ -0,0 +1,281 @@ +"""Agentic Circuit's portable Python construction surface.""" + +from __future__ import annotations + +from pkgutil import extend_path +from typing import Never + + +__path__ = extend_path(__path__, __name__) + +from ._definitions import ( + extern_module, + generated_module, + interface, + module, + packet, + process, + protocol, + struct, + system, + transaction, +) +from ._jit import config, jit +from ._resources import address_map, address_space, queue +from ._types import ( + Endpoint, + Flow, + ResourceRef, + Static, + const, + s8, + s16, + s32, + s64, + u1, + u2, + u4, + u8, + u16, + u32, + u64, +) + + +__all__ = ( + "system", + "module", + "extern_module", + "generated_module", + "struct", + "packet", + "transaction", + "protocol", + "interface", + "process", + "scope", + "array", + "map", + "set", + "instances", + "view", + "queue", + "ResourceRef", + "address_space", + "address_map", + "Static", + "Flow", + "Endpoint", + "source", + "popcount", + "memory", + "sink", + "observe", + "expect", + "atomic", + "compute", + "pipeline", + "config", + "const", + "jit", + "route", + "merge", + "schedule", + "engine", + "reorder", + "round_robin", + "priority", + "fork", + "barrier", + "table", + "u1", + "u2", + "u4", + "u8", + "u16", + "u32", + "u64", + "s8", + "s16", + "s32", + "s64", +) + + +def _not_implemented(primitive: str) -> Never: + raise NotImplementedError( + f"{primitive} is part of the public surface but is not implemented yet" + ) + + +def scope(name: str) -> Never: + return _not_implemented("scope") + + +def array(*values: object) -> Never: + return _not_implemented("array") + + +def map(*values: object) -> Never: + return _not_implemented("map") + + +def set(*values: object) -> Never: + return _not_implemented("set") + + +def instances(*values: object) -> Never: + return _not_implemented("instances") + + +def view(value: object, *selectors: object) -> Never: + return _not_implemented("view") + + +def source( + payload: object, *, depth: int = 1, latency: int = 1, rate: int = 1 +) -> Never: + return _not_implemented("source") + + +def popcount(value: object) -> Never: + return _not_implemented("popcount") + + +def memory( + data_type: object, *, entries: int = 16, init: int = 0, latency: int = 1 +) -> Never: + return _not_implemented("memory") + + +def sink(value: object) -> Never: + return _not_implemented("sink") + + +def observe(value: object) -> Never: + return _not_implemented("observe") + + +def expect(value: object, *, predicate: object, message: str) -> Never: + return _not_implemented("expect") + + +def atomic() -> Never: + return _not_implemented("atomic") + + +def compute( + value: object, + function: object, + *, + depth: int = 1, + latency: int = 1, + rate: int = 1, +) -> Never: + return _not_implemented("compute") + + +def pipeline( + value: object, + *, + stages: int = 1, + depth: int = 1, + rate: int = 1, +) -> Never: + return _not_implemented("pipeline") + + +round_robin = "round_robin" +priority = "priority" + + +def route( + value: object, + *, + by: object, + outputs: int, + depth: int = 1, + latency: int = 1, +) -> Never: + return _not_implemented("route") + + +def merge( + *values: object, + policy: object = priority, + depth: int = 1, + latency: int = 1, +) -> Never: + return _not_implemented("merge") + + +def schedule( + value: object, + *, + by: object, + waits_for: object, + resource: object, + cost: object, + entries: int = 16, + resources: int = 1, + no_dependency: int = 0, + depth: int = 1, + latency: int = 1, +) -> Never: + return _not_implemented("schedule") + + +def engine( + value: object, + *, + cost: object, + lanes: int = 1, + depth: int = 1, + latency: int = 1, +) -> Never: + return _not_implemented("engine") + + +def reorder( + value: object, + *, + by: object, + entries: int = 16, + start: int = 0, + depth: int = 1, + latency: int = 1, +) -> Never: + return _not_implemented("reorder") + + +def fork( + value: object, + *, + outputs: int, + depth: int = 1, + latency: int = 1, +) -> Never: + return _not_implemented("fork") + + +def barrier( + *values: object, + depth: int = 1, + latency: int = 1, +) -> Never: + return _not_implemented("barrier") + + +def table( + value: object, + *, + address: object, + write: object, + data: object, + result: object, + entries: int = 16, + init: int = 0, + depth: int = 1, + latency: int = 1, +) -> Never: + return _not_implemented("table") diff --git a/components/agentic-circuit/src/agentic_circuit/_acpy.py b/components/agentic-circuit/src/agentic_circuit/_acpy.py new file mode 100644 index 00000000..b26b0043 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_acpy.py @@ -0,0 +1,264 @@ +"""Closed, immutable ACPy semantic document records.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Literal, TypeAlias + +from ._canonical_json import JsonValue, canonical_json_bytes, utf16_sort_key +from ._diagnostics import Diagnostic, SourceSpan +from ._static_eval import FrozenMap, StaticValue + + +EntityKind: TypeAlias = Literal[ + "system", + "module", + "scope", + "arg", + "call", + "result", + "get_result", + "bind", + "static_if", + "static_for", + "collection", + "get_static", + "return", + "capture", + "escape", + "process", +] +_ENTITY_KINDS = { + "system", + "module", + "scope", + "arg", + "call", + "result", + "get_result", + "bind", + "static_if", + "static_for", + "collection", + "get_static", + "return", + "capture", + "escape", + "process", +} +_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +_PROPERTY_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +def _span_json(source: SourceSpan) -> dict[str, JsonValue]: + return { + "file": source.file, + "start_line": source.start_line, + "start_column": source.start_column, + "end_line": source.end_line, + "end_column": source.end_column, + } + + +def _static_json(value: StaticValue) -> JsonValue: + if value is None or type(value) in (bool, int, float, str): + return value + if isinstance(value, tuple): + return [_static_json(item) for item in value] + if isinstance(value, FrozenMap): + return {key: _static_json(item) for key, item in value.entries} + raise ValueError(f"unsupported ACPy property value {type(value).__name__}") + + +@dataclass(frozen=True, slots=True) +class SourceFile: + path: str + sha256: str + + def to_json(self) -> dict[str, JsonValue]: + return {"path": self.path, "sha256": self.sha256} + + +@dataclass(frozen=True, slots=True) +class SchemaRef: + identity: str + fingerprint: str + + def to_json(self) -> dict[str, JsonValue]: + return {"identity": self.identity, "fingerprint": self.fingerprint} + + +@dataclass(frozen=True, slots=True) +class Property: + name: str + value: StaticValue + + def to_json(self) -> dict[str, JsonValue]: + return {"name": self.name, "value": _static_json(self.value)} + + +@dataclass(frozen=True, slots=True) +class Entity: + id: str + kind: EntityKind + source: SourceSpan | None + parent: str | None + scope: str + type: str | None + definition: str | None + uses: tuple[str, ...] + schema_ref: SchemaRef | None + properties: tuple[Property, ...] + + def to_json(self) -> dict[str, JsonValue]: + return { + "id": self.id, + "kind": self.kind, + "source": _span_json(self.source) if self.source is not None else None, + "parent": self.parent, + "scope": self.scope, + "type": self.type, + "definition": self.definition, + "uses": list(self.uses), + "schema_ref": ( + self.schema_ref.to_json() if self.schema_ref is not None else None + ), + "properties": [property_.to_json() for property_ in self.properties], + } + + +@dataclass(frozen=True, slots=True) +class AcpyDocument: + entry: str + sources: tuple[SourceFile, ...] + entities: tuple[Entity, ...] + schema: str = "agentic-circuit-acpy" + version: str = "0.1" + contract_epoch: str = "0.3" + + def to_json(self) -> dict[str, JsonValue]: + return { + "schema": self.schema, + "version": self.version, + "contract_epoch": self.contract_epoch, + "entry": self.entry, + "sources": [source.to_json() for source in self.sources], + "entities": [entity.to_json() for entity in self.entities], + } + + def _diagnostic(self, message: str, source: SourceSpan | None = None) -> Diagnostic: + return Diagnostic( + stage="acpy-verify", + code="ACPY-TYPE-DOCUMENT", + severity="error", + message=message, + source=source, + ) + + def verify(self) -> tuple[Diagnostic, ...]: + errors: list[Diagnostic] = [] + if (self.schema, self.version, self.contract_epoch) != ( + "agentic-circuit-acpy", + "0.1", + "0.3", + ): + errors.append(self._diagnostic("ACPy schema identity must be epoch 0.3")) + + expected_ids = [f"e{index}" for index in range(len(self.entities))] + actual_ids = [entity.id for entity in self.entities] + if actual_ids != expected_ids: + errors.append(self._diagnostic("ACPy entity IDs must be dense and ordered")) + entity_ids = set(actual_ids) + if self.entry not in entity_ids: + errors.append(self._diagnostic("ACPy entry does not resolve")) + + source_paths = [source.path for source in self.sources] + if source_paths != sorted(source_paths, key=utf16_sort_key) or len( + source_paths + ) != len(set(source_paths)): + errors.append(self._diagnostic("ACPy sources must be unique and ordered")) + for source in self.sources: + if not source.path or not _DIGEST.fullmatch(source.sha256): + errors.append(self._diagnostic("ACPy source record is invalid")) + + for entity in self.entities: + if entity.kind not in _ENTITY_KINDS: + errors.append(self._diagnostic("ACPy entity kind is invalid", entity.source)) + if not entity.scope: + errors.append(self._diagnostic("ACPy entity scope is empty", entity.source)) + if entity.source is not None and entity.source.file not in source_paths: + errors.append( + self._diagnostic("ACPy entity source does not resolve", entity.source) + ) + references = [entity.parent, entity.definition, *entity.uses] + if any(reference is not None and reference not in entity_ids for reference in references): + errors.append( + self._diagnostic("ACPy entity reference does not resolve", entity.source) + ) + if len(entity.uses) != len(set(entity.uses)): + errors.append(self._diagnostic("ACPy uses must be unique", entity.source)) + property_names = [property_.name for property_ in entity.properties] + if property_names != sorted(property_names, key=utf16_sort_key) or len( + property_names + ) != len(set(property_names)): + errors.append( + self._diagnostic("ACPy properties must be unique and ordered", entity.source) + ) + if any(not _PROPERTY_NAME.fullmatch(name) for name in property_names): + errors.append(self._diagnostic("ACPy property name is invalid", entity.source)) + if entity.schema_ref is not None and ( + not entity.schema_ref.identity + or not _DIGEST.fullmatch(entity.schema_ref.fingerprint) + ): + errors.append(self._diagnostic("ACPy schema reference is invalid", entity.source)) + + try: + canonical_json_bytes(self.to_json()) + except ValueError as error: + errors.append(self._diagnostic(f"ACPy contains non-I-JSON data: {error}")) + return tuple(errors) + + def canonical_bytes(self) -> bytes: + errors = self.verify() + if errors: + raise ValueError("; ".join(error.message for error in errors)) + return canonical_json_bytes(self.to_json()) + + +class EntityAllocator: + __slots__ = ("_entities",) + + def __init__(self) -> None: + self._entities: list[Entity] = [] + + def allocate( + self, + *, + kind: EntityKind, + scope: str, + source: SourceSpan | None = None, + parent: str | None = None, + type: str | None = None, + definition: str | None = None, + uses: tuple[str, ...] = (), + schema_ref: SchemaRef | None = None, + properties: tuple[Property, ...] = (), + ) -> Entity: + entity = Entity( + id=f"e{len(self._entities)}", + kind=kind, + source=source, + parent=parent, + scope=scope, + type=type, + definition=definition, + uses=uses, + schema_ref=schema_ref, + properties=properties, + ) + self._entities.append(entity) + return entity + + def freeze(self) -> tuple[Entity, ...]: + return tuple(self._entities) diff --git a/components/agentic-circuit/src/agentic_circuit/_canonical_json.py b/components/agentic-circuit/src/agentic_circuit/_canonical_json.py new file mode 100644 index 00000000..c9ef1b77 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_canonical_json.py @@ -0,0 +1,136 @@ +"""Shared JSON value types and content digest spelling.""" + +from __future__ import annotations + +import hashlib +import json +import math +from typing import TypeAlias + + +JsonScalar: TypeAlias = None | bool | int | float | str +JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"] + + +def sha256_bytes(data: bytes) -> str: + """Return a SHA-256 digest using the repository's public spelling.""" + + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def _validate_string(value: str) -> None: + try: + value.encode("utf-8", errors="strict") + except UnicodeEncodeError as error: + raise ValueError("JSON strings must contain Unicode scalar values") from error + + +def validate_ijson_value(value: JsonValue) -> None: + if value is None or type(value) is bool or type(value) is str: + if isinstance(value, str): + _validate_string(value) + return + if type(value) is int: + if not -((1 << 53) - 1) <= value <= (1 << 53) - 1: + raise ValueError("JSON integer is outside the portable I-JSON range") + return + if type(value) is float: + if not math.isfinite(value): + raise ValueError("JSON number must be finite binary64") + if value == 0.0 and math.copysign(1.0, value) < 0: + raise ValueError("negative zero cannot be canonicalized") + return + if type(value) is list: + for item in value: + validate_ijson_value(item) + return + if type(value) is dict: + for key, item in value.items(): + if type(key) is not str: + raise ValueError("JSON object names must be strings") + _validate_string(key) + validate_ijson_value(item) + return + raise ValueError(f"unsupported JSON value {type(value).__name__}") + + +def utf16_sort_key(value: str) -> bytes: + _validate_string(value) + return value.encode("utf-16-be") + + +def _encode_string(value: str) -> str: + _validate_string(value) + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + + +def _encode_float(value: float) -> str: + if not math.isfinite(value): + raise ValueError("JSON number must be finite binary64") + if value == 0.0: + if math.copysign(1.0, value) < 0: + raise ValueError("negative zero cannot be canonicalized") + return "0" + + shortest = repr(value).lower() + negative = shortest.startswith("-") + magnitude = shortest[1:] if negative else shortest + if "e" in magnitude: + mantissa, exponent_text = magnitude.split("e", 1) + explicit_exponent = int(exponent_text) + else: + mantissa = magnitude + explicit_exponent = 0 + decimal = mantissa.find(".") + if decimal < 0: + decimal = len(mantissa) + digits = mantissa.replace(".", "") + first_nonzero = next( + (index for index, character in enumerate(digits) if character != "0"), + None, + ) + if first_nonzero is None: + return "0" + scientific_exponent = explicit_exponent + decimal - first_nonzero - 1 + digits = digits[first_nonzero:].rstrip("0") or "0" + + prefix = "-" if negative else "" + if 0 <= scientific_exponent < 21: + integer_digits = scientific_exponent + 1 + if len(digits) <= integer_digits: + return prefix + digits + "0" * (integer_digits - len(digits)) + return prefix + digits[:integer_digits] + "." + digits[integer_digits:] + if -6 <= scientific_exponent < 0: + return prefix + "0." + "0" * (-scientific_exponent - 1) + digits + mantissa_text = digits[0] + if len(digits) > 1: + mantissa_text += "." + digits[1:] + exponent_sign = "+" if scientific_exponent >= 0 else "" + return prefix + mantissa_text + "e" + exponent_sign + str(scientific_exponent) + + +def _encode_value(value: JsonValue) -> str: + if value is None: + return "null" + if type(value) is bool: + return "true" if value else "false" + if type(value) is str: + return _encode_string(value) + if type(value) is int: + return str(value) + if type(value) is float: + return _encode_float(value) + if type(value) is list: + return "[" + ",".join(_encode_value(item) for item in value) + "]" + if type(value) is dict: + properties = sorted(value.items(), key=lambda item: utf16_sort_key(item[0])) + return "{" + ",".join( + _encode_string(key) + ":" + _encode_value(item) + for key, item in properties + ) + "}" + raise ValueError(f"unsupported JSON value {type(value).__name__}") + + +def canonical_json_bytes(value: JsonValue) -> bytes: + validate_ijson_value(value) + return _encode_value(value).encode("utf-8") diff --git a/components/agentic-circuit/src/agentic_circuit/_capabilities.py b/components/agentic-circuit/src/agentic_circuit/_capabilities.py new file mode 100644 index 00000000..1422c9ff --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_capabilities.py @@ -0,0 +1,234 @@ +"""Packaged schema discovery and exact capability assembly.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Mapping + +from ._canonical_json import JsonValue, canonical_json_bytes, sha256_bytes +from ._native_api import NativeCapabilities, capabilities as native_capabilities +from ._package_data import resource_directory + + +EXACT_CONTRACT_IDENTITIES: dict[str, str] = { + "acpy": "acpy@0.1", + "acir": "acir@0.1", + "acsim": "acsim@0.1", + "cli": "agentic-circuit-cli@0.1", + "component_schema": "agentic-circuit-component@0.1", + "opcode_catalog": "agentic-circuit-opcode-catalog@0.2", + "block_spec": "agentic-circuit-block-spec@0.3", + "cxx_source_contract": "gfsim-cxx20@0.1", + "pto_trace": "pto-trace@0.1", + "diagnostic": "agentic-circuit-diagnostic@0.1", + "build_manifest": "agentic-circuit-build-manifest@0.1", + "run_manifest": "agentic-circuit-run-manifest@0.1", + "run_result": "agentic-circuit-run-result@0.1", +} + + +def schema_root() -> Path: + return resource_directory("schemas") + + +def diagnostics_catalog_path() -> Path: + return resource_directory("resources") / "diagnostics.json" + + +def load_json(path: Path) -> dict[str, JsonValue]: + value = json.loads(path.read_text(encoding="utf-8")) + if type(value) is not dict: + raise ValueError(f"packaged resource is not an object: {path.name}") + return value + + +def standard_library_catalog() -> dict[str, JsonValue]: + return load_json(schema_root() / "stdlib" / "catalog.json") + + +def opcode_catalog() -> dict[str, JsonValue]: + return load_json(schema_root() / "opcodes.json") + + +def block_spec() -> dict[str, JsonValue]: + return load_json(schema_root() / "blocks.json") + + +def diagnostic_catalog() -> dict[str, JsonValue]: + return load_json(diagnostics_catalog_path()) + + +@dataclass(frozen=True, slots=True) +class CapabilityDocument: + contract_identities: Mapping[str, str] + items: tuple[Mapping[str, JsonValue], ...] + compiler_build_id: str + runtime_build_id: str + + def to_json(self) -> dict[str, JsonValue]: + return { + "schema": "agentic-circuit-capabilities", + "version": "0.1", + "contract_epoch": "0.3", + "contract_identities": dict(self.contract_identities), + "items": [dict(item) for item in self.items], + "compiler_build_id": self.compiler_build_id, + "runtime_build_id": self.runtime_build_id, + } + + +def _synthetic_fingerprint(kind: str, name: str) -> str: + return sha256_bytes(f"{kind}:{name}@0.1".encode("utf-8")) + + +def _base_items( + catalog: dict[str, JsonValue], + opcodes: dict[str, JsonValue], + blocks: dict[str, JsonValue], +) -> list[dict[str, JsonValue]]: + entries = catalog.get("entries") + if type(entries) is not list: + raise ValueError("standard-library catalog entries are invalid") + items: list[dict[str, JsonValue]] = [] + for raw in entries: + if type(raw) is not dict: + raise ValueError("standard-library catalog entry is invalid") + name = raw.get("canonical_name") + availability = raw.get("availability") + fingerprint = raw.get("schema_fingerprint") + if not all(type(value) is str for value in (name, availability, fingerprint)): + raise ValueError("standard-library catalog entry fields are invalid") + schema_path = str(raw["schema_path"]).removeprefix("schemas/") + schema = load_json(schema_root() / schema_path) + kind = "protocol" if schema.get("family") == "protocol" else "component" + items.append( + { + "kind": kind, + "name": name, + "availability": availability, + "schema_fingerprint": fingerprint, + "implementation_fingerprint": None, + } + ) + catalog_fingerprint = sha256_bytes(canonical_json_bytes(catalog)) + items.append( + { + "kind": "provider", + "name": "ac", + "availability": "available", + "schema_fingerprint": catalog_fingerprint, + "implementation_fingerprint": None, + } + ) + opcode_entries = opcodes.get("entries") + if type(opcode_entries) is not list: + raise ValueError("official opcode catalog entries are invalid") + for entry in opcode_entries: + if type(entry) is not dict or type(entry.get("operation")) is not str: + raise ValueError("official opcode catalog entry is invalid") + items.append( + { + "kind": "opcode", + "name": entry["operation"], + "availability": "available", + "schema_fingerprint": sha256_bytes(canonical_json_bytes(entry)), + "implementation_fingerprint": None, + } + ) + block_entries = blocks.get("blocks") + if type(block_entries) is not list: + raise ValueError("high-level BlockSpec entries are invalid") + for entry in block_entries: + if type(entry) is not dict or type(entry.get("operation")) is not str: + raise ValueError("high-level BlockSpec entry is invalid") + items.append( + { + "kind": "block", + "name": entry["operation"], + "availability": "available", + "schema_fingerprint": sha256_bytes(canonical_json_bytes(entry)), + "implementation_fingerprint": None, + } + ) + for profile in ("custom", "fast", "validated"): + items.append( + { + "kind": "policy", + "name": profile, + "availability": "available", + "schema_fingerprint": _synthetic_fingerprint("policy", profile), + "implementation_fingerprint": None, + } + ) + items.append( + { + "kind": "interface", + "name": "ac.Stream", + "availability": "available", + "schema_fingerprint": _synthetic_fingerprint("interface", "ac.Stream"), + "implementation_fingerprint": None, + } + ) + for output_format in ("dot", "json", "jsonl", "text"): + items.append( + { + "kind": "output_format", + "name": output_format, + "availability": "available", + "schema_fingerprint": _synthetic_fingerprint( + "output_format", output_format + ), + "implementation_fingerprint": None, + } + ) + return items + + +def capability_document( + native: NativeCapabilities | None = None, +) -> CapabilityDocument: + catalog = standard_library_catalog() + opcodes = opcode_catalog() + blocks = block_spec() + native = native or native_capabilities() + items = _base_items(catalog, opcodes, blocks) + native_items = {(item.get("kind"), item.get("name")): item for item in native.items} + for item in items: + override = native_items.get((item["kind"], item["name"])) + if override is not None: + if override.get("availability") in ( + "available", + "declared_unavailable", + ): + item["availability"] = override["availability"] + if override.get("implementation_fingerprint") is not None: + item["implementation_fingerprint"] = override[ + "implementation_fingerprint" + ] + if ( + item["availability"] == "available" + and item["implementation_fingerprint"] is None + ): + build_id = ( + native.compiler_build_id + if item["kind"] in ("policy", "output_format") + else native.runtime_build_id + ) + identity = { + "kind": item["kind"], + "name": item["name"], + "schema_fingerprint": item["schema_fingerprint"], + "build_id": build_id, + } + item["implementation_fingerprint"] = sha256_bytes( + canonical_json_bytes(identity) + ) + items.sort(key=lambda item: (str(item["kind"]), str(item["name"]))) + return CapabilityDocument( + contract_identities=EXACT_CONTRACT_IDENTITIES, + items=tuple(items), + compiler_build_id=native.compiler_build_id, + runtime_build_id=native.runtime_build_id, + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_capture_worker.py b/components/agentic-circuit/src/agentic_circuit/_capture_worker.py new file mode 100644 index 00000000..bc056216 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_capture_worker.py @@ -0,0 +1,325 @@ +"""Fresh-process trusted project capture and verified result transport.""" + +from __future__ import annotations + +import argparse +import contextlib +import importlib.util +import io +import json +import os +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +from ._canonical_json import JsonValue, canonical_json_bytes +from ._capabilities import schema_root +from ._diagnostics import Diagnostic, FixIt, RelatedLocation, SourceSpan +from ._frontend import CaptureRequest, elaborate_frontend +from ._output import OutputSink +from ._schemas import SchemaRegistry +from ._staging import ArtifactStage +from ._static_eval import FrozenMap, StaticValue + + +@dataclass(frozen=True, slots=True) +class CaptureWorkerRequest: + python: str + workspace: Path + entry: Path + system: str + static_arguments: tuple[tuple[str, JsonValue], ...] + component_roots: tuple[Path, ...] + private_output: Path + timeout: float = 30.0 + + +@dataclass(frozen=True, slots=True) +class CaptureWorkerResult: + acpy: bytes | None + acir: bytes | None + diagnostics: tuple[Diagnostic, ...] + project_report: bytes | None + + +_STAGE_FILES = ( + "model.ac.mlir", + "model.acpy.json", + "project-report.txt", + "request.json", + "result.json", +) + + +def _source(value: object) -> SourceSpan | None: + if value is None: + return None + if type(value) is not dict or set(value) != {"file", "line", "column"}: + raise ValueError("worker diagnostic source is invalid") + return SourceSpan( + str(value["file"]), + int(value["line"]), + int(value["column"]), + int(value["line"]), + int(value["column"]), + ) + + +def _diagnostic(value: object) -> Diagnostic: + if type(value) is not dict: + raise ValueError("worker diagnostic is invalid") + related = tuple( + RelatedLocation( + message=item["message"], + source=_source(item["source"]), + object_path=item["object_path"], + ) + for item in value["related"] + ) + fixits = tuple(FixIt(item["message"]) for item in value["fixits"]) + return Diagnostic( + stage=value["stage"], + code=value["code"], + severity=value["severity"], + message=value["message"], + source=_source(value["source"]), + object_path=value["object_path"], + expected=value["expected"], + actual=value["actual"], + related=related, + fixits=fixits, + ) + + +def _failure(code: str, message: str) -> CaptureWorkerResult: + return CaptureWorkerResult( + acpy=None, + acir=None, + diagnostics=( + Diagnostic( + stage="frontend-capture", + code=code, + severity="error", + message=message, + ), + ), + project_report=None, + ) + + +def _request_json(request: CaptureWorkerRequest, output: Path) -> dict[str, JsonValue]: + return { + "schema": "agentic-circuit-capture-request", + "version": "0.1", + "contract_epoch": "0.3", + "workspace": request.workspace.resolve().as_posix(), + "entry": request.entry.resolve().as_posix(), + "system": request.system, + "static_arguments": {key: value for key, value in request.static_arguments}, + "component_roots": [ + path.resolve().relative_to(request.workspace.resolve()).as_posix() + for path in request.component_roots + ], + "output": output.resolve().as_posix(), + } + + +def run_capture_worker(request: CaptureWorkerRequest) -> CaptureWorkerResult: + package_parents = os.pathsep.join( + sorted( + { + str(Path(location).resolve().parent) + for location in sys.modules["agentic_circuit"].__path__ + } + ) + ) + bootstrap = ( + "import os,runpy,sys;" + "sys.path[:0]=sys.argv.pop(1).split(os.pathsep);" + "runpy.run_module('agentic_circuit._capture_worker',run_name='__main__')" + ) + with ArtifactStage(request.private_output, expected=_STAGE_FILES) as stage: + assert stage.path is not None + request_path = stage.path / "request.json" + request_path.write_bytes( + canonical_json_bytes(_request_json(request, stage.path)) + ) + environment = { + "PATH": os.environ.get("PATH", ""), + "PYTHONHASHSEED": "0", + } + try: + completed = subprocess.run( + ( + request.python, + "-I", + "-c", + bootstrap, + package_parents, + "--request", + os.fspath(request_path), + ), + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=request.timeout, + check=False, + env=environment, + ) + except subprocess.TimeoutExpired: + return _failure("ACPY-CAPTURE-002", "frontend capture timed out") + if completed.returncode != 0: + detail, _ = OutputSink.bounded_capture( + (completed.stderr or completed.stdout).decode("utf-8", errors="replace") + ) + return _failure( + "ACPY-CAPTURE-001", + "frontend capture worker failed" + (f": {detail}" if detail else ""), + ) + try: + stage.verify() + response = json.loads((stage.path / "result.json").read_text()) + if set(response) != { + "schema", + "version", + "contract_epoch", + "has_acpy", + "has_acir", + "diagnostics", + }: + raise ValueError("worker result fields are invalid") + diagnostics = tuple(_diagnostic(item) for item in response["diagnostics"]) + acpy = ( + (stage.path / "model.acpy.json").read_bytes() + if response["has_acpy"] + else None + ) + acir = ( + (stage.path / "model.ac.mlir").read_bytes() + if response["has_acir"] + else None + ) + report = (stage.path / "project-report.txt").read_bytes() or None + except (OSError, UnicodeError, ValueError, KeyError, TypeError) as error: + return _failure("ACPY-CAPTURE-001", f"capture result is invalid: {error}") + return CaptureWorkerResult(acpy, acir, diagnostics, report) + + +def _load_project(entry: Path, workspace: Path) -> dict[str, object]: + sys.path.insert(1, os.fspath(workspace)) + spec = importlib.util.spec_from_file_location("_agentic_architecture", entry) + if spec is None or spec.loader is None: + raise ImportError(f"cannot load architecture entry {entry}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return vars(module) + + +def _worker_diagnostic(error: BaseException, entry: Path) -> Diagnostic: + if isinstance(error, SyntaxError): + line = error.lineno or 1 + column = error.offset or 1 + source = SourceSpan(entry.name, line, column, line, column) + code = "ACPY-SYNTAX-001" + else: + source = None + code = "ACPY-CAPTURE-001" + return Diagnostic( + stage="frontend-capture", + code=code, + severity="error", + message=f"trusted project execution failed: {error}", + source=source, + ) + + +def _static_value(value: JsonValue) -> StaticValue: + if value is None or type(value) in (bool, int, float, str): + return value + if type(value) is list: + return tuple(_static_value(item) for item in value) + if type(value) is dict: + return FrozenMap( + tuple(sorted((key, _static_value(item)) for key, item in value.items())) + ) + raise TypeError("capture static argument is not an I-JSON value") + + +def _worker_main(request_path: Path) -> int: + request = json.loads(request_path.read_text()) + workspace = Path(request["workspace"]).resolve() + entry = Path(request["entry"]).resolve() + output = Path(request["output"]).resolve() + captured_stdout = io.StringIO() + captured_stderr = io.StringIO() + document = None + acir = None + diagnostics: tuple[Diagnostic, ...] + try: + with contextlib.redirect_stdout(captured_stdout), contextlib.redirect_stderr( + captured_stderr + ): + namespace = _load_project(entry, workspace) + schemas = SchemaRegistry.from_catalog( + schema_root() / "stdlib" / "catalog.json", schema_root().parent + ) + component_roots = tuple( + (workspace / value).resolve() + for value in request["component_roots"] + ) + if any(not path.is_relative_to(workspace) for path in component_roots): + raise ValueError("component root escapes the workspace") + schemas = schemas.with_component_roots(component_roots) + result = elaborate_frontend( + CaptureRequest( + entry=entry, + workspace=workspace, + system=request["system"], + static_arguments=tuple( + sorted( + (key, _static_value(value)) + for key, value in request["static_arguments"].items() + ) + ), + ), + namespace, + schemas, + ) + document = result.document + acir = result.acir + diagnostics = result.diagnostics + except BaseException as error: + diagnostics = (_worker_diagnostic(error, entry),) + report, _ = OutputSink.bounded_capture( + captured_stdout.getvalue() + captured_stderr.getvalue() + ) + (output / "model.acpy.json").write_bytes( + document.canonical_bytes() if document is not None else b"" + ) + (output / "model.ac.mlir").write_bytes( + acir.encode("utf-8") if acir is not None else b"" + ) + (output / "project-report.txt").write_text(report, encoding="utf-8") + response = { + "schema": "agentic-circuit-capture-result", + "version": "0.1", + "contract_epoch": "0.3", + "has_acpy": document is not None, + "has_acir": acir is not None, + "diagnostics": [item.to_json() for item in diagnostics], + } + (output / "result.json").write_bytes(canonical_json_bytes(response)) + return 0 + + +def _main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--request", type=Path, required=True) + arguments = parser.parse_args() + return _worker_main(arguments.request) + + +if __name__ == "__main__": + raise SystemExit(_main()) diff --git a/components/agentic-circuit/src/agentic_circuit/_cli.py b/components/agentic-circuit/src/agentic_circuit/_cli.py new file mode 100644 index 00000000..c1dc54a4 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_cli.py @@ -0,0 +1,325 @@ +"""Exact public command parser and top-level exit policy.""" + +from __future__ import annotations + +import argparse +from enum import IntEnum +from pathlib import Path +from typing import Callable, Sequence + +from ._commands import init as init_command +from ._commands import inspect as inspect_command +from ._commands import doctor as doctor_command +from ._commands import check as check_command +from ._commands import build as build_command +from ._commands import compile as compile_command +from ._commands import elaborate as elaborate_command +from ._commands import explain as explain_command +from ._commands import run as run_command +from ._commands import schema as schema_command +from ._diagnostics import Diagnostic +from ._output import OutputSink +from ._workspace import UserInputError, discover_workspace, load_workspace + + +EXACT_COMMANDS = ( + "init", + "schema", + "check", + "elaborate", + "compile", + "build", + "run", + "inspect", + "explain", + "doctor", +) + + +class ExitCode(IntEnum): + SUCCESS = 0 + USER_INPUT = 2 + INTERNAL = 3 + BUILD = 4 + PREFLIGHT = 5 + SIMULATION = 6 + INCOMPLETE = 7 + INTERRUPTED = 130 + + +class _OnceValue(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: object, + option_string: str | None = None, + ) -> None: + marker = f"_agentic_seen_{self.dest}" + if getattr(namespace, marker, False): + parser.error(f"{option_string} may be specified only once") + setattr(namespace, marker, True) + setattr(namespace, self.dest, values) + + +class _OnceTrue(_OnceValue): + def __init__(self, option_strings: Sequence[str], dest: str, **kwargs: object): + super().__init__( + option_strings, dest, nargs=0, const=True, default=False, **kwargs + ) + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: object, + option_string: str | None = None, + ) -> None: + super().__call__(parser, namespace, self.const, option_string) + + +def _positive(value: str) -> int: + parsed = int(value) + if parsed < 1: + raise argparse.ArgumentTypeError("value must be positive") + return parsed + + +def _uint64(value: str) -> int: + parsed = int(value) + if not 0 <= parsed <= (1 << 64) - 1: + raise argparse.ArgumentTypeError("value must be an unsigned 64-bit integer") + return parsed + + +def _add_output_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--json", action=_OnceTrue) + parser.add_argument( + "--diagnostic-format", + choices=("text", "json", "jsonl"), + default=None, + action=_OnceValue, + ) + parser.add_argument("--no-color", action=_OnceTrue) + parser.add_argument("--quiet", action=_OnceTrue) + parser.add_argument("--warnings-as-errors", action=_OnceTrue) + + +def _add_workspace_options( + parser: argparse.ArgumentParser, + *, + output: bool = False, + jobs: bool = False, + seed: bool = False, + seed_type: Callable[[str], int] = int, +) -> None: + parser.add_argument("--project", type=Path, action=_OnceValue) + parser.add_argument("--system", action=_OnceValue) + if output: + parser.add_argument("--output-dir", type=Path, action=_OnceValue) + if jobs: + parser.add_argument("--jobs", type=_positive, action=_OnceValue) + if seed: + parser.add_argument("--seed", type=seed_type, action=_OnceValue) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="agentic-circuit", allow_abbrev=False) + commands = parser.add_subparsers(dest="command", required=True) + + init = commands.add_parser("init", allow_abbrev=False) + init.add_argument("directory", nargs="?", default=".") + init.add_argument("--dry-run", action=_OnceTrue) + init.add_argument("--force", action="append", default=[]) + _add_output_options(init) + + schema = commands.add_parser("schema", allow_abbrev=False) + schema.add_argument( + "kind", + choices=( + "block", + "component", + "opcode", + "protocol", + "interface", + "packet", + "diagnostic", + "capabilities", + ), + ) + schema.add_argument("name", nargs="?") + _add_output_options(schema) + + check = commands.add_parser("check", allow_abbrev=False) + check.add_argument("architecture", nargs="?") + check.add_argument("--stop-after", choices=("acpy-verify",), action=_OnceValue) + _add_workspace_options(check, jobs=True) + _add_output_options(check) + + elaborate = commands.add_parser("elaborate", allow_abbrev=False) + elaborate.add_argument("architecture", nargs="?") + elaborate.add_argument( + "--emit", choices=("acpy", "acir"), default="acir", action=_OnceValue + ) + elaborate.add_argument("-o", "--output", type=Path, action=_OnceValue) + _add_workspace_options(elaborate, output=True, jobs=True) + _add_output_options(elaborate) + + compile_parser = commands.add_parser("compile", allow_abbrev=False) + compile_parser.add_argument("architecture", nargs="?") + compile_parser.add_argument("--emit", action=_OnceValue) + compile_parser.add_argument( + "--profile", + choices=("fast", "validated", "custom"), + action=_OnceValue, + ) + compile_parser.add_argument("--stop-after", action=_OnceValue) + compile_parser.add_argument("--dump-before", action="append", default=[]) + compile_parser.add_argument("--dump-after", action="append", default=[]) + compile_parser.add_argument("--dump-after-each", action=_OnceTrue) + compile_parser.add_argument("--verify-after-each", action=_OnceTrue) + compile_parser.add_argument("--pass-pipeline", action=_OnceValue) + _add_workspace_options(compile_parser, output=True, jobs=True) + _add_output_options(compile_parser) + + build = commands.add_parser("build", allow_abbrev=False) + build.add_argument("architecture", nargs="?") + build.add_argument( + "-o", "--output", dest="output_dir", type=Path, action=_OnceValue + ) + build.add_argument( + "--profile", + choices=("fast", "validated", "custom"), + action=_OnceValue, + ) + build.add_argument("--verify-after-each", action=_OnceTrue) + build.add_argument("--pass-pipeline", action=_OnceValue) + _add_workspace_options(build, output=True, jobs=True) + _add_output_options(build) + + run = commands.add_parser("run", allow_abbrev=False) + run.add_argument("architecture", nargs="?") + run.add_argument("--trace", type=Path, action=_OnceValue) + run.add_argument("--deadlock-window", type=_positive, action=_OnceValue) + run.add_argument("--max-ticks", type=_positive, action=_OnceValue) + run.add_argument("--max-domain-cycles", action="append", default=[]) + run.add_argument("--expect-termination", action=_OnceTrue) + run.add_argument("--stats-format", choices=("json",), action=_OnceValue) + run.add_argument("--event-log", choices=("jsonl",), action=_OnceValue) + run.add_argument("--replay-manifest", type=Path, action=_OnceValue) + _add_workspace_options(run, output=True, jobs=True, seed=True, seed_type=_uint64) + _add_output_options(run) + + inspect = commands.add_parser("inspect", allow_abbrev=False) + inspect.add_argument( + "view", + choices=( + "graph", + "hierarchy", + "ports", + "resources", + "address-map", + "protocols", + "specialization", + "artifacts", + ), + ) + inspect.add_argument("--path", action=_OnceValue) + inspect.add_argument("--format", choices=("json", "dot", "text"), action=_OnceValue) + _add_workspace_options(inspect) + _add_output_options(inspect) + + explain = commands.add_parser("explain", allow_abbrev=False) + explain.add_argument("code") + _add_output_options(explain) + + doctor = commands.add_parser("doctor", allow_abbrev=False) + _add_output_options(doctor) + return parser + + +def command_names(parser: argparse.ArgumentParser) -> tuple[str, ...]: + for action in parser._actions: + if isinstance(action, argparse._SubParsersAction): + return tuple(action.choices) + return () + + +def _workspace_start(arguments: argparse.Namespace) -> Path: + architecture = getattr(arguments, "architecture", None) + return Path(architecture) if architecture else Path.cwd() + + +def _placeholder_result( + arguments: argparse.Namespace, project: str +) -> dict[str, object]: + return { + "schema": "agentic-circuit-command-result", + "version": "0.1", + "contract_epoch": "0.3", + "command": arguments.command, + "project": project, + "status": "accepted", + } + + +def main(argv: Sequence[str] | None = None) -> int: + arguments = build_parser().parse_args(argv) + sink = OutputSink.from_arguments(arguments) + try: + if arguments.command == "init": + return init_command.run(arguments, sink) + if arguments.command == "schema": + return schema_command.run(arguments, sink) + if arguments.command == "explain": + return explain_command.run(arguments, sink) + if arguments.command == "doctor": + return doctor_command.run(arguments, sink) + if arguments.command == "run" and arguments.replay_manifest is not None: + return run_command.run(arguments, None, sink) + workspace = ( + load_workspace(arguments.project) + if arguments.project is not None + else discover_workspace(_workspace_start(arguments)) + ) + sink = OutputSink.from_arguments( + arguments, workspace_format=workspace.diagnostic_format + ) + if arguments.command == "check": + return check_command.run(arguments, workspace, sink) + if arguments.command == "elaborate": + return elaborate_command.run(arguments, workspace, sink) + if arguments.command == "compile": + return compile_command.run(arguments, workspace, sink) + if arguments.command == "build": + return build_command.run(arguments, workspace, sink) + if arguments.command == "run": + return run_command.run(arguments, workspace, sink) + if arguments.command == "inspect": + return inspect_command.run(arguments, workspace, sink) + sink.result( + _placeholder_result(arguments, workspace.project_name), + human=f"{arguments.command} accepted for {workspace.project_name}", + ) + return ExitCode.SUCCESS + except UserInputError as error: + sink.diagnostics((error.diagnostic,)) + return ExitCode.USER_INPUT + except KeyboardInterrupt: + return ExitCode.INTERRUPTED + except Exception: + sink.diagnostics( + ( + Diagnostic( + stage="cli", + code="ACPY-INTERNAL-001", + severity="error", + message="internal command failure", + ), + ) + ) + return ExitCode.INTERNAL + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/src/agentic_circuit/_collections.py b/components/agentic-circuit/src/agentic_circuit/_collections.py new file mode 100644 index 00000000..1206d2bd --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_collections.py @@ -0,0 +1,61 @@ +"""Deterministic static collection classification.""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Literal, TypeAlias + +from ._resolve import ResolvedCall + + +CollectionElement: TypeAlias = ResolvedCall | Sequence["CollectionElement"] + + +class CollectionError(ValueError): + """Raised when a static collection has no canonical fixed shape.""" + + +@dataclass(frozen=True, slots=True) +class CollectionPlan: + kind: Literal["array", "instances"] + shape: tuple[int, ...] + elements: tuple[str, ...] + element_schema: str | None + + +def _shape_and_flatten( + element: CollectionElement, +) -> tuple[tuple[int, ...], tuple[ResolvedCall, ...]]: + if isinstance(element, ResolvedCall): + return (), (element,) + if isinstance(element, (str, bytes)) or not isinstance(element, Sequence): + raise CollectionError("ACPY-STATIC-COLLECTION: invalid collection element") + if not element: + return (0,), () + children = [_shape_and_flatten(child) for child in element] + child_shape = children[0][0] + if any(shape != child_shape for shape, _ in children[1:]): + raise CollectionError("ACPY-STATIC-COLLECTION: ragged collection") + flat = tuple(item for _, values in children for item in values) + return (len(element), *child_shape), flat + + +def classify_collection(elements: Sequence[CollectionElement]) -> CollectionPlan: + shape, flat = _shape_and_flatten(elements) + if not flat: + return CollectionPlan("instances", shape, (), None) + specialization = ( + flat[0].schema.identity, + flat[0].static_arguments, + ) + homogeneous = all( + (item.schema.identity, item.static_arguments) == specialization + for item in flat[1:] + ) + return CollectionPlan( + kind="array" if homogeneous else "instances", + shape=shape, + elements=tuple(item.entity_key for item in flat), + element_schema=flat[0].schema.identity if homogeneous else None, + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/__init__.py b/components/agentic-circuit/src/agentic_circuit/_commands/__init__.py new file mode 100644 index 00000000..df8aa3de --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/__init__.py @@ -0,0 +1 @@ +"""Private implementations of the exact public CLI commands.""" diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/build.py b/components/agentic-circuit/src/agentic_circuit/_commands/build.py new file mode 100644 index 00000000..d54795b9 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/build.py @@ -0,0 +1,343 @@ +"""Immutable native build orchestration.""" + +from __future__ import annotations + +import json +import platform +import shutil +import subprocess +import sys +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Literal, NoReturn + +from .._diagnostics import Diagnostic +from .._canonical_json import canonical_json_bytes +from .._native_api import NativeRequest, native_extension_path, run_native_compiler +from .._output import OutputSink +from .._workspace import UserInputError, WorkspaceConfig +from .check import _has_errors, capture + + +Profile = Literal["fast", "validated", "custom"] + + +@dataclass(frozen=True, slots=True) +class BuildOptions: + profile: Profile + pass_pipeline: str | None + verify_after_each: bool + instrumentation_layers: tuple[str, ...] + + +@dataclass(frozen=True, slots=True) +class BuildPublication: + directory: Path + executable: Path + manifest: Path + fingerprint: str + cache_hit: bool + + +@dataclass(frozen=True, slots=True) +class BuildAttempt: + publication: BuildPublication | None + diagnostics: tuple[Diagnostic, ...] + exit_code: int + profile: Profile + + +def _fail(code: str, message: str) -> NoReturn: + raise UserInputError( + Diagnostic(stage="build", code=code, severity="error", message=message) + ) + + +def build_options(arguments: object, workspace: WorkspaceConfig) -> BuildOptions: + profile = getattr(arguments, "profile", None) or workspace.build_profile + pipeline = getattr(arguments, "pass_pipeline", None) + if profile == "custom" and not pipeline: + _fail("ACPY-CLI-PIPELINE", "custom requires --pass-pipeline") + if profile != "custom" and pipeline is not None: + _fail("ACPY-CLI-PIPELINE", "--pass-pipeline requires profile custom") + return BuildOptions( + profile=profile, + pass_pipeline=pipeline, + verify_after_each=( + profile == "validated" + or bool(getattr(arguments, "verify_after_each", False)) + ), + instrumentation_layers=workspace.instrumentation_layers, + ) + + +def _output(arguments: object) -> Path: + value = getattr(arguments, "output_dir", None) + if value is None: + _fail("ACPY-CLI-OUTPUT", "build requires -o/--output-dir") + return Path(value).resolve() + + +def _source_files(acpy: bytes) -> list[dict[str, str]]: + try: + document = json.loads(acpy) + values = document["sources"] + if type(values) is not list: + raise TypeError("sources is not a list") + result: list[dict[str, str]] = [] + for value in values: + if type(value) is not dict or set(value) != {"path", "sha256"}: + raise TypeError("source identity is not closed") + path = value["path"] + sha256 = value["sha256"] + if type(path) is not str or type(sha256) is not str: + raise TypeError("source identity fields are not strings") + result.append({"path": path, "sha256": sha256}) + return result + except (UnicodeError, json.JSONDecodeError, KeyError, TypeError) as error: + _fail("ACPY-VERIFY-001", f"ACPy source provenance is invalid: {error}") + + +def _runtime_linkage() -> tuple[list[str], list[str]]: + for root in native_extension_path().parents: + include = root / "include" + build_tree = ( + root / "lib/gfsim/libgfsim.a", + root / "lib/Bindings/libACIRBindings.a", + ) + installed = (root / "lib/libgfsim.a", root / "lib/libACIRBindings.a") + for libraries in (build_tree, installed): + if (include / "gfsim").is_dir() and all( + library.is_file() for library in libraries + ): + return ( + [include.resolve().as_posix()], + [library.resolve().as_posix() for library in libraries], + ) + raise RuntimeError("Agentic Circuit runtime development files are unavailable") + + +def _binding_registry( + component_roots: tuple[Path, ...], *, native_target: str | None = None +) -> bytes: + candidates: list[object] = [] + requests: list[object] = [] + for root in sorted(component_roots): + for path in sorted(root.rglob("*.binding.json")): + try: + document = json.loads(path.read_text()) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + _fail("ACLOWER-BINDING-OPTIONS", f"cannot load {path}: {error}") + if type(document) is not dict or set(document) != { + "candidates", + "requests", + }: + _fail( + "ACLOWER-BINDING-OPTIONS", + f"binding registry {path} is not a closed registry", + ) + if type(document["candidates"]) is not list or type( + document["requests"] + ) is not list: + _fail( + "ACLOWER-BINDING-OPTIONS", + f"binding registry {path} arrays are invalid", + ) + for candidate in document["candidates"]: + if ( + native_target is not None + and type(candidate) is dict + and candidate.get("target") == "native" + ): + candidate = dict(candidate) + candidate["target"] = native_target + candidates.append(candidate) + requests.extend(document["requests"]) + return canonical_json_bytes( + {"candidates": candidates, "requests": requests} + ) + + +def _logical_diagnostics( + diagnostics: tuple[Diagnostic, ...], +) -> tuple[Diagnostic, ...]: + stages = { + "acir-parse": "acir-elaboration", + "acir-verify": "acir-core", + "acir-normalize": "collection-canonicalization", + "acir-freeze": "topology-freeze", + "acsim-lower": "process-state-lowering", + "acsim-verify": "acsim", + "cxx-emit": "cxx", + "cxx-contract": "cxx", + "compile": "cxx", + "link": "cxx", + "publish": "cxx", + } + return tuple( + replace(item, stage=stages.get(item.stage, item.stage)) + for item in diagnostics + ) + + +def _failure_exit(diagnostics: tuple[Diagnostic, ...]) -> int: + if any( + item.stage == "cxx" + or item.code.startswith(("ACBUILD-", "ACLOWER-CXX", "ACLOWER-COMPILE")) + for item in diagnostics + ): + return 4 + if all( + item.code.startswith(("ACPY-", "ACELAB-", "ACIR-", "ACLOWER-")) + for item in diagnostics + ): + return 2 + return 3 + + +def build_publication( + arguments: object, + workspace: WorkspaceConfig, + *, + output: Path | None = None, +) -> BuildAttempt: + options = build_options(arguments, workspace) + destination = output.resolve() if output is not None else _output(arguments) + frontend = capture(arguments, workspace) + if _has_errors(frontend.diagnostics): + return BuildAttempt(None, frontend.diagnostics, 2, options.profile) + if frontend.acpy is None or frontend.acir is None: + _fail("ACPY-VERIFY-001", "frontend produced incomplete build artifacts") + compiler = shutil.which(workspace.compiler) + if compiler is None: + return BuildAttempt( + None, + ( + Diagnostic( + stage="cxx", + code="ACBUILD-COMPILER-001", + severity="error", + message=f"C++ compiler is unavailable: {workspace.compiler}", + ), + ), + 4, + options.profile, + ) + + try: + native_target = subprocess.run( + [compiler, "-dumpmachine"], + check=True, + capture_output=True, + text=True, + ).stdout.splitlines()[0].strip() + except (OSError, subprocess.CalledProcessError, IndexError) as error: + return BuildAttempt( + None, + ( + Diagnostic( + stage="cxx", + code="ACBUILD-COMPILER-001", + severity="error", + message=f"cannot query C++ compiler target: {error}", + ), + ), + 4, + options.profile, + ) + + include_roots, link_inputs = _runtime_linkage() + native_options: list[tuple[str, object]] = [ + ("profile", options.profile), + ("binding_lock", b"[]"), + ( + "binding_registry", + _binding_registry( + workspace.component_roots, native_target=native_target + ), + ), + ("frontend_acpy", frontend.acpy), + ("frontend_acir", frontend.acir), + ( + "build", + { + "project_name": workspace.project_name, + "project_identity": ( + f"project:{workspace.project_name}@{workspace.project_version}" + ), + "system_name": getattr(arguments, "system", None) + or workspace.default_system, + "system_identity": ( + "system:" + + (getattr(arguments, "system", None) or workspace.default_system) + ), + "source_files": _source_files(frontend.acpy), + "python_version": ( + f"{sys.implementation.name} {platform.python_version()}" + ), + "helper_identities": [], + "compiler": compiler, + "standard_library": workspace.standard_library, + "instrumentation_layers": list(options.instrumentation_layers), + "output_root": destination.as_posix(), + "include_roots": include_roots, + "link_inputs": link_inputs, + }, + ), + ] + if options.pass_pipeline is not None: + native_options.append(("custom_pipeline", options.pass_pipeline)) + if options.verify_after_each: + native_options.append(("verify_after_each", True)) + native = run_native_compiler( + NativeRequest( + acir=frontend.acir, + stop_after="publish", + emits=("executable",), + options=tuple(native_options), + ) + ) + diagnostics = _logical_diagnostics(native.diagnostics) + if _has_errors(diagnostics): + return BuildAttempt( + None, diagnostics, _failure_exit(diagnostics), options.profile + ) + if ( + native.build_directory is None + or native.executable is None + or native.build_fingerprint is None + or native.cache_hit is None + ): + raise RuntimeError("native build returned incomplete publication identity") + publication = BuildPublication( + directory=Path(native.build_directory), + executable=Path(native.executable), + manifest=Path(native.build_directory) / "build-manifest.json", + fingerprint=native.build_fingerprint, + cache_hit=native.cache_hit, + ) + return BuildAttempt(publication, diagnostics, 0, options.profile) + + +def run(arguments: object, workspace: WorkspaceConfig, sink: OutputSink) -> int: + attempt = build_publication(arguments, workspace) + if attempt.publication is None: + sink.diagnostics(attempt.diagnostics) + return attempt.exit_code + publication = attempt.publication + sink.result( + { + "schema": "agentic-circuit-build-result", + "version": "0.1", + "contract_epoch": "0.3", + "status": "passed", + "profile": attempt.profile, + "directory": publication.directory.as_posix(), + "executable": publication.executable.as_posix(), + "manifest": publication.manifest.as_posix(), + "build_fingerprint": publication.fingerprint, + "cache_hit": publication.cache_hit, + }, + human=f"built {publication.executable}", + ) + return 0 diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/check.py b/components/agentic-circuit/src/agentic_circuit/_commands/check.py new file mode 100644 index 00000000..4727de6d --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/check.py @@ -0,0 +1,102 @@ +"""Fast trusted frontend validation through native ACIR verification.""" + +from __future__ import annotations + +import sys +import tempfile +from pathlib import Path + +from .._capture_worker import ( + CaptureWorkerRequest, + CaptureWorkerResult, + run_capture_worker, +) +from .._diagnostics import Diagnostic +from .._native_api import NativeRequest, run_native_compiler +from .._output import OutputSink +from .._workspace import UserInputError, WorkspaceConfig + + +def _entry(arguments: object, workspace: WorkspaceConfig) -> Path: + value = getattr(arguments, "architecture", None) + entry = (workspace.root / value).resolve() if value else workspace.architecture + if not entry.is_relative_to(workspace.root): + raise UserInputError( + Diagnostic( + stage="frontend-capture", + code="ACPY-CONFIG-003", + severity="error", + message="architecture entry escapes the workspace", + ) + ) + return entry + + +def capture( + arguments: object, workspace: WorkspaceConfig +) -> CaptureWorkerResult: + with tempfile.TemporaryDirectory(prefix="agentic-capture-") as temporary: + return run_capture_worker( + CaptureWorkerRequest( + python=sys.executable, + workspace=workspace.root, + entry=_entry(arguments, workspace), + system=getattr(arguments, "system", None) or workspace.default_system, + static_arguments=(), + component_roots=workspace.component_roots, + private_output=Path(temporary) / "capture", + ) + ) + + +def _has_errors(diagnostics: tuple[Diagnostic, ...]) -> bool: + return any(item.severity == "error" for item in diagnostics) + + +def run(arguments: object, workspace: WorkspaceConfig, sink: OutputSink) -> int: + frontend = capture(arguments, workspace) + if _has_errors(frontend.diagnostics): + sink.diagnostics(frontend.diagnostics) + return 2 + stage = "acpy-verify" + if getattr(arguments, "stop_after", None) != "acpy-verify": + if frontend.acir is None: + sink.diagnostics( + ( + Diagnostic( + stage="acir-elaboration", + code="ACPY-VERIFY-001", + severity="error", + message="frontend produced no ACIR artifact", + ), + ) + ) + return 2 + from .build import _binding_registry + + native = run_native_compiler( + NativeRequest( + acir=frontend.acir, + stop_after="acir-verify", + emits=(), + options=(("binding_registry", _binding_registry(workspace.component_roots)),), + ) + ) + if _has_errors(native.diagnostics): + sink.diagnostics(native.diagnostics) + return 2 + stage = "acir-core" + sink.result( + { + "schema": "agentic-circuit-check-result", + "version": "0.1", + "contract_epoch": "0.3", + "project": workspace.project_name, + "system": getattr(arguments, "system", None) + or workspace.default_system, + "stage": stage, + "status": "passed", + }, + human=f"check passed through {stage}", + ) + return 0 diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/compile.py b/components/agentic-circuit/src/agentic_circuit/_commands/compile.py new file mode 100644 index 00000000..b105720e --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/compile.py @@ -0,0 +1,363 @@ +"""Deterministic logical-stage compilation and artifact publication.""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Literal, NoReturn + +from .._canonical_json import sha256_bytes +from .._diagnostics import Diagnostic +from .._native_api import NativeRequest, NativeResult, run_native_compiler +from .._output import OutputSink +from .._staging import ArtifactStage +from .._workspace import UserInputError, WorkspaceConfig +from .check import _has_errors, capture + + +Emit = Literal["acpy", "acir", "frozen-acir", "acsim", "cpp"] + +STAGES = ( + "frontend-capture", + "acpy-construction", + "acpy-verify", + "acir-elaboration", + "process-construction", + "collection-canonicalization", + "acir-core", + "topology-freeze", + "process-state-lowering", + "acsim", + "cxx", +) +EMITS: tuple[Emit, ...] = ("acpy", "acir", "frozen-acir", "acsim", "cpp") + +_EMIT_STAGE = { + "acpy": "acpy-verify", + "acir": "acir-core", + "frozen-acir": "topology-freeze", + "acsim": "acsim", + "cpp": "cxx", +} +_NATIVE_STOP = { + "acir-elaboration": "acir-verify", + "process-construction": "acir-verify", + "collection-canonicalization": "acir-verify", + "acir-core": "acir-verify", + "topology-freeze": "acir-freeze", + "process-state-lowering": "acsim-lower", + "acsim": "acsim-verify", + "cxx": "cxx-contract", +} +_NATIVE_DIAGNOSTIC_STAGE = { + "acir-parse": "acir-elaboration", + "acir-verify": "acir-core", + "acir-normalize": "collection-canonicalization", + "acir-freeze": "topology-freeze", + "acsim-lower": "process-state-lowering", + "acsim-verify": "acsim", + "cxx-emit": "cxx", + "cxx-contract": "cxx", +} + + +@dataclass(frozen=True, slots=True) +class CompileOptions: + emits: tuple[Emit, ...] + stop_after: str | None + dump_before: tuple[str, ...] + dump_after: tuple[str, ...] + dump_after_each: bool + verify_after_each: bool + pass_pipeline: str | None + + @property + def final_stage(self) -> str: + return self.stop_after or "cxx" + + +def _fail(code: str, message: str) -> NoReturn: + raise UserInputError( + Diagnostic( + stage="compile", + code=code, + severity="error", + message=message, + ) + ) + + +def _normalize_emits(value: str | None) -> tuple[Emit, ...]: + raw = value.split(",") if value is not None else list(EMITS) + if not raw or any(item not in EMITS for item in raw): + _fail("ACPY-CLI-EMIT", "compile emits must use exact supported names") + if len(set(raw)) != len(raw): + _fail("ACPY-CLI-EMIT", "compile emits must not contain duplicates") + return tuple(raw) # type: ignore[return-value] + + +def _normalize_dumps( + values: object, *, option: str, final_stage: str +) -> tuple[str, ...]: + if type(values) is not list or not all(type(item) is str for item in values): + _fail("ACPY-CLI-DUMP", f"{option} values must be logical stage names") + result = tuple(values) + if len(set(result)) != len(result): + _fail("ACPY-CLI-DUMP", f"{option} contains a duplicate stage") + final_index = STAGES.index(final_stage) + for stage in result: + if stage not in STAGES: + _fail("ACPY-CLI-DUMP", f"unknown logical dump stage: {stage}") + if STAGES.index(stage) > final_index: + _fail("ACPY-CLI-DUMP", f"dump stage is beyond --stop-after: {stage}") + return result + + +def normalize_options(arguments: object, profile: str) -> CompileOptions: + emits = _normalize_emits(getattr(arguments, "emit", None)) + stop_after = getattr(arguments, "stop_after", None) + if stop_after is not None and stop_after not in STAGES: + _fail("ACPY-CLI-STAGE", f"unknown compile stage: {stop_after}") + final_stage = stop_after or "cxx" + final_index = STAGES.index(final_stage) + for emit in emits: + if STAGES.index(_EMIT_STAGE[emit]) > final_index: + _fail("ACPY-CLI-STAGE", f"emit {emit} is beyond --stop-after") + + pass_pipeline = getattr(arguments, "pass_pipeline", None) + if profile == "custom" and not pass_pipeline: + _fail("ACPY-CLI-PIPELINE", "custom requires --pass-pipeline") + if profile != "custom" and pass_pipeline is not None: + _fail("ACPY-CLI-PIPELINE", "--pass-pipeline requires profile custom") + + return CompileOptions( + emits=emits, + stop_after=stop_after, + dump_before=_normalize_dumps( + getattr(arguments, "dump_before", []), + option="--dump-before", + final_stage=final_stage, + ), + dump_after=_normalize_dumps( + getattr(arguments, "dump_after", []), + option="--dump-after", + final_stage=final_stage, + ), + dump_after_each=bool(getattr(arguments, "dump_after_each", False)), + verify_after_each=( + profile == "validated" + or bool(getattr(arguments, "verify_after_each", False)) + ), + pass_pipeline=pass_pipeline, + ) + + +def _physical_dump_stage(logical: str, *, before: bool) -> str | None: + if logical == "topology-freeze": + return "acir-freeze" + if logical == "process-state-lowering": + return "acsim-lower" + if logical == "acsim": + return "acsim-verify" + if logical == "cxx": + return "cxx-emit" if before else "cxx-contract" + return None + + +def _unique_physical(stages: tuple[str, ...], *, before: bool) -> tuple[str, ...]: + result: list[str] = [] + for logical in stages: + physical = _physical_dump_stage(logical, before=before) + if physical is not None and physical not in result: + result.append(physical) + return tuple(result) + + +def _after_stages(options: CompileOptions) -> tuple[str, ...]: + selected = list(options.dump_after) + if options.dump_after_each: + final_index = STAGES.index(options.final_stage) + selected.extend(STAGES[: final_index + 1]) + return tuple(dict.fromkeys(selected)) + + +def _native_request( + acir: bytes, options: CompileOptions, profile: str, + binding_registry: bytes = b'{"candidates":[],"requests":[]}', +) -> NativeRequest: + native_emits: list[str] = [] + if "frozen-acir" in options.emits: + native_emits.append("frozen-acir") + if "acsim" in options.emits: + native_emits.append("acsim") + if "cpp" in options.emits: + native_emits.extend(("cpp-header", "cpp-source")) + + native_options: list[tuple[str, object]] = [ + ("profile", profile), + ("binding_registry", binding_registry), + ] + if options.pass_pipeline is not None: + native_options.append(("custom_pipeline", options.pass_pipeline)) + before = _unique_physical(options.dump_before, before=True) + after = _unique_physical(_after_stages(options), before=False) + if before: + native_options.append(("dump_before", before)) + if after: + native_options.append(("dump_after", after)) + if options.verify_after_each: + native_options.append(("verify_after_each", True)) + return NativeRequest( + acir=acir, + stop_after=_NATIVE_STOP[options.final_stage], + emits=tuple(native_emits), + options=tuple(native_options), + ) + + +def _logical_diagnostics(diagnostics: tuple[Diagnostic, ...]) -> tuple[Diagnostic, ...]: + return tuple( + replace(item, stage=_NATIVE_DIAGNOSTIC_STAGE.get(item.stage, item.stage)) + for item in diagnostics + ) + + +def _failure_exit(diagnostics: tuple[Diagnostic, ...]) -> int: + if any( + item.stage == "cxx" or item.code.startswith(("ACBUILD-", "ACLOWER-CXX")) + for item in diagnostics + ): + return 4 + if all( + item.code.startswith(("ACPY-", "ACELAB-", "ACIR-", "ACLOWER-")) + for item in diagnostics + ): + return 2 + return 3 + + +def _frontend_dump(logical: str, acpy: bytes, acir: bytes) -> bytes | None: + index = STAGES.index(logical) + if index <= STAGES.index("acpy-verify"): + return acpy + if index <= STAGES.index("acir-core"): + return acir + return None + + +def _add_dumps( + artifacts: dict[str, bytes], + options: CompileOptions, + acpy: bytes, + acir: bytes, + native: NativeResult | None, +) -> None: + native_artifacts = { + item.path: item.data for item in native.artifacts + } if native is not None else {} + for label, requested, before in ( + ("before", options.dump_before, True), + ("after", _after_stages(options), False), + ): + for logical in requested: + data = _frontend_dump(logical, acpy, acir) + physical = _physical_dump_stage(logical, before=before) + if physical is not None: + data = native_artifacts.get(f"dumps/{physical}-{label}.mlir") + if data is None: + _fail("ACPY-CLI-DUMP", f"compiler produced no dump for {logical}") + artifacts[f"dumps/{logical}-{label}.mlir"] = data + + +def _publish(output: Path, artifacts: dict[str, bytes]) -> tuple[str, ...]: + ordered = tuple(sorted(artifacts)) + if output.is_symlink(): + _fail("ACPY-CLI-OUTPUT", "compile output must not be a symlink") + if output.exists() and not output.is_dir(): + _fail("ACPY-CLI-OUTPUT", "compile output must be a directory") + entries = tuple(output.rglob("*")) if output.exists() else () + if any(item.is_symlink() for item in entries): + _fail("ACPY-CLI-OUTPUT", "compile output must not contain symlinks") + existing = tuple(item for item in entries if not item.is_dir()) + unexpected = sorted( + item.relative_to(output).as_posix() + for item in existing + if item.relative_to(output).as_posix() not in artifacts + ) + if unexpected: + _fail("ACPY-CLI-OUTPUT", f"compile output contains stale artifact: {unexpected[0]}") + + with ArtifactStage(output, expected=ordered) as stage: + for path in ordered: + stage.write_bytes(path, artifacts[path]) + stage.commit( + allow_replace=(path for path in ordered if output.joinpath(path).exists()) + ) + return ordered + + +def run(arguments: object, workspace: WorkspaceConfig, sink: OutputSink) -> int: + profile = getattr(arguments, "profile", None) or workspace.build_profile + options = normalize_options(arguments, profile) + frontend = capture(arguments, workspace) + if _has_errors(frontend.diagnostics): + sink.diagnostics(frontend.diagnostics) + return 2 + if frontend.acpy is None or frontend.acir is None: + _fail("ACPY-VERIFY-001", "frontend produced incomplete compile artifacts") + + native: NativeResult | None = None + if STAGES.index(options.final_stage) > STAGES.index("acpy-verify"): + from .build import _binding_registry + + native = run_native_compiler( + _native_request( + frontend.acir, + options, + profile, + _binding_registry(workspace.component_roots), + ) + ) + diagnostics = _logical_diagnostics(native.diagnostics) + if _has_errors(diagnostics): + sink.diagnostics(diagnostics) + return _failure_exit(diagnostics) + + artifacts: dict[str, bytes] = {} + if "acpy" in options.emits: + artifacts["input/model.acpy.json"] = frontend.acpy + if "acir" in options.emits: + artifacts["input/model.ac.mlir"] = frontend.acir + if native is not None: + for artifact in native.artifacts: + if not artifact.path.startswith("dumps/"): + artifacts[artifact.path] = artifact.data + _add_dumps(artifacts, options, frontend.acpy, frontend.acir, native) + + output_value = getattr(arguments, "output_dir", None) + output = ( + Path(output_value).resolve() + if output_value is not None + else workspace.build_root / "compile" + ) + ordered = _publish(output, artifacts) + sink.result( + { + "schema": "agentic-circuit-compile-result", + "version": "0.1", + "contract_epoch": "0.3", + "project": workspace.project_name, + "system": getattr(arguments, "system", None) + or workspace.default_system, + "profile": profile, + "stage": options.final_stage, + "status": "passed", + "output_dir": output.as_posix(), + "artifacts": list(ordered), + "artifact_sha256": { + path: sha256_bytes(artifacts[path]) for path in ordered + }, + }, + human=f"compiled {len(ordered)} artifacts to {output}", + ) + return 0 diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/doctor.py b/components/agentic-circuit/src/agentic_circuit/_commands/doctor.py new file mode 100644 index 00000000..b0b26bf6 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/doctor.py @@ -0,0 +1,117 @@ +"""Read-only installed-toolchain checks.""" + +from __future__ import annotations + +import shutil +import sys +from dataclasses import dataclass +from typing import Literal + +from .._canonical_json import canonical_json_bytes +from .._capabilities import standard_library_catalog +from .._native_api import capabilities +from .._output import OutputSink + + +@dataclass(frozen=True, slots=True) +class DoctorCheck: + name: str + status: Literal["passed", "failed"] + observed: str + required: str + diagnostic_code: str | None + + def to_json(self) -> dict[str, str | None]: + return { + "name": self.name, + "status": self.status, + "observed": self.observed, + "required": self.required, + "diagnostic_code": self.diagnostic_code, + } + + +def _check(name: str, passed: bool, observed: str, required: str) -> DoctorCheck: + return DoctorCheck( + name=name, + status="passed" if passed else "failed", + observed=observed, + required=required, + diagnostic_code=None if passed else "ACPY-DOCTOR-001", + ) + + +def run(arguments: object, sink: OutputSink) -> int: + checks: list[DoctorCheck] = [] + python_version = ( + f"{sys.version_info.major}.{sys.version_info.minor}." + f"{sys.version_info.micro}" + ) + checks.append( + _check("python", sys.version_info >= (3, 11), python_version, ">=3.11") + ) + checks.append(_check("contract_epoch", True, "0.3", "0.3")) + + try: + native = capabilities() + except (ImportError, RuntimeError, TypeError, ValueError) as error: + checks.append(_check("native_extension", False, str(error), "available")) + compiler_build_id = "unavailable" + runtime_build_id = "unavailable" + else: + compiler_build_id = native.compiler_build_id + runtime_build_id = native.runtime_build_id + checks.append(_check("native_extension", True, "available", "available")) + checks.append( + _check( + "llvm_mlir", + "llvm-22.1.8" in compiler_build_id, + compiler_build_id, + "LLVM/MLIR 22.1.8", + ) + ) + checks.append( + _check( + "gfsim_source_contract", + "cxx20" in runtime_build_id, + runtime_build_id, + "gfsim-cxx20@0.1", + ) + ) + + compiler = shutil.which("c++") + checks.append( + _check("cxx_compiler", compiler is not None, compiler or "missing", "C++20") + ) + try: + catalog = standard_library_catalog() + catalog_identity = f"{catalog.get('catalog')}@{catalog.get('version')}" + except (OSError, TypeError, ValueError) as error: + catalog_identity = str(error) + checks.append( + _check( + "standard_library", + catalog_identity == "ac@0.1", + catalog_identity, + "ac@0.1", + ) + ) + canonical = canonical_json_bytes({"epoch": "0.3"}) + checks.append( + _check( + "canonical_json", + canonical == b'{"epoch":"0.3"}', + canonical.decode("utf-8"), + 'RFC 8785 {"epoch":"0.3"}', + ) + ) + passed = all(check.status == "passed" for check in checks) + document = { + "schema": "agentic-circuit-doctor-result", + "version": "0.1", + "contract_epoch": "0.3", + "status": "passed" if passed else "failed", + "checks": [check.to_json() for check in checks], + } + sink.result(document, human=f"doctor: {document['status']}") + return 0 if passed else 3 diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/elaborate.py b/components/agentic-circuit/src/agentic_circuit/_commands/elaborate.py new file mode 100644 index 00000000..e5eb2e6e --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/elaborate.py @@ -0,0 +1,81 @@ +"""Deterministic frontend artifact publication.""" + +from __future__ import annotations + +from pathlib import Path + +from .._canonical_json import sha256_bytes +from .._diagnostics import Diagnostic +from .._native_api import NativeRequest, run_native_compiler +from .._output import OutputSink +from .._staging import ArtifactStage +from .._workspace import UserInputError, WorkspaceConfig +from .check import _has_errors, capture + + +def _output_path(arguments: object) -> Path: + value = getattr(arguments, "output", None) + if value is None: + raise UserInputError( + Diagnostic( + stage="elaborate", + code="ACPY-CLI-OUTPUT", + severity="error", + message="elaborate requires -o/--output", + ) + ) + path = Path(value) + return path.resolve() if path.is_absolute() else (Path.cwd() / path).resolve() + + +def run(arguments: object, workspace: WorkspaceConfig, sink: OutputSink) -> int: + frontend = capture(arguments, workspace) + if _has_errors(frontend.diagnostics): + sink.diagnostics(frontend.diagnostics) + return 2 + emit = getattr(arguments, "emit") + data = frontend.acpy if emit == "acpy" else frontend.acir + if data is None: + sink.diagnostics( + ( + Diagnostic( + stage="elaborate", + code="ACPY-VERIFY-001", + severity="error", + message=f"frontend produced no {emit} artifact", + ), + ) + ) + return 2 + if emit == "acir": + from .build import _binding_registry + + native = run_native_compiler( + NativeRequest( + acir=data, + stop_after="acir-verify", + emits=(), + options=(("binding_registry", _binding_registry(workspace.component_roots)),), + ) + ) + if _has_errors(native.diagnostics): + sink.diagnostics(native.diagnostics) + return 2 + output = _output_path(arguments) + relative = output.name + with ArtifactStage(output.parent, expected=(relative,)) as stage: + stage.write_bytes(relative, data) + stage.commit(allow_replace=(relative,) if output.exists() else ()) + fingerprint = sha256_bytes(data) + sink.result( + { + "schema": "agentic-circuit-elaborate-result", + "version": "0.1", + "contract_epoch": "0.3", + "emit": emit, + "path": output.as_posix(), + "sha256": fingerprint, + }, + human=f"wrote {output}", + ) + return 0 diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/explain.py b/components/agentic-circuit/src/agentic_circuit/_commands/explain.py new file mode 100644 index 00000000..fe7c6ac6 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/explain.py @@ -0,0 +1,37 @@ +"""Stable diagnostic explanation lookup.""" + +from __future__ import annotations + +from .._capabilities import diagnostic_catalog +from .._diagnostics import Diagnostic +from .._output import OutputSink +from .._workspace import UserInputError + + +def run(arguments: object, sink: OutputSink) -> int: + code = getattr(arguments, "code") + entries = diagnostic_catalog().get("entries") + if type(entries) is not list: + raise ValueError("packaged diagnostic catalog is invalid") + matches = [ + item + for item in entries + if type(item) is dict and item.get("code") == code + ] + if len(matches) != 1: + raise UserInputError( + Diagnostic( + stage="explain", + code="ACPY-SCHEMA-001", + severity="error", + message=f"diagnostic code is unknown: {code}", + ) + ) + document = { + "schema": "agentic-circuit-diagnostic-explanation", + "version": "0.1", + "contract_epoch": "0.3", + **matches[0], + } + sink.result(document, human=f"{code}: {matches[0]['rule']}") + return 0 diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/init.py b/components/agentic-circuit/src/agentic_circuit/_commands/init.py new file mode 100644 index 00000000..41cb7cff --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/init.py @@ -0,0 +1,66 @@ +"""Specification-only workspace initialization.""" + +from __future__ import annotations + +from pathlib import Path, PurePosixPath + +from .._diagnostics import Diagnostic +from .._output import OutputSink +from .._package_data import resource_directory +from .._staging import ArtifactStage +from .._workspace import UserInputError + +_FILE_NAMES = ("agentic-circuit.toml", "architecture.py") + + +def _files() -> dict[str, str]: + templates = resource_directory("resources") / "templates" + return { + name: templates.joinpath(name).read_text(encoding="utf-8") + for name in _FILE_NAMES + } + + +def _error(code: str, message: str) -> UserInputError: + return UserInputError( + Diagnostic(stage="init", code=code, severity="error", message=message) + ) + + +def run(arguments: object, sink: OutputSink) -> int: + files = _files() + destination = Path(getattr(arguments, "directory", ".")).resolve() + force_values = tuple(getattr(arguments, "force", ()) or ()) + force = {PurePosixPath(item).as_posix() for item in force_values} + unknown_force = sorted(force - set(files)) + if unknown_force: + raise _error("ACPY-INIT-002", f"unknown init target: {unknown_force[0]}") + conflicts = sorted( + name + for name in files + if destination.joinpath(name).exists() and name not in force + ) + if conflicts: + raise _error( + "ACPY-INIT-001", + f"init target already exists and is not forced: {conflicts[0]}", + ) + + result = { + "schema": "agentic-circuit-init-result", + "version": "0.1", + "contract_epoch": "0.3", + "directory": destination.as_posix(), + "files": sorted(files), + "dry_run": bool(getattr(arguments, "dry_run", False)), + } + if result["dry_run"]: + sink.result(result, human=f"Would initialize {destination}") + return 0 + + with ArtifactStage(destination, expected=files) as stage: + for name, contents in sorted(files.items()): + stage.write_text(name, contents) + stage.commit(allow_replace=force) + sink.result(result, human=f"Initialized {destination}") + return 0 diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/inspect.py b/components/agentic-circuit/src/agentic_circuit/_commands/inspect.py new file mode 100644 index 00000000..456fa2b0 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/inspect.py @@ -0,0 +1,184 @@ +"""Read-only deterministic architecture inspection command.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path, PurePosixPath +from typing import NoReturn + +from .._canonical_json import canonical_json_bytes, sha256_bytes +from .._diagnostics import Diagnostic +from .._inspect import ( + InspectionError, + InspectionRequest, + inspect_build, + inspect_model, + render_dot, + render_text, +) +from .._output import OutputSink +from .._workspace import UserInputError, WorkspaceConfig +from .check import _has_errors, capture + + +_BUILD_MANIFEST_KEYS = { + "schema", + "version", + "contract_epoch", + "project", + "system", + "source_files", + "normalized_acir_sha256", + "compiler", + "pass_pipeline", + "providers", + "component_specializations", + "protocol_identities", + "artifacts", + "validation_gates", + "build_profile", + "instrumentation_layers", + "specialization_inputs", + "build_fingerprint", +} +_FINGERPRINT = re.compile(r"^sha256:[0-9a-f]{64}$") + + +def _fail(message: str) -> NoReturn: + raise UserInputError( + Diagnostic( + stage="inspect", + code="ACPY-INSPECT-001", + severity="error", + message=message, + ) + ) + + +def _relative_path(value: object) -> PurePosixPath: + if type(value) is not str: + _fail("current build pointer path is invalid") + path = PurePosixPath(value) + if ( + path.is_absolute() + or not path.parts + or any(part in ("", ".", "..") for part in path.parts) + or "\\" in value + ): + _fail("current build pointer path is invalid") + return path + + +def _read_json(path: Path, label: str) -> dict[str, object]: + try: + if path.is_symlink() or not path.is_file(): + _fail(f"{label} is not a regular file") + data = path.read_bytes() + value = json.loads(data.decode("utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + _fail(f"{label} is invalid: {error}") + if type(value) is not dict: + _fail(f"{label} must be a JSON object") + try: + if canonical_json_bytes(value) != data: + _fail(f"{label} is not canonical") + except ValueError as error: + _fail(f"{label} is not canonical: {error}") + return value + + +def _current_build( + workspace: WorkspaceConfig, system: str +) -> dict[str, object] | None: + for root in (workspace.build_root / system, workspace.build_root): + pointer_path = root / "current.json" + if not pointer_path.exists(): + continue + pointer = _read_json(pointer_path, "current build pointer") + if set(pointer) != {"build_fingerprint", "path"}: + _fail("current build pointer has unknown fields") + fingerprint = pointer["build_fingerprint"] + if type(fingerprint) is not str or not _FINGERPRINT.fullmatch(fingerprint): + _fail("current build pointer fingerprint is invalid") + relative = _relative_path(pointer["path"]) + if relative != PurePosixPath("builds") / fingerprint: + _fail("current build pointer path does not match its fingerprint") + manifest_path = root.joinpath(*relative.parts, "build-manifest.json") + if not manifest_path.resolve().is_relative_to(root.resolve()): + _fail("current build pointer escapes its build root") + manifest = _read_json(manifest_path, "current build manifest") + if ( + set(manifest) != _BUILD_MANIFEST_KEYS + or manifest.get("schema") != "agentic-circuit-build-manifest" + or manifest.get("version") != "0.1" + or manifest.get("contract_epoch") != "0.3" + or manifest.get("build_fingerprint") != pointer["build_fingerprint"] + ): + _fail("current build manifest identity does not match its pointer") + artifacts = manifest.get("artifacts") + if type(artifacts) is not list: + _fail("current build manifest artifacts are invalid") + for artifact in artifacts: + if type(artifact) is not dict or set(artifact) != {"path", "kind", "sha256"}: + _fail("current build manifest artifact identity is invalid") + artifact_path = _relative_path(artifact["path"]) + artifact_hash = artifact["sha256"] + if type(artifact_hash) is not str or not _FINGERPRINT.fullmatch(artifact_hash): + _fail("current build manifest artifact hash is invalid") + resolved = manifest_path.parent.joinpath(*artifact_path.parts) + try: + if ( + not resolved.resolve().is_relative_to(manifest_path.parent.resolve()) + or resolved.is_symlink() + or not resolved.is_file() + ): + _fail("current build artifact is not a regular file") + if sha256_bytes(resolved.read_bytes()) != artifact_hash: + _fail("current build artifact hash does not match") + except OSError as error: + _fail(f"cannot verify current build artifact: {error}") + return manifest + return None + + +def run(arguments: object, workspace: WorkspaceConfig, sink: OutputSink) -> int: + kind = getattr(arguments, "view") + path = getattr(arguments, "path", None) + selected_format = getattr(arguments, "format", None) + if kind == "artifacts" and path is not None: + _fail("artifact inspection does not accept --path") + if selected_format == "dot" and kind != "graph": + _fail("--format dot requires inspect graph") + if selected_format == "dot" and sink.format != "text": + _fail("--format dot cannot be combined with structured output") + if selected_format == "text" and sink.format != "text": + _fail("--format text cannot be combined with structured output") + if selected_format == "json": + sink.format = "json" + system = getattr(arguments, "system", None) or workspace.default_system + try: + request = InspectionRequest( + kind=kind, + system=system, + path=path, + format=selected_format or ("json" if sink.format != "text" else "text"), + ) + build_manifest = _current_build(workspace, system) if kind == "artifacts" else None + if build_manifest is not None: + result = inspect_build(build_manifest, request) + else: + frontend = capture(arguments, workspace) + if _has_errors(frontend.diagnostics): + sink.diagnostics(frontend.diagnostics) + return 2 + if frontend.acpy is None or frontend.acir is None: + _fail("frontend produced incomplete inspection artifacts") + result = inspect_model(frontend.acpy, frontend.acir, request) + except InspectionError as error: + _fail(str(error)) + if selected_format == "dot": + sink.stdout.write(render_dot(result)) + else: + sink.result(result.to_json(), human=render_text(result)) + return 0 diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/run.py b/components/agentic-circuit/src/agentic_circuit/_commands/run.py new file mode 100644 index 00000000..40e5169d --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/run.py @@ -0,0 +1,155 @@ +"""End-to-end run and immutable replay command.""" + +from __future__ import annotations + +import re +from pathlib import Path +from typing import NoReturn + +from .._diagnostics import Diagnostic +from .._output import OutputSink +from .._run import RunFailure, RunOptions, RunPublication, execute_run, replay_run +from .._workspace import UserInputError, WorkspaceConfig +from .build import build_publication + + +_DOMAIN = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$") + + +def _fail(code: str, message: str) -> NoReturn: + raise UserInputError( + Diagnostic(stage="runtime", code=code, severity="error", message=message) + ) + + +def _output(arguments: object) -> Path: + value = getattr(arguments, "output_dir", None) + if value is None: + _fail("ACRUN-INPUT-001", "run requires --output-dir") + output = Path(value) + if output.is_symlink(): + _fail("ACRUN-INPUT-001", "run output must not be a symlink") + return output.resolve() + + +def _domain_limits(values: list[str]) -> tuple[tuple[str, int], ...]: + limits: dict[str, int] = {} + for value in values: + name, separator, maximum_text = value.partition("=") + try: + maximum = int(maximum_text) + except ValueError: + maximum = 0 + if ( + not separator + or not _DOMAIN.fullmatch(name) + or not 1 <= maximum <= (1 << 64) - 1 + or name in limits + ): + _fail( + "ACRUN-INPUT-001", + "--max-domain-cycles requires unique DOMAIN=POSITIVE values", + ) + limits[name] = maximum + return tuple(sorted(limits.items())) + + +def _trace(arguments: object, workspace: WorkspaceConfig) -> Path: + value = getattr(arguments, "trace", None) + trace = Path(value) if value is not None else workspace.default_trace + if trace is None: + _fail("ACRUN-INPUT-001", "run requires --trace or [run].default_trace") + if trace.is_symlink(): + _fail("ACRUN-INPUT-001", "trace must not be a symlink") + resolved = trace.resolve() + if not any(resolved.is_relative_to(root) for root in workspace.trace_roots): + _fail("ACRUN-INPUT-001", "trace is outside the configured trace roots") + return resolved + + +def _options(arguments: object, workspace: WorkspaceConfig) -> RunOptions: + seed = getattr(arguments, "seed", None) + return RunOptions( + trace=_trace(arguments, workspace), + output_directory=_output(arguments), + seed=0 if seed is None else seed, + deadlock_window=getattr(arguments, "deadlock_window", None), + max_ticks=getattr(arguments, "max_ticks", None), + max_domain_cycles=_domain_limits( + list(getattr(arguments, "max_domain_cycles", ())) + ), + stats_format=getattr(arguments, "stats_format", None) or "json", + event_log=getattr(arguments, "event_log", None) or "disabled", + termination_kind=( + "complete" if getattr(arguments, "expect_termination", False) else "any" + ), + ) + + +def _result(publication: RunPublication) -> dict[str, object]: + return { + "schema": "agentic-circuit-run-command-result", + "version": "0.1", + "contract_epoch": "0.3", + "status": publication.status, + "termination_reason": publication.termination_reason, + "directory": publication.directory.as_posix(), + "manifest": publication.manifest.as_posix(), + "result": publication.result.as_posix(), + } + + +def _reject_replay_overrides(arguments: object) -> None: + overrides = ( + ("architecture", getattr(arguments, "architecture", None)), + ("--trace", getattr(arguments, "trace", None)), + ("--seed", getattr(arguments, "seed", None)), + ("--deadlock-window", getattr(arguments, "deadlock_window", None)), + ("--max-ticks", getattr(arguments, "max_ticks", None)), + ("--max-domain-cycles", getattr(arguments, "max_domain_cycles", [])), + ("--stats-format", getattr(arguments, "stats_format", None)), + ("--event-log", getattr(arguments, "event_log", None)), + ("--expect-termination", getattr(arguments, "expect_termination", False)), + ("--jobs", getattr(arguments, "jobs", None)), + ("--project", getattr(arguments, "project", None)), + ("--system", getattr(arguments, "system", None)), + ) + for name, value in overrides: + if value not in (None, False, []): + _fail("ACRUN-REPLAY-001", f"replay rejects ambient override {name}") + + +def run( + arguments: object, + workspace: WorkspaceConfig | None, + sink: OutputSink, +) -> int: + try: + replay = getattr(arguments, "replay_manifest", None) + if replay is not None: + _reject_replay_overrides(arguments) + publication = replay_run(Path(replay), _output(arguments)) + else: + if workspace is None: + raise RuntimeError("normal run requires a workspace") + options = _options(arguments, workspace) + system = getattr(arguments, "system", None) or workspace.default_system + attempt = build_publication( + arguments, + workspace, + output=workspace.build_root / system, + ) + if attempt.publication is None: + sink.diagnostics(attempt.diagnostics) + return attempt.exit_code + publication = execute_run(attempt.publication, options) + sink.result( + _result(publication), + human=( + f"run {publication.status}: {publication.termination_reason}" + ), + ) + return publication.exit_code + except RunFailure as error: + sink.diagnostics((error.diagnostic,)) + return error.exit_code diff --git a/components/agentic-circuit/src/agentic_circuit/_commands/schema.py b/components/agentic-circuit/src/agentic_circuit/_commands/schema.py new file mode 100644 index 00000000..b39e1114 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_commands/schema.py @@ -0,0 +1,151 @@ +"""Read-only packaged schema and capability queries.""" + +from __future__ import annotations + +from typing import NoReturn + +from .._capabilities import ( + block_spec, + capability_document, + diagnostic_catalog, + load_json, + opcode_catalog, + schema_root, + standard_library_catalog, +) +from .._canonical_json import JsonValue +from .._diagnostics import Diagnostic +from .._output import OutputSink +from .._workspace import UserInputError + + +def _fail(message: str) -> NoReturn: + raise UserInputError( + Diagnostic( + stage="schema", + code="ACPY-SCHEMA-001", + severity="error", + message=message, + ) + ) + + +def _catalog_entries() -> list[dict[str, JsonValue]]: + entries = standard_library_catalog().get("entries") + if type(entries) is not list or not all(type(item) is dict for item in entries): + raise ValueError("packaged standard-library catalog is invalid") + return entries + + +def _component_records(kind: str) -> list[tuple[str, dict[str, JsonValue]]]: + records: list[tuple[str, dict[str, JsonValue]]] = [] + for entry in _catalog_entries(): + path = entry.get("schema_path") + name = entry.get("canonical_name") + if type(path) is not str or type(name) is not str: + raise ValueError("packaged catalog entry is invalid") + record = load_json(schema_root() / path.removeprefix("schemas/")) + is_protocol = record.get("family") == "protocol" + if (kind == "protocol") == is_protocol: + records.append((name, record)) + records.sort(key=lambda item: item[0]) + return records + + +def _select( + records: list[tuple[str, dict[str, JsonValue]]], name: str +) -> dict[str, JsonValue]: + candidates = [record for identity, record in records if name == identity] + if len(candidates) != 1: + _fail(f"schema identity is unknown: {name}") + return candidates[0] + + +def _listing(kind: str, names: list[str]) -> dict[str, JsonValue]: + return { + "schema": "agentic-circuit-schema-list", + "version": "0.1", + "contract_epoch": "0.3", + "kind": kind, + "items": sorted(names), + } + + +def _diagnostics(name: str | None) -> dict[str, JsonValue]: + entries = diagnostic_catalog().get("entries") + if type(entries) is not list or not all(type(item) is dict for item in entries): + raise ValueError("packaged diagnostic catalog is invalid") + if name is None: + return _listing("diagnostic", [str(item["code"]) for item in entries]) + matches = [item for item in entries if item.get("code") == name] + if len(matches) != 1: + _fail(f"diagnostic code is unknown: {name}") + return matches[0] + + +def _opcodes(name: str | None) -> dict[str, JsonValue]: + entries = opcode_catalog().get("entries") + if type(entries) is not list or not all(type(item) is dict for item in entries): + raise ValueError("packaged official opcode catalog is invalid") + if name is None: + return _listing("opcode", [str(item["operation"]) for item in entries]) + matches = [item for item in entries if item.get("operation") == name] + if len(matches) != 1: + _fail(f"opcode identity is unknown: {name}") + return matches[0] + + +def _blocks(name: str | None) -> dict[str, JsonValue]: + entries = block_spec().get("blocks") + if type(entries) is not list or not all(type(item) is dict for item in entries): + raise ValueError("packaged high-level BlockSpec is invalid") + if name is None: + return _listing("block", [str(item["operation"]) for item in entries]) + matches = [item for item in entries if item.get("operation") == name] + if len(matches) != 1: + _fail(f"block identity is unknown: {name}") + return matches[0] + + +def run(arguments: object, sink: OutputSink) -> int: + kind = getattr(arguments, "kind") + name = getattr(arguments, "name", None) + if kind == "capabilities": + if name is not None: + _fail("capabilities does not accept a name") + document = capability_document().to_json() + elif kind in ("component", "protocol"): + records = _component_records(kind) + document = ( + _listing(kind, [identity for identity, _ in records]) + if name is None + else _select(records, name) + ) + elif kind == "diagnostic": + document = _diagnostics(name) + elif kind == "opcode": + document = _opcodes(name) + elif kind == "block": + document = _blocks(name) + elif kind == "interface": + names = ["ac.Stream"] + if name is None: + document = _listing(kind, names) + elif name == "ac.Stream": + document = { + "schema": "agentic-circuit-interface-definition", + "version": "0.1", + "contract_epoch": "0.3", + "canonical_name": "ac.Stream", + "availability": "available", + } + else: + _fail(f"interface identity is unknown: {name}") + elif kind == "packet": + if name is not None: + _fail(f"packet identity is unknown: {name}") + document = _listing(kind, []) + else: + _fail(f"schema kind is unknown: {kind}") + sink.result(document, human=str(document)) + return 0 diff --git a/components/agentic-circuit/src/agentic_circuit/_data/__init__.py b/components/agentic-circuit/src/agentic_circuit/_data/__init__.py new file mode 100644 index 00000000..28dd70ec --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_data/__init__.py @@ -0,0 +1,6 @@ +"""Installed Agentic Circuit data resources.""" + +from pkgutil import extend_path + + +__path__ = extend_path(__path__, __name__) diff --git a/components/agentic-circuit/src/agentic_circuit/_definitions.py b/components/agentic-circuit/src/agentic_circuit/_definitions.py new file mode 100644 index 00000000..e5ba09ce --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_definitions.py @@ -0,0 +1,143 @@ +"""Immutable metadata for Python architecture definitions.""" + +from __future__ import annotations + +import inspect +from dataclasses import dataclass +from pathlib import Path +from typing import Callable, Literal, TypeAlias, TypeVar, overload + + +DefinitionKind: TypeAlias = Literal[ + "system", + "module", + "extern_module", + "generated_module", + "struct", + "packet", + "transaction", + "protocol", + "interface", + "process", +] +F = TypeVar("F", bound=Callable[..., object]) + + +@dataclass(frozen=True, slots=True) +class Definition: + """A captured definition registered without executing its body.""" + + kind: DefinitionKind + function: Callable[..., object] + qualified_name: str + explicit_options: tuple[tuple[str, object], ...] + module_name: str + source_file: str | None + source_line: int | None + + def __repr__(self) -> str: + return ( + f"Definition(kind={self.kind!r}, qualified_name={self.qualified_name!r})" + ) + + @property + def __signature__(self) -> inspect.Signature: + return inspect.signature(self.function) + + @property + def __name__(self) -> str: + return self.function.__name__ + + def __call__(self, *args: object, **kwargs: object) -> "PendingDefinitionCall": + try: + bound = self.__signature__.bind(*args, **kwargs) + except TypeError as error: + raise TypeError(f"ACPY-CALL-003: {error}") from error + bound.apply_defaults() + return PendingDefinitionCall(self, tuple(bound.arguments.items())) + + +@dataclass(frozen=True, slots=True) +class PendingDefinitionCall: + definition: Definition + arguments: tuple[tuple[str, object], ...] + + +@overload +def _decorate(definition_kind: DefinitionKind, function: F) -> Definition: ... + + +@overload +def _decorate( + definition_kind: DefinitionKind, function: None = None, **options: object +) -> Callable[[F], Definition]: ... + + +def _decorate( + definition_kind: DefinitionKind, function: F | None = None, **options: object +) -> Definition | Callable[[F], Definition]: + def apply(target: F) -> Definition: + module_name = getattr(target, "__module__", "") + source_file = inspect.getsourcefile(target) + source_line: int | None + code = getattr(target, "__code__", None) + if code is not None: + source_line = code.co_firstlineno + else: + try: + _, source_line = inspect.getsourcelines(target) + except (OSError, TypeError): + source_line = None + return Definition( + kind=definition_kind, + function=target, + qualified_name=target.__qualname__, + explicit_options=tuple(sorted(options.items())), + module_name=module_name, + source_file=( + str(Path(source_file).resolve()) if source_file is not None else None + ), + source_line=source_line, + ) + + return apply(function) if function is not None else apply + + +def system(function: F | None = None, **options: object): + return _decorate("system", function, **options) + + +def module(function: F | None = None, **options: object): + return _decorate("module", function, **options) + + +def extern_module(function: F | None = None, **options: object): + return _decorate("extern_module", function, **options) + + +def generated_module(function: F | None = None, **options: object): + return _decorate("generated_module", function, **options) + + +def struct(function: F | None = None, **options: object): + return _decorate("struct", function, **options) + + +def packet(function: F | None = None, **options: object): + return _decorate("packet", function, **options) + + +def transaction(function: F | None = None, **options: object): + return _decorate("transaction", function, **options) + + +def protocol(function: F | None = None, **options: object): + return _decorate("protocol", function, **options) + + +def interface(function: F | None = None, **options: object): + return _decorate("interface", function, **options) + + +def process(function: F | None = None, **options: object): + return _decorate("process", function, **options) diff --git a/components/agentic-circuit/src/agentic_circuit/_diagnostics.py b/components/agentic-circuit/src/agentic_circuit/_diagnostics.py new file mode 100644 index 00000000..3a0671d1 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_diagnostics.py @@ -0,0 +1,152 @@ +"""Immutable frontend diagnostics matching diagnostic.schema.json.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Literal + +from ._canonical_json import JsonValue + + +Severity = Literal["error", "warning", "note"] +_CODE = re.compile(r"^AC(PY|ELAB|IR-[A-Z]+|LOWER|BUILD|TRACE|RUN)-[A-Z0-9-]+$") + + +@dataclass(frozen=True, order=True, slots=True) +class SourceSpan: + file: str + start_line: int + start_column: int + end_line: int + end_column: int + + def __post_init__(self) -> None: + if not self.file: + raise ValueError("source file must not be empty") + if min( + self.start_line, + self.start_column, + self.end_line, + self.end_column, + ) < 1: + raise ValueError("source positions are one-based") + if (self.end_line, self.end_column) < ( + self.start_line, + self.start_column, + ): + raise ValueError("source span end precedes its start") + + def to_json(self) -> dict[str, JsonValue]: + return { + "file": self.file, + "line": self.start_line, + "column": self.start_column, + } + + +@dataclass(frozen=True, slots=True) +class RelatedLocation: + message: str + source: SourceSpan | None = None + object_path: str | None = None + + def __post_init__(self) -> None: + if not self.message: + raise ValueError("related-location message must not be empty") + + def to_json(self) -> dict[str, JsonValue]: + return { + "message": self.message, + "source": self.source.to_json() if self.source is not None else None, + "object_path": self.object_path, + } + + +@dataclass(frozen=True, slots=True) +class FixIt: + message: str + + def __post_init__(self) -> None: + if not self.message: + raise ValueError("fix-it message must not be empty") + + def to_json(self) -> dict[str, JsonValue]: + return {"message": self.message} + + +@dataclass(frozen=True, slots=True) +class Diagnostic: + stage: str + code: str + severity: Severity + message: str + source: SourceSpan | None = None + object_path: str | None = None + expected: JsonValue = None + actual: JsonValue = None + related: tuple[RelatedLocation, ...] = () + fixits: tuple[FixIt, ...] = () + schema: str = "agentic-circuit-diagnostic" + version: str = "0.1" + contract_epoch: str = "0.3" + + def __post_init__(self) -> None: + if ( + self.schema, + self.version, + self.contract_epoch, + ) != ("agentic-circuit-diagnostic", "0.1", "0.3"): + raise ValueError("diagnostic schema identity is fixed at epoch 0.3") + if not self.stage: + raise ValueError("diagnostic stage must not be empty") + if not _CODE.fullmatch(self.code): + raise ValueError(f"invalid diagnostic code: {self.code!r}") + if self.severity not in ("error", "warning", "note"): + raise ValueError(f"invalid diagnostic severity: {self.severity!r}") + if not self.message: + raise ValueError("diagnostic message must not be empty") + + def sort_key(self) -> tuple[object, ...]: + source = self.source + return ( + source is None, + source.file if source is not None else "", + source.start_line if source is not None else 0, + source.start_column if source is not None else 0, + self.object_path or "", + self.code, + self.message, + ) + + def to_json(self) -> dict[str, JsonValue]: + return { + "schema": self.schema, + "version": self.version, + "contract_epoch": self.contract_epoch, + "code": self.code, + "stage": self.stage, + "severity": self.severity, + "source": self.source.to_json() if self.source is not None else None, + "object_path": self.object_path, + "message": self.message, + "expected": self.expected, + "actual": self.actual, + "related": [location.to_json() for location in self.related], + "fixits": [fixit.to_json() for fixit in self.fixits], + } + + +class DiagnosticBag: + """Mutable collector with deterministic immutable snapshots.""" + + __slots__ = ("_items",) + + def __init__(self) -> None: + self._items: list[Diagnostic] = [] + + def add(self, diagnostic: Diagnostic) -> None: + self._items.append(diagnostic) + + def freeze(self) -> tuple[Diagnostic, ...]: + return tuple(sorted(self._items, key=Diagnostic.sort_key)) diff --git a/components/agentic-circuit/src/agentic_circuit/_frontend.py b/components/agentic-circuit/src/agentic_circuit/_frontend.py new file mode 100644 index 00000000..c5190955 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_frontend.py @@ -0,0 +1,329 @@ +"""Composition boundary for deterministic definition capture.""" + +from __future__ import annotations + +import ast +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path + +from ._acpy import AcpyDocument +from ._definitions import Definition +from ._diagnostics import Diagnostic, DiagnosticBag, SourceSpan +from ._schemas import SchemaRegistry +from ._source import DefinitionSite, SourceUnit, load_source_unit +from ._static_eval import StaticValue, evaluate_static, StaticEnvironment + + +@dataclass(frozen=True, slots=True) +class CaptureRequest: + entry: Path + workspace: Path + system: str + static_arguments: tuple[tuple[str, StaticValue], ...] = () + + +@dataclass(frozen=True, slots=True) +class FrontendResult: + document: AcpyDocument | None + acir: str | None + diagnostics: tuple[Diagnostic, ...] + + +@dataclass(frozen=True, slots=True) +class CapturedProgram: + source: SourceUnit + definitions: tuple[Definition, ...] + selected_system: Definition | None + registry: SchemaRegistry + static_arguments: tuple[tuple[str, StaticValue], ...] = () + diagnostics: tuple[Diagnostic, ...] = () + + +def _diagnostic( + bag: DiagnosticBag, code: str, message: str, site: DefinitionSite | None = None +) -> None: + bag.add( + Diagnostic( + stage="definition-capture", + code=code, + severity="error", + message=message, + source=site.span if site is not None else None, + ) + ) + + +def _decorator_options(site: DefinitionSite, kind: str) -> tuple[tuple[str, object], ...] | None: + for decorator in site.node.decorator_list: + candidate = decorator.func if isinstance(decorator, ast.Call) else decorator + if not isinstance(candidate, ast.Name) or candidate.id != kind: + continue + if not isinstance(decorator, ast.Call): + return () + if decorator.args or any(keyword.arg is None for keyword in decorator.keywords): + return None + options: list[tuple[str, object]] = [] + try: + for keyword in decorator.keywords: + assert keyword.arg is not None + options.append( + ( + keyword.arg, + evaluate_static(keyword.value, StaticEnvironment({})), + ) + ) + except ValueError: + return None + return tuple(sorted(options)) + return None + + +def _all_arguments(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.arg]: + return [ + *node.args.posonlyargs, + *node.args.args, + *node.args.kwonlyargs, + *([node.args.vararg] if node.args.vararg is not None else []), + *([node.args.kwarg] if node.args.kwarg is not None else []), + ] + + +def _symbolic_annotation(annotation: ast.expr) -> bool: + value = annotation.value if isinstance(annotation, ast.Subscript) else annotation + return isinstance(value, ast.Name) and value.id in {"Flow", "Endpoint", "ResourceRef"} + + +def _validate_signature( + definition: Definition, site: DefinitionSite, diagnostics: DiagnosticBag +) -> None: + if not isinstance(site.node, (ast.FunctionDef, ast.AsyncFunctionDef)): + _diagnostic( + diagnostics, + "ACPY-TYPE-DEFINITION", + f"{definition.qualified_name!r} must decorate a function", + site, + ) + return + if site.node.args.vararg is not None or site.node.args.kwarg is not None: + _diagnostic( + diagnostics, + "ACPY-TYPE-SIGNATURE", + f"{definition.qualified_name!r} cannot use variadic public parameters", + site, + ) + for argument in _all_arguments(site.node): + if argument.annotation is None: + _diagnostic( + diagnostics, + "ACPY-TYPE-ANNOTATION", + f"public parameter {argument.arg!r} requires an annotation", + site, + ) + elif definition.kind == "system" and _symbolic_annotation(argument.annotation): + _diagnostic( + diagnostics, + "ACPY-TYPE-SYSTEM", + f"system parameter {argument.arg!r} must be static", + site, + ) + defaults = [ + *site.node.args.defaults, + *(default for default in site.node.args.kw_defaults if default is not None), + ] + for default in defaults: + try: + evaluate_static(default, StaticEnvironment({})) + except ValueError: + _diagnostic( + diagnostics, + "ACPY-STATIC-DEFAULT", + f"{definition.qualified_name!r} has a non-static default", + site, + ) + if site.node.returns is None: + _diagnostic( + diagnostics, + "ACPY-TYPE-RETURN", + f"{definition.qualified_name!r} requires a return annotation", + site, + ) + + +def _match_definitions( + namespace: Mapping[str, object], + unit: SourceUnit, + entry: Path, + diagnostics: DiagnosticBag, +) -> tuple[Definition, ...]: + sites = {(site.qualified_name, site.span.start_line): site for site in unit.definitions} + module_name = namespace.get("__name__") + found: list[Definition] = [] + seen: set[int] = set() + for value in namespace.values(): + if not isinstance(value, Definition) or id(value) in seen: + continue + seen.add(id(value)) + key = (value.qualified_name, value.source_line) + site = sites.get(key) + source_matches = ( + value.source_file is not None + and Path(value.source_file).resolve() == entry.resolve() + ) + options = _decorator_options(site, value.kind) if site is not None else None + if ( + site is None + or not source_matches + or value.module_name != module_name + or options != value.explicit_options + ): + _diagnostic( + diagnostics, + "ACPY-SYMBOL-DEFINITION", + f"registered definition {value.qualified_name!r} does not match source", + site, + ) + continue + _validate_signature(value, site, diagnostics) + found.append(value) + return tuple( + sorted(found, key=lambda item: (item.source_line or 0, item.qualified_name)) + ) + + +def capture_definitions( + request: CaptureRequest, + namespace: Mapping[str, object], + registry: SchemaRegistry, +) -> CapturedProgram: + unit = load_source_unit(request.entry, request.workspace) + diagnostics = DiagnosticBag() + definitions = _match_definitions(namespace, unit, request.entry, diagnostics) + candidates = [ + definition + for definition in definitions + if definition.kind == "system" and definition.qualified_name == request.system + ] + selected: Definition | None = None + if len(candidates) == 1: + selected = candidates[0] + elif not candidates: + _diagnostic( + diagnostics, + "ACPY-SYMBOL-SYSTEM", + f"system {request.system!r} was not found", + ) + else: + _diagnostic( + diagnostics, + "ACPY-SYMBOL-SYSTEM", + f"system {request.system!r} is ambiguous", + ) + return CapturedProgram( + source=unit, + definitions=definitions, + selected_system=selected, + registry=registry, + static_arguments=request.static_arguments, + diagnostics=diagnostics.freeze(), + ) + + +def construct_captured_process( + captured: CapturedProgram, definition: str, effects: object +): + """Construct one captured ``@process`` without executing its Python body.""" + + from ._process import EffectRegistry, ProcessConstructionError, construct_process + + if not isinstance(effects, EffectRegistry): + raise TypeError("effects must be an EffectRegistry") + matches = [ + site + for site in captured.source.definitions + if site.qualified_name == definition or site.name == definition + ] + if len(matches) != 1: + raise ProcessConstructionError( + "ACPY-PROCESS-001", + f"captured process {definition!r} is missing or ambiguous", + captured.source.definitions[0].span + if captured.source.definitions + else SourceSpan(captured.source.path, 1, 1, 1, 1), + ) + return construct_process(matches[0], effects) + + +def elaborate_frontend( + request: CaptureRequest, + namespace: Mapping[str, object], + schemas: SchemaRegistry, +) -> FrontendResult: + """Capture, normalize, verify, and lower one frontend architecture atomically.""" + + from ._lower_acir import build_verified_acpy, lower_to_acir + from ._normalize import normalize_program + from ._process import EffectDeclaration, EffectRegistry + + captured = capture_definitions(request, namespace, schemas) + if captured.diagnostics: + return FrontendResult(None, None, captured.diagnostics) + assert captured.selected_system is not None + options = dict(captured.selected_system.explicit_options) + root = options.get("root") + if type(root) is not str or not root: + diagnostic = Diagnostic( + stage="frontend", + code="ACPY-SYMBOL-SYSTEM", + severity="error", + message="selected system requires a non-empty static root option", + ) + return FrontendResult(None, None, (diagnostic,)) + program = normalize_program(captured, definition=root) + if program.diagnostics: + return FrontendResult(None, None, program.diagnostics) + + try: + effects = EffectRegistry( + ( + EffectDeclaration("wait_until", "suspension", suspension=True), + EffectDeclaration("wait_for", "suspension", suspension=True), + EffectDeclaration("await_event", "suspension", suspension=True), + EffectDeclaration("yield_sim", "suspension", suspension=True), + ) + ) + process_records = [] + process_programs = [] + for definition in captured.definitions: + if definition.kind != "process": + continue + process = construct_captured_process( + captured, definition.qualified_name, effects + ) + kind = dict(definition.explicit_options).get("kind", "control") + if kind not in {"control", "workload", "monitor"}: + raise ValueError( + f"ACPY-PROCESS-003: process {definition.qualified_name!r} " + "has invalid kind" + ) + process_programs.append(process) + process_records.append((process, kind)) + document = build_verified_acpy(captured, program, tuple(process_programs)) + artifact = lower_to_acir( + program, + document, + system_name=captured.selected_system.__name__, + processes=tuple(process_records), + ) + except ValueError as error: + message = str(error) + candidate = message.partition(":")[0] + code = candidate if candidate.startswith("ACPY-") else "ACPY-VERIFY-001" + diagnostic = Diagnostic( + stage="acir-lowering", + code=code, + severity="error", + message=message, + ) + return FrontendResult(None, None, (diagnostic,)) + return FrontendResult(document, artifact.text, ()) diff --git a/components/agentic-circuit/src/agentic_circuit/_inspect.py b/components/agentic-circuit/src/agentic_circuit/_inspect.py new file mode 100644 index 00000000..bce8c27f --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_inspect.py @@ -0,0 +1,412 @@ +"""Deterministic read-only views over verified frontend and build artifacts.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from types import MappingProxyType +from typing import Literal, Mapping, TypeAlias + +from ._canonical_json import JsonValue, canonical_json_bytes, sha256_bytes + + +InspectionKind: TypeAlias = Literal[ + "graph", + "hierarchy", + "ports", + "resources", + "address-map", + "protocols", + "specialization", + "artifacts", +] +InspectionFormat: TypeAlias = Literal["json", "dot", "text"] + + +@dataclass(frozen=True, slots=True) +class InspectionRequest: + kind: InspectionKind + system: str + path: str | None = None + format: InspectionFormat = "json" + + +@dataclass(frozen=True, slots=True) +class InspectionResult: + kind: InspectionKind + system: str + path: str | None + records: tuple[Mapping[str, JsonValue], ...] + + def to_json(self) -> dict[str, JsonValue]: + return { + "schema": "agentic-circuit-inspection", + "version": "0.1", + "contract_epoch": "0.3", + "kind": self.kind, + "system": self.system, + "path": self.path, + "records": [dict(record) for record in self.records], + } + + +class InspectionError(ValueError): + pass + + +def _document(data: bytes, label: str) -> dict[str, object]: + try: + value = json.loads(data) + except (UnicodeError, json.JSONDecodeError) as error: + raise InspectionError(f"{label} is invalid JSON: {error}") from error + if type(value) is not dict: + raise InspectionError(f"{label} must be a JSON object") + return value + + +def _properties(entity: dict[str, object]) -> dict[str, JsonValue]: + values = entity.get("properties") + if type(values) is not list: + raise InspectionError("ACPy entity properties are invalid") + result: dict[str, JsonValue] = {} + for item in values: + if type(item) is not dict or set(item) != {"name", "value"}: + raise InspectionError("ACPy entity property is invalid") + name = item["name"] + if type(name) is not str or name in result: + raise InspectionError("ACPy entity property name is invalid") + result[name] = item["value"] + return result + + +def _entity_name(entity: dict[str, object]) -> str | None: + properties = _properties(entity) + for key in ("name", "instance_name"): + value = properties.get(key) + if type(value) is str and value: + return value + return None + + +def _entities(acpy: dict[str, object]) -> tuple[dict[str, object], ...]: + if ( + acpy.get("schema") != "agentic-circuit-acpy" + or acpy.get("version") != "0.1" + or acpy.get("contract_epoch") != "0.3" + or type(acpy.get("entities")) is not list + ): + raise InspectionError("ACPy has an invalid envelope") + entities = tuple(acpy["entities"]) + if not all(type(entity) is dict for entity in entities): + raise InspectionError("ACPy entities are invalid") + expected = tuple(f"e{index}" for index in range(len(entities))) + if tuple(entity.get("id") for entity in entities) != expected: + raise InspectionError("ACPy entity IDs are not canonical") + return entities + + +def _hierarchy_paths( + entities: tuple[dict[str, object], ...], +) -> tuple[dict[str, str], dict[str, str]]: + object_kinds = {"module", "scope", "call", "collection", "process"} + paths: dict[str, str] = {} + owners: dict[str, str] = {} + for entity in entities: + identifier = entity["id"] + assert isinstance(identifier, str) + parent = entity.get("parent") + inherited = owners.get(parent) if isinstance(parent, str) else None + kind = entity.get("kind") + if kind not in object_kinds: + if inherited is not None: + owners[identifier] = inherited + continue + name = _entity_name(entity) + if name is None: + raise InspectionError(f"hierarchy entity {identifier} has no stable name") + path = f"{inherited}.{name}" if inherited else name + if path in paths.values(): + raise InspectionError(f"duplicate hierarchy path {path!r}") + paths[identifier] = path + owners[identifier] = path + return paths, owners + + +def _record(**values: JsonValue) -> Mapping[str, JsonValue]: + return MappingProxyType(dict(values)) + + +def _selected(path: str, requested: str | None) -> bool: + return requested is None or path == requested or path.startswith(requested + ".") + + +def _hierarchy_records( + entities: tuple[dict[str, object], ...], + paths: dict[str, str], + requested: str | None, +) -> tuple[Mapping[str, JsonValue], ...]: + records = [] + for entity in entities: + identifier = entity["id"] + if identifier not in paths or not _selected(paths[identifier], requested): + continue + schema = entity.get("schema_ref") + records.append( + _record( + id=identifier, + kind=str(entity["kind"]), + path=paths[identifier], + schema=(schema.get("identity") if type(schema) is dict else None), + ) + ) + return tuple(sorted(records, key=lambda item: str(item["path"]))) + + +def _graph_records( + entities: tuple[dict[str, object], ...], + paths: dict[str, str], + owners: dict[str, str], + requested: str | None, +) -> tuple[Mapping[str, JsonValue], ...]: + nodes = [] + for entity in entities: + identifier = str(entity["id"]) + path = paths.get(identifier) + if path is None or not _selected(path, requested): + continue + nodes.append( + _record(record="node", id=identifier, path=path, kind=str(entity["kind"])) + ) + edges: set[tuple[str, str, str, str | None]] = set() + for entity in entities: + target = owners.get(str(entity["id"])) + if target is None or not _selected(target, requested): + continue + parent = entity.get("parent") + source = owners.get(parent) if isinstance(parent, str) else None + if source is not None and source != target and _selected(source, requested): + edges.add((source, target, "ownership", None)) + uses = entity.get("uses") + if type(uses) is list: + for used in uses: + source = owners.get(used) if isinstance(used, str) else None + if source is not None and source != target and _selected(source, requested): + edges.add((source, target, "data", None)) + edge_records = tuple( + _record(record="edge", source=source, target=target, kind=kind, port=port) + for source, target, kind, port in sorted(edges) + ) + return tuple(sorted(nodes, key=lambda item: str(item["path"]))) + edge_records + + +def _port_records( + entities: tuple[dict[str, object], ...], + owners: dict[str, str], + requested: str | None, +) -> tuple[Mapping[str, JsonValue], ...]: + directions = {"arg": "input", "result": "output", "bind": "binding"} + records = [] + for entity in entities: + direction = directions.get(str(entity.get("kind"))) + owner = owners.get(str(entity["id"])) + if direction is None or owner is None or not _selected(owner, requested): + continue + records.append( + _record( + id=str(entity["id"]), + path=owner, + name=_entity_name(entity) or str(entity["id"]), + direction=direction, + type=entity.get("type"), + ) + ) + return tuple(sorted(records, key=lambda item: (str(item["path"]), str(item["name"])))) + + +def _matching_records( + entities: tuple[dict[str, object], ...], + owners: dict[str, str], + requested: str | None, + tokens: tuple[str, ...], +) -> tuple[Mapping[str, JsonValue], ...]: + records = [] + for entity in entities: + owner = owners.get(str(entity["id"])) + if owner is None or not _selected(owner, requested): + continue + schema = entity.get("schema_ref") + schema_identity = schema.get("identity") if type(schema) is dict else None + searchable = " ".join( + ( + str(entity.get("kind", "")), + str(entity.get("type", "")), + str(schema_identity or ""), + " ".join(_properties(entity)), + ) + ).lower() + if not any(token in searchable for token in tokens): + continue + records.append( + _record( + id=str(entity["id"]), + path=owner, + kind=str(entity["kind"]), + schema=schema_identity, + type=entity.get("type"), + ) + ) + return tuple(sorted(records, key=lambda item: (str(item["path"]), str(item["id"])))) + + +def _specialization_records( + entities: tuple[dict[str, object], ...], + paths: dict[str, str], + requested: str | None, +) -> tuple[Mapping[str, JsonValue], ...]: + records = [] + for entity in entities: + identifier = str(entity["id"]) + if identifier not in paths or not _selected(paths[identifier], requested): + continue + schema = entity.get("schema_ref") + records.append( + _record( + id=identifier, + path=paths[identifier], + kind=str(entity["kind"]), + schema=(schema.get("identity") if type(schema) is dict else None), + schema_fingerprint=( + schema.get("fingerprint") if type(schema) is dict else None + ), + properties=_properties(entity), + ) + ) + return tuple(sorted(records, key=lambda item: str(item["path"]))) + + +def _artifact_records( + acpy: dict[str, object], acir: bytes, build_manifest: dict[str, object] | None +) -> tuple[Mapping[str, JsonValue], ...]: + if build_manifest is not None: + artifacts = build_manifest.get("artifacts") + if type(artifacts) is not list: + raise InspectionError("build manifest artifacts are invalid") + records = [ + _record( + path="build-manifest.json", + kind="build_manifest", + build_fingerprint=str(build_manifest.get("build_fingerprint")), + ) + ] + for artifact in artifacts: + if type(artifact) is not dict or set(artifact) != {"path", "kind", "sha256"}: + raise InspectionError("build manifest artifact is invalid") + records.append( + _record( + path=str(artifact["path"]), + kind=str(artifact["kind"]), + sha256=str(artifact["sha256"]), + ) + ) + return tuple(sorted(records, key=lambda item: str(item["path"]))) + sources = acpy.get("sources") + if type(sources) is not list: + raise InspectionError("ACPy sources are invalid") + records = [ + _record(path="model.ac.mlir", kind="acir", sha256=sha256_bytes(acir)) + ] + for source in sources: + if type(source) is not dict or set(source) != {"path", "sha256"}: + raise InspectionError("ACPy source artifact is invalid") + records.append( + _record( + path=str(source["path"]), + kind="source", + sha256=str(source["sha256"]), + ) + ) + return tuple(sorted(records, key=lambda item: str(item["path"]))) + + +def inspect_model( + acpy_bytes: bytes, + acir_bytes: bytes, + request: InspectionRequest, + *, + build_manifest: dict[str, object] | None = None, +) -> InspectionResult: + acpy = _document(acpy_bytes, "ACPy") + entities = _entities(acpy) + paths, owners = _hierarchy_paths(entities) + if request.path is not None and request.path not in set(paths.values()): + raise InspectionError(f"unknown canonical hierarchy path {request.path!r}") + if request.kind == "graph": + records = _graph_records(entities, paths, owners, request.path) + elif request.kind == "hierarchy": + records = _hierarchy_records(entities, paths, request.path) + elif request.kind == "ports": + records = _port_records(entities, owners, request.path) + elif request.kind == "resources": + records = _matching_records( + entities, owners, request.path, ("resource", "queue", "storage") + ) + elif request.kind == "address-map": + records = _matching_records( + entities, owners, request.path, ("address", "range") + ) + elif request.kind == "protocols": + records = _matching_records( + entities, owners, request.path, ("protocol", "flow", "endpoint") + ) + elif request.kind == "specialization": + records = _specialization_records(entities, paths, request.path) + else: + records = _artifact_records(acpy, acir_bytes, build_manifest) + return InspectionResult(request.kind, request.system, request.path, records) + + +def inspect_build( + build_manifest: dict[str, object], request: InspectionRequest +) -> InspectionResult: + if request.kind != "artifacts" or request.path is not None: + raise InspectionError("build inspection requires the unscoped artifacts view") + return InspectionResult( + request.kind, + request.system, + None, + _artifact_records({}, b"", build_manifest), + ) + + +def render_json(result: InspectionResult) -> bytes: + return canonical_json_bytes(result.to_json()) + b"\n" + + +def render_dot(result: InspectionResult) -> str: + if result.kind != "graph": + raise InspectionError("Graphviz DOT is available only for graph inspection") + lines = ["digraph agentic_circuit {"] + for record in result.records: + if record.get("record") != "node": + continue + identifier = json.dumps(str(record["path"]), ensure_ascii=False) + label = json.dumps(f"{record['path']}\n{record['kind']}", ensure_ascii=False) + lines.append(f" {identifier} [label={label}];") + for record in result.records: + if record.get("record") != "edge": + continue + source = json.dumps(str(record["source"]), ensure_ascii=False) + target = json.dumps(str(record["target"]), ensure_ascii=False) + label = json.dumps(str(record["kind"]), ensure_ascii=False) + lines.append(f" {source} -> {target} [label={label}];") + lines.append("}") + return "\n".join(lines) + "\n" + + +def render_text(result: InspectionResult) -> str: + if result.kind == "hierarchy": + return "".join(f"{record['path']}\n" for record in result.records) + return "".join( + canonical_json_bytes(dict(record)).decode("utf-8") + "\n" + for record in result.records + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_jit.py b/components/agentic-circuit/src/agentic_circuit/_jit.py new file mode 100644 index 00000000..cf43e482 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_jit.py @@ -0,0 +1,444 @@ +"""Closed configuration records and deterministic JIT specialization metadata.""" + +from __future__ import annotations + +import dataclasses +import enum +import inspect +import os +from dataclasses import dataclass +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +from typing import get_origin + +from ._canonical_json import canonical_json_bytes, sha256_bytes +from ._definitions import Definition +from ._static_eval import FrozenMap, StaticValue, validate_ijson_value +from ._types import Static + + +def config(cls: type[object]) -> type[object]: + """Freeze one closed elaboration-time configuration record.""" + + if not isinstance(cls, type): + raise TypeError("ACPY-JIT-001: config must decorate a class") + if dataclasses.is_dataclass(cls): + raise TypeError("ACPY-JIT-001: config class must not already be a dataclass") + frozen = dataclasses.dataclass(frozen=True, slots=True)(cls) + setattr(frozen, "__ac_config__", True) + return frozen + + +def _is_const_annotation(annotation: object) -> bool: + if get_origin(annotation) is Static: + return True + if isinstance(annotation, str): + compact = annotation.replace(" ", "") + return compact.startswith(("const[", "ac.const[", "Static[", "ac.Static[")) + return False + + +def _closed(value: object) -> StaticValue: + if value is None or type(value) in {bool, int, float, str}: + result = value + elif isinstance(value, enum.Enum): + result = _closed(value.value) + elif dataclasses.is_dataclass(value) and not isinstance(value, type): + result = FrozenMap( + tuple( + sorted( + ( + field.name, + _closed(getattr(value, field.name)), + ) + for field in dataclasses.fields(value) + ) + ) + ) + elif type(value) in {tuple, list}: + result = tuple(_closed(item) for item in value) + elif type(value) is dict: + if any(type(key) is not str or not key for key in value): + raise TypeError("ACPY-JIT-002: const map keys must be non-empty strings") + result = FrozenMap( + tuple(sorted((key, _closed(item)) for key, item in value.items())) + ) + else: + raise TypeError(f"ACPY-JIT-002: unsupported const value {type(value).__name__}") + try: + validate_ijson_value(result) + except ValueError as error: + raise TypeError(f"ACPY-JIT-002: {error}") from error + return result + + +def _json_value(value: StaticValue): + if isinstance(value, FrozenMap): + return {key: _json_value(item) for key, item in value.entries} + if isinstance(value, tuple): + return [_json_value(item) for item in value] + return value + + +def _display(value: StaticValue): + if isinstance(value, FrozenMap): + return tuple((key, _display(item)) for key, item in value.entries) + if isinstance(value, tuple): + return tuple(_display(item) for item in value) + return value + + +@dataclass(frozen=True, slots=True) +class JitSpecialization: + definition: Definition + arguments: tuple[tuple[str, StaticValue], ...] + fingerprint: str + + @property + def canonical_arguments(self) -> tuple[tuple[str, object], ...]: + return tuple((name, _display(value)) for name, value in self.arguments) + + def __repr__(self) -> str: + return ( + "JitSpecialization(" + f"system={self.definition.qualified_name!r}, " + f"fingerprint={self.fingerprint!r})" + ) + + def _source(self) -> str: + if self.definition.source_file is None: + raise RuntimeError("ACPY-JIT-003: system has no readable source file") + path = Path(self.definition.source_file) + if not path.is_file(): + raise RuntimeError("ACPY-JIT-003: system source file is unavailable") + return path.read_text(encoding="utf-8") + + def lower_acir(self) -> str: + """Materialize the specialization as Queue/Var ACIR text.""" + + from ._queue_frontend import lower_queue_source + + return lower_queue_source( + self._source(), + self.definition.__name__, + static_arguments=dict(self.arguments), + specialization_fingerprint=self.fingerprint, + ) + + def lower_cpp(self) -> str: + """Materialize the specialization as typed queue-wired gfsim C++.""" + + from ._queue_codegen import lower_queue_program_to_cpp + from ._queue_frontend import parse_queue_program + + program = parse_queue_program( + self._source(), + self.definition.__name__, + static_arguments=dict(self.arguments), + specialization_fingerprint=self.fingerprint, + ) + return lower_queue_program_to_cpp(program) + + def materialize_cpp( + self, + cache_root: str | Path, + *, + compiler: str | Path | None = None, + ) -> "JitArtifact": + """Compile one optimized C++ specialization into a content cache.""" + + selected = str(compiler or shutil.which("c++") or "") + if not selected: + raise RuntimeError("ACPY-JIT-004: no C++ compiler is available") + version = subprocess.run( + (selected, "--version"), + text=True, + capture_output=True, + check=False, + ) + if version.returncode != 0: + raise RuntimeError("ACPY-JIT-004: C++ compiler identity failed") + compiler_identity = version.stdout.splitlines()[0].strip() + cpp = self.lower_cpp() + cpp_hash = sha256_bytes(cpp.encode("utf-8")) + key = sha256_bytes( + canonical_json_bytes( + { + "schema": "agentic-circuit-provider-specialization", + "version": "0.3", + "frontend_specialization": self.fingerprint, + "backend": "gfsim-cpp", + "provider_source_sha256": cpp_hash, + "compiler": compiler_identity, + } + ) + ) + directory = Path(cache_root) / "gfsim-cpp" / key.removeprefix("sha256:") + source = directory / "model.cpp" + artifact = directory / "model.o" + manifest = directory / "manifest.json" + if source.is_file() and artifact.is_file() and manifest.is_file(): + return JitArtifact(key, directory, source, artifact, manifest, True) + + directory.mkdir(parents=True, exist_ok=True) + _write_atomic(source, cpp.encode("utf-8")) + include_root = Path(__file__).resolve().parents[2] / "include" + with tempfile.TemporaryDirectory(dir=directory) as temporary: + candidate = Path(temporary) / "model.o" + completed = subprocess.run( + ( + selected, + "-std=c++20", + "-O3", + "-I", + str(include_root), + "-c", + str(source), + "-o", + str(candidate), + ), + text=True, + capture_output=True, + check=False, + ) + if completed.returncode != 0: + raise RuntimeError( + "ACPY-JIT-004: C++ specialization failed:\n" + completed.stderr + ) + os.replace(candidate, artifact) + manifest_value = { + "schema": "agentic-circuit-jit-artifact", + "version": "0.3", + "backend": "gfsim-cpp", + "specialization": key, + "frontend_specialization": self.fingerprint, + "compiler": compiler_identity, + "provider_source_sha256": cpp_hash, + "artifact_sha256": sha256_bytes(artifact.read_bytes()), + } + _write_atomic(manifest, canonical_json_bytes(manifest_value) + b"\n") + return JitArtifact(key, directory, source, artifact, manifest, False) + + def materialize_pyc( + self, + cache_root: str | Path, + *, + pycgen_tool: str | Path, + pycc: str | Path, + toolchain_metadata: str | Path, + compiler: str | Path | None = None, + verilator: str | Path | None = None, + ) -> "JitPycArtifact": + """Build cached canonical PYC, C++, and Verilog artifacts.""" + + repo = Path(__file__).resolve().parents[2] + bundle_tool = repo / "tools" / "ac-queue-pyc-build.py" + lock = repo / "toolchains" / "pyc.lock.json" + selected_cxx = Path(compiler or shutil.which("c++") or "") + selected_verilator = Path(verilator or shutil.which("verilator") or "") + paths = { + "pycgen": Path(pycgen_tool), + "pycc": Path(pycc), + "metadata": Path(toolchain_metadata), + "cxx": selected_cxx, + "verilator": selected_verilator, + "bundle": bundle_tool, + "lock": lock, + } + for name, path in paths.items(): + if not path.is_file(): + raise RuntimeError( + f"ACPY-JIT-005: required {name} path is unavailable: {path}" + ) + acir = self.lower_acir().encode("utf-8") + key = sha256_bytes( + canonical_json_bytes( + { + "schema": "agentic-circuit-provider-specialization", + "version": "0.3", + "frontend_specialization": self.fingerprint, + "backend": "pyc-cpp-verilog", + "acir_sha256": sha256_bytes(acir), + "tools": { + name: sha256_bytes(path.read_bytes()) + for name, path in sorted(paths.items()) + }, + } + ) + ) + directory = Path(cache_root) / "pyc-cpp-verilog" / key.removeprefix("sha256:") + pyc = directory / "model.pyc" + cpp = directory / "cpp" + verilog_output = directory / "verilog" + bundle_manifest = directory / "bundle-manifest.json" + manifest = directory / "manifest.json" + if all( + path.exists() + for path in (pyc, cpp, verilog_output, bundle_manifest, manifest) + ): + return JitPycArtifact( + key, + directory, + pyc, + cpp, + verilog_output, + bundle_manifest, + manifest, + True, + ) + + directory.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(dir=directory.parent) as temporary: + stage = Path(temporary) / "stage" + stage.mkdir() + frozen = stage / "model.ac.mlir" + _write_atomic(frozen, acir) + command = ( + sys.executable, + str(bundle_tool), + str(frozen), + "--pycgen-tool", + str(paths["pycgen"]), + "--pycc", + str(paths["pycc"]), + "--toolchain-lock", + str(lock), + "--toolchain-metadata", + str(paths["metadata"]), + "--cxx", + str(paths["cxx"]), + "--verilator", + str(paths["verilator"]), + "--pyc-output", + str(stage / "model.pyc"), + "--cpp-output-dir", + str(stage / "cpp"), + "--verilog-output-dir", + str(stage / "verilog"), + "--manifest", + str(stage / "bundle-manifest.json"), + ) + completed = subprocess.run( + command, + text=True, + capture_output=True, + check=False, + env={**os.environ, "PYTHONPATH": str(repo / "src")}, + ) + if completed.returncode != 0: + raise RuntimeError( + "ACPY-JIT-005: PYC specialization failed:\n" + completed.stderr + ) + manifest_value = { + "schema": "agentic-circuit-jit-artifact", + "version": "0.3", + "backend": "pyc-cpp-verilog", + "specialization": key, + "frontend_specialization": self.fingerprint, + "acir_sha256": sha256_bytes(acir), + "bundle_manifest_sha256": sha256_bytes( + (stage / "bundle-manifest.json").read_bytes() + ), + } + _write_atomic( + stage / "manifest.json", + canonical_json_bytes(manifest_value) + b"\n", + ) + if directory.exists(): + raise RuntimeError( + "ACPY-JIT-005: specialization cache entry appeared incomplete" + ) + os.replace(stage, directory) + return JitPycArtifact( + key, + directory, + pyc, + cpp, + verilog_output, + bundle_manifest, + manifest, + False, + ) + + +@dataclass(frozen=True, slots=True) +class JitArtifact: + fingerprint: str + directory: Path + source: Path + artifact: Path + manifest: Path + cache_hit: bool + + +@dataclass(frozen=True, slots=True) +class JitPycArtifact: + fingerprint: str + directory: Path + pyc: Path + cpp: Path + verilog: Path + bundle_manifest: Path + manifest: Path + cache_hit: bool + + +def _write_atomic(path: Path, content: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.") + try: + with os.fdopen(descriptor, "wb") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +def jit(system: Definition, /, **constants: object) -> JitSpecialization: + """Create one deterministic, const-only system specialization. + + This captures metadata only. It deliberately does not execute the system + body or compile a backend artifact; downstream Queue elaboration consumes + the frozen arguments. + """ + + if not isinstance(system, Definition) or system.kind != "system": + raise TypeError("ACPY-JIT-001: jit requires an @ac.system definition") + signature = inspect.signature(system.function) + for parameter in signature.parameters.values(): + if not _is_const_annotation(parameter.annotation): + raise TypeError( + f"ACPY-JIT-001: system parameter {parameter.name!r} must use ac.const" + ) + try: + bound = signature.bind(**constants) + except TypeError as error: + raise TypeError(f"ACPY-JIT-001: {error}") from error + bound.apply_defaults() + arguments = tuple( + (name, _closed(bound.arguments[name])) for name in signature.parameters + ) + source_hash: str | None = None + source_file = system.source_file + if source_file is not None: + path = Path(source_file) + if path.is_file(): + source_hash = sha256_bytes(path.read_bytes()) + preimage = { + "schema": "agentic-circuit-jit-specialization", + "version": "0.3", + "system": system.qualified_name, + "source_sha256": source_hash, + "arguments": {name: _json_value(value) for name, value in arguments}, + } + return JitSpecialization( + system, + arguments, + sha256_bytes(canonical_json_bytes(preimage)), + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_lower_acir.py b/components/agentic-circuit/src/agentic_circuit/_lower_acir.py new file mode 100644 index 00000000..0e94c042 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_lower_acir.py @@ -0,0 +1,346 @@ +"""Deterministic ACPy construction and canonical ACIR text emission.""" + +from __future__ import annotations + +import json +import re +from dataclasses import dataclass + +from ._acpy import ( + AcpyDocument, + EntityAllocator, + Property, + SchemaRef, + SourceFile, +) +from ._canonical_json import sha256_bytes, utf16_sort_key +from ._diagnostics import SourceSpan +from ._frontend import CapturedProgram +from ._normalize import NormalizedProgram +from ._process import ProcessProgram +from ._resolve import ValueVersion +from ._static_eval import StaticValue + + +_SYMBOL = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +@dataclass(frozen=True, slots=True) +class AcirArtifact: + text: str + sha256: str + source_map: tuple[tuple[str, SourceSpan], ...] + + +def _properties(**values: StaticValue) -> tuple[Property, ...]: + return tuple( + Property(name, value) + for name, value in sorted(values.items(), key=lambda item: utf16_sort_key(item[0])) + ) + + +def build_verified_acpy( + captured: CapturedProgram, + program: NormalizedProgram, + processes: tuple[ProcessProgram, ...] = (), +) -> AcpyDocument: + """Build the closed semantic document in dense semantic order.""" + + selected = captured.selected_system + if selected is None: + raise ValueError("ACPY-VERIFY-001: a selected system is required") + sites = {site.qualified_name: site for site in captured.source.definitions} + system_site = sites[selected.qualified_name] + module_site = sites.get(program.definition) + if module_site is None: + raise ValueError("ACPY-VERIFY-001: root module source is missing") + + allocator = EntityAllocator() + system = allocator.allocate( + kind="system", + scope=selected.qualified_name, + source=system_site.span, + properties=_properties(root=program.definition), + ) + module = allocator.allocate( + kind="module", + scope=program.definition, + source=module_site.span, + parent=system.id, + properties=_properties(name=program.definition), + ) + values: dict[str, str] = {} + for argument in program.arguments: + entity = allocator.allocate( + kind="arg", + scope=program.definition, + source=module_site.span, + parent=module.id, + type=argument.type_key, + properties=_properties(name=argument.source_name), + ) + values[argument.name] = entity.id + + for call in program.calls: + uses = tuple(values[binding.value.name] for binding in call.inputs) + call_entity = allocator.allocate( + kind="call", + scope=program.definition, + source=call.source, + parent=module.id, + uses=uses, + schema_ref=SchemaRef(call.schema.identity, call.schema.fingerprint), + properties=_properties(instance_name=call.instance_name), + ) + for binding in call.results: + result = allocator.allocate( + kind="result", + scope=program.definition, + source=call.source, + parent=call_entity.id, + type=binding.value.type_key, + uses=(call_entity.id,), + properties=_properties(name=binding.value.name), + ) + values[binding.value.name] = result.id + + process_definitions = { + definition.qualified_name: definition + for definition in captured.definitions + if definition.kind == "process" + } + for process in processes: + definition = next( + ( + item + for item in process_definitions.values() + if item.__name__ == process.name + ), + None, + ) + source = sites[definition.qualified_name].span if definition is not None else None + allocator.allocate( + kind="process", + scope=program.definition, + source=source, + parent=module.id, + properties=_properties(name=process.name), + ) + + allocator.allocate( + kind="return", + scope=program.definition, + source=module_site.span, + parent=module.id, + uses=tuple(values[value.name] for value in program.returns), + ) + document = AcpyDocument( + entry=system.id, + sources=(SourceFile(captured.source.path, captured.source.sha256),), + entities=allocator.freeze(), + ) + errors = document.verify() + if errors: + raise ValueError( + "ACPY-VERIFY-001: " + "; ".join(error.message for error in errors) + ) + return document + + +def _symbol(value: str) -> str: + candidate = value.rsplit(".", 1)[-1] + if not _SYMBOL.fullmatch(candidate): + raise ValueError(f"ACPY-VERIFY-001: invalid ACIR symbol {value!r}") + return candidate + + +def _ssa(value: ValueVersion) -> str: + candidate = f"{value.source_name}_{value.version}" + return re.sub(r"[^A-Za-z0-9_]", "_", candidate) + + +def _static_attribute(value: StaticValue) -> str: + if value is None: + return "unit" + if type(value) is bool: + return "true" if value else "false" + if type(value) is int: + return str(value) + if type(value) is str: + return json.dumps(value, ensure_ascii=False) + raise ValueError( + f"ACPY-VERIFY-001: unsupported ACIR static value {type(value).__name__}" + ) + + +def _static_arguments(values: tuple[tuple[str, StaticValue], ...]) -> str: + return ", ".join( + f"{name} = {_static_attribute(value)}" for name, value in values + ) + + +def _argument_types(program: NormalizedProgram) -> dict[str, str]: + inferred: dict[str, str] = {} + for call in program.calls: + for port, binding in zip(call.schema.ports, call.inputs, strict=True): + inferred.setdefault(binding.value.name, port.acir_type) + for argument in program.arguments: + inferred.setdefault( + argument.name, + "i1" if argument.type_key in {"bool", "RuntimeBool"} else "i32", + ) + return inferred + + +def _component_declarations(program: NormalizedProgram) -> list[str]: + schemas = {call.schema.identity: call.schema for call in program.calls} + symbols: set[str] = set() + lines: list[str] = [] + for identity in sorted(schemas, key=utf16_sort_key): + schema = schemas[identity] + symbol = _symbol(identity) + if symbol in symbols: + raise ValueError("ACPY-CALL-006: component symbol collision") + symbols.add(symbol) + arguments = ", ".join( + f"%{port.name} : {port.acir_type}" for port in schema.ports + ) + result_types = tuple(result.acir_type for result in schema.results) + result_signature = ( + "" + if not result_types + else " -> " + ( + result_types[0] + if len(result_types) == 1 + else "(" + ", ".join(result_types) + ")" + ) + ) + calls = [call for call in program.calls if call.schema.identity == identity] + schema_parameters = calls[0].static_arguments if calls else () + parameter_text = _static_arguments(schema_parameters) + if schema.external_binding is not None: + lines.append( + f" ac.module.extern @{symbol} : ({', '.join(port.acir_type for port in schema.ports)})" + f" -> {('()' if not result_types else result_signature.removeprefix(' -> '))} " + f"parameters {{{parameter_text}}} implementation " + f"{{registry = \"cpp\", name = {json.dumps(schema.external_binding)}}}" + ) + continue + lines.append( + f" ac.module @{symbol}({arguments}){result_signature} parameters {{}} graph {{" + ) + returned: list[str] = [] + for result in schema.results: + if result.source_binding is None: + raise ValueError( + f"ACPY-VERIFY-001: {identity} result {result.name!r} has no structural source" + ) + returned.append(f"%{result.source_binding}") + if returned: + lines.append( + " ac.return " + + ", ".join(returned) + + " : " + + ", ".join(result_types) + ) + else: + lines.append(" ac.return") + lines.append(" }") + return lines + + +def _emit_process(process: ProcessProgram, kind: str) -> list[str]: + if process.captures: + raise ValueError("ACPY-VERIFY-001: captured process lowering is not closed yet") + if len(process.blocks) != 1: + raise ValueError("ACPY-VERIFY-001: multi-block process requires CFG lowering") + block = process.blocks[0] + if block.actions or block.edge.kind != "suspend" or block.edge.operation != "yield_sim": + raise ValueError("ACPY-VERIFY-001: unsupported process operation shape") + return [ + f" ac.process @{_symbol(process.name)} kind {json.dumps(kind)} {{", + " ac.yield_sim", + " }", + ] + + +def lower_to_acir( + program: NormalizedProgram, + document: AcpyDocument, + *, + system_name: str, + processes: tuple[tuple[ProcessProgram, str], ...] = (), +) -> AcirArtifact: + errors = document.verify() + if errors: + raise ValueError("ACPY-VERIFY-001: lowering requires verified ACPy") + types = _argument_types(program) + root = _symbol(program.definition) + lines = ['module attributes {ac.contract_epoch = "0.3"} {'] + workload = next( + (process.name for process, kind in processes if kind == "workload"), None + ) + system = f" ac.system @{_symbol(system_name)} root @{root} as \"root\" tick 0 \"cycle\"" + if workload is not None: + system += f" workload @{root}::@{_symbol(workload)}" + system += ( + " seed {kind = \"fixed\", value = 0 : i64} instrumentation [] " + "results {id = \"default\", format = \"json\"} selected true" + ) + lines.append(system) + declarations = _component_declarations(program) + if declarations: + lines.extend(declarations) + + arguments = ", ".join( + f"%{argument.source_name} : {types[argument.name]}" + for argument in program.arguments + ) + return_types = tuple(value.type_key for value in program.returns) + result_signature = ( + "" + if not return_types + else " -> " + + (return_types[0] if len(return_types) == 1 else "(" + ", ".join(return_types) + ")") + ) + lines.append( + f" ac.module @{root}({arguments}){result_signature} parameters {{}} graph {{" + ) + source_map: list[tuple[str, SourceSpan]] = [] + names = {argument.name: f"%{argument.source_name}" for argument in program.arguments} + for call in program.calls: + operands = ", ".join(names[binding.value.name] for binding in call.inputs) + operand_types = ", ".join( + port.acir_type for port in call.schema.ports + ) + results = tuple(binding.value for binding in call.results) + if len(results) > 1: + raise ValueError("ACPY-VERIFY-001: multi-result instance emission is not closed") + prefix = "" + if results: + name = f"%{_ssa(results[0])}" + names[results[0].name] = name + prefix = name + " = " + result_types = ", ".join(result.acir_type for result in call.schema.results) + arrow = result_types if len(call.schema.results) == 1 else f"({result_types})" + lines.append( + f" {prefix}ac.instance @{_symbol(call.instance_name)} of @{_symbol(call.schema.identity)}({operands}) " + f"static {{{_static_arguments(call.static_arguments)}}} id {json.dumps(call.instance_name)} " + f"path {json.dumps(call.instance_name)} : ({operand_types}) -> {arrow}" + ) + source_map.append((f"@{root}::@{call.instance_name}", call.source)) + + for process, kind in processes: + lines.extend(_emit_process(process, kind)) + if program.returns: + operands = ", ".join(names[value.name] for value in program.returns) + lines.append(f" ac.return {operands} : {', '.join(return_types)}") + else: + lines.append(" ac.return") + lines.extend((" }", "}")) + text = "\n".join(lines) + "\n" + return AcirArtifact( + text=text, + sha256=sha256_bytes(text.encode("utf-8")), + source_map=tuple(source_map), + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_naming.py b/components/agentic-circuit/src/agentic_circuit/_naming.py new file mode 100644 index 00000000..c0c97730 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_naming.py @@ -0,0 +1,52 @@ +"""Stable source-ordered instance naming.""" + +from __future__ import annotations + +import re + + +_SEGMENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +class StableNameError(ValueError): + """Raised when an instance name is invalid or collides.""" + + +def normalize_schema_base(schema_base: str) -> str: + return schema_base.rsplit(".", 1)[-1] + + +def validate_instance_segment(candidate: str) -> None: + if not _SEGMENT.fullmatch(candidate): + raise StableNameError( + f"ACPY-NAME-001: invalid instance name segment {candidate!r}" + ) + + +class StableNameAllocator: + __slots__ = ("_used",) + + def __init__(self) -> None: + self._used: set[str] = set() + + def allocate( + self, + schema_base: str, + assignment: str | None, + explicit: str | None, + source_key: tuple[int, int], + ) -> str: + if explicit is not None: + candidate = explicit + elif assignment is not None: + candidate = assignment + else: + base = normalize_schema_base(schema_base) + candidate = f"{base}_{source_key[0]}_{source_key[1]}" + validate_instance_segment(candidate) + if candidate in self._used: + raise StableNameError( + f"ACPY-NAME-002: duplicate instance name {candidate!r}" + ) + self._used.add(candidate) + return candidate diff --git a/components/agentic-circuit/src/agentic_circuit/_native_api.py b/components/agentic-circuit/src/agentic_circuit/_native_api.py new file mode 100644 index 00000000..83da929e --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_native_api.py @@ -0,0 +1,294 @@ +"""Immutable private Python records for the native compiler bridge.""" + +from __future__ import annotations + +import importlib.machinery +import importlib.util +import sys +from dataclasses import dataclass +from pathlib import Path +from types import ModuleType +from typing import Mapping + +from ._canonical_json import JsonValue, validate_ijson_value +from ._diagnostics import Diagnostic, FixIt, RelatedLocation, SourceSpan + + +@dataclass(frozen=True, slots=True) +class NativeRequest: + acir: bytes + stop_after: str | None + emits: tuple[str, ...] + options: tuple[tuple[str, object], ...] = () + + def __post_init__(self) -> None: + if type(self.acir) is not bytes: + raise TypeError("native ACIR input must be bytes") + if self.stop_after is not None and type(self.stop_after) is not str: + raise TypeError("native stop stage must be a string or None") + if type(self.emits) is not tuple or not all( + type(item) is str for item in self.emits + ): + raise TypeError("native emits must be a tuple of strings") + if type(self.options) is not tuple: + raise TypeError("native options must be a tuple of pairs") + names: set[str] = set() + for item in self.options: + if type(item) is not tuple or len(item) != 2 or type(item[0]) is not str: + raise TypeError("native options must be string/value pairs") + if item[0] in names: + raise ValueError(f"duplicate native option: {item[0]}") + names.add(item[0]) + value = item[1] + if item[0] in ("dump_before", "dump_after"): + if type(value) is not tuple or not all( + type(name) is str for name in value + ): + raise TypeError(f"native {item[0]} must be a tuple of strings") + elif item[0] in ( + "binding_lock", + "binding_registry", + "frontend_acpy", + "frontend_acir", + ): + if type(value) is not bytes: + raise TypeError(f"native {item[0]} must be bytes") + else: + validate_ijson_value(value) # type: ignore[arg-type] + + +@dataclass(frozen=True, slots=True) +class NativeArtifact: + path: str + kind: str + data: bytes + sha256: str + + +@dataclass(frozen=True, slots=True) +class NativeResult: + artifacts: tuple[NativeArtifact, ...] + diagnostics: tuple[Diagnostic, ...] + build_directory: str | None + executable: str | None + build_fingerprint: str | None + cache_hit: bool | None + + +@dataclass(frozen=True, slots=True) +class NativeCapabilities: + compiler_build_id: str + runtime_build_id: str + items: tuple[Mapping[str, JsonValue], ...] + + +_NATIVE: ModuleType | None = None + + +def _load_native() -> ModuleType: + global _NATIVE + if _NATIVE is not None: + return _NATIVE + try: + from . import _native as imported + except ImportError as first_error: + for entry in map(Path, sys.path): + package = entry / "agentic_circuit" + for suffix in importlib.machinery.EXTENSION_SUFFIXES: + candidate = package / ("_native" + suffix) + if not candidate.is_file(): + continue + spec = importlib.util.spec_from_file_location( + "agentic_circuit._native", candidate + ) + if spec is None or spec.loader is None: + continue + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + _NATIVE = module + return module + raise ImportError("the Agentic Circuit native extension is unavailable") from first_error + _NATIVE = imported + return imported + + +def native_extension_path() -> Path: + path = getattr(_load_native(), "__file__", None) + if type(path) is not str: + raise RuntimeError("the Agentic Circuit native extension has no file identity") + return Path(path).resolve() + + +def _closed_dict(value: object, keys: frozenset[str], label: str) -> dict[str, object]: + if type(value) is not dict: + raise TypeError(f"native {label} must be a dictionary") + result = value + if set(result) != keys: + raise ValueError(f"native {label} has unexpected fields") + return result + + +def _optional_string(value: object, label: str) -> str | None: + if value is not None and type(value) is not str: + raise TypeError(f"native {label} must be a string or None") + return value + + +def _source(value: object) -> SourceSpan | None: + if value is None: + return None + item = _closed_dict(value, frozenset({"file", "line", "column"}), "source") + file = item["file"] + line = item["line"] + column = item["column"] + if type(file) is not str or type(line) is not int or type(column) is not int: + raise TypeError("native source fields have invalid types") + return SourceSpan(file, line, column, line, column) + + +def _diagnostic(value: object) -> Diagnostic: + item = _closed_dict( + value, + frozenset( + { + "stage", + "code", + "severity", + "message", + "source", + "object_path", + "expected", + "actual", + "related", + "fixits", + } + ), + "diagnostic", + ) + related_value = item["related"] + fixits_value = item["fixits"] + if type(related_value) is not tuple or type(fixits_value) is not tuple: + raise TypeError("native diagnostic collections must be tuples") + related: list[RelatedLocation] = [] + for value in related_value: + entry = _closed_dict( + value, + frozenset({"message", "source", "object_path"}), + "related location", + ) + if type(entry["message"]) is not str: + raise TypeError("native related message must be a string") + related.append( + RelatedLocation( + message=entry["message"], + source=_source(entry["source"]), + object_path=_optional_string(entry["object_path"], "object path"), + ) + ) + fixits: list[FixIt] = [] + for value in fixits_value: + entry = _closed_dict(value, frozenset({"message"}), "fix-it") + if type(entry["message"]) is not str: + raise TypeError("native fix-it message must be a string") + fixits.append(FixIt(entry["message"])) + expected = item["expected"] + actual = item["actual"] + validate_ijson_value(expected) # type: ignore[arg-type] + validate_ijson_value(actual) # type: ignore[arg-type] + for name in ("stage", "code", "severity", "message"): + if type(item[name]) is not str: + raise TypeError(f"native diagnostic {name} must be a string") + severity = "note" if item["severity"] == "remark" else item["severity"] + return Diagnostic( + stage=item["stage"], + code=item["code"], + severity=severity, # type: ignore[arg-type] + message=item["message"], + source=_source(item["source"]), + object_path=_optional_string(item["object_path"], "object path"), + expected=expected, # type: ignore[arg-type] + actual=actual, # type: ignore[arg-type] + related=tuple(related), + fixits=tuple(fixits), + ) + + +def _result(value: object) -> NativeResult: + item = _closed_dict( + value, + frozenset( + { + "artifacts", + "diagnostics", + "build_directory", + "executable", + "build_fingerprint", + "cache_hit", + } + ), + "compiler result", + ) + artifact_values = item["artifacts"] + diagnostic_values = item["diagnostics"] + if type(artifact_values) is not tuple or type(diagnostic_values) is not tuple: + raise TypeError("native result collections must be tuples") + artifacts: list[NativeArtifact] = [] + for value in artifact_values: + artifact = _closed_dict( + value, frozenset({"path", "kind", "data", "sha256"}), "artifact" + ) + if not ( + type(artifact["path"]) is str + and type(artifact["kind"]) is str + and type(artifact["data"]) is bytes + and type(artifact["sha256"]) is str + ): + raise TypeError("native artifact fields have invalid types") + artifacts.append(NativeArtifact(**artifact)) # type: ignore[arg-type] + diagnostics = tuple( + sorted(map(_diagnostic, diagnostic_values), key=Diagnostic.sort_key) + ) + cache_hit = item["cache_hit"] + if cache_hit is not None and type(cache_hit) is not bool: + raise TypeError("native cache hit must be a boolean or None") + return NativeResult( + artifacts=tuple(artifacts), + diagnostics=diagnostics, + build_directory=_optional_string(item["build_directory"], "build directory"), + executable=_optional_string(item["executable"], "executable"), + build_fingerprint=_optional_string( + item["build_fingerprint"], "build fingerprint" + ), + cache_hit=cache_hit, + ) + + +def run_native_compiler(request: NativeRequest) -> NativeResult: + if type(request) is not NativeRequest: + raise TypeError("request must be an exact NativeRequest") + raw = _load_native().run_compiler( + { + "acir": request.acir, + "stop_after": request.stop_after, + "emits": request.emits, + "options": dict(request.options), + } + ) + return _result(raw) + + +def capabilities() -> NativeCapabilities: + raw = _closed_dict( + _load_native().capabilities(), + frozenset({"compiler_build_id", "runtime_build_id", "items"}), + "capabilities", + ) + compiler = raw["compiler_build_id"] + runtime = raw["runtime_build_id"] + items = raw["items"] + if type(compiler) is not str or type(runtime) is not str or type(items) is not tuple: + raise TypeError("native capabilities fields have invalid types") + for item in items: + validate_ijson_value(item) + return NativeCapabilities(compiler, runtime, items) # type: ignore[arg-type] diff --git a/components/agentic-circuit/src/agentic_circuit/_normalize.py b/components/agentic-circuit/src/agentic_circuit/_normalize.py new file mode 100644 index 00000000..42eb2d34 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_normalize.py @@ -0,0 +1,433 @@ +"""Source-ordered assignment, call, and SSA normalization.""" + +from __future__ import annotations + +import ast +from dataclasses import dataclass + +from ._diagnostics import Diagnostic, DiagnosticBag, RelatedLocation, SourceSpan +from ._frontend import CapturedProgram +from ._naming import StableNameAllocator, StableNameError +from ._resolve import ( + ResolvedCall, + ResolutionError, + UnresolvedCall, + ValueCategory, + ValueVersion, + resolve_call, +) +from ._schemas import ComponentSchema, signature_for +from ._static_eval import StaticEnvironment, StaticValue, evaluate_static + + +@dataclass(frozen=True, slots=True) +class NormalizedScopeRegion: + key: str + name: str + parent: str | None + call_keys: tuple[str, ...] + value_names: tuple[str, ...] + source: SourceSpan + + +@dataclass(frozen=True, slots=True) +class NormalizedProgram: + definition: str + arguments: tuple[ValueVersion, ...] + values: tuple[ValueVersion, ...] + calls: tuple[ResolvedCall, ...] + returns: tuple[ValueVersion, ...] + diagnostics: tuple[Diagnostic, ...] + scopes: tuple[NormalizedScopeRegion, ...] = () + + def value_names(self) -> tuple[str, ...]: + return tuple(value.name for value in self.values) + + def return_names(self) -> tuple[str, ...]: + return tuple(value.name for value in self.returns) + + +def _span(path: str, node: ast.AST) -> SourceSpan: + return SourceSpan( + path, + node.lineno, + node.col_offset + 1, + node.end_lineno or node.lineno, + (node.end_col_offset or node.col_offset) + 1, + ) + + +def _annotation_category(annotation: ast.expr | None) -> ValueCategory: + value = annotation.value if isinstance(annotation, ast.Subscript) else annotation + if isinstance(value, ast.Name): + return { + "Static": "static", + "Flow": "flow", + "Endpoint": "endpoint", + "ResourceRef": "resource", + }.get(value.id, "static") + return "static" + + +def _result_category(type_key: str) -> ValueCategory: + lowered = type_key.lower() + if "flow" in lowered: + return "flow" + if "endpoint" in lowered: + return "endpoint" + if "resource" in lowered: + return "resource" + return "result" + + +class _Normalizer: + def __init__( + self, + captured: CapturedProgram, + definition: str, + node: ast.FunctionDef | ast.AsyncFunctionDef, + ) -> None: + self._captured = captured + self._definition = definition + self._node = node + self._diagnostics = DiagnosticBag() + self._versions: dict[str, int] = {} + self._current: dict[str, ValueVersion] = {} + self._values: list[ValueVersion] = [] + self._calls: list[ResolvedCall] = [] + self._returns: tuple[ValueVersion, ...] = () + self._scopes: list[NormalizedScopeRegion | None] = [] + self._scope_stack: list[str] = [] + self._names = StableNameAllocator() + self._static_values = dict(captured.static_arguments) + arguments: list[ValueVersion] = [] + for argument in [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]: + value = ValueVersion( + source_name=argument.arg, + version=0, + category=_annotation_category(argument.annotation), + type_key=ast.unparse(argument.annotation) if argument.annotation else "unknown", + producer=None, + ) + arguments.append(value) + self._current[argument.arg] = value + self._arguments = tuple(arguments) + + def _error( + self, + code: str, + message: str, + node: ast.AST, + related: tuple[RelatedLocation, ...] = (), + ) -> None: + self._diagnostics.add( + Diagnostic( + stage="ssa-normalization", + code=code, + severity="error", + message=message, + source=_span(self._captured.source.path, node), + related=related, + ) + ) + + def _new_value( + self, + source_name: str, + category: ValueCategory, + type_key: str, + producer: str, + ownership: str = "borrowed", + ) -> ValueVersion: + version = self._versions.get(source_name, 0) + self._versions[source_name] = version + 1 + value = ValueVersion( + source_name, version, category, type_key, producer, ownership + ) + self._current[source_name] = value + self._values.append(value) + return value + + def _value(self, expression: ast.expr) -> ValueVersion: + if not isinstance(expression, ast.Name) or expression.id not in self._current: + raise ResolutionError("ACPY-SYMBOL-001: expression is not a bound value") + return self._current[expression.id] + + def _static(self, expression: ast.expr | object) -> StaticValue: + if isinstance(expression, ast.expr): + return evaluate_static( + expression, StaticEnvironment(self._static_values) + ) + return expression + + def _bound_candidate( + self, node: ast.Call, schema: ComponentSchema + ) -> tuple[ + tuple[tuple[str, ValueVersion], ...], + tuple[tuple[str, StaticValue], ...], + str | None, + ]: + if any(keyword.arg is None for keyword in node.keywords): + raise ResolutionError("keyword unpacking is not supported") + signature = signature_for(schema) + keyword_values = { + keyword.arg: keyword.value + for keyword in node.keywords + if keyword.arg is not None + } + try: + bound = signature.bind(*node.args, **keyword_values) + except TypeError as error: + raise ResolutionError(str(error)) from error + bound.apply_defaults() + ports = tuple( + (port.name, self._value(bound.arguments[port.name])) + for port in schema.ports + ) + static_arguments = tuple( + (parameter.name, self._static(bound.arguments[parameter.name])) + for parameter in schema.parameters + ) + instance_name = self._static(bound.arguments["name"]) + if instance_name is not None and type(instance_name) is not str: + raise ResolutionError("instance name must be a static string") + return ports, static_arguments, instance_name + + def _target_names(self, target: ast.expr) -> tuple[str, ...]: + if isinstance(target, ast.Name): + return (target.id,) + if isinstance(target, (ast.Tuple, ast.List)) and all( + isinstance(item, ast.Name) for item in target.elts + ): + return tuple(item.id for item in target.elts) + raise ResolutionError("assignment target must be a name or name tuple") + + def _call(self, target: ast.expr | None, node: ast.Call) -> None: + if not isinstance(node.func, ast.Name): + self._error("ACPY-CALL-001", "call target must be a registered name", node) + return + candidates = self._captured.registry.candidates(node.func.id) + viable: list[ + tuple[ + ComponentSchema, + tuple[tuple[str, ValueVersion], ...], + tuple[tuple[str, StaticValue], ...], + str | None, + ] + ] = [] + attempts: list[RelatedLocation] = [] + source = _span(self._captured.source.path, node) + for schema in candidates: + try: + ports, static_arguments, explicit_name = self._bound_candidate( + node, schema + ) + except (ResolutionError, ValueError) as error: + attempts.append( + RelatedLocation( + f"attempted {schema.identity}: {error}", source, None + ) + ) + else: + viable.append((schema, ports, static_arguments, explicit_name)) + attempts.append( + RelatedLocation(f"attempted {schema.identity}: viable", source, None) + ) + if len(viable) != 1: + code = "ACPY-CALL-006" if len(viable) > 1 else "ACPY-CALL-003" + message = ( + f"call {node.func.id!r} is ambiguous" + if len(viable) > 1 + else f"call {node.func.id!r} has no exact binding" + ) + self._error(code, message, node, tuple(attempts)) + return + + schema, ports, static_arguments, explicit_name = viable[0] + if target is None: + target_names = () + else: + try: + target_names = self._target_names(target) + except ResolutionError as error: + self._error("ACPY-CALL-005", str(error), target) + return + if len(target_names) != len(schema.results): + self._error( + "ACPY-CALL-005", + f"{schema.identity} returns {len(schema.results)} values, not {len(target_names)}", + target, + ) + return + entity_key = f"call:{node.lineno}:{node.col_offset + 1}" + assignment_name = target_names[0] if len(target_names) == 1 else None + try: + instance_name = self._names.allocate( + schema.identity, + assignment_name if schema.effect_kind == "stateful" else None, + explicit_name, + (node.lineno, node.col_offset + 1), + ) + except StableNameError as error: + self._error("ACPY-NAME-002", str(error), node) + return + result_values = tuple( + self._new_value( + target_name, + _result_category(result.acir_type), + result.acir_type, + entity_key, + result.ownership, + ) + for target_name, result in zip(target_names, schema.results, strict=True) + ) + unresolved = UnresolvedCall( + entity_key=entity_key, + instance_name=instance_name, + static_arguments=static_arguments, + port_values=ports, + result_values=result_values, + source=source, + ) + try: + self._calls.append(resolve_call(unresolved, schema)) + except ResolutionError as error: + self._error("ACPY-CALL-004", str(error), node) + + def _assignment(self, statement: ast.Assign) -> None: + if len(statement.targets) != 1: + self._error( + "ACPY-SYNTAX-001", "chained assignment is not supported", statement + ) + return + if isinstance(statement.value, ast.Call): + self._call(statement.targets[0], statement.value) + return + try: + source = self._value(statement.value) + targets = self._target_names(statement.targets[0]) + except ResolutionError as error: + self._error("ACPY-SYMBOL-001", str(error), statement) + return + if len(targets) != 1: + self._error("ACPY-SYNTAX-001", "value shape does not match target", statement) + return + self._new_value(targets[0], source.category, source.type_key, source.producer or "bind") + + def _return(self, statement: ast.Return) -> None: + if statement.value is None: + self._returns = () + return + expressions = ( + tuple(statement.value.elts) + if isinstance(statement.value, (ast.Tuple, ast.List)) + else (statement.value,) + ) + try: + self._returns = tuple(self._value(expression) for expression in expressions) + except ResolutionError as error: + self._error("ACPY-SYMBOL-001", str(error), statement) + + def _with_scope(self, statement: ast.With) -> None: + valid = ( + len(statement.items) == 1 + and statement.items[0].optional_vars is None + and isinstance(statement.items[0].context_expr, ast.Call) + and isinstance(statement.items[0].context_expr.func, ast.Name) + and statement.items[0].context_expr.func.id == "scope" + and len(statement.items[0].context_expr.args) == 1 + and not statement.items[0].context_expr.keywords + ) + if not valid: + self._error( + "ACPY-SCOPE-001", "only with scope(static_name) is supported", statement + ) + return + context = statement.items[0].context_expr + assert isinstance(context, ast.Call) + try: + name = self._static(context.args[0]) + except ValueError as error: + self._error("ACPY-SCOPE-001", str(error), context.args[0]) + return + if type(name) is not str or not name: + self._error( + "ACPY-SCOPE-001", "scope name must be a non-empty static string", context + ) + return + key = f"scope:{statement.lineno}:{statement.col_offset + 1}" + parent = self._scope_stack[-1] if self._scope_stack else None + index = len(self._scopes) + self._scopes.append(None) + call_start = len(self._calls) + value_start = len(self._values) + self._scope_stack.append(key) + for nested in statement.body: + self._statement(nested) + self._scope_stack.pop() + self._scopes[index] = NormalizedScopeRegion( + key=key, + name=name, + parent=parent, + call_keys=tuple(call.entity_key for call in self._calls[call_start:]), + value_names=tuple(value.name for value in self._values[value_start:]), + source=_span(self._captured.source.path, statement), + ) + + def _statement(self, statement: ast.stmt) -> None: + if isinstance(statement, ast.Assign): + self._assignment(statement) + elif isinstance(statement, ast.Return): + self._return(statement) + elif isinstance(statement, ast.With): + self._with_scope(statement) + elif isinstance(statement, ast.Expr) and isinstance( + statement.value, ast.Call + ): + self._call(None, statement.value) + elif isinstance(statement, ast.Pass): + return + else: + self._error( + "ACPY-SYNTAX-001", + f"{type(statement).__name__} normalization is not supported yet", + statement, + ) + + def run(self) -> NormalizedProgram: + for statement in self._node.body: + self._statement(statement) + return NormalizedProgram( + definition=self._definition, + arguments=self._arguments, + values=tuple(self._values), + calls=tuple(self._calls), + returns=self._returns, + diagnostics=self._diagnostics.freeze(), + scopes=tuple(scope for scope in self._scopes if scope is not None), + ) + + +def normalize_program( + captured: CapturedProgram, *, definition: str | None = None +) -> NormalizedProgram: + qualified_name = definition or ( + captured.selected_system.qualified_name + if captured.selected_system is not None + else "" + ) + sites = { + site.qualified_name: site + for site in captured.source.definitions + if isinstance(site.node, (ast.FunctionDef, ast.AsyncFunctionDef)) + } + site = sites.get(qualified_name) + if site is None or not isinstance(site.node, (ast.FunctionDef, ast.AsyncFunctionDef)): + diagnostic = Diagnostic( + stage="ssa-normalization", + code="ACPY-SYMBOL-DEFINITION", + severity="error", + message=f"definition {qualified_name!r} is not captured", + ) + return NormalizedProgram(qualified_name, (), (), (), (), (diagnostic,)) + return _Normalizer(captured, qualified_name, site.node).run() diff --git a/components/agentic-circuit/src/agentic_circuit/_output.py b/components/agentic-circuit/src/agentic_circuit/_output.py new file mode 100644 index 00000000..46e8f7c9 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_output.py @@ -0,0 +1,88 @@ +"""Deterministic human and structured CLI output routing.""" + +from __future__ import annotations + +import sys +from dataclasses import dataclass, field +from typing import Iterable, Literal, TextIO + +from ._canonical_json import JsonValue, canonical_json_bytes +from ._diagnostics import Diagnostic + + +OutputFormat = Literal["text", "json", "jsonl"] + + +@dataclass(slots=True) +class OutputSink: + format: OutputFormat = "text" + quiet: bool = False + stdout: TextIO = field(default_factory=lambda: sys.stdout) + stderr: TextIO = field(default_factory=lambda: sys.stderr) + _single_json_written: bool = False + + @classmethod + def from_arguments( + cls, arguments: object, *, workspace_format: OutputFormat | None = None + ) -> "OutputSink": + explicit = getattr(arguments, "diagnostic_format", None) + selected: OutputFormat = explicit or workspace_format or "text" + if getattr(arguments, "json", False): + selected = "json" + return cls(format=selected, quiet=bool(getattr(arguments, "quiet", False))) + + def _json_line(self, value: JsonValue) -> str: + return canonical_json_bytes(value).decode("utf-8") + "\n" + + def result(self, value: JsonValue, *, human: str = "") -> None: + if self.format == "json": + if self._single_json_written: + raise RuntimeError("single-JSON stdout already contains a value") + self.stdout.write(self._json_line(value)) + self._single_json_written = True + elif self.format == "jsonl": + self.stdout.write(self._json_line(value)) + elif human and not self.quiet: + self.stdout.write(human if human.endswith("\n") else human + "\n") + + def diagnostics(self, diagnostics: Iterable[Diagnostic]) -> None: + ordered = tuple(sorted(diagnostics, key=Diagnostic.sort_key)) + if not ordered: + return + if self.format == "json": + if len(ordered) == 1: + self.result(ordered[0].to_json()) + else: + self.result([item.to_json() for item in ordered]) + return + if self.format == "jsonl": + for diagnostic in ordered: + self.stdout.write(self._json_line(diagnostic.to_json())) + return + if self.quiet: + return + for diagnostic in ordered: + location = "" + if diagnostic.source is not None: + location = ( + f"{diagnostic.source.file}:{diagnostic.source.start_line}:" + f"{diagnostic.source.start_column}: " + ) + self.stderr.write( + f"{location}{diagnostic.severity}: {diagnostic.code}: " + f"{diagnostic.message}\n" + ) + + @staticmethod + def bounded_capture(text: str, *, limit: int = 1 << 20) -> tuple[str, bool]: + if limit < 0: + raise ValueError("capture limit must be nonnegative") + encoded = text.encode("utf-8") + if len(encoded) <= limit: + return text, False + clipped = encoded[:limit] + while True: + try: + return clipped.decode("utf-8"), True + except UnicodeDecodeError as error: + clipped = clipped[: error.start] diff --git a/components/agentic-circuit/src/agentic_circuit/_package_data.py b/components/agentic-circuit/src/agentic_circuit/_package_data.py new file mode 100644 index 00000000..275c800f --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_package_data.py @@ -0,0 +1,14 @@ +"""Filesystem-backed access to installed package resources.""" + +from __future__ import annotations + +from importlib.resources import files +from pathlib import Path + + +def resource_directory(name: str) -> Path: + resource = files(f"agentic_circuit._data.{name}") + path = Path(str(resource)) + if not path.is_dir(): + raise FileNotFoundError(f"Agentic Circuit {name} resources are unavailable") + return path diff --git a/components/agentic-circuit/src/agentic_circuit/_process.py b/components/agentic-circuit/src/agentic_circuit/_process.py new file mode 100644 index 00000000..f500e976 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_process.py @@ -0,0 +1,600 @@ +"""Immutable process CFG construction from the portable Python subset.""" + +from __future__ import annotations + +import ast +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import Literal, TypeAlias + +from ._diagnostics import SourceSpan +from ._resolve import ValueVersion +from ._source import DefinitionSite +from ._validate import validate_process_site + + +EffectKind: TypeAlias = Literal[ + "pure", + "queue", + "resource", + "storage", + "event", + "trace", + "statistics", + "suspension", +] +EdgeKind: TypeAlias = Literal["branch", "jump", "suspend", "return"] + + +class ProcessConstructionError(ValueError): + def __init__(self, code: str, message: str, source: SourceSpan) -> None: + self.code = code + self.message = message + self.source = source + super().__init__(f"{code}: {message}") + + +@dataclass(frozen=True, slots=True) +class EffectDeclaration: + operation: str + kind: EffectKind + suspension: bool = False + linear_arguments: tuple[int, ...] = () + + def __post_init__(self) -> None: + if not self.operation: + raise ValueError("effect operation must be non-empty") + if self.kind == "suspension" and not self.suspension: + raise ValueError("suspension effects must declare suspension=True") + if self.kind != "suspension" and self.suspension: + raise ValueError("only suspension effects may suspend") + if ( + any(type(index) is not int or index < 0 for index in self.linear_arguments) + or len(self.linear_arguments) != len(set(self.linear_arguments)) + ): + raise ValueError("linear effect arguments must be unique non-negative indices") + + +class EffectRegistry: + def __init__(self, declarations: tuple[EffectDeclaration, ...]) -> None: + indexed: dict[str, EffectDeclaration] = {} + for declaration in declarations: + if declaration.operation in indexed: + raise ValueError(f"duplicate effect {declaration.operation!r}") + indexed[declaration.operation] = declaration + self._declarations = tuple(declarations) + self._indexed = MappingProxyType(indexed) + + @property + def declarations(self) -> tuple[EffectDeclaration, ...]: + return self._declarations + + def find(self, operation: str) -> EffectDeclaration | None: + return self._indexed.get(operation) + + +@dataclass(frozen=True, slots=True) +class ProcessAction: + operation: str + arguments: tuple[str, ...] + result: str | None + effect_kind: EffectKind | None + source: SourceSpan + + +@dataclass(frozen=True, slots=True) +class ProcessEdge: + kind: EdgeKind + targets: tuple[str, ...] + condition: str | None = None + operation: str | None = None + arguments: tuple[str, ...] = () + + +@dataclass(frozen=True, slots=True) +class ProcessEffect: + operation: str + kind: EffectKind + source: SourceSpan + + +@dataclass(frozen=True, slots=True) +class ProcessBlock: + name: str + arguments: tuple[ValueVersion, ...] + actions: tuple[ProcessAction, ...] + edge: ProcessEdge + + +@dataclass(frozen=True, slots=True) +class ProcessProgram: + name: str + entry: str + captures: tuple[ValueVersion, ...] + blocks: tuple[ProcessBlock, ...] + effects: tuple[ProcessEffect, ...] + + +@dataclass(slots=True) +class _MutableBlock: + name: str + actions: list[ProcessAction] + edge: ProcessEdge | None = None + uses: set[str] = field(default_factory=set) + definitions: set[str] = field(default_factory=set) + + +def _span(path: str, node: ast.AST) -> SourceSpan: + line = getattr(node, "lineno", 1) + column = getattr(node, "col_offset", 0) + end_line = getattr(node, "end_lineno", line) + end_column = getattr(node, "end_col_offset", column) + return SourceSpan(path, line, column + 1, end_line, end_column + 1) + + +def _annotation_category(annotation: ast.expr | None) -> str: + value = annotation.value if isinstance(annotation, ast.Subscript) else annotation + if isinstance(value, ast.Name): + return { + "Static": "static", + "Flow": "flow", + "Endpoint": "endpoint", + "ResourceRef": "resource", + }.get(value.id, "result") + return "result" + + +class _ProcessBuilder: + def __init__(self, site: DefinitionSite, effects: EffectRegistry) -> None: + node = site.node + assert isinstance(node, ast.FunctionDef) + self._site = site + self._path = site.span.file + self._node = node + self._registry = effects + self._blocks: list[_MutableBlock] = [] + self._block_names: set[str] = set() + self._effects: list[ProcessEffect] = [] + self._loop_stack: list[tuple[str, str]] = [] + self._local_order: list[str] = [] + arguments = [ + *node.args.posonlyargs, + *node.args.args, + *node.args.kwonlyargs, + ] + self._versions = {argument.arg: 0 for argument in arguments} + self._consumed_linear: set[tuple[str, int]] = set() + self._current = self._new_block("entry") + + def _error(self, code: str, message: str, node: ast.AST) -> None: + raise ProcessConstructionError(code, message, _span(self._path, node)) + + def _new_block(self, base: str) -> _MutableBlock: + name = base + suffix = 2 + while name in self._block_names: + name = f"{base}_{suffix}" + suffix += 1 + block = _MutableBlock(name, []) + self._block_names.add(name) + self._blocks.append(block) + return block + + def _finish(self, block: _MutableBlock, edge: ProcessEdge) -> None: + if block.edge is not None: + self._error( + "ACPY-PROCESS-003", "process block already has a terminator", self._node + ) + block.edge = edge + + @staticmethod + def _names(nodes: tuple[ast.AST, ...]) -> tuple[str, ...]: + found = { + candidate.id + for node in nodes + for candidate in ast.walk(node) + if isinstance(candidate, ast.Name) and isinstance(candidate.ctx, ast.Load) + } + return tuple(sorted(found)) + + def _record_uses( + self, block: _MutableBlock, nodes: tuple[ast.AST, ...] + ) -> None: + block.uses.update( + name for name in self._names(nodes) if name not in block.definitions + ) + + def _record_definition(self, block: _MutableBlock, name: str) -> None: + block.definitions.add(name) + if name not in self._local_order: + self._local_order.append(name) + self._versions[name] = self._versions.get(name, -1) + 1 + + def _call(self, node: ast.Call, result: str | None) -> None: + if not isinstance(node.func, ast.Name): + self._error( + "ACPY-EFFECT-003", "process call target must be a declared name", node + ) + declaration = self._registry.find(node.func.id) + if declaration is None: + self._error( + "ACPY-EFFECT-003", + f"operation {node.func.id!r} has no declared process effect", + node, + ) + if any(keyword.arg is None for keyword in node.keywords): + self._error( + "ACPY-PROCESS-003", "process calls cannot unpack keywords", node + ) + for index in declaration.linear_arguments: + if index >= len(node.args) or not isinstance(node.args[index], ast.Name): + self._error( + "ACPY-PROCESS-007", + f"linear argument {index} of {node.func.id!r} must be one SSA name", + node, + ) + argument = node.args[index] + key = (argument.id, self._versions.get(argument.id, 0)) + if key in self._consumed_linear: + self._error( + "ACPY-PROCESS-007", + f"linear value {argument.id!r} is consumed more than once", + argument, + ) + self._consumed_linear.add(key) + self._record_uses( + self._current, + (*node.args, *(keyword.value for keyword in node.keywords)), + ) + arguments = tuple(ast.unparse(argument) for argument in node.args) + tuple( + f"{keyword.arg}={ast.unparse(keyword.value)}" for keyword in node.keywords + ) + source = _span(self._path, node) + self._effects.append(ProcessEffect(node.func.id, declaration.kind, source)) + if declaration.suspension: + if result is not None: + self._error( + "ACPY-PROCESS-003", + "a suspension point cannot produce a Python assignment result", + node, + ) + if node.func.id == "yield_sim": + target = "entry" + else: + target = "resume" if "resume" not in self._block_names else "done" + continuation = self._new_block(target) + target = continuation.name + self._finish( + self._current, + ProcessEdge( + "suspend", + (target,), + operation=node.func.id, + arguments=arguments, + ), + ) + if node.func.id != "yield_sim": + self._current = continuation + return + self._current.actions.append( + ProcessAction(node.func.id, arguments, result, declaration.kind, source) + ) + if result is not None: + self._record_definition(self._current, result) + + def _can_reach_backedge_without_suspension( + self, statements: list[ast.stmt] + ) -> bool: + can_fall_through = True + for statement in statements: + if not can_fall_through: + return False + if isinstance(statement, ast.Expr) and isinstance( + statement.value, ast.Call + ): + function = statement.value.func + declaration = ( + self._registry.find(function.id) + if isinstance(function, ast.Name) + else None + ) + if declaration is not None and declaration.suspension: + can_fall_through = False + elif isinstance(statement, ast.If): + can_fall_through = ( + self._can_reach_backedge_without_suspension(statement.body) + or self._can_reach_backedge_without_suspension(statement.orelse) + ) + elif isinstance(statement, (ast.Return, ast.Break)): + can_fall_through = False + elif isinstance(statement, ast.Continue): + return True + return can_fall_through + + def _assignment(self, statement: ast.Assign | ast.AnnAssign) -> None: + targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target] + if len(targets) != 1 or not isinstance(targets[0], ast.Name): + self._error( + "ACPY-PROCESS-003", "process assignment target must be one name", statement + ) + value = statement.value + if value is None: + return + if isinstance(value, ast.Call): + self._call(value, targets[0].id) + return + self._record_uses(self._current, (value,)) + self._current.actions.append( + ProcessAction( + "assign", + (ast.unparse(value),), + targets[0].id, + None, + _span(self._path, statement), + ) + ) + self._record_definition(self._current, targets[0].id) + + def _if(self, statement: ast.If) -> None: + then_block = self._new_block("then") + else_block = self._new_block("else") + self._record_uses(self._current, (statement.test,)) + self._finish( + self._current, + ProcessEdge( + "branch", + (then_block.name, else_block.name), + condition=ast.unparse(statement.test), + ), + ) + + self._current = then_block + self._statements(statement.body) + then_end = self._current + + self._current = else_block + self._statements(statement.orelse) + else_end = self._current + + open_blocks = tuple( + block for block in (then_end, else_end) if block.edge is None + ) + if not open_blocks: + self._current = else_end + return + join_block = self._new_block("resume") + for block in open_blocks: + self._finish(block, ProcessEdge("jump", (join_block.name,))) + self._current = join_block + + def _while(self, statement: ast.While) -> None: + if self._can_reach_backedge_without_suspension(statement.body): + self._error( + "ACPY-PROCESS-006", + "runtime while loop must make progress through suspension", + statement, + ) + loop = self._new_block("loop") + body = self._new_block("loop_body") + after = self._new_block("after_loop") + self._finish(self._current, ProcessEdge("jump", (loop.name,))) + self._record_uses(loop, (statement.test,)) + self._finish( + loop, + ProcessEdge( + "branch", + (body.name, after.name), + condition=ast.unparse(statement.test), + ), + ) + self._loop_stack.append((loop.name, after.name)) + self._current = body + self._statements(statement.body) + if self._current.edge is None: + self._finish(self._current, ProcessEdge("jump", (loop.name,))) + self._loop_stack.pop() + self._current = after + self._statements(statement.orelse) + + @staticmethod + def _integer(node: ast.expr) -> int | None: + if isinstance(node, ast.Constant) and type(node.value) is int: + return node.value + if ( + isinstance(node, ast.UnaryOp) + and isinstance(node.op, ast.USub) + and isinstance(node.operand, ast.Constant) + and type(node.operand.value) is int + ): + return -node.operand.value + return None + + def _range_trip_count(self, node: ast.expr) -> int | None: + if not ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "range" + and not node.keywords + and 1 <= len(node.args) <= 3 + ): + return None + values = [self._integer(argument) for argument in node.args] + if any(value is None for value in values): + return None + exact = [value for value in values if value is not None] + start, stop, step = ( + (0, exact[0], 1) + if len(exact) == 1 + else (exact[0], exact[1], 1) + if len(exact) == 2 + else (exact[0], exact[1], exact[2]) + ) + if step <= 0: + return None + return len(range(start, stop, step)) + + def _for(self, statement: ast.For) -> None: + if not isinstance(statement.target, ast.Name): + self._error( + "ACPY-PROCESS-003", "runtime for target must be one name", statement + ) + trip_count = self._range_trip_count(statement.iter) + if trip_count is None or trip_count > 1_000_000: + self._error( + "ACPY-PROCESS-006", + "runtime for loop requires a finite positive static range", + statement, + ) + loop = self._new_block("for_loop") + body = self._new_block("for_body") + after = self._new_block("after_for") + self._finish(self._current, ProcessEdge("jump", (loop.name,))) + self._record_uses(loop, (statement.iter,)) + self._record_definition(loop, statement.target.id) + self._finish( + loop, + ProcessEdge( + "branch", + (body.name, after.name), + condition=( + f"{statement.target.id} in {ast.unparse(statement.iter)}" + ), + ), + ) + self._loop_stack.append((loop.name, after.name)) + self._current = body + self._statements(statement.body) + if self._current.edge is None: + self._finish(self._current, ProcessEdge("jump", (loop.name,))) + self._loop_stack.pop() + self._current = after + self._statements(statement.orelse) + + def _statement(self, statement: ast.stmt) -> None: + if self._current.edge is not None: + self._error( + "ACPY-PROCESS-003", "statement follows a process terminator", statement + ) + if isinstance(statement, (ast.Assign, ast.AnnAssign)): + self._assignment(statement) + elif isinstance(statement, ast.Expr) and isinstance(statement.value, ast.Call): + self._call(statement.value, None) + elif isinstance(statement, ast.If): + self._if(statement) + elif isinstance(statement, ast.While): + self._while(statement) + elif isinstance(statement, ast.For): + self._for(statement) + elif isinstance(statement, ast.Return): + if statement.value is not None and not ( + isinstance(statement.value, ast.Constant) + and statement.value.value is None + ): + self._error( + "ACPY-PROCESS-003", "process return cannot carry a value", statement + ) + self._finish(self._current, ProcessEdge("return", ())) + elif isinstance(statement, ast.Break): + if not self._loop_stack: + self._error("ACPY-PROCESS-003", "break is outside a loop", statement) + self._finish( + self._current, ProcessEdge("jump", (self._loop_stack[-1][1],)) + ) + elif isinstance(statement, ast.Continue): + if not self._loop_stack: + self._error("ACPY-PROCESS-003", "continue is outside a loop", statement) + self._finish( + self._current, ProcessEdge("jump", (self._loop_stack[-1][0],)) + ) + elif isinstance(statement, ast.Pass): + return + else: + self._error( + "ACPY-PROCESS-003", + f"{type(statement).__name__} is not supported in a process", + statement, + ) + + def _statements(self, statements: list[ast.stmt]) -> None: + for statement in statements: + self._statement(statement) + + def build(self) -> ProcessProgram: + self._statements(self._node.body) + if self._current.edge is None: + self._finish(self._current, ProcessEdge("return", ())) + live_in: dict[str, set[str]] = {block.name: set() for block in self._blocks} + changed = True + while changed: + changed = False + for block in reversed(self._blocks): + assert block.edge is not None + live_out = set().union( + *(live_in[target] for target in block.edge.targets) + ) + updated = block.uses | (live_out - block.definitions) + if updated != live_in[block.name]: + live_in[block.name] = updated + changed = True + local_values = { + name: ValueVersion(name, 0, "result", "unknown", "process-local") + for name in self._local_order + } + blocks = tuple( + ProcessBlock( + block.name, + tuple( + local_values[name] + for name in self._local_order + if name in live_in[block.name] + ), + tuple(block.actions), + block.edge + if block.edge is not None + else ProcessEdge("return", ()), + ) + for block in self._blocks + ) + names = {block.name for block in blocks} + if any(target not in names for block in blocks for target in block.edge.targets): + self._error( + "ACPY-PROCESS-003", "process CFG has an unresolved edge", self._node + ) + arguments = [ + *self._node.args.posonlyargs, + *self._node.args.args, + *self._node.args.kwonlyargs, + ] + captures = tuple( + ValueVersion( + argument.arg, + 0, + _annotation_category(argument.annotation), + ast.unparse(argument.annotation) + if argument.annotation is not None + else "unknown", + None, + ) + for argument in arguments + ) + return ProcessProgram( + self._site.name, "entry", captures, blocks, tuple(self._effects) + ) + + +def construct_process( + definition: DefinitionSite, effects: EffectRegistry +) -> ProcessProgram: + if "process" not in definition.decorator_names: + raise ProcessConstructionError( + "ACPY-PROCESS-001", + f"definition {definition.qualified_name!r} is not decorated with @process", + definition.span, + ) + issues = validate_process_site(definition) + if issues: + issue = issues[0] + raise ProcessConstructionError( + issue.code, issue.message, _span(definition.span.file, issue.node) + ) + return _ProcessBuilder(definition, effects).build() diff --git a/components/agentic-circuit/src/agentic_circuit/_queue_codegen.py b/components/agentic-circuit/src/agentic_circuit/_queue_codegen.py new file mode 100644 index 00000000..ba22d56b --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_queue_codegen.py @@ -0,0 +1,979 @@ +"""Deterministic QueueProgram to typed gfsim C++ lowering.""" + +from __future__ import annotations + +import ast +from dataclasses import dataclass + +from ._queue_frontend import ( + CollectionBinding, + Payload, + QueueBinding, + QueueFrontendError, + QueueProgram, + StaticQueueCollection, + parse_queue_program, +) + + +def _cpp_type(acir_type: str) -> str: + if acir_type.startswith("i") and acir_type[1:].isdigit(): + width = int(acir_type[1:]) + if width == 1: + return "bool" + if width <= 8: + return "std::uint8_t" + if width <= 16: + return "std::uint16_t" + if width <= 32: + return "std::uint32_t" + if width <= 64: + return "std::int64_t" + prefix = "!ac.struct<@types::@" + if acir_type.startswith(prefix) and acir_type.endswith(">"): + return acir_type[len(prefix) : -1] + raise QueueFrontendError(f"ACLOWER-TYPE-MISMATCH: no C++ type for {acir_type}") + + +class _CppExpression: + def __init__(self, argument: str) -> None: + self.argument = argument + + def emit(self, node: ast.expr) -> str: + if isinstance(node, ast.Name) and node.id == self.argument: + return "item" + if isinstance(node, ast.Constant) and type(node.value) in {int, bool}: + if node.value is True: + return "true" + if node.value is False: + return "false" + return str(node.value) + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == self.argument + ): + if ( + node.func.attr in {"peek", "pop"} + and not node.args + and not node.keywords + ): + return "item" + if node.func.attr == "push" and len(node.args) == 1 and not node.keywords: + return self.emit(node.args[0]) + if isinstance(node, ast.Attribute): + return f"{self.emit(node.value)}.{node.attr}" + if isinstance(node, ast.BinOp) and isinstance( + node.op, (ast.Add, ast.Sub, ast.Mult) + ): + operator = {ast.Add: "+", ast.Sub: "-", ast.Mult: "*"}[type(node.op)] + return f"({self.emit(node.left)} {operator} {self.emit(node.right)})" + if isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And): + return "(" + " && ".join(self.emit(value) for value in node.values) + ")" + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): + return f"(!{self.emit(node.operand)})" + if ( + isinstance(node, ast.Compare) + and len(node.ops) == len(node.comparators) == 1 + ): + operators = { + ast.Eq: "==", + ast.NotEq: "!=", + ast.Lt: "<", + ast.LtE: "<=", + ast.Gt: ">", + ast.GtE: ">=", + } + operator = operators.get(type(node.ops[0])) + if operator is not None: + return ( + f"({self.emit(node.left)} {operator} " + f"{self.emit(node.comparators[0])})" + ) + raise QueueFrontendError( + "ACLOWER-UNSUPPORTED-CONSTRUCT: lambda is outside C++ expression subset" + ) + + +def _policy_body(queue: QueueBinding) -> list[str]: + assert queue.argument is not None and queue.expression is not None + return _expression_policy_body(queue.argument, queue.expression) + + +def _expression_policy_body(argument: str, node: ast.expr) -> list[str]: + expression = _CppExpression(argument) + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "push" + and len(node.args) == 1 + and not node.keywords + ): + node = node.args[0] + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "with_fields" + and not node.args + ): + lines = [" auto result = item;"] + for keyword in node.keywords: + if keyword.arg is None: + raise QueueFrontendError( + "ACLOWER-UNSUPPORTED-CONSTRUCT: field unpacking is forbidden" + ) + lines.append( + f" result.{keyword.arg} = {expression.emit(keyword.value)};" + ) + lines.extend((" return result;",)) + return lines + return [f" return {expression.emit(node)};"] + + +@dataclass(frozen=True, slots=True) +class _ObjectIds: + queues: dict[str, int] + fanout_queues: dict[str, int] + feedback_states: tuple[int, ...] + broadcasts: tuple[int, ...] + transforms: dict[str, int] + routes: tuple[int, ...] + forks: tuple[int, ...] + merges: tuple[int, ...] + barriers: tuple[int, ...] + feedbacks: tuple[int, ...] + reorders: tuple[int, ...] + dependencies: tuple[int, ...] + credits: tuple[int, ...] + memories: tuple[int, ...] + sinks: tuple[int, ...] + + +@dataclass(frozen=True, slots=True) +class _Fanout: + source: str + outputs: tuple[str, ...] + consumers: tuple[str, ...] + payload: str + scope: tuple[str, ...] + + +def _common_scope(scopes: list[tuple[str, ...]]) -> tuple[str, ...]: + common: list[str] = [] + for parts in zip(*scopes, strict=False): + if len(set(parts)) != 1: + break + common.append(parts[0]) + return tuple(common) + + +def _fanouts(program: QueueProgram) -> tuple[_Fanout, ...]: + consumers: dict[str, list[QueueBinding]] = {} + for queue in program.queues: + if queue.input_name is not None: + consumers.setdefault(queue.input_name, []).append(queue) + existing = {queue.name for queue in program.queues} + fanouts: list[_Fanout] = [] + for source, group in consumers.items(): + if len(group) < 2: + continue + outputs = tuple(f"{source}__fanout{index}" for index in range(len(group))) + if existing.intersection(outputs): + raise QueueFrontendError( + "ACLOWER-OWNERSHIP: inferred broadcast Queue name collides with source" + ) + source_queue = next(queue for queue in program.queues if queue.name == source) + fanouts.append( + _Fanout( + source, + outputs, + tuple(queue.name for queue in group), + source_queue.payload, + _common_scope([source_queue.scope, *(queue.scope for queue in group)]), + ) + ) + return tuple(fanouts) + + +def _collection_leaves( + value: StaticQueueCollection, + path: tuple[int, ...] = (), +) -> tuple[tuple[str, tuple[int, ...]], ...]: + leaves: list[tuple[str, tuple[int, ...]]] = [] + for index, (_, member) in enumerate(value.members): + member_path = (*path, index) + if isinstance(member, str): + leaves.append((member, member_path)) + else: + leaves.extend(_collection_leaves(member, member_path)) + return tuple(leaves) + + +def _owning_arrays(program: QueueProgram) -> tuple[CollectionBinding, ...]: + queue_names = {queue.name for queue in program.queues} + result: list[CollectionBinding] = [] + claimed: set[str] = set() + for collection in program.collections: + if collection.value.kind != "array": + continue + leaves = {name for name, _ in _collection_leaves(collection.value)} + if ( + not leaves + or not leaves.issubset(queue_names) + or claimed.intersection(leaves) + ): + continue + result.append(collection) + claimed.update(leaves) + return tuple(result) + + +def _array_cpp_type( + value: StaticQueueCollection, queues: dict[str, QueueBinding] +) -> str: + first = value.members[0][1] + element = ( + f"gfsim::SimQueue<{_cpp_type(queues[first].payload)}>" + if isinstance(first, str) + else _array_cpp_type(first, queues) + ) + return f"std::array<{element}, {len(value.members)}>" + + +def _object_ids(program: QueueProgram, fanouts: tuple[_Fanout, ...]) -> _ObjectIds: + next_id = 0 + queues: dict[str, int] = {} + for queue in program.queues: + queues[queue.name] = next_id + next_id += 1 + fanout_queues: dict[str, int] = {} + for fanout in fanouts: + for output in fanout.outputs: + fanout_queues[output] = next_id + next_id += 1 + feedback_states = tuple(range(next_id, next_id + len(program.feedbacks))) + next_id += len(feedback_states) + broadcasts = tuple(range(next_id, next_id + len(fanouts))) + next_id += len(broadcasts) + transforms: dict[str, int] = {} + for queue in program.queues: + if queue.input_name is not None: + transforms[queue.name] = next_id + next_id += 1 + routes = tuple(range(next_id, next_id + len(program.routes))) + next_id += len(routes) + forks = tuple(range(next_id, next_id + len(program.forks))) + next_id += len(forks) + merges = tuple(range(next_id, next_id + len(program.merges))) + next_id += len(merges) + barriers = tuple(range(next_id, next_id + len(program.barriers))) + next_id += len(barriers) + feedbacks = tuple(range(next_id, next_id + len(program.feedbacks))) + next_id += len(feedbacks) + reorders = tuple(range(next_id, next_id + len(program.reorders))) + next_id += len(reorders) + dependencies = tuple(range(next_id, next_id + len(program.dependencies))) + next_id += len(dependencies) + credits = tuple(range(next_id, next_id + len(program.credits))) + next_id += len(credits) + memories = tuple(range(next_id, next_id + len(program.memories))) + next_id += len(memories) + sinks = tuple(range(next_id, next_id + len(program.sinks))) + return _ObjectIds( + queues, + fanout_queues, + feedback_states, + broadcasts, + transforms, + routes, + forks, + merges, + barriers, + feedbacks, + reorders, + dependencies, + credits, + memories, + sinks, + ) + + +def lower_queue_program_to_cpp(program: QueueProgram) -> str: + fanouts = _fanouts(program) + arrays = _owning_arrays(program) + ids = _object_ids(program, fanouts) + queues_by_name = {queue.name: queue for queue in program.queues} + array_leaf: dict[str, tuple[str, tuple[int, ...]]] = { + leaf: (collection.name, path) + for collection in arrays + for leaf, path in _collection_leaves(collection.value) + } + + def queue_ref(name: str) -> str: + owner = array_leaf.get(name) + if owner is None: + return f"{name}_" + collection, path = owner + return f"{collection}_" + "".join(f"[{index}]" for index in path) + + def queue_initializer(name: str) -> str: + queue = queues_by_name[name] + return ( + f'gfsim::SimQueue<{_cpp_type(queue.payload)}>("{queue.name}", ' + f"{ids.queues[queue.name]}, {module_ptr(queue_owner[queue.name])}, " + f"{queue.depth}, " + "std::numeric_limits::max(), nullptr, " + f"{queue.latency}, {queue.rate})" + ) + + def array_initializer(value: StaticQueueCollection) -> str: + members = [] + for _, member in value.members: + members.append( + queue_initializer(member) + if isinstance(member, str) + else array_initializer(member) + ) + return "{{" + ", ".join(members) + "}}" + + effective_input = { + consumer: output + for fanout in fanouts + for consumer, output in zip(fanout.consumers, fanout.outputs, strict=True) + } + scope_members = { + scope.path: "scope_" + "__".join(scope.path) + "_" for scope in program.scopes + } + + def module_ptr(path: tuple[str, ...]) -> str: + return "this" if not path else f"&{scope_members[path]}" + + def attach(path: tuple[str, ...], member: str) -> str: + if not path: + return f" attachChild({member});" + return f" {scope_members[path]}.attachChild({member});" + + queue_uses: dict[str, list[tuple[str, ...]]] = { + queue.name: [] for queue in program.queues + } + for queue in program.queues: + if queue.input_name is not None: + queue_uses[queue.input_name].append(queue.scope) + for route in program.routes: + queue_uses[route.input_name].append(route.scope) + for fork in program.forks: + queue_uses[fork.input_name].append(fork.scope) + for merge in program.merges: + for input_name in merge.inputs: + queue_uses[input_name].append(merge.scope) + for feedback in program.feedbacks: + queue_uses[feedback.input_name].append(feedback.scope) + for reorder in program.reorders: + queue_uses[reorder.input_name].append(reorder.scope) + for dependency in program.dependencies: + queue_uses[dependency.input_name].append(dependency.scope) + for credit in program.credits: + queue_uses[credit.input_name].append(credit.scope) + for barrier in program.barriers: + for input_name in barrier.inputs: + queue_uses[input_name].append(barrier.scope) + for memory in program.memories: + queue_uses[memory.input_name].append(memory.scope) + for sink in program.sinks: + queue_uses[sink.queue].append(sink.scope) + queue_owner = { + queue.name: _common_scope([queue.scope, *queue_uses[queue.name]]) + if queue_uses[queue.name] + else queue.scope + for queue in program.queues + } + lines = [ + "// Generated by Agentic Circuit Queue/Var; do not edit.", + '#include "gfsim/dispatch.h"', + '#include "gfsim/object.h"', + '#include "gfsim/queue.h"', + '#include "gfsim/queue_blocks.h"', + "", + "#include ", + "#include ", + "#include ", + "#include ", + "", + "namespace ac_generated {", + "", + ] + if program.specialization_fingerprint is not None: + lines.insert(1, f"// Specialization: {program.specialization_fingerprint}") + for payload in program.payloads: + lines.extend(_emit_payload(payload)) + for queue in program.queues: + if queue.input_name is None: + continue + payload = _cpp_type(queue.payload) + lines.extend( + ( + f"struct {queue.name}_policy {{", + f" {payload} operator()(const {payload} &item) const {{", + *_policy_body(queue), + " }", + "};", + "", + ) + ) + for index, route in enumerate(program.routes): + payload = _cpp_type( + next( + queue.payload + for queue in program.queues + if queue.name == route.input_name + ) + ) + expression = _CppExpression(route.argument).emit(route.selector) + lines.extend( + ( + f"struct route_{index}_policy {{", + f" size_t operator()(const {payload} &item) const {{", + f" return static_cast({expression});", + " }", + "};", + "", + ) + ) + for index, feedback in enumerate(program.feedbacks): + payload = _cpp_type( + next( + queue.payload + for queue in program.queues + if queue.name == feedback.output_name + ) + ) + lines.extend( + ( + f"struct feedback_{index}_update_policy {{", + f" {payload} operator()(const {payload} &item) const {{", + *_expression_policy_body(feedback.argument, feedback.update), + " }", + "};", + "", + f"struct feedback_{index}_condition_policy {{", + f" bool operator()(const {payload} &item) const {{", + f" return {_CppExpression(feedback.argument).emit(feedback.condition)};", + " }", + "};", + "", + ) + ) + for index, reorder in enumerate(program.reorders): + payload = _cpp_type(queues_by_name[reorder.input_name].payload) + expression = _CppExpression(reorder.argument).emit(reorder.key) + lines.extend( + ( + f"struct reorder_{index}_key_policy {{", + f" std::uint64_t operator()(const {payload} &item) const {{", + f" return static_cast({expression});", + " }", + "};", + "", + ) + ) + for index, dependency in enumerate(program.dependencies): + payload = _cpp_type(queues_by_name[dependency.input_name].payload) + for suffix, expression_node in ( + ("key", dependency.key), + ("dependency", dependency.waits_for), + ("resource", dependency.resource), + ("cost", dependency.cost), + ): + expression = _CppExpression(dependency.argument).emit(expression_node) + lines.extend( + ( + f"struct dependency_{index}_{suffix}_policy {{", + f" std::uint64_t operator()(const {payload} &item) const {{", + f" return static_cast({expression});", + " }", + "};", + "", + ) + ) + for index, credit in enumerate(program.credits): + payload = _cpp_type(queues_by_name[credit.input_name].payload) + expression = _CppExpression(credit.argument).emit(credit.cost) + lines.extend( + ( + f"struct credit_{index}_cost_policy {{", + f" std::uint64_t operator()(const {payload} &item) const {{", + f" return static_cast({expression});", + " }", + "};", + "", + ) + ) + for index, memory in enumerate(program.memories): + payload = _cpp_type(queues_by_name[memory.input_name].payload) + data_type = _cpp_type(memory.data_type) + policies = ( + ("address", "std::uint64_t", memory.address), + ("write", "bool", memory.write), + ("data", data_type, memory.data), + ) + for suffix, result_type, expression_node in policies: + expression = _CppExpression(memory.argument).emit(expression_node) + lines.extend( + ( + f"struct memory_{index}_{suffix}_policy {{", + f" {result_type} operator()(size_t, const {payload} &item) const {{", + f" return static_cast<{result_type}>({expression});", + " }", + "};", + "", + ) + ) + lines.extend( + ( + f"struct memory_{index}_response_policy {{", + f" {payload} operator()(size_t, const {payload} &item, " + f"const {data_type} &old_data) const {{", + " auto result = item;", + f" result.{memory.result_field} = old_data;", + " return result;", + " }", + "};", + "", + ) + ) + + class_name = "".join(part.capitalize() for part in program.system.split("_")) + lines.extend((f"class {class_name} final : public gfsim::Module {{", "public:")) + lines.append( + f' {class_name}() : gfsim::Module("{program.system}", ' + "gfsim::kInvalidObjectId, nullptr)," + ) + initializers: list[str] = [] + for scope in program.scopes: + initializers.append( + f'{scope_members[scope.path]}("{scope.name}", ' + f"gfsim::kInvalidObjectId, {module_ptr(scope.path[:-1])})" + ) + for queue in program.queues: + if queue.name in array_leaf: + continue + payload = _cpp_type(queue.payload) + initializers.append( + f'{queue.name}_("{queue.name}", {ids.queues[queue.name]}, ' + f"{module_ptr(queue_owner[queue.name])}, " + f"{queue.depth}, std::numeric_limits::max(), nullptr, " + f"{queue.latency}, {queue.rate})" + ) + for collection in arrays: + initializers.append(f"{collection.name}_" + array_initializer(collection.value)) + for fanout in fanouts: + for output in fanout.outputs: + initializers.append( + f'{output}_("{output}", {ids.fanout_queues[output]}, ' + f"{module_ptr(fanout.scope)}, " + "1, std::numeric_limits::max(), nullptr, 1)" + ) + for index, feedback in enumerate(program.feedbacks): + payload = _cpp_type( + next( + queue.payload + for queue in program.queues + if queue.name == feedback.output_name + ) + ) + initializers.append( + f'feedback_{index}_state_("feedback_{index}_state", ' + f"{ids.feedback_states[index]}, {module_ptr(feedback.scope)}, 1, " + "std::numeric_limits::max(), nullptr, 1)" + ) + for index, reorder in enumerate(program.reorders): + initializers.append( + f'reorder_{index}_block_("reorder_{index}", ' + f"{ids.reorders[index]}, {module_ptr(reorder.scope)}, " + f"{queue_ref(reorder.input_name)}, {queue_ref(reorder.output_name)})" + ) + for index, dependency in enumerate(program.dependencies): + suffix = ( + ")" + if dependency.provider == "schedule" + else f", {dependency.capacity}, {dependency.resources}, " + f"{dependency.no_dependency})" + ) + initializers.append( + f'dependency_{index}_block_("{dependency.provider}_{index}", ' + f"{ids.dependencies[index]}, {module_ptr(dependency.scope)}, " + f"{queue_ref(dependency.input_name)}, " + f"{queue_ref(dependency.output_name)}{suffix}" + ) + for index, credit in enumerate(program.credits): + suffix = ")" if credit.provider == "engine" else f", {credit.credits})" + initializers.append( + f'credit_{index}_block_("{credit.provider}_{index}", ' + f"{ids.credits[index]}, {module_ptr(credit.scope)}, " + f"{queue_ref(credit.input_name)}, {queue_ref(credit.output_name)}" + f"{suffix}" + ) + for index, memory in enumerate(program.memories): + payload = _cpp_type(queues_by_name[memory.input_name].payload) + initializers.append( + f'memory_{index}_block_("table_{index}", ' + f"{ids.memories[index]}, {module_ptr(memory.scope)}, " + f"std::array *, 1>{{" + f"&{queue_ref(memory.input_name)}}}, " + f"std::array *, 1>{{" + f"&{queue_ref(memory.output_name)}}}, {memory.entries}, " + f"{memory.init}, {memory.latency})" + ) + for index, fanout in enumerate(fanouts): + payload = _cpp_type(fanout.payload) + outputs = ", ".join(f"&{queue_ref(name)}" for name in fanout.outputs) + initializers.append( + f'broadcast_{index}_block_("broadcast_{index}", ' + f"{ids.broadcasts[index]}, {module_ptr(fanout.scope)}, " + f"{queue_ref(fanout.source)}, " + f"std::array *, {len(fanout.outputs)}>" + f"{{{outputs}}})" + ) + for index, route in enumerate(program.routes): + payload = _cpp_type( + next( + queue.payload + for queue in program.queues + if queue.name == route.input_name + ) + ) + outputs = ", ".join(f"&{queue_ref(name)}" for name in route.outputs) + initializers.append( + f'route_{index}_block_("route_{index}", {ids.routes[index]}, ' + f"{module_ptr(route.scope)}, " + f"{queue_ref(route.input_name)}, std::array *, " + f"{len(route.outputs)}>{{{outputs}}})" + ) + for index, fork in enumerate(program.forks): + payload = _cpp_type(queues_by_name[fork.input_name].payload) + outputs = ", ".join(f"&{queue_ref(name)}" for name in fork.outputs) + initializers.append( + f'fork_{index}_block_("fork_{index}", {ids.forks[index]}, ' + f"{module_ptr(fork.scope)}, {queue_ref(fork.input_name)}, " + f"std::array *, {len(fork.outputs)}>" + f"{{{outputs}}})" + ) + for index, merge in enumerate(program.merges): + payload = _cpp_type( + next( + queue.payload for queue in program.queues if queue.name == merge.output + ) + ) + inputs = ", ".join(f"&{queue_ref(name)}" for name in merge.inputs) + policy = ( + "gfsim::QueueMergePolicy::RoundRobin" + if merge.policy == "round_robin" + else "gfsim::QueueMergePolicy::Priority" + ) + initializers.append( + f'merge_{index}_block_("merge_{index}", {ids.merges[index]}, ' + f"{module_ptr(merge.scope)}, " + f"std::array *, {len(merge.inputs)}>" + f"{{{inputs}}}, {queue_ref(merge.output)}, {policy})" + ) + for index, barrier in enumerate(program.barriers): + inputs = ", ".join(f"&{queue_ref(name)}" for name in barrier.inputs) + outputs = ", ".join(f"&{queue_ref(name)}" for name in barrier.outputs) + initializers.append( + f'barrier_{index}_block_("barrier_{index}", ' + f"{ids.barriers[index]}, {module_ptr(barrier.scope)}, " + f"std::tuple{{{inputs}}}, std::tuple{{{outputs}}})" + ) + for index, feedback in enumerate(program.feedbacks): + initializers.append( + f'feedback_{index}_block_("feedback_{index}", ' + f"{ids.feedbacks[index]}, {module_ptr(feedback.scope)}, " + f"{queue_ref(feedback.input_name)}, " + f"feedback_{index}_state_, {queue_ref(feedback.output_name)}, " + f"{feedback.max_iterations})" + ) + for queue in program.queues: + if queue.input_name is None: + continue + input_name = effective_input.get(queue.name, queue.input_name) + initializers.append( + f'{queue.name}_block_("{queue.name}_transform", ' + f"{ids.transforms[queue.name]}, {module_ptr(queue.scope)}, " + f"{queue_ref(input_name)}, " + f"{queue_ref(queue.name)})" + ) + for index, sink in enumerate(program.sinks): + initializers.append( + f'sink_{index}_("sink_{index}", {ids.sinks[index]}, ' + f"{module_ptr(sink.scope)}, " + f"{queue_ref(sink.queue)})" + ) + for index, initializer in enumerate(initializers): + suffix = "," if index + 1 < len(initializers) else "" + lines.append(f" {initializer}{suffix}") + lines.extend((" {", f' setPath("/{program.system}");')) + for scope in program.scopes: + lines.append(attach(scope.path[:-1], scope_members[scope.path])) + for queue in program.queues: + lines.append(attach(queue_owner[queue.name], queue_ref(queue.name))) + for fanout in fanouts: + for output in fanout.outputs: + lines.append(attach(fanout.scope, f"{output}_")) + for index, _ in enumerate(program.feedbacks): + lines.append(attach(program.feedbacks[index].scope, f"feedback_{index}_state_")) + for index, reorder in enumerate(program.reorders): + lines.append(attach(reorder.scope, f"reorder_{index}_block_")) + for index, dependency in enumerate(program.dependencies): + lines.append(attach(dependency.scope, f"dependency_{index}_block_")) + for index, credit in enumerate(program.credits): + lines.append(attach(credit.scope, f"credit_{index}_block_")) + for index, memory in enumerate(program.memories): + lines.append(attach(memory.scope, f"memory_{index}_block_")) + for index, _ in enumerate(fanouts): + lines.append(attach(fanouts[index].scope, f"broadcast_{index}_block_")) + for queue in program.queues: + if queue.input_name is not None: + lines.append(attach(queue.scope, f"{queue.name}_block_")) + for index, _ in enumerate(program.routes): + lines.append(attach(program.routes[index].scope, f"route_{index}_block_")) + for index, fork in enumerate(program.forks): + lines.append(attach(fork.scope, f"fork_{index}_block_")) + for index, _ in enumerate(program.merges): + lines.append(attach(program.merges[index].scope, f"merge_{index}_block_")) + for index, barrier in enumerate(program.barriers): + lines.append(attach(barrier.scope, f"barrier_{index}_block_")) + for index, _ in enumerate(program.feedbacks): + lines.append(attach(program.feedbacks[index].scope, f"feedback_{index}_block_")) + for index, _ in enumerate(program.sinks): + lines.append(attach(program.sinks[index].scope, f"sink_{index}_")) + lines.extend((" }", "")) + for queue in program.queues: + payload = _cpp_type(queue.payload) + lines.append( + f" gfsim::SimQueue<{payload}> &{queue.name}() {{ " + f"return {queue_ref(queue.name)}; }}" + ) + for index, sink in enumerate(program.sinks): + payload = _cpp_type( + next(q.payload for q in program.queues if q.name == sink.queue) + ) + lines.append( + f" const std::vector<{payload}> &sink_{index}_values() const {{ " + f"return sink_{index}_.received(); }}" + ) + object_count = ( + len(program.queues) + + len(ids.fanout_queues) + + len(ids.feedback_states) + + len(ids.broadcasts) + + len(ids.transforms) + + len(ids.routes) + + len(ids.forks) + + len(ids.merges) + + len(ids.barriers) + + len(ids.feedbacks) + + len(ids.reorders) + + len(ids.dependencies) + + len(ids.credits) + + len(ids.memories) + + len(ids.sinks) + ) + lines.extend( + ( + "", + f" std::array dispatch_rows() {{", + f" return std::array{{", + ) + ) + rows: list[str] = [] + for queue in program.queues: + rows.append(f"gfsim::makeDispatchRow(&{queue_ref(queue.name)})") + for fanout in fanouts: + for output in fanout.outputs: + rows.append(f"gfsim::makeDispatchRow(&{output}_)") + for index, _ in enumerate(program.feedbacks): + rows.append(f"gfsim::makeDispatchRow(&feedback_{index}_state_)") + for index, _ in enumerate(fanouts): + rows.append(f"gfsim::makeDispatchRow(&broadcast_{index}_block_)") + for queue in program.queues: + if queue.input_name is not None: + rows.append(f"gfsim::makeDispatchRow(&{queue.name}_block_)") + for index, _ in enumerate(program.routes): + rows.append(f"gfsim::makeDispatchRow(&route_{index}_block_)") + for index, _ in enumerate(program.forks): + rows.append(f"gfsim::makeDispatchRow(&fork_{index}_block_)") + for index, _ in enumerate(program.merges): + rows.append(f"gfsim::makeDispatchRow(&merge_{index}_block_)") + for index, _ in enumerate(program.barriers): + rows.append(f"gfsim::makeDispatchRow(&barrier_{index}_block_)") + for index, _ in enumerate(program.feedbacks): + rows.append(f"gfsim::makeDispatchRow(&feedback_{index}_block_)") + for index, _ in enumerate(program.reorders): + rows.append(f"gfsim::makeDispatchRow(&reorder_{index}_block_)") + for index, _ in enumerate(program.dependencies): + rows.append(f"gfsim::makeDispatchRow(&dependency_{index}_block_)") + for index, _ in enumerate(program.credits): + rows.append(f"gfsim::makeDispatchRow(&credit_{index}_block_)") + for index, _ in enumerate(program.memories): + rows.append(f"gfsim::makeDispatchRow(&memory_{index}_block_)") + for index, _ in enumerate(program.sinks): + rows.append(f"gfsim::makeDispatchRow(&sink_{index}_)") + for index, row in enumerate(rows): + suffix = "," if index + 1 < len(rows) else "" + lines.append(f" {row}{suffix}") + lines.extend((" };", " }", "", "private:")) + for scope in program.scopes: + lines.append(f" gfsim::Module {scope_members[scope.path]};") + for queue in program.queues: + if queue.name in array_leaf: + continue + lines.append(f" gfsim::SimQueue<{_cpp_type(queue.payload)}> {queue.name}_;") + for collection in arrays: + lines.append( + f" {_array_cpp_type(collection.value, queues_by_name)} {collection.name}_;" + ) + for fanout in fanouts: + for output in fanout.outputs: + lines.append(f" gfsim::SimQueue<{_cpp_type(fanout.payload)}> {output}_;") + for index, feedback in enumerate(program.feedbacks): + payload = _cpp_type( + next( + queue.payload + for queue in program.queues + if queue.name == feedback.output_name + ) + ) + lines.append( + f" gfsim::SimQueue> " + f"feedback_{index}_state_;" + ) + for index, reorder in enumerate(program.reorders): + payload = _cpp_type(queues_by_name[reorder.input_name].payload) + lines.append( + f" gfsim::Reorder<{payload}, {reorder.capacity}, {reorder.start}, " + f"reorder_{index}_key_policy> " + f"reorder_{index}_block_;" + ) + for index, dependency in enumerate(program.dependencies): + payload = _cpp_type(queues_by_name[dependency.input_name].payload) + if dependency.provider == "schedule": + lines.append( + f" gfsim::Schedule<{payload}, {dependency.capacity}, " + f"{dependency.resources}, {dependency.no_dependency}, " + f"dependency_{index}_key_policy, " + f"dependency_{index}_dependency_policy, " + f"dependency_{index}_resource_policy, " + f"dependency_{index}_cost_policy> dependency_{index}_block_;" + ) + else: + lines.append( + f" gfsim::QueueDependency<{payload}, " + f"dependency_{index}_key_policy, " + f"dependency_{index}_dependency_policy, " + f"dependency_{index}_resource_policy, " + f"dependency_{index}_cost_policy> dependency_{index}_block_;" + ) + for index, credit in enumerate(program.credits): + payload = _cpp_type(queues_by_name[credit.input_name].payload) + provider = ( + f"gfsim::Engine<{payload}, {credit.credits}, " + if credit.provider == "engine" + else f"gfsim::QueueCredit<{payload}, " + ) + lines.append(f" {provider}credit_{index}_cost_policy> credit_{index}_block_;") + for index, memory in enumerate(program.memories): + payload = _cpp_type(queues_by_name[memory.input_name].payload) + data_type = _cpp_type(memory.data_type) + lines.append( + f" gfsim::QueueMemoryArbiter<{payload}, {data_type}, 1, " + f"memory_{index}_address_policy, " + f"memory_{index}_write_policy, memory_{index}_data_policy, " + f"memory_{index}_response_policy> memory_{index}_block_;" + ) + for index, fanout in enumerate(fanouts): + payload = _cpp_type(fanout.payload) + lines.append( + f" gfsim::QueueBroadcast<{payload}, {len(fanout.outputs)}> " + f"broadcast_{index}_block_;" + ) + for queue in program.queues: + if queue.input_name is None: + continue + payload = _cpp_type(queue.payload) + if queue.provider == "compute": + provider = ( + f"gfsim::Compute<{payload}, {payload}, {queue.rate}, " + f"{queue.name}_policy>" + ) + elif queue.provider == "pipeline": + provider = f"gfsim::Pipeline<{payload}, {queue.latency}, {queue.rate}>" + else: + provider = ( + f"gfsim::QueueTransform<{payload}, {payload}, {queue.name}_policy>" + ) + lines.append(f" {provider} {queue.name}_block_;") + for index, route in enumerate(program.routes): + payload = _cpp_type( + next( + queue.payload + for queue in program.queues + if queue.name == route.input_name + ) + ) + lines.append( + f" gfsim::QueueRoute<{payload}, {len(route.outputs)}, " + f"route_{index}_policy> route_{index}_block_;" + ) + for index, fork in enumerate(program.forks): + payload = _cpp_type(queues_by_name[fork.input_name].payload) + lines.append( + f" gfsim::QueueFork<{payload}, {len(fork.outputs)}> fork_{index}_block_;" + ) + for index, merge in enumerate(program.merges): + payload = _cpp_type( + next( + queue.payload for queue in program.queues if queue.name == merge.output + ) + ) + lines.append( + f" gfsim::QueueMerge<{payload}, {len(merge.inputs)}> merge_{index}_block_;" + ) + for index, barrier in enumerate(program.barriers): + types = ", ".join( + _cpp_type(queues_by_name[name].payload) for name in barrier.inputs + ) + lines.append( + f" gfsim::QueueBarrier> barrier_{index}_block_;" + ) + for index, feedback in enumerate(program.feedbacks): + payload = _cpp_type( + next( + queue.payload + for queue in program.queues + if queue.name == feedback.output_name + ) + ) + lines.append( + f" gfsim::QueueFeedback<{payload}, feedback_{index}_update_policy, " + f"feedback_{index}_condition_policy> feedback_{index}_block_;" + ) + for index, sink in enumerate(program.sinks): + payload = _cpp_type( + next(q.payload for q in program.queues if q.name == sink.queue) + ) + lines.append(f" gfsim::QueueSink<{payload}> sink_{index}_;") + lines.extend(("};", "", "} // namespace ac_generated", "")) + return "\n".join(lines) + + +def _emit_payload(payload: Payload) -> list[str]: + lines = [f"struct {payload.name} {{"] + for name, typ in payload.fields: + lines.append(f" {_cpp_type(typ)} {name}{{}};") + lines.extend(("};", "")) + return lines + + +def lower_queue_source_to_cpp(text: str, system: str) -> str: + return lower_queue_program_to_cpp(parse_queue_program(text, system)) diff --git a/components/agentic-circuit/src/agentic_circuit/_queue_frontend.py b/components/agentic-circuit/src/agentic_circuit/_queue_frontend.py new file mode 100644 index 00000000..a19f4e84 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_queue_frontend.py @@ -0,0 +1,4314 @@ +"""serial-Python to Queue/Var ACIR construction.""" + +from __future__ import annotations + +import ast +import copy +import json +from collections.abc import Mapping +from dataclasses import dataclass + +from ._static_eval import StaticEnvironment, StaticValue, evaluate_static + + +class QueueFrontendError(ValueError): + """A stable rejection from the queue frontend.""" + + +@dataclass(frozen=True, slots=True) +class Payload: + name: str + fields: tuple[tuple[str, str], ...] + + @property + def acir_type(self) -> str: + return f"!ac.struct<@types::@{self.name}>" + + +@dataclass(frozen=True, slots=True) +class QueueBinding: + name: str + payload: str + depth: int + latency: int + input_name: str | None + argument: str | None = None + expression: ast.expr | None = None + scope: tuple[str, ...] = () + order: int = 0 + route_output: bool = False + feedback_output: bool = False + merge_output: bool = False + reorder_output: bool = False + dependency_output: bool = False + credit_output: bool = False + memory_output: bool = False + barrier_output: bool = False + select_output: bool = False + firing_effect: bool = False + atomic_group: int | None = None + provider: str = "transform" + rate: int = 1 + + +@dataclass(frozen=True, slots=True) +class ScopeBinding: + name: str + path: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class SinkBinding: + queue: str + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class ObservationBinding: + queue: str + name: str + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class ExpectBinding: + queue: str + argument: str + predicate: ast.expr + message: str + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class RouteBinding: + input_name: str + outputs: tuple[str, ...] + argument: str + selector: ast.expr + depth: int + latency: int + scope: tuple[str, ...] + order: int + boolean_selector: bool = False + + +@dataclass(frozen=True, slots=True) +class ForkBinding: + input_name: str + outputs: tuple[str, ...] + depth: int + latency: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class FeedbackBinding: + input_name: str + output_name: str + argument: str + condition: ast.expr + update: ast.expr + depth: int + latency: int + max_iterations: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class MergeBinding: + inputs: tuple[str, ...] + output: str + policy: str + depth: int + latency: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class ReorderBinding: + input_name: str + output_name: str + argument: str + key: ast.expr + capacity: int + start: int + depth: int + latency: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class DependencyBinding: + input_name: str + output_name: str + argument: str + key: ast.expr + waits_for: ast.expr + resource: ast.expr + cost: ast.expr + capacity: int + resources: int + no_dependency: int + depth: int + latency: int + scope: tuple[str, ...] + order: int + provider: str = "dependency" + + +@dataclass(frozen=True, slots=True) +class CreditBinding: + input_name: str + output_name: str + argument: str + cost: ast.expr + credits: int + depth: int + latency: int + scope: tuple[str, ...] + order: int + provider: str = "credit" + + +@dataclass(frozen=True, slots=True) +class BarrierBinding: + inputs: tuple[str, ...] + outputs: tuple[str, ...] + depth: int + latency: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class SelectBinding: + control: str + inputs: tuple[str, ...] + output: str + argument: str + selector: ast.expr + depth: int + latency: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class MemoryInstanceBinding: + name: str + data_type: str + entries: int + init: int + latency: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class MemoryRequestBinding: + instance: str + input_name: str + output_name: str + argument: str + address: ast.expr + write: ast.expr + data: ast.expr + result_field: str + depth: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class MemoryBinding: + input_name: str + output_name: str + argument: str + address: ast.expr + write: ast.expr + data: ast.expr + data_type: str + entries: int + init: int + result_field: str + depth: int + latency: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class StaticMemoryArrayBinding: + name: str + members: tuple[str, ...] + data_type: str + entries: int + init: int + latency: int + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class SelectedMemoryBinding: + name: str + array: str + input_name: str + routed_inputs: tuple[str, ...] + argument: str + selector: ast.expr + depth: int + latency: int + scope: tuple[str, ...] + order: int + provider: str = "memory" + + +@dataclass(frozen=True, slots=True) +class AtomicBinding: + queues: tuple[str, ...] + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class StaticQueueCollection: + kind: str + members: tuple[tuple[str | int | bool, str | StaticQueueCollection], ...] + + +@dataclass(frozen=True, slots=True) +class RecursiveQueueHelper: + queue_parameter: str + count_parameter: str + argument: str + expression: ast.expr + apply_call: ast.Call + + +@dataclass(frozen=True, slots=True) +class CollectionBinding: + name: str + value: StaticQueueCollection + scope: tuple[str, ...] + order: int + + +@dataclass(frozen=True, slots=True) +class QueueProgram: + system: str + payloads: tuple[Payload, ...] + queues: tuple[QueueBinding, ...] + scopes: tuple[ScopeBinding, ...] + routes: tuple[RouteBinding, ...] + forks: tuple[ForkBinding, ...] + feedbacks: tuple[FeedbackBinding, ...] + merges: tuple[MergeBinding, ...] + reorders: tuple[ReorderBinding, ...] + dependencies: tuple[DependencyBinding, ...] + credits: tuple[CreditBinding, ...] + barriers: tuple[BarrierBinding, ...] + selects: tuple[SelectBinding, ...] + memory_instances: tuple[MemoryInstanceBinding, ...] + memory_requests: tuple[MemoryRequestBinding, ...] + memories: tuple[MemoryBinding, ...] + atomics: tuple[AtomicBinding, ...] + collections: tuple[CollectionBinding, ...] + observations: tuple[ObservationBinding, ...] + expectations: tuple[ExpectBinding, ...] + sinks: tuple[SinkBinding, ...] + specialization_fingerprint: str | None = None + + +def _decorator_name(node: ast.expr) -> str: + if isinstance(node, ast.Call): + return _decorator_name(node.func) + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + prefix = _decorator_name(node.value) + return f"{prefix}.{node.attr}" if prefix else node.attr + return "" + + +def _scalar_type(node: ast.expr) -> str: + name = _decorator_name(node).rsplit(".", 1)[-1] + if name == "int": + return "i64" + if name == "bool": + return "i1" + widths = { + "u1": 1, + "u2": 2, + "u4": 4, + "u8": 8, + "u16": 16, + "u32": 32, + "u64": 64, + "s8": 8, + "s16": 16, + "s32": 32, + "s64": 64, + } + if name in widths: + return f"i{widths[name]}" + raise QueueFrontendError("ACPY-QUEUE-002: unsupported field type") + + +def _payloads(tree: ast.Module) -> tuple[Payload, ...]: + result: list[Payload] = [] + for node in tree.body: + if not isinstance(node, ast.ClassDef) or not any( + _decorator_name(item).rsplit(".", 1)[-1] == "struct" + for item in node.decorator_list + ): + continue + fields: list[tuple[str, str]] = [] + for statement in node.body: + if not isinstance(statement, ast.AnnAssign) or not isinstance( + statement.target, ast.Name + ): + raise QueueFrontendError( + "ACPY-QUEUE-002: struct body requires annotated fields" + ) + fields.append((statement.target.id, _scalar_type(statement.annotation))) + if not fields or len({name for name, _ in fields}) != len(fields): + raise QueueFrontendError( + "ACPY-QUEUE-002: struct requires unique compile-time fields" + ) + result.append(Payload(node.name, tuple(fields))) + return tuple(result) + + +def _static_int_value(node: ast.expr, values: Mapping[str, StaticValue]) -> int | None: + try: + value = evaluate_static(node, StaticEnvironment(values)) + except ValueError: + return None + return value if type(value) is int else None + + +def _positive_int_value( + call: ast.Call, + name: str, + default: int, + static_values: Mapping[str, StaticValue] | None = None, +) -> int: + matches = [keyword for keyword in call.keywords if keyword.arg == name] + if len(matches) > 1: + raise QueueFrontendError(f"ACPY-QUEUE-001: repeated {name!r}") + if not matches: + return default + value = _static_int_value(matches[0].value, static_values or {}) + if value is None: + raise QueueFrontendError( + f"ACPY-QUEUE-001: {name} must be a compile-time integer" + ) + if value <= 0: + raise QueueFrontendError(f"ACPY-QUEUE-001: {name} must be positive") + return value + + +def _nonnegative_int_value( + call: ast.Call, + name: str, + default: int, + static_values: Mapping[str, StaticValue] | None = None, +) -> int: + matches = [keyword for keyword in call.keywords if keyword.arg == name] + if len(matches) > 1: + raise QueueFrontendError(f"ACPY-QUEUE-001: repeated {name!r}") + if not matches: + return default + value = _static_int_value(matches[0].value, static_values or {}) + if value is None: + raise QueueFrontendError( + f"ACPY-QUEUE-001: {name} must be a compile-time integer" + ) + if value < 0: + raise QueueFrontendError(f"ACPY-QUEUE-001: {name} must be non-negative") + return value + + +def _payload(node: ast.expr, payloads: dict[str, Payload]) -> str: + try: + return _scalar_type(node) + except QueueFrontendError: + pass + if isinstance(node, ast.Name) and node.id in payloads: + return payloads[node.id].acir_type + raise QueueFrontendError( + "ACPY-QUEUE-002: source payload must be a compile-time supported type" + ) + + +def _lambda_value(node: ast.expr) -> tuple[str, ast.expr]: + if not isinstance(node, ast.Lambda) or len(node.args.args) != 1: + raise QueueFrontendError("ACPY-QUEUE-003: apply requires a one-argument lambda") + return node.args.args[0].arg, node.body + + +def _constantize_expression( + node: ast.expr, + argument: str, + values: Mapping[str, StaticValue], +) -> ast.expr: + class Constantizer(ast.NodeTransformer): + def _constant(self, candidate: ast.expr) -> ast.expr | None: + try: + value = evaluate_static(candidate, StaticEnvironment(values)) + except ValueError: + return None + if value is None or type(value) in {bool, int, float, str}: + return ast.copy_location(ast.Constant(value=value), candidate) + return None + + def visit_Name(self, candidate: ast.Name) -> ast.expr: + if candidate.id == argument: + return candidate + return self._constant(candidate) or candidate + + def visit_Attribute(self, candidate: ast.Attribute) -> ast.expr: + return self._constant(candidate) or self.generic_visit(candidate) + + result = Constantizer().visit(copy.deepcopy(node)) + assert isinstance(result, ast.expr) + return ast.fix_missing_locations(result) + + +def _validate_firing_expression(node: ast.expr, argument: str) -> None: + if ( + not isinstance(node, ast.Call) + or not isinstance(node.func, ast.Attribute) + or node.func.attr != "push" + or not isinstance(node.func.value, ast.Name) + or node.func.value.id != argument + or len(node.args) != 1 + or node.keywords + ): + raise QueueFrontendError("ACPY-QUEUE-019: firing must return queue.push(value)") + pops = 0 + pushes = 0 + for child in ast.walk(node): + if not isinstance(child, ast.Call) or not isinstance(child.func, ast.Attribute): + continue + if ( + not isinstance(child.func.value, ast.Name) + or child.func.value.id != argument + ): + continue + if child.func.attr in {"peek", "pop"}: + if child.args or child.keywords: + raise QueueFrontendError( + "ACPY-QUEUE-019: firing peek/pop take no arguments" + ) + pops += child.func.attr == "pop" + elif child.func.attr == "push": + if len(child.args) != 1 or child.keywords: + raise QueueFrontendError( + "ACPY-QUEUE-019: firing push requires one value" + ) + pushes += 1 + else: + raise QueueFrontendError("ACPY-QUEUE-019: unsupported queue effect method") + if pops != 1 or pushes != 1: + raise QueueFrontendError( + "ACPY-QUEUE-019: firing requires exactly one pop and one push" + ) + + +def parse_queue_program( + text: str, + system: str, + static_arguments: Mapping[str, StaticValue] | None = None, + specialization_fingerprint: str | None = None, +) -> QueueProgram: + tree = ast.parse(text, filename="", type_comments=True) + for node in tree.body: + decorators = getattr(node, "decorator_list", ()) + if any( + _decorator_name(decorator).rsplit(".", 1)[-1] + in {"opcode", "provider", "backend"} + for decorator in decorators + ): + raise QueueFrontendError( + "ACPY-QUEUE-010: user opcode or backend providers are forbidden" + ) + payloads = _payloads(tree) + payload_map = {item.name: item for item in payloads} + candidates = [ + node + for node in tree.body + if isinstance(node, ast.FunctionDef) + and node.name == system + and any( + _decorator_name(d).rsplit(".", 1)[-1] == "system" + for d in node.decorator_list + ) + ] + if len(candidates) != 1: + raise QueueFrontendError( + f"ACPY-QUEUE-001: system {system!r} is missing or ambiguous" + ) + function = candidates[0] + if specialization_fingerprint is not None: + prefix = "sha256:" + digest = specialization_fingerprint.removeprefix(prefix) + if ( + not specialization_fingerprint.startswith(prefix) + or len(digest) != 64 + or any(character not in "0123456789abcdef" for character in digest) + ): + raise QueueFrontendError( + "ACPY-QUEUE-022: specialization fingerprint is invalid" + ) + if function.args.vararg is not None or function.args.kwarg is not None: + raise QueueFrontendError( + "ACPY-QUEUE-001: a queue system cannot use variadic parameters" + ) + parameters = [ + *function.args.posonlyargs, + *function.args.args, + *function.args.kwonlyargs, + ] + supplied = dict(static_arguments or {}) + parameter_names = {parameter.arg for parameter in parameters} + extras = sorted(set(supplied) - parameter_names) + if extras: + raise QueueFrontendError( + f"ACPY-QUEUE-001: unknown static argument {extras[0]!r}" + ) + + positional_defaults: dict[str, ast.expr] = {} + positional = [*function.args.posonlyargs, *function.args.args] + if function.args.defaults: + for parameter, default in zip( + positional[-len(function.args.defaults) :], + function.args.defaults, + strict=True, + ): + positional_defaults[parameter.arg] = default + keyword_defaults = { + parameter.arg: default + for parameter, default in zip( + function.args.kwonlyargs, + function.args.kw_defaults, + strict=True, + ) + if default is not None + } + for parameter in parameters: + annotation_name = ( + _decorator_name(parameter.annotation.value).rsplit(".", 1)[-1] + if isinstance(parameter.annotation, ast.Subscript) + else "" + ) + if annotation_name != "const": + raise QueueFrontendError( + f"ACPY-QUEUE-022: system parameter {parameter.arg!r} must use ac.const" + ) + if parameter.arg in supplied: + continue + default = positional_defaults.get(parameter.arg) or keyword_defaults.get( + parameter.arg + ) + if default is None: + raise QueueFrontendError( + f"ACPY-QUEUE-022: system requires static argument {parameter.arg!r}" + ) + try: + supplied[parameter.arg] = evaluate_static( + default, StaticEnvironment(supplied) + ) + except ValueError as error: + raise QueueFrontendError( + f"ACPY-QUEUE-022: default for {parameter.arg!r} is not static" + ) from error + system_static_values: Mapping[str, StaticValue] = supplied + + def _static_int( + node: ast.expr, + values: Mapping[str, StaticValue] | None = None, + ) -> int | None: + return _static_int_value( + node, system_static_values if values is None else values + ) + + def _positive_int( + call: ast.Call, + name: str, + default: int, + values: Mapping[str, StaticValue] | None = None, + ) -> int: + return _positive_int_value( + call, + name, + default, + system_static_values if values is None else values, + ) + + def _nonnegative_int( + call: ast.Call, + name: str, + default: int, + values: Mapping[str, StaticValue] | None = None, + ) -> int: + return _nonnegative_int_value( + call, + name, + default, + system_static_values if values is None else values, + ) + + def _lambda(node: ast.expr) -> tuple[str, ast.expr]: + argument, expression = _lambda_value(node) + return argument, _constantize_expression( + expression, argument, system_static_values + ) + + recursive_helpers: dict[str, RecursiveQueueHelper] = {} + for helper in tree.body: + if ( + not isinstance(helper, ast.FunctionDef) + or helper is function + or helper.decorator_list + or len(helper.args.args) != 2 + or helper.args.posonlyargs + or helper.args.kwonlyargs + or len(helper.body) != 2 + or not isinstance(helper.body[0], ast.If) + or not isinstance(helper.body[1], ast.Return) + ): + continue + queue_parameter = helper.args.args[0].arg + count_parameter = helper.args.args[1].arg + base = helper.body[0] + recursive_return = helper.body[1] + if ( + not isinstance(base.test, ast.Compare) + or len(base.test.ops) != 1 + or not isinstance(base.test.ops[0], ast.Eq) + or len(base.test.comparators) != 1 + or not isinstance(base.test.left, ast.Name) + or base.test.left.id != count_parameter + or not isinstance(base.test.comparators[0], ast.Constant) + or base.test.comparators[0].value != 0 + or len(base.body) != 1 + or not isinstance(base.body[0], ast.Return) + or not isinstance(base.body[0].value, ast.Name) + or base.body[0].value.id != queue_parameter + or base.orelse + or not isinstance(recursive_return.value, ast.Call) + ): + continue + recursive_call = recursive_return.value + if ( + not isinstance(recursive_call.func, ast.Name) + or recursive_call.func.id != helper.name + or len(recursive_call.args) != 2 + or recursive_call.keywords + or not isinstance(recursive_call.args[0], ast.Call) + or not isinstance(recursive_call.args[1], ast.BinOp) + or not isinstance(recursive_call.args[1].op, ast.Sub) + or not isinstance(recursive_call.args[1].left, ast.Name) + or recursive_call.args[1].left.id != count_parameter + or not isinstance(recursive_call.args[1].right, ast.Constant) + or recursive_call.args[1].right.value != 1 + ): + continue + apply_call = recursive_call.args[0] + if ( + not isinstance(apply_call.func, ast.Attribute) + or apply_call.func.attr != "apply" + or not isinstance(apply_call.func.value, ast.Name) + or apply_call.func.value.id != queue_parameter + or len(apply_call.args) != 1 + ): + continue + argument, expression = _lambda(apply_call.args[0]) + recursive_helpers[helper.name] = RecursiveQueueHelper( + queue_parameter, + count_parameter, + argument, + expression, + apply_call, + ) + queues: list[QueueBinding] = [] + scopes: list[ScopeBinding] = [] + routes: list[RouteBinding] = [] + forks: list[ForkBinding] = [] + feedbacks: list[FeedbackBinding] = [] + merges: list[MergeBinding] = [] + reorders: list[ReorderBinding] = [] + dependencies: list[DependencyBinding] = [] + credits: list[CreditBinding] = [] + barriers: list[BarrierBinding] = [] + selects: list[SelectBinding] = [] + memory_instances: list[MemoryInstanceBinding] = [] + memory_requests: list[MemoryRequestBinding] = [] + memories: list[MemoryBinding] = [] + memory_by_name: dict[str, MemoryInstanceBinding] = {} + memory_arrays: dict[str, StaticMemoryArrayBinding] = {} + selected_memories: dict[str, SelectedMemoryBinding] = {} + consumed_selected_memories: set[str] = set() + atomics: list[AtomicBinding] = [] + sinks: list[SinkBinding] = [] + observations: list[ObservationBinding] = [] + expectations: list[ExpectBinding] = [] + by_name: dict[str, QueueBinding] = {} + collections: dict[str, StaticQueueCollection] = {} + collection_bindings: list[CollectionBinding] = [] + order = 0 + + def call_name(call: ast.Call) -> str: + return _decorator_name(call.func).rsplit(".", 1)[-1] + + def keyword_value(call: ast.Call, name: str) -> ast.expr: + matches = [keyword.value for keyword in call.keywords if keyword.arg == name] + if len(matches) != 1: + raise QueueFrontendError( + f"ACPY-QUEUE-024: high-level block requires one {name!r} parameter" + ) + return matches[0] + + def field_expression( + node: ast.expr, + queue: QueueBinding, + argument: str = "item", + ) -> ast.expr: + if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name): + raise QueueFrontendError( + "ACPY-QUEUE-024: high-level block requires a typed field descriptor" + ) + payload = next( + (item for item in payloads if item.acir_type == queue.payload), None + ) + if payload is None or node.value.id != payload.name: + raise QueueFrontendError( + "ACPY-QUEUE-024: field descriptor payload does not match Queue" + ) + if node.attr not in {name for name, _ in payload.fields}: + raise QueueFrontendError( + f"ACPY-QUEUE-024: payload has no field {node.attr!r}" + ) + return ast.copy_location( + ast.Attribute( + value=ast.Name(id=argument, ctx=ast.Load()), + attr=node.attr, + ctx=ast.Load(), + ), + node, + ) + + def policy_value(call: ast.Call) -> str: + matches = [ + keyword.value for keyword in call.keywords if keyword.arg == "policy" + ] + if not matches: + return "priority" + if len(matches) != 1: + raise QueueFrontendError("ACPY-QUEUE-024: repeated merge policy") + node = matches[0] + if isinstance(node, ast.Constant) and type(node.value) is str: + policy = node.value + else: + policy = _decorator_name(node).rsplit(".", 1)[-1] + if policy not in {"priority", "round_robin"}: + raise QueueFrontendError( + "ACPY-QUEUE-024: merge policy must be priority or round_robin" + ) + return policy + + def static_reference( + node: ast.expr, + aliases: dict[str, str | StaticQueueCollection], + ) -> str | StaticQueueCollection: + if isinstance(node, ast.Name): + if node.id in aliases: + return aliases[node.id] + if node.id in by_name: + return by_name[node.id].name + if node.id in collections: + return collections[node.id] + if ( + isinstance(node, ast.Subscript) + and isinstance(node.slice, ast.Constant) + and type(node.slice.value) in {str, int, bool} + ): + collection = static_reference(node.value, aliases) + if not isinstance(collection, StaticQueueCollection): + raise QueueFrontendError( + "ACPY-QUEUE-005: static indexing requires a collection" + ) + for key, value in collection.members: + if type(key) is type(node.slice.value) and key == node.slice.value: + return value + raise QueueFrontendError( + f"ACPY-QUEUE-005: collection has no key {node.slice.value!r}" + ) + raise QueueFrontendError( + "ACPY-QUEUE-005: collection reference must be statically resolvable" + ) + + def queue_reference( + node: ast.expr, + aliases: dict[str, str | StaticQueueCollection], + ) -> str: + value = static_reference(node, aliases) + if isinstance(value, str): + return value + raise QueueFrontendError( + "ACPY-QUEUE-005: a collection cannot be used as one Queue" + ) + + def collection_signature( + value: str | StaticQueueCollection, + ) -> tuple[object, ...]: + if isinstance(value, str): + return ("queue", by_name[value].payload) + keys = tuple(key for key, _ in value.members) + members = tuple(collection_signature(member) for _, member in value.members) + return (value.kind, keys, members) + + def stable_collection_identity(value: str | StaticQueueCollection) -> str: + if isinstance(value, str): + return value + return ( + value.kind + + "(" + + ",".join( + f"{key}:{stable_collection_identity(member)}" + for key, member in value.members + ) + + ")" + ) + + def source_binding( + name: str, + call: ast.Call, + scope_path: tuple[str, ...], + current_order: int, + static_values: Mapping[str, StaticValue] | None = None, + ) -> QueueBinding: + if call_name(call) != "source" or len(call.args) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-005: collection elements must be Queue sources" + ) + depth = _positive_int(call, "depth", 1, static_values) + rate = _positive_int(call, "rate", 1, static_values) + if rate > depth: + raise QueueFrontendError( + "ACPY-QUEUE-025: Queue rate must not exceed depth" + ) + return QueueBinding( + name, + _payload(call.args[0], payload_map), + depth, + _positive_int(call, "latency", 1, static_values), + None, + scope=scope_path, + order=current_order, + rate=rate, + ) + + def memory_instance_binding( + name: str, + call: ast.Call, + scope_path: tuple[str, ...], + current_order: int, + static_values: dict[str, int] | None = None, + ) -> MemoryInstanceBinding: + if call_name(call) != "memory" or len(call.args) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory requires one data type" + ) + if any( + keyword.arg is None + or keyword.arg not in {"entries", "init", "latency"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory instance has an unsupported keyword" + ) + data_type = _payload(call.args[0], payload_map) + if not data_type.startswith("i"): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory data type must be an integer" + ) + entries = _positive_int(call, "entries", 16, static_values) + init = _nonnegative_int(call, "init", 0, static_values) + latency = _positive_int(call, "latency", 1, static_values) + if init != 0: + raise QueueFrontendError("ACPY-QUEUE-015: memory init must be zero") + return MemoryInstanceBinding( + name, data_type, entries, init, latency, scope_path, current_order + ) + + def memory_request_parameters( + call: ast.Call, + incoming: QueueBinding, + data_type: str, + extra_keywords: set[str] | None = None, + ) -> tuple[str, ast.expr, ast.expr, ast.expr, str, int]: + allowed_keywords = { + "address", + "write", + "data", + "result_field", + "depth", + *(extra_keywords or set()), + } + if any( + keyword.arg is None or keyword.arg not in allowed_keywords + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory request has an unsupported keyword" + ) + policies: dict[str, ast.expr] = {} + for policy in ("address", "write", "data"): + values = [ + keyword.value for keyword in call.keywords if keyword.arg == policy + ] + if len(values) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory request requires one " + f"{policy} lambda" + ) + policies[policy] = values[0] + arguments_and_values = [_lambda(policies[item]) for item in policies] + if len({argument for argument, _ in arguments_and_values}) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory request lambdas require one argument name" + ) + result_fields = [ + keyword.value + for keyword in call.keywords + if keyword.arg == "result_field" + ] + if ( + len(result_fields) != 1 + or not isinstance(result_fields[0], ast.Constant) + or type(result_fields[0].value) is not str + or not result_fields[0].value + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory request requires one static result_field" + ) + payload = next( + ( + declaration + for declaration in payloads + if declaration.acir_type == incoming.payload + ), + None, + ) + result_field = result_fields[0].value + field_types = dict(payload.fields) if payload is not None else {} + if result_field not in field_types: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory result_field is unknown" + ) + if field_types[result_field] != data_type: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory result_field must match instance data type" + ) + return ( + arguments_and_values[0][0], + arguments_and_values[0][1], + arguments_and_values[1][1], + arguments_and_values[2][1], + result_field, + _positive_int(call, "depth", 1), + ) + + def collection_binding( + name: str, + call: ast.Call, + scope_path: tuple[str, ...], + current_order: int, + aliases: dict[str, str | StaticQueueCollection], + static_values: Mapping[str, StaticValue] | None = None, + ) -> StaticQueueCollection | None: + static_values = system_static_values if static_values is None else static_values + kind = call_name(call) + if kind == "array": + extent = ( + _static_int(call.args[0], static_values) + if len(call.args) == 2 + else None + ) + if len(call.args) != 2 or extent is None or extent <= 0: + raise QueueFrontendError( + "ACPY-QUEUE-005: array requires a positive compile-time extent" + ) + argument, body = _lambda(call.args[1]) + members: list[tuple[str | int, str | StaticQueueCollection]] = [] + for index in range(extent): + if not isinstance(body, ast.Call): + raise QueueFrontendError( + "ACPY-QUEUE-005: array generator must produce a Queue" + ) + leaf = f"{name}__{index}" + values = {**static_values, argument: index} + if call_name(body) == "source": + binding = source_binding( + leaf, body, scope_path, current_order, values + ) + queues.append(binding) + by_name[leaf] = binding + member: str | StaticQueueCollection = leaf + else: + nested = collection_binding( + leaf, + body, + scope_path, + current_order, + aliases, + values, + ) + if nested is None: + raise QueueFrontendError( + "ACPY-QUEUE-005: array generator must produce a Queue " + "or static collection" + ) + member = nested + members.append((index, member)) + signatures = {collection_signature(member) for _, member in members} + if len(signatures) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-005: array elements must have one static shape" + ) + return StaticQueueCollection("array", tuple(members)) + if kind == "map": + if len(call.args) != 1 or not isinstance(call.args[0], ast.Dict): + raise QueueFrontendError( + "ACPY-QUEUE-005: map requires one compile-time dictionary" + ) + entries: list[tuple[str | int | bool, str | StaticQueueCollection]] = [] + for key, value in zip(call.args[0].keys, call.args[0].values, strict=True): + if ( + not isinstance(key, ast.Constant) + or type(key.value) not in {str, int, bool} + or (type(key.value) is str and not key.value) + ): + raise QueueFrontendError( + "ACPY-QUEUE-005: map keys must be compile-time bool/int/str" + ) + entries.append((key.value, static_reference(value, aliases))) + rank = {bool: 0, int: 1, str: 2} + entries.sort(key=lambda item: (rank[type(item[0])], item[0])) + if not entries or len({(type(key), key) for key, _ in entries}) != len( + entries + ): + raise QueueFrontendError( + "ACPY-QUEUE-005: map keys must be unique and non-empty" + ) + if len({collection_signature(value) for _, value in entries}) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-005: map values must have one static shape" + ) + return StaticQueueCollection("map", tuple(entries)) + if kind == "set": + if len(call.args) != 1 or not isinstance( + call.args[0], (ast.Set, ast.List, ast.Tuple) + ): + raise QueueFrontendError( + "ACPY-QUEUE-005: set requires one compile-time collection" + ) + members = [static_reference(item, aliases) for item in call.args[0].elts] + identities = [stable_collection_identity(member) for member in members] + if not members or len(set(identities)) != len(members): + raise QueueFrontendError( + "ACPY-QUEUE-005: set members must be unique and non-empty" + ) + members.sort(key=stable_collection_identity) + if len({collection_signature(member) for member in members}) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-005: set members must have one static shape" + ) + return StaticQueueCollection( + "set", tuple((index, member) for index, member in enumerate(members)) + ) + return None + + def visit( + statements: list[ast.stmt], + scope_path: tuple[str, ...], + aliases: dict[str, str | StaticQueueCollection] | None = None, + atomic_group: int | None = None, + ) -> None: + nonlocal order + aliases = {} if aliases is None else aliases + for statement in statements: + current_order = order + order += 1 + assigned_names: tuple[str, ...] = () + if isinstance(statement, ast.Assign) and len(statement.targets) == 1: + target = statement.targets[0] + if isinstance(target, ast.Name): + assigned_names = (target.id,) + elif isinstance(target, (ast.Tuple, ast.List)) and all( + isinstance(item, ast.Name) for item in target.elts + ): + assigned_names = tuple(item.id for item in target.elts) + if any( + name in memory_by_name + or name in memory_arrays + or name in selected_memories + for name in assigned_names + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory binding cannot be rebound" + ) + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and call_name(statement.value) == "memory" + and len(statement.value.args) == 1 + ): + name = statement.targets[0].id + call = statement.value + if ( + name in by_name + or name in collections + or name in memory_by_name + or name in memory_arrays + or name in selected_memories + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory instance requires one fresh name" + ) + instance = memory_instance_binding( + name, call, scope_path, current_order + ) + memory_instances.append(instance) + memory_by_name[name] = instance + continue + if isinstance(statement, ast.If): + if ( + isinstance(statement.test, ast.Constant) + and type(statement.test.value) is bool + ): + selected = ( + statement.body if statement.test.value else statement.orelse + ) + visit(selected, scope_path, aliases, atomic_group) + continue + if atomic_group is not None: + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if cannot be nested in atomic" + ) + + def parse_arm( + body: list[ast.stmt], + ) -> tuple[str, str, ast.Call, str, ast.expr]: + if ( + len(body) != 1 + or not isinstance(body[0], ast.Assign) + or len(body[0].targets) != 1 + or not isinstance(body[0].targets[0], ast.Name) + or not isinstance(body[0].value, ast.Call) + ): + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if requires one apply " + "assignment in each branch" + ) + target = body[0].targets[0].id + call = body[0].value + if ( + not isinstance(call.func, ast.Attribute) + or call.func.attr != "apply" + or len(call.args) != 1 + ): + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if requires one apply " + "assignment in each branch" + ) + input_name = queue_reference(call.func.value, aliases) + argument, expression = _lambda(call.args[0]) + return target, input_name, call, argument, expression + + false_arm = parse_arm(statement.orelse) + true_arm = parse_arm(statement.body) + if false_arm[0] != true_arm[0]: + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if branches require one result name" + ) + if false_arm[1] != true_arm[1]: + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if branches must consume one Queue" + ) + name = true_arm[0] + input_name = true_arm[1] + if name in by_name or name in collections: + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if result requires one fresh name" + ) + incoming = by_name[input_name] + + condition_names: dict[str, str] = {} + for node in ast.walk(statement.test): + if not isinstance(node, ast.Name): + continue + try: + referenced = queue_reference(node, aliases) + except QueueFrontendError: + continue + condition_names[node.id] = referenced + if set(condition_names.values()) != {input_name}: + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if condition must read its branch Queue" + ) + + argument = "item" + + class QueueCondition(ast.NodeTransformer): + def visit_Name(self, node: ast.Name) -> ast.expr: + if condition_names.get(node.id) == input_name: + return ast.copy_location(ast.Name(id=argument), node) + return node + + condition = QueueCondition().visit(copy.deepcopy(statement.test)) + assert isinstance(condition, ast.expr) + _, condition_type = _ExpressionEmitter( + payload_map, argument, incoming.payload + ).emit(condition) + if condition_type != "i1": + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if condition must lower to bool" + ) + conditional = len([route for route in routes if route.boolean_selector]) + false_input = f"{name}__if_false{conditional}_in" + true_input = f"{name}__if_true{conditional}_in" + false_output = f"{name}__if_false{conditional}" + true_output = f"{name}__if_true{conditional}" + for route_name in (false_input, true_input): + binding = QueueBinding( + route_name, + incoming.payload, + 1, + 1, + None, + scope=scope_path, + order=current_order, + route_output=True, + ) + queues.append(binding) + by_name[route_name] = binding + routes.append( + RouteBinding( + input_name, + (false_input, true_input), + argument, + condition, + 1, + 1, + scope_path, + current_order, + True, + ) + ) + + for arm, arm_input, arm_output in ( + (false_arm, false_input, false_output), + (true_arm, true_input, true_output), + ): + branch_order = order + order += 1 + binding = QueueBinding( + arm_output, + incoming.payload, + _positive_int(arm[2], "depth", 1), + _positive_int(arm[2], "latency", 1), + arm_input, + arm[3], + arm[4], + scope_path, + branch_order, + ) + queues.append(binding) + by_name[arm_output] = binding + + merge_order = order + order += 1 + output = QueueBinding( + name, + incoming.payload, + 1, + 1, + None, + scope=scope_path, + order=merge_order, + merge_output=True, + ) + queues.append(output) + by_name[name] = output + merges.append( + MergeBinding( + (false_output, true_output), + name, + "priority", + 1, + 1, + scope_path, + merge_order, + ) + ) + continue + if isinstance(statement, ast.With) and len(statement.items) == 1: + item = statement.items[0] + call = item.context_expr + if ( + item.optional_vars is None + and isinstance(call, ast.Call) + and call_name(call) == "scope" + and len(call.args) == 1 + and isinstance(call.args[0], ast.Constant) + and type(call.args[0].value) is str + and call.args[0].value + ): + path = (*scope_path, call.args[0].value) + if any(existing.path == path for existing in scopes): + raise QueueFrontendError("ACPY-QUEUE-004: duplicate scope path") + scopes.append(ScopeBinding(call.args[0].value, path, current_order)) + visit(statement.body, path, aliases, atomic_group) + continue + if ( + isinstance(statement, ast.With) + and len(statement.items) == 1 + and atomic_group is None + ): + item = statement.items[0] + call = item.context_expr + if ( + item.optional_vars is None + and isinstance(call, ast.Call) + and call_name(call) == "atomic" + and not call.args + and not call.keywords + ): + group = len(atomics) + queue_start = len(queues) + scope_start = len(scopes) + route_start = len(routes) + fork_start = len(forks) + merge_start = len(merges) + reorder_start = len(reorders) + dependency_start = len(dependencies) + credit_start = len(credits) + barrier_start = len(barriers) + select_start = len(selects) + memory_instance_start = len(memory_instances) + memory_request_start = len(memory_requests) + feedback_start = len(feedbacks) + sink_start = len(sinks) + observation_start = len(observations) + expectation_start = len(expectations) + visit(statement.body, scope_path, aliases, group) + created = queues[queue_start:] + if ( + len(created) < 2 + or any(queue.atomic_group != group for queue in created) + or len(scopes) != scope_start + or len(routes) != route_start + or len(forks) != fork_start + or len(merges) != merge_start + or len(reorders) != reorder_start + or len(dependencies) != dependency_start + or len(credits) != credit_start + or len(barriers) != barrier_start + or len(selects) != select_start + or len(memory_instances) != memory_instance_start + or len(memory_requests) != memory_request_start + or len(feedbacks) != feedback_start + or len(sinks) != sink_start + or len(observations) != observation_start + or len(expectations) != expectation_start + ): + raise QueueFrontendError( + "ACPY-QUEUE-009: atomic requires at least two direct " + "Queue apply statements" + ) + inputs = [queue.input_name for queue in created] + if None in inputs or len(set(inputs)) != len(inputs): + raise QueueFrontendError( + "ACPY-QUEUE-009: atomic inputs must be unique Queues" + ) + atomics.append( + AtomicBinding( + tuple(queue.name for queue in created), + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and call_name(statement.value) in {"array", "map", "set"} + ): + name = statement.targets[0].id + if ( + name in by_name + or name in collections + or name in memory_by_name + or name in memory_arrays + or name in selected_memories + ): + raise QueueFrontendError( + "ACPY-QUEUE-005: collection assignment requires one fresh name" + ) + call = statement.value + is_memory_array = False + if call_name(call) == "array" and len(call.args) == 2: + _, generator = _lambda(call.args[1]) + is_memory_array = ( + isinstance(generator, ast.Call) + and call_name(generator) == "memory" + ) + if is_memory_array: + extent = _static_int(call.args[0], {}) + if extent is None or extent <= 0: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory array requires a positive " + "compile-time extent" + ) + argument, generator = _lambda(call.args[1]) + assert isinstance(generator, ast.Call) + pending: list[MemoryInstanceBinding] = [] + for index in range(extent): + member_name = f"{name}__{index}" + if ( + member_name in by_name + or member_name in collections + or member_name in memory_by_name + or member_name in memory_arrays + or member_name in selected_memories + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory array element name " + "collides with an existing binding" + ) + pending.append( + memory_instance_binding( + member_name, + generator, + scope_path, + current_order, + {argument: index}, + ) + ) + configurations = { + (item.data_type, item.entries, item.init, item.latency) + for item in pending + } + if len(configurations) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory array elements must be homogeneous" + ) + for instance in pending: + memory_instances.append(instance) + memory_by_name[instance.name] = instance + first = pending[0] + memory_arrays[name] = StaticMemoryArrayBinding( + name, + tuple(instance.name for instance in pending), + first.data_type, + first.entries, + first.init, + first.latency, + scope_path, + current_order, + ) + continue + collection = collection_binding( + name, + call, + scope_path, + current_order, + aliases, + ) + assert collection is not None + collections[name] = collection + collection_bindings.append( + CollectionBinding(name, collection, scope_path, current_order) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "select" + and isinstance(statement.value.func.value, ast.Name) + and statement.value.func.value.id in memory_arrays + ): + name = statement.targets[0].id + if ( + name in by_name + or name in collections + or name in memory_by_name + or name in memory_arrays + or name in selected_memories + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: selected memory requires one fresh name" + ) + call = statement.value + if len(call.args) != 1 or any( + keyword.arg is None + or keyword.arg not in {"key", "depth", "latency"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory array select requires one request Queue" + ) + keys = [ + keyword.value for keyword in call.keywords if keyword.arg == "key" + ] + if len(keys) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory array select requires one key lambda" + ) + array = memory_arrays[call.func.value.id] + if len(scope_path) < len(array.scope) or ( + scope_path[: len(array.scope)] != array.scope + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory array is only visible in its " + "declaration scope and descendants" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name[input_name] + argument, selector = _lambda(keys[0]) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + routed_inputs = tuple( + f"{name}__bank{index}_request" + for index in range(len(array.members)) + ) + for routed in routed_inputs: + if routed in by_name: + raise QueueFrontendError( + "ACPY-QUEUE-015: selected memory synthetic Queue " + "name collides with an existing binding" + ) + output = QueueBinding( + routed, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + route_output=True, + ) + queues.append(output) + by_name[routed] = output + routes.append( + RouteBinding( + input_name, + routed_inputs, + argument, + selector, + depth, + latency, + scope_path, + current_order, + ) + ) + selected_memories[name] = SelectedMemoryBinding( + name, + array.name, + input_name, + routed_inputs, + argument, + selector, + depth, + latency, + scope_path, + current_order, + ) + continue + if ( + isinstance(statement, ast.For) + and isinstance(statement.target, ast.Name) + and isinstance(statement.iter, ast.Call) + and call_name(statement.iter) == "range" + and len(statement.iter.args) == 1 + and not statement.iter.keywords + and not statement.orelse + ): + extent = _static_int(statement.iter.args[0]) + if extent is None or extent < 0: + raise QueueFrontendError( + "ACPY-QUEUE-005: range extent must be a non-negative " + "compile-time integer" + ) + + class StaticIndex(ast.NodeTransformer): + def visit_Name(self, node: ast.Name) -> ast.expr: + if node.id == statement.target.id: + return ast.copy_location(ast.Constant(index), node) + return node + + for index in range(extent): + expanded = [ + StaticIndex().visit(copy.deepcopy(body)) + for body in statement.body + ] + visit(expanded, scope_path, aliases, atomic_group) + continue + if ( + isinstance(statement, ast.For) + and isinstance(statement.target, ast.Name) + and not statement.orelse + ): + collection = static_reference(statement.iter, aliases) + if not isinstance(collection, StaticQueueCollection): + raise QueueFrontendError( + "ACPY-QUEUE-005: compile-time for requires a static collection" + ) + for _, member in collection.members: + visit( + statement.body, + scope_path, + {**aliases, statement.target.id: member}, + atomic_group, + ) + continue + if isinstance(statement, ast.While) and not statement.orelse: + body = list(statement.body) + break_test: ast.expr | None = None + continue_test: ast.expr | None = None + if ( + body + and isinstance(body[0], ast.If) + and len(body[0].body) == 1 + and isinstance(body[0].body[0], ast.Break) + and not body[0].orelse + ): + break_test = body.pop(0).test + if ( + body + and isinstance(body[-1], ast.If) + and len(body[-1].body) == 1 + and isinstance(body[-1].body[0], ast.Continue) + and not body[-1].orelse + ): + continue_test = body.pop().test + if ( + len(body) != 1 + or not isinstance(body[0], ast.Assign) + or len(body[0].targets) != 1 + or not isinstance(body[0].targets[0], ast.Name) + or not isinstance(body[0].value, ast.Call) + ): + raise QueueFrontendError( + "ACPY-QUEUE-007: runtime while requires optional break, " + "one Queue update, and optional tail continue" + ) + update_statement = body[0] + variable = update_statement.targets[0].id + call = update_statement.value + incoming = by_name.get(variable) + if ( + incoming is None + or not isinstance(call.func, ast.Attribute) + or call.func.attr != "apply" + or not isinstance(call.func.value, ast.Name) + or call.func.value.id != variable + or len(call.args) != 1 + ): + raise QueueFrontendError( + "ACPY-QUEUE-007: runtime while must rebind one Queue through apply" + ) + argument, update = _lambda(call.args[0]) + + class QueueCondition(ast.NodeTransformer): + def visit_Name(self, node: ast.Name) -> ast.expr: + if node.id == variable: + return ast.copy_location(ast.Name(id=argument), node) + return node + + condition = QueueCondition().visit(copy.deepcopy(statement.test)) + assert isinstance(condition, ast.expr) + if break_test is not None: + rewritten_break = QueueCondition().visit(copy.deepcopy(break_test)) + assert isinstance(rewritten_break, ast.expr) + condition = ast.BoolOp( + op=ast.And(), + values=[ + condition, + ast.UnaryOp(op=ast.Not(), operand=rewritten_break), + ], + ) + if continue_test is not None: + rewritten_continue = QueueCondition().visit( + copy.deepcopy(continue_test) + ) + if not isinstance(rewritten_continue, ast.expr): + raise QueueFrontendError( + "ACPY-QUEUE-007: continue condition is invalid" + ) + continue_probe = ast.UnaryOp( + op=ast.Not(), operand=rewritten_continue + ) + condition = ast.BoolOp( + op=ast.And(), + values=[ + condition, + ast.Compare( + left=continue_probe, + ops=[ast.Eq()], + comparators=[copy.deepcopy(continue_probe)], + ), + ], + ) + output_name = f"{variable}__feedback{len(feedbacks)}" + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + output = QueueBinding( + output_name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + feedback_output=True, + ) + queues.append(output) + by_name[variable] = output + feedbacks.append( + FeedbackBinding( + incoming.name, + output_name, + argument, + condition, + update, + depth, + latency, + 1024, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "merge" + and isinstance(statement.value.func.value, ast.Name) + and statement.value.func.value.id in by_name + ): + name = statement.targets[0].id + if name in by_name or name in collections: + raise QueueFrontendError( + "ACPY-QUEUE-008: merge output requires one fresh name" + ) + call = statement.value + operands = [call.func.value, *call.args] + inputs = tuple( + queue_reference(operand, aliases) for operand in operands + ) + if len(inputs) < 2: + raise QueueFrontendError( + "ACPY-QUEUE-008: merge requires at least two Queues" + ) + payload = by_name[inputs[0]].payload + if any(by_name[input_name].payload != payload for input_name in inputs): + raise QueueFrontendError( + "ACPY-QUEUE-008: merge Queue payloads must match" + ) + policies = [ + keyword.value + for keyword in call.keywords + if keyword.arg == "policy" + ] + if len(policies) > 1 or ( + policies + and ( + not isinstance(policies[0], ast.Constant) + or policies[0].value not in {"round_robin", "priority"} + ) + ): + raise QueueFrontendError( + "ACPY-QUEUE-008: merge policy must be round_robin or priority" + ) + policy = policies[0].value if policies else "round_robin" + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + output = QueueBinding( + name, + payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + merge_output=True, + ) + queues.append(output) + by_name[name] = output + merges.append( + MergeBinding( + inputs, + name, + policy, + depth, + latency, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "reorder" + and isinstance(statement.value.func.value, ast.Name) + and not statement.value.args + ): + name = statement.targets[0].id + if name in by_name or name in collections: + raise QueueFrontendError( + "ACPY-QUEUE-013: reorder output requires one fresh name" + ) + call = statement.value + incoming = by_name.get(call.func.value.id) + if incoming is None: + raise QueueFrontendError("ACPY-QUEUE-013: reorder input is unbound") + allowed_keywords = {"key", "capacity", "start", "depth", "latency"} + if any( + keyword.arg is None or keyword.arg not in allowed_keywords + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-013: reorder has an unsupported keyword" + ) + keys = [ + keyword.value for keyword in call.keywords if keyword.arg == "key" + ] + if len(keys) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-013: reorder requires one key lambda" + ) + argument, key = _lambda(keys[0]) + capacity = _positive_int(call, "capacity", 16) + start = _nonnegative_int(call, "start", 0) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + output = QueueBinding( + name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + reorder_output=True, + ) + queues.append(output) + by_name[name] = output + reorders.append( + ReorderBinding( + incoming.name, + name, + argument, + key, + capacity, + start, + depth, + latency, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "depend" + and isinstance(statement.value.func.value, ast.Name) + and not statement.value.args + ): + name = statement.targets[0].id + if name in by_name or name in collections: + raise QueueFrontendError( + "ACPY-QUEUE-014: dependency output requires one fresh name" + ) + call = statement.value + incoming = by_name.get(call.func.value.id) + if incoming is None: + raise QueueFrontendError( + "ACPY-QUEUE-014: dependency input is unbound" + ) + allowed_keywords = { + "key", + "waits_for", + "resource", + "cost", + "capacity", + "resources", + "no_dependency", + "depth", + "latency", + } + if any( + keyword.arg is None or keyword.arg not in allowed_keywords + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-014: dependency has an unsupported keyword" + ) + policies: dict[str, ast.expr] = {} + for policy in ("key", "waits_for", "resource", "cost"): + values = [ + keyword.value + for keyword in call.keywords + if keyword.arg == policy + ] + if len(values) != 1: + raise QueueFrontendError( + f"ACPY-QUEUE-014: dependency requires one {policy} lambda" + ) + policies[policy] = values[0] + key_argument, key = _lambda(policies["key"]) + waits_argument, waits_for = _lambda(policies["waits_for"]) + resource_argument, resource = _lambda(policies["resource"]) + cost_argument, cost = _lambda(policies["cost"]) + if ( + len( + { + key_argument, + waits_argument, + resource_argument, + cost_argument, + } + ) + != 1 + ): + raise QueueFrontendError( + "ACPY-QUEUE-014: dependency lambdas require one argument name" + ) + capacity = _positive_int(call, "capacity", 16) + resources = _positive_int(call, "resources", 1) + no_dependency = _nonnegative_int(call, "no_dependency", 255) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + output = QueueBinding( + name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + dependency_output=True, + ) + queues.append(output) + by_name[name] = output + dependencies.append( + DependencyBinding( + incoming.name, + name, + key_argument, + key, + waits_for, + resource, + cost, + capacity, + resources, + no_dependency, + depth, + latency, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "select" + and isinstance(statement.value.func.value, ast.Name) + and statement.value.func.value.id in collections + ): + name = statement.targets[0].id + if name in by_name or name in collections: + raise QueueFrontendError( + "ACPY-QUEUE-018: select output requires one fresh name" + ) + call = statement.value + if len(call.args) != 1 or any( + keyword.arg is None + or keyword.arg not in {"key", "depth", "latency"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-018: select requires one control Queue" + ) + control = queue_reference(call.args[0], aliases) + collection = collections[call.func.value.id] + if any(not isinstance(member, str) for _, member in collection.members): + raise QueueFrontendError( + "ACPY-QUEUE-018: select requires a flat Queue collection" + ) + inputs = tuple( + member + for _, member in collection.members + if isinstance(member, str) + ) + if len(inputs) < 2 or control in inputs: + raise QueueFrontendError( + "ACPY-QUEUE-018: select requires two unique data Queues" + ) + payload = by_name[inputs[0]].payload + if any(by_name[input_name].payload != payload for input_name in inputs): + raise QueueFrontendError( + "ACPY-QUEUE-018: select data Queue payloads must match" + ) + keys = [ + keyword.value for keyword in call.keywords if keyword.arg == "key" + ] + if len(keys) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-018: select requires one key lambda" + ) + argument, selector = _lambda(keys[0]) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + output = QueueBinding( + name, + payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + select_output=True, + ) + queues.append(output) + by_name[name] = output + selects.append( + SelectBinding( + control, + inputs, + name, + argument, + selector, + depth, + latency, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "credit" + and isinstance(statement.value.func.value, ast.Name) + and not statement.value.args + ): + name = statement.targets[0].id + if name in by_name or name in collections: + raise QueueFrontendError( + "ACPY-QUEUE-016: credit output requires one fresh name" + ) + call = statement.value + incoming = by_name.get(call.func.value.id) + if incoming is None: + raise QueueFrontendError("ACPY-QUEUE-016: credit input is unbound") + allowed_keywords = {"cost", "credits", "depth", "latency"} + if any( + keyword.arg is None or keyword.arg not in allowed_keywords + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-016: credit has an unsupported keyword" + ) + costs = [ + keyword.value for keyword in call.keywords if keyword.arg == "cost" + ] + if len(costs) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-016: credit requires one cost lambda" + ) + argument, cost = _lambda(costs[0]) + credit_count = _positive_int(call, "credits", 16) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + output = QueueBinding( + name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + credit_output=True, + ) + queues.append(output) + by_name[name] = output + credits.append( + CreditBinding( + incoming.name, + name, + argument, + cost, + credit_count, + depth, + latency, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "request" + and isinstance(statement.value.func.value, ast.Name) + and statement.value.func.value.id in selected_memories + and not statement.value.args + ): + name = statement.targets[0].id + if ( + name in by_name + or name in collections + or name in memory_by_name + or name in memory_arrays + or name in selected_memories + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory request output requires one fresh name" + ) + call = statement.value + selected_name = call.func.value.id + selected = selected_memories[selected_name] + if selected_name in consumed_selected_memories: + raise QueueFrontendError( + "ACPY-QUEUE-015: selected memory may be requested only once" + ) + if selected.scope != scope_path: + raise QueueFrontendError( + "ACPY-QUEUE-015: selected memory must be requested in the " + "same lexical scope" + ) + incoming = by_name[selected.input_name] + array = memory_arrays[selected.array] + ( + argument, + address, + write, + data, + result_field, + depth, + ) = memory_request_parameters( + call, + incoming, + array.data_type, + {"merge_policy", "merge_depth", "merge_latency"}, + ) + merge_policies = [ + keyword.value + for keyword in call.keywords + if keyword.arg == "merge_policy" + ] + if len(merge_policies) > 1 or ( + merge_policies + and ( + not isinstance(merge_policies[0], ast.Constant) + or merge_policies[0].value not in {"priority", "round_robin"} + ) + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: merge_policy must be priority or round_robin" + ) + merge_policy = ( + merge_policies[0].value if merge_policies else "priority" + ) + merge_depth = _positive_int(call, "merge_depth", 1) + merge_latency = _positive_int(call, "merge_latency", 1) + response_names = tuple( + f"{name}__bank{index}" + for index in range(len(array.members)) + ) + for instance_name, input_name, output_name in zip( + array.members, + selected.routed_inputs, + response_names, + strict=True, + ): + if output_name in by_name: + raise QueueFrontendError( + "ACPY-QUEUE-015: selected memory response Queue " + "name collides with an existing binding" + ) + output = QueueBinding( + output_name, + incoming.payload, + depth, + 1, + None, + scope=scope_path, + order=current_order, + memory_output=True, + ) + queues.append(output) + by_name[output_name] = output + memory_requests.append( + MemoryRequestBinding( + instance_name, + input_name, + output_name, + argument, + address, + write, + data, + result_field, + depth, + scope_path, + current_order, + ) + ) + merge_order = current_order + 1 + output = QueueBinding( + name, + incoming.payload, + merge_depth, + merge_latency, + None, + scope=scope_path, + order=merge_order, + merge_output=True, + ) + queues.append(output) + by_name[name] = output + merges.append( + MergeBinding( + response_names, + name, + str(merge_policy), + merge_depth, + merge_latency, + scope_path, + merge_order, + ) + ) + consumed_selected_memories.add(selected_name) + order += 1 + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "request" + and isinstance(statement.value.func.value, ast.Name) + and len(statement.value.args) == 1 + ): + name = statement.targets[0].id + if name in by_name or name in collections: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory request output requires one fresh name" + ) + call = statement.value + instance = memory_by_name.get(call.func.value.id) + if instance is None: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory request instance is unbound" + ) + if len(scope_path) < len(instance.scope) or ( + scope_path[: len(instance.scope)] != instance.scope + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory instance is only visible in its " + "declaration scope and descendants" + ) + incoming_name = queue_reference(call.args[0], aliases) + incoming = by_name.get(incoming_name) + if incoming is None: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory request input is unbound" + ) + ( + argument, + address, + write, + data, + result_field, + depth, + ) = memory_request_parameters(call, incoming, instance.data_type) + output = QueueBinding( + name, + incoming.payload, + depth, + 1, + None, + scope=scope_path, + order=current_order, + memory_output=True, + ) + queues.append(output) + by_name[name] = output + memory_requests.append( + MemoryRequestBinding( + instance.name, + incoming.name, + name, + argument, + address, + write, + data, + result_field, + depth, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.value, ast.Call) + and isinstance(statement.value.func, ast.Attribute) + and statement.value.func.attr == "memory" + ): + raise QueueFrontendError( + "ACPY-QUEUE-015: Queue.memory was removed; declare " + "ac.memory(...) and connect it with instance.request(...)" + ) + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], ast.Name) + and isinstance(statement.value, ast.Call) + ): + name, call = statement.targets[0].id, statement.value + if ( + isinstance(call.func, ast.Name) + and call.func.id in recursive_helpers + ): + if ( + name in by_name + or name in collections + or len(call.args) != 2 + or call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-020: recursive helper call is malformed" + ) + input_name = queue_reference(call.args[0], aliases) + extent = _static_int(call.args[1]) + if extent is None or extent < 0 or extent > 1024: + raise QueueFrontendError( + "ACPY-QUEUE-020: recursion depth must be a compile-time " + "integer in [0, 1024]" + ) + helper = recursive_helpers[call.func.id] + incoming = by_name[input_name] + if extent == 0: + by_name[name] = incoming + continue + previous = incoming + for index in range(extent): + output_name = ( + name if index + 1 == extent else f"{name}__rec{index}" + ) + binding = QueueBinding( + output_name, + incoming.payload, + _positive_int(helper.apply_call, "depth", 1), + _positive_int(helper.apply_call, "latency", 1), + previous.name, + helper.argument, + copy.deepcopy(helper.expression), + scope_path, + current_order, + atomic_group=atomic_group, + ) + queues.append(binding) + by_name[output_name] = binding + previous = binding + continue + if name in by_name or name in collections: + raise QueueFrontendError( + "ACPY-QUEUE-001: queue assignment requires one fresh name" + ) + if call_name(call) == "source" and len(call.args) == 1: + binding = source_binding(name, call, scope_path, current_order) + elif call_name(call) == "compute" and len(call.args) == 2: + if any( + keyword.arg is None + or keyword.arg not in {"depth", "latency", "rate"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-023: compute has an unsupported keyword" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name[input_name] + argument, expression = _lambda(call.args[1]) + depth = _positive_int(call, "depth", 1) + rate = _positive_int(call, "rate", incoming.rate) + if rate > depth: + raise QueueFrontendError( + "ACPY-QUEUE-025: Queue rate must not exceed depth" + ) + binding = QueueBinding( + name, + incoming.payload, + depth, + _positive_int(call, "latency", 1), + incoming.name, + argument, + expression, + scope_path, + current_order, + atomic_group=atomic_group, + provider="compute", + rate=rate, + ) + elif call_name(call) == "pipeline" and len(call.args) == 1: + if any( + keyword.arg is None + or keyword.arg not in {"stages", "depth", "rate"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-024: pipeline parameters are invalid" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name[input_name] + stages = _positive_int(call, "stages", 1) + depth = _positive_int(call, "depth", 1) + rate = _positive_int(call, "rate", incoming.rate) + if rate > depth: + raise QueueFrontendError( + "ACPY-QUEUE-025: Queue rate must not exceed depth" + ) + binding = QueueBinding( + name, + incoming.payload, + depth, + stages, + incoming.name, + "item", + ast.Name(id="item", ctx=ast.Load()), + scope_path, + current_order, + atomic_group=atomic_group, + provider="pipeline", + rate=rate, + ) + elif call_name(call) == "merge": + if len(call.args) < 2 or any( + keyword.arg is None + or keyword.arg not in {"policy", "depth", "latency"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-024: merge requires two or more Queues and " + "static policy/depth/latency" + ) + input_names = tuple( + queue_reference(argument, aliases) for argument in call.args + ) + if len(set(input_names)) != len(input_names): + raise QueueFrontendError( + "ACPY-QUEUE-024: merge inputs must be unique Queues" + ) + payloads_used = {by_name[item].payload for item in input_names} + if len(payloads_used) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-024: merge inputs require one payload type" + ) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + policy = policy_value(call) + binding = QueueBinding( + name, + by_name[input_names[0]].payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + merge_output=True, + ) + merges.append( + MergeBinding( + input_names, + name, + policy, + depth, + latency, + scope_path, + current_order, + ) + ) + elif call_name(call) == "schedule": + if len(call.args) != 1 or any( + keyword.arg is None + or keyword.arg + not in { + "by", + "waits_for", + "resource", + "cost", + "entries", + "resources", + "no_dependency", + "depth", + "latency", + } + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-024: schedule parameters are invalid" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name[input_name] + argument = "item" + key = field_expression(keyword_value(call, "by"), incoming) + waits_for = field_expression( + keyword_value(call, "waits_for"), incoming + ) + resource = field_expression( + keyword_value(call, "resource"), incoming + ) + cost = field_expression(keyword_value(call, "cost"), incoming) + capacity = _positive_int(call, "entries", 16) + resources = _positive_int(call, "resources", 1) + no_dependency = _nonnegative_int(call, "no_dependency", 0) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + binding = QueueBinding( + name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + dependency_output=True, + ) + dependencies.append( + DependencyBinding( + input_name, + name, + argument, + key, + waits_for, + resource, + cost, + capacity, + resources, + no_dependency, + depth, + latency, + scope_path, + current_order, + provider="schedule", + ) + ) + elif call_name(call) == "engine": + if len(call.args) != 1 or any( + keyword.arg is None + or keyword.arg not in {"cost", "lanes", "depth", "latency"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-024: engine parameters are invalid" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name[input_name] + argument = "item" + cost = field_expression(keyword_value(call, "cost"), incoming) + lane_count = _positive_int(call, "lanes", 1) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + binding = QueueBinding( + name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + credit_output=True, + ) + credits_binding = CreditBinding( + input_name, + name, + argument, + cost, + lane_count, + depth, + latency, + scope_path, + current_order, + provider="engine", + ) + credits.append(credits_binding) + elif call_name(call) == "reorder": + if len(call.args) != 1 or any( + keyword.arg is None + or keyword.arg + not in {"by", "entries", "start", "depth", "latency"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-024: reorder parameters are invalid" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name[input_name] + argument = "item" + key = field_expression(keyword_value(call, "by"), incoming) + capacity = _positive_int(call, "entries", 16) + start = _nonnegative_int(call, "start", 0) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + binding = QueueBinding( + name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + reorder_output=True, + ) + reorders.append( + ReorderBinding( + input_name, + name, + argument, + key, + capacity, + start, + depth, + latency, + scope_path, + current_order, + ) + ) + elif call_name(call) == "table": + if len(call.args) != 1 or any( + keyword.arg is None + or keyword.arg + not in { + "address", + "write", + "data", + "result", + "entries", + "init", + "depth", + "latency", + } + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-024: table parameters are invalid" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name[input_name] + argument = "item" + address = field_expression(keyword_value(call, "address"), incoming) + write = field_expression(keyword_value(call, "write"), incoming) + data_node = keyword_value(call, "data") + data = field_expression(data_node, incoming) + result_node = keyword_value(call, "result") + field_expression(result_node, incoming) + assert isinstance(data_node, ast.Attribute) + assert isinstance(result_node, ast.Attribute) + payload = next( + item for item in payloads if item.acir_type == incoming.payload + ) + data_type = dict(payload.fields)[data_node.attr] + result_type = dict(payload.fields)[result_node.attr] + if data_type != result_type: + raise QueueFrontendError( + "ACPY-QUEUE-024: table data and result fields must match" + ) + entries = _positive_int(call, "entries", 16) + init = _nonnegative_int(call, "init", 0) + if init != 0: + raise QueueFrontendError( + "ACPY-QUEUE-024: table init must be zero" + ) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + instance_name = f"{name}__table" + if instance_name in memory_by_name: + raise QueueFrontendError( + "ACPY-QUEUE-024: table storage identity collides with " + "an existing memory" + ) + instance = MemoryInstanceBinding( + instance_name, + data_type, + entries, + init, + latency, + scope_path, + current_order, + ) + memory_instances.append(instance) + memory_by_name[instance_name] = instance + memory_requests.append( + MemoryRequestBinding( + instance_name, + input_name, + name, + argument, + address, + write, + data, + result_node.attr, + depth, + scope_path, + current_order, + ) + ) + binding = QueueBinding( + name, + incoming.payload, + depth, + 1, + None, + scope=scope_path, + order=current_order, + memory_output=True, + ) + memories.append( + MemoryBinding( + input_name, + name, + argument, + address, + write, + data, + data_type, + entries, + init, + result_node.attr, + depth, + latency, + scope_path, + current_order, + ) + ) + elif ( + isinstance(call.func, ast.Attribute) + and call.func.attr in {"apply", "firing"} + and isinstance(call.func.value, ast.Name) + and len(call.args) == 1 + ): + input_name = call.func.value.id + incoming = by_name.get(input_name) + if incoming is None: + raise QueueFrontendError( + f"ACPY-QUEUE-001: input queue {input_name!r} is unbound" + ) + argument, expression = _lambda(call.args[0]) + firing_effect = call.func.attr == "firing" + if firing_effect: + _validate_firing_expression(expression, argument) + binding = QueueBinding( + name, + incoming.payload, + _positive_int(call, "depth", 1), + _positive_int(call, "latency", 1), + incoming.name, + argument, + expression, + scope_path, + current_order, + firing_effect=firing_effect, + atomic_group=atomic_group, + ) + else: + raise QueueFrontendError( + "ACPY-QUEUE-001: unsupported queue-producing call" + ) + queues.append(binding) + by_name[name] = binding + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], (ast.Tuple, ast.List)) + and all( + isinstance(item, ast.Name) for item in statement.targets[0].elts + ) + and isinstance(statement.value, ast.Call) + and call_name(statement.value) == "barrier" + ): + call = statement.value + if any( + keyword.arg is None or keyword.arg not in {"depth", "latency"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-017: barrier has an unsupported keyword" + ) + method_style = ( + isinstance(call.func, ast.Attribute) + and isinstance(call.func.value, ast.Name) + and call.func.value.id in by_name + ) + operands = ( + [call.func.value, *call.args] if method_style else list(call.args) + ) + inputs = tuple( + queue_reference(operand, aliases) for operand in operands + ) + outputs = tuple(item.id for item in statement.targets[0].elts) + if len(inputs) < 2 or len(outputs) != len(inputs): + raise QueueFrontendError( + "ACPY-QUEUE-017: barrier requires matching input/output arity" + ) + if len(set(inputs)) != len(inputs): + raise QueueFrontendError( + "ACPY-QUEUE-017: barrier inputs must be unique Queues" + ) + if len(set(outputs)) != len(outputs) or any( + output in by_name or output in collections for output in outputs + ): + raise QueueFrontendError( + "ACPY-QUEUE-017: barrier outputs require fresh tuple names" + ) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + for input_name, output_name in zip(inputs, outputs, strict=True): + output = QueueBinding( + output_name, + by_name[input_name].payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + barrier_output=True, + ) + queues.append(output) + by_name[output_name] = output + barriers.append( + BarrierBinding( + inputs, + outputs, + depth, + latency, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], (ast.Tuple, ast.List)) + and all( + isinstance(item, ast.Name) for item in statement.targets[0].elts + ) + and isinstance(statement.value, ast.Call) + and call_name(statement.value) == "route" + ): + call = statement.value + method_style = ( + isinstance(call.func, ast.Attribute) + and isinstance(call.func.value, ast.Name) + and call.func.value.id in by_name + ) + if method_style: + assert isinstance(call.func, ast.Attribute) + assert isinstance(call.func.value, ast.Name) + input_name = call.func.value.id + if call.args: + raise QueueFrontendError( + "ACPY-QUEUE-006: method route takes no positional arguments" + ) + else: + if len(call.args) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-024: route requires one input Queue" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name.get(input_name) + if incoming is None: + raise QueueFrontendError( + f"ACPY-QUEUE-001: input queue {input_name!r} is unbound" + ) + output_count = _positive_int(call, "outputs", 0) + names = tuple(item.id for item in statement.targets[0].elts) + if output_count != len(names) or len(set(names)) != len(names): + raise QueueFrontendError( + "ACPY-QUEUE-006: route outputs must match fresh tuple names" + ) + if method_style: + key = [ + keyword.value + for keyword in call.keywords + if keyword.arg == "key" + ] + if len(key) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-006: route requires one key lambda" + ) + argument, selector = _lambda(key[0]) + else: + argument = "item" + selector = field_expression( + keyword_value(call, "by"), incoming, argument + ) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + for name in names: + if name in by_name: + raise QueueFrontendError( + "ACPY-QUEUE-006: route output name is already bound" + ) + output = QueueBinding( + name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + route_output=True, + ) + queues.append(output) + by_name[name] = output + routes.append( + RouteBinding( + incoming.name, + names, + argument, + selector, + depth, + latency, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Assign) + and len(statement.targets) == 1 + and isinstance(statement.targets[0], (ast.Tuple, ast.List)) + and all( + isinstance(item, ast.Name) for item in statement.targets[0].elts + ) + and isinstance(statement.value, ast.Call) + and call_name(statement.value) == "fork" + ): + call = statement.value + method_style = ( + isinstance(call.func, ast.Attribute) + and isinstance(call.func.value, ast.Name) + and call.func.value.id in by_name + ) + if method_style: + assert isinstance(call.func, ast.Attribute) + assert isinstance(call.func.value, ast.Name) + if call.args: + raise QueueFrontendError( + "ACPY-QUEUE-012: method fork takes no positional arguments" + ) + input_name = call.func.value.id + else: + if len(call.args) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-024: fork requires one input Queue" + ) + input_name = queue_reference(call.args[0], aliases) + incoming = by_name.get(input_name) + if incoming is None: + raise QueueFrontendError("ACPY-QUEUE-012: fork input is unbound") + output_count = _positive_int(call, "outputs", 0) + names = tuple(item.id for item in statement.targets[0].elts) + if output_count != len(names) or len(names) < 2: + raise QueueFrontendError( + "ACPY-QUEUE-012: fork outputs must match tuple arity" + ) + depth = _positive_int(call, "depth", 1) + latency = _positive_int(call, "latency", 1) + for name in names: + if name in by_name: + raise QueueFrontendError( + "ACPY-QUEUE-012: fork output name is already bound" + ) + output = QueueBinding( + name, + incoming.payload, + depth, + latency, + None, + scope=scope_path, + order=current_order, + route_output=True, + ) + queues.append(output) + by_name[name] = output + forks.append( + ForkBinding( + incoming.name, + names, + depth, + latency, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Expr) + and isinstance(statement.value, ast.Call) + and call_name(statement.value) == "expect" + and len(statement.value.args) == 1 + ): + call = statement.value + if any( + keyword.arg is None or keyword.arg not in {"predicate", "message"} + for keyword in call.keywords + ): + raise QueueFrontendError( + "ACPY-QUEUE-021: expect has an unsupported keyword" + ) + predicates = [ + keyword.value + for keyword in call.keywords + if keyword.arg == "predicate" + ] + messages = [ + keyword.value + for keyword in call.keywords + if keyword.arg == "message" + ] + if len(predicates) != 1 or len(messages) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-021: expect requires predicate and message" + ) + if ( + not isinstance(messages[0], ast.Constant) + or type(messages[0].value) is not str + or not messages[0].value + ): + raise QueueFrontendError( + "ACPY-QUEUE-021: expect message must be a static string" + ) + argument, predicate = _lambda(predicates[0]) + expectations.append( + ExpectBinding( + queue_reference(call.args[0], aliases), + argument, + predicate, + messages[0].value, + scope_path, + current_order, + ) + ) + continue + if ( + isinstance(statement, ast.Expr) + and isinstance(statement.value, ast.Call) + and call_name(statement.value) == "observe" + and len(statement.value.args) == 1 + ): + name = queue_reference(statement.value.args[0], aliases) + observations.append( + ObservationBinding( + name, f"observe_{current_order}", scope_path, current_order + ) + ) + continue + if ( + isinstance(statement, ast.Expr) + and isinstance(statement.value, ast.Call) + and call_name(statement.value) == "sink" + and len(statement.value.args) == 1 + ): + name = queue_reference(statement.value.args[0], aliases) + sinks.append(SinkBinding(name, scope_path, current_order)) + continue + if isinstance(statement, ast.Return) and statement.value is None: + continue + raise QueueFrontendError( + f"ACPY-QUEUE-001: unsupported statement {type(statement).__name__}" + ) + + visit(function.body, ()) + unused_selected = sorted(set(selected_memories) - consumed_selected_memories) + if unused_selected: + raise QueueFrontendError( + "ACPY-QUEUE-015: selected memory is not requested: " + + ", ".join(repr(name) for name in unused_selected) + ) + requests_by_instance: dict[str, list[MemoryRequestBinding]] = {} + for request in memory_requests: + requests_by_instance.setdefault(request.instance, []).append(request) + for instance in memory_instances: + endpoints = requests_by_instance.get(instance.name, []) + if not endpoints: + raise QueueFrontendError( + f"ACPY-QUEUE-015: memory instance {instance.name!r} is not connected" + ) + payload_types = {by_name[endpoint.input_name].payload for endpoint in endpoints} + if len(payload_types) != 1: + raise QueueFrontendError( + "ACPY-QUEUE-015: all endpoints of one memory require one payload struct" + ) + if not queues or not sinks: + raise QueueFrontendError( + "ACPY-QUEUE-001: a queue system requires source and sink boundaries" + ) + return QueueProgram( + system, + payloads, + tuple(queues), + tuple(scopes), + tuple(routes), + tuple(forks), + tuple(feedbacks), + tuple(merges), + tuple(reorders), + tuple(dependencies), + tuple(credits), + tuple(barriers), + tuple(selects), + tuple(memory_instances), + tuple(memory_requests), + tuple(memories), + tuple(atomics), + tuple(collection_bindings), + tuple(observations), + tuple(expectations), + tuple(sinks), + specialization_fingerprint, + ) + + +class _ExpressionEmitter: + def __init__( + self, + payloads: dict[str, Payload], + argument: str, + payload: str, + *, + root_name: str = "item", + prefix: str = "", + allow_queue_effects: bool = False, + ) -> None: + self.payloads = payloads + self.argument = argument + self.payload = payload + self.root_name = root_name + self.prefix = prefix + self.allow_queue_effects = allow_queue_effects + self.lines: list[str] = [] + self.index = 0 + + def _new(self) -> str: + name = f"{self.prefix}v{self.index}" + self.index += 1 + return name + + def emit(self, node: ast.expr, expected: str | None = None) -> tuple[str, str]: + if isinstance(node, ast.Name) and node.id == self.argument: + return self.root_name, self.payload + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == self.argument + and node.func.attr in {"peek", "pop", "push"} + ): + if not self.allow_queue_effects: + raise QueueFrontendError( + "ACPY-QUEUE-019: queue effects require firing()" + ) + if node.func.attr in {"peek", "pop"}: + if node.args or node.keywords: + raise QueueFrontendError( + "ACPY-QUEUE-019: firing peek/pop take no arguments" + ) + return self.root_name, self.payload + if len(node.args) != 1 or node.keywords: + raise QueueFrontendError( + "ACPY-QUEUE-019: firing push requires one value" + ) + return self.emit(node.args[0], self.payload) + if isinstance(node, ast.Constant) and type(node.value) in {int, bool}: + typ = expected or ("i1" if type(node.value) is bool else "i64") + name = self._new() + value = ( + "true" + if node.value is True + else "false" + if node.value is False + else str(node.value) + ) + attribute = value if type(node.value) is bool else f"{value} : {typ}" + self.lines.append( + f" %{name} = ac.var.constant {attribute} as !ac.var<{typ}>" + ) + return name, typ + if isinstance(node, ast.Attribute): + record, record_type = self.emit(node.value) + payload_name = record_type.removeprefix( + "!ac.struct<@types::@" + ).removesuffix(">") + definition = self.payloads.get(payload_name) + field_type = dict(definition.fields).get(node.attr) if definition else None + if field_type is None: + raise QueueFrontendError(f"ACPY-QUEUE-003: unknown field {node.attr!r}") + name = self._new() + self.lines.append( + f' %{name} = ac.var.get %{record} field "{node.attr}" : !ac.var<{record_type}> -> !ac.var<{field_type}>' + ) + return name, field_type + if isinstance(node, ast.BinOp) and isinstance( + node.op, (ast.Add, ast.Sub, ast.Mult) + ): + left, left_type = self.emit(node.left) + right, right_type = self.emit(node.right, left_type) + if left_type != right_type: + raise QueueFrontendError( + "ACPY-QUEUE-003: arithmetic operands must match" + ) + opcode = {ast.Add: "add", ast.Sub: "sub", ast.Mult: "mul"}[type(node.op)] + name = self._new() + self.lines.append( + f" %{name} = ac.var.{opcode} %{left}, %{right} : !ac.var<{left_type}>" + ) + return name, left_type + if isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And): + if len(node.values) < 2: + raise QueueFrontendError( + "ACPY-QUEUE-003: boolean and requires two operands" + ) + current, current_type = self.emit(node.values[0], "i1") + if current_type != "i1": + raise QueueFrontendError("ACPY-QUEUE-003: boolean operands must be i1") + for operand in node.values[1:]: + value, value_type = self.emit(operand, "i1") + if value_type != "i1": + raise QueueFrontendError( + "ACPY-QUEUE-003: boolean operands must be i1" + ) + name = self._new() + self.lines.append( + f" %{name} = ac.var.mul %{current}, %{value} : !ac.var" + ) + current = name + return current, "i1" + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): + value, value_type = self.emit(node.operand, "i1") + if value_type != "i1": + raise QueueFrontendError("ACPY-QUEUE-003: boolean not requires i1") + false_value = self._new() + self.lines.append( + f" %{false_value} = ac.var.constant false as !ac.var" + ) + name = self._new() + self.lines.append( + f' %{name} = ac.var.cmp "eq" %{value}, %{false_value} : ' + "!ac.var -> !ac.var" + ) + return name, "i1" + if ( + isinstance(node, ast.Compare) + and len(node.ops) == len(node.comparators) == 1 + ): + left, left_type = self.emit(node.left) + right, right_type = self.emit(node.comparators[0], left_type) + if left_type != right_type: + raise QueueFrontendError( + "ACPY-QUEUE-003: comparison operands must match" + ) + predicates = { + ast.Eq: "eq", + ast.NotEq: "ne", + ast.Lt: "slt", + ast.LtE: "sle", + ast.Gt: "sgt", + ast.GtE: "sge", + } + predicate = predicates.get(type(node.ops[0])) + if predicate is None: + raise QueueFrontendError("ACPY-QUEUE-003: unsupported comparison") + name = self._new() + self.lines.append( + f' %{name} = ac.var.cmp "{predicate}" %{left}, %{right} : ' + f"!ac.var<{left_type}> -> !ac.var" + ) + return name, "i1" + if ( + isinstance(node, ast.Call) + and _decorator_name(node.func).rsplit(".", 1)[-1] == "popcount" + ): + if len(node.args) != 1 or node.keywords: + raise QueueFrontendError( + "ACPY-QUEUE-003: popcount requires exactly one positional operand" + ) + value, value_type = self.emit(node.args[0]) + if not value_type.startswith("i") or not value_type[1:].isdigit(): + raise QueueFrontendError( + "ACPY-QUEUE-003: popcount operand must be an integer payload" + ) + width = int(value_type[1:]) + if width <= 0: + raise QueueFrontendError( + "ACPY-QUEUE-003: popcount operand width must be positive" + ) + result_width = width.bit_length() + name = self._new() + self.lines.append( + f" %{name} = ac.var.popcount %{value} : !ac.var<{value_type}> -> !ac.var" + ) + return name, f"i{result_width}" + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "with_fields" + and not node.args + ): + record, record_type = self.emit(node.func.value) + current = record + for keyword in node.keywords: + if keyword.arg is None: + raise QueueFrontendError( + "ACPY-QUEUE-003: field unpacking is forbidden" + ) + payload_name = record_type.removeprefix( + "!ac.struct<@types::@" + ).removesuffix(">") + definition = self.payloads.get(payload_name) + field_type = ( + dict(definition.fields).get(keyword.arg) if definition else None + ) + if field_type is None: + raise QueueFrontendError( + f"ACPY-QUEUE-003: unknown field {keyword.arg!r}" + ) + value, value_type = self.emit(keyword.value, field_type) + if value_type != field_type: + raise QueueFrontendError( + "ACPY-QUEUE-003: field update type mismatch" + ) + name = self._new() + self.lines.append( + f' %{name} = ac.var.with %{current}, %{value} field "{keyword.arg}" : !ac.var<{record_type}>, !ac.var<{field_type}> -> !ac.var<{record_type}>' + ) + current = name + return current, record_type + raise QueueFrontendError("ACPY-QUEUE-003: unsupported lambda expression") + + +def lower_queue_program(program: QueueProgram) -> str: + specialization = ( + "" + if program.specialization_fingerprint is None + else f', ac.specialization = "{program.specialization_fingerprint}"' + ) + lines = [ + f'module attributes {{ac.contract_epoch = "0.3", ' + f'ac.system = "{program.system}"{specialization}}} {{' + ] + payloads = {item.name: item for item in program.payloads} + if program.payloads: + lines.append(" ac.type_scope @types {") + for payload in program.payloads: + fields = ", ".join( + f'{{name = "{name}", type = {typ}}}' for name, typ in payload.fields + ) + lines.append(f" ac.struct @{payload.name} fields [{fields}]") + layouts: list[str] = [] + for payload in program.payloads: + sizes = [ + max(1, (int(typ.removeprefix("i")) + 7) // 8) + for _, typ in payload.fields + ] + alignment = max(sizes) + offset = 0 + for size in sizes: + offset = ((offset + size - 1) // size) * size + size + total = ((offset + alignment - 1) // alignment) * alignment + layouts.append( + f"!ac.struct<@types::@{payload.name}> = " + f'{{abi_alignment = {alignment} : i64, endianness = "little", ' + f"preferred_alignment = {alignment} : i64, size = {total} : i64}}" + ) + lines.append(" } {dlti.dl_spec = #dlti.dl_spec<" + ", ".join(layouts) + ">}") + for instance in sorted( + program.memory_instances, key=lambda value: (value.scope, value.order, value.name) + ): + owner = "/" + "/".join(instance.scope) if instance.scope else "/" + stable_id = ( + "/".join((*instance.scope, instance.name)) + if instance.scope + else instance.name + ) + lines.append( + f' ac.memory.instance @{instance.name} data {instance.data_type} ' + f'entries {instance.entries} init {instance.init} ' + f'latency {instance.latency} owner "{owner}" ' + f'stable_id "memory/{stable_id}"' + ) + by_name = {item.name: item for item in program.queues} + memory_ordinals: dict[tuple[str, str], int] = {} + requests_by_instance: dict[str, list[MemoryRequestBinding]] = {} + for request in program.memory_requests: + requests_by_instance.setdefault(request.instance, []).append(request) + for instance, requests in requests_by_instance.items(): + for ordinal, request in enumerate( + sorted(requests, key=lambda value: (value.scope, value.order, value.output_name)) + ): + memory_ordinals[(instance, request.output_name)] = ordinal + + def name_array(names: list[str] | tuple[str, ...]) -> str: + return "[" + ", ".join(f'"{name}"' for name in names) + "]" + + consumers: dict[str, list[QueueBinding]] = {} + for queue in program.queues: + if queue.input_name is not None: + consumers.setdefault(queue.input_name, []).append(queue) + fanouts: dict[str, tuple[tuple[str, ...], tuple[QueueBinding, ...]]] = {} + + def common_scope(scopes: list[tuple[str, ...]]) -> tuple[str, ...]: + common: list[str] = [] + for parts in zip(*scopes, strict=False): + if len(set(parts)) != 1: + break + common.append(parts[0]) + return tuple(common) + + for source_name, group in consumers.items(): + if len(group) < 2: + continue + fanouts[source_name] = ( + common_scope([consumer.scope for consumer in group]), + tuple(group), + ) + payload_by_queue = {name: queue.payload for name, queue in by_name.items()} + queue_scope = {name: queue.scope for name, queue in by_name.items()} + effective_input: dict[str, str] = {} + for source_name, (fanout_scope, group) in fanouts.items(): + for index, consumer in enumerate(group): + synthetic = f"{source_name}__fanout{index}" + effective_input[consumer.name] = synthetic + payload_by_queue[synthetic] = by_name[source_name].payload + queue_scope[synthetic] = fanout_scope + + uses: dict[str, list[tuple[str, ...]]] = {name: [] for name in payload_by_queue} + for queue in program.queues: + if queue.input_name is None: + continue + selected = effective_input.get(queue.name, queue.input_name) + uses[selected].append(queue.scope) + for source_name, (fanout_scope, _) in fanouts.items(): + uses[source_name].append(fanout_scope) + for sink_binding in program.sinks: + uses[sink_binding.queue].append(sink_binding.scope) + for observation in program.observations: + uses[observation.queue].append(observation.scope) + for expectation in program.expectations: + uses[expectation.queue].append(expectation.scope) + for route in program.routes: + uses[route.input_name].append(route.scope) + for fork in program.forks: + uses[fork.input_name].append(fork.scope) + for feedback in program.feedbacks: + uses[feedback.input_name].append(feedback.scope) + for merge in program.merges: + for input_name in merge.inputs: + uses[input_name].append(merge.scope) + for reorder in program.reorders: + uses[reorder.input_name].append(reorder.scope) + for dependency in program.dependencies: + uses[dependency.input_name].append(dependency.scope) + for credit in program.credits: + uses[credit.input_name].append(credit.scope) + for barrier in program.barriers: + for input_name in barrier.inputs: + uses[input_name].append(barrier.scope) + for select in program.selects: + uses[select.control].append(select.scope) + for input_name in select.inputs: + uses[input_name].append(select.scope) + for request in program.memory_requests: + uses[request.input_name].append(request.scope) + + def inside(container: tuple[str, ...], candidate: tuple[str, ...]) -> bool: + return candidate[: len(container)] == container + + def scope_io(path: tuple[str, ...]) -> tuple[list[str], list[str]]: + inputs = [ + name + for name, producer_scope in queue_scope.items() + if not inside(path, producer_scope) + and any(inside(path, use) for use in uses[name]) + ] + outputs = [ + name + for name, producer_scope in queue_scope.items() + if inside(path, producer_scope) + and any(not inside(path, use) for use in uses[name]) + ] + return inputs, outputs + + def queue_attributes(name: str, rates: tuple[int, ...]) -> str: + attributes = [f'ac.name = "{name}"'] + if any(rate != 1 for rate in rates): + attributes.append( + "ac.output_rates = array" + ) + return "{" + ", ".join(attributes) + "}" + + def emit_queue( + queue: QueueBinding, + output_ssa: str, + mapping: dict[str, str], + indent: str, + ) -> None: + if queue.input_name is None: + lines.append( + f"{indent}%{output_ssa} = ac.source depth {queue.depth} " + f"latency {queue.latency} " + f"{queue_attributes(queue.name, (queue.rate,))} : " + f"!ac.queue<{queue.payload}>" + ) + mapping[queue.name] = output_ssa + return + assert queue.argument is not None and queue.expression is not None + emitter = _ExpressionEmitter( + payloads, + queue.argument, + queue.payload, + allow_queue_effects=queue.firing_effect, + ) + result, result_type = emitter.emit(queue.expression) + if result_type != queue.payload: + raise QueueFrontendError( + "ACPY-QUEUE-003: lambda result must preserve Queue payload type" + ) + input_name = effective_input.get(queue.name, queue.input_name) + input_ssa = mapping[input_name] + lines.append( + f"{indent}%{output_ssa} = ac.transform %{input_ssa} " + f"depths [{queue.depth}] latencies [{queue.latency}] {{" + ) + lines.append(f"{indent}^transform(%item: !ac.var<{queue.payload}>):") + lines.extend(indent + line[2:] for line in emitter.lines) + lines.append( + f"{indent} ac.transform.yield %{result} : !ac.var<{queue.payload}>" + ) + lines.append( + f"{indent}}} {queue_attributes(queue.name, (queue.rate,))} : " + f"(!ac.queue<{queue.payload}>) -> " + f"!ac.queue<{queue.payload}>" + ) + mapping[queue.name] = output_ssa + + def render_items( + path: tuple[str, ...], mapping: dict[str, str], indent: str + ) -> None: + def visible_order(consumer: QueueBinding) -> int: + if consumer.scope == path: + return consumer.order + child_path = (*path, consumer.scope[len(path)]) + return next( + scope.order for scope in program.scopes if scope.path == child_path + ) + + events: list[tuple[float, str, object]] = [] + events.extend( + (queue.order, "queue", queue) + for queue in program.queues + if queue.scope == path + and not queue.route_output + and not queue.feedback_output + and not queue.merge_output + and not queue.reorder_output + and not queue.dependency_output + and not queue.credit_output + and not queue.memory_output + and not queue.barrier_output + and not queue.select_output + and queue.atomic_group is None + ) + events.extend( + (atomic.order, "atomic", atomic) + for atomic in program.atomics + if atomic.scope == path + ) + events.extend( + (fork.order, "fork", fork) for fork in program.forks if fork.scope == path + ) + events.extend( + (route.order, "route", route) + for route in program.routes + if route.scope == path + ) + events.extend( + (merge.order, "merge", merge) + for merge in program.merges + if merge.scope == path + ) + events.extend( + (feedback.order, "feedback", feedback) + for feedback in program.feedbacks + if feedback.scope == path + ) + events.extend( + (reorder.order, "reorder", reorder) + for reorder in program.reorders + if reorder.scope == path + ) + events.extend( + (dependency.order, "dependency", dependency) + for dependency in program.dependencies + if dependency.scope == path + ) + events.extend( + (credit.order, "credit", credit) + for credit in program.credits + if credit.scope == path + ) + events.extend( + (barrier.order, "barrier", barrier) + for barrier in program.barriers + if barrier.scope == path + ) + events.extend( + (select.order, "select", select) + for select in program.selects + if select.scope == path + ) + events.extend( + (request.order, "memory_request", request) + for request in program.memory_requests + if request.scope == path + ) + events.extend( + ( + min(visible_order(consumer) for consumer in group) - 0.5, + "broadcast", + source, + ) + for source, (fanout_scope, group) in fanouts.items() + if fanout_scope == path + ) + events.extend( + (scope.order, "scope", scope) + for scope in program.scopes + if scope.path[:-1] == path + ) + events.extend( + (observation.order, "observe", observation) + for observation in program.observations + if observation.scope == path + ) + events.extend( + (expectation.order, "expect", expectation) + for expectation in program.expectations + if expectation.scope == path + ) + events.extend( + (sink_binding.order, "sink", sink_binding) + for sink_binding in program.sinks + if sink_binding.scope == path + ) + for _, kind, item in sorted(events, key=lambda event: event[0]): + if kind == "queue": + queue = item + assert isinstance(queue, QueueBinding) + output = queue.name if not path else f"{queue.name}__local" + emit_queue(queue, output, mapping, indent) + elif kind == "scope": + scope = item + assert isinstance(scope, ScopeBinding) + render_scope(scope, mapping, indent) + elif kind == "broadcast": + source = item + assert isinstance(source, str) + _, group = fanouts[source] + outputs = [f"{source}__fanout{index}" for index in range(len(group))] + lhs = ", ".join(f"%{name}" for name in outputs) + depths = ", ".join("1" for _ in outputs) + payload = payload_by_queue[source] + output_types = ", ".join(f"!ac.queue<{payload}>" for _ in outputs) + lines.append( + f"{indent}{lhs} = ac.broadcast %{mapping[source]} depths " + f"[{depths}] latencies [{depths}] " + f"{{ac.output_names = {name_array(outputs)}}} : " + f"!ac.queue<{payload}> -> " + f"({output_types})" + ) + for consumer, output in zip(group, outputs, strict=True): + mapping[effective_input[consumer.name]] = output + elif kind == "barrier": + barrier = item + assert isinstance(barrier, BarrierBinding) + output_names = [ + name if not path else f"{name}__local" for name in barrier.outputs + ] + lhs = ", ".join(f"%{name}" for name in output_names) + operands = ", ".join( + f"%{mapping[input_name]}" for input_name in barrier.inputs + ) + depths = ", ".join(str(barrier.depth) for _ in output_names) + latencies = ", ".join(str(barrier.latency) for _ in output_names) + input_types = ", ".join( + f"!ac.queue<{by_name[input_name].payload}>" + for input_name in barrier.inputs + ) + output_types = ", ".join( + f"!ac.queue<{by_name[input_name].payload}>" + for input_name in barrier.inputs + ) + lines.append( + f"{indent}{lhs} = ac.barrier {operands} depths [{depths}] " + f"latencies [{latencies}] " + f"{{ac.output_names = {name_array(barrier.outputs)}}} : " + f"({input_types}) -> ({output_types})" + ) + for name, output in zip(barrier.outputs, output_names, strict=True): + mapping[name] = output + elif kind == "select": + select = item + assert isinstance(select, SelectBinding) + control = by_name[select.control] + emitter = _ExpressionEmitter(payloads, select.argument, control.payload) + selector, selector_type = emitter.emit(select.selector) + if not selector_type.startswith("i"): + raise QueueFrontendError( + "ACPY-QUEUE-018: select key must lower to an integer" + ) + output = select.output if not path else f"{select.output}__local" + operands = ", ".join( + f"%{mapping[name]}" for name in (select.control, *select.inputs) + ) + input_types = ", ".join( + f"!ac.queue<{by_name[name].payload}>" + for name in (select.control, *select.inputs) + ) + lines.append( + f"{indent}%{output} = ac.select {operands} " + f"depth {select.depth} latency {select.latency} key {{" + ) + lines.append(f"{indent}^key(%item: !ac.var<{control.payload}>):") + lines.extend(indent + line[2:] for line in emitter.lines) + lines.append( + f"{indent} ac.select.yield %{selector} : !ac.var<{selector_type}>" + ) + lines.append( + f'{indent}}} {{ac.name = "{select.output}"}} : ' + f"({input_types}) -> !ac.queue<{by_name[select.output].payload}>" + ) + mapping[select.output] = output + elif kind == "route": + route = item + assert isinstance(route, RouteBinding) + incoming = by_name[route.input_name] + emitter = _ExpressionEmitter(payloads, route.argument, incoming.payload) + selector, selector_type = emitter.emit(route.selector) + if route.boolean_selector and selector_type != "i1": + raise QueueFrontendError( + "ACPY-QUEUE-011: runtime if condition must lower to bool" + ) + if not route.boolean_selector and not selector_type.startswith("i"): + raise QueueFrontendError( + "ACPY-QUEUE-006: route key must lower to an integer" + ) + output_names = [ + name if not path else f"{name}__local" for name in route.outputs + ] + lhs = ", ".join(f"%{name}" for name in output_names) + depths = ", ".join(str(route.depth) for _ in output_names) + latencies = ", ".join(str(route.latency) for _ in output_names) + output_types = ", ".join( + f"!ac.queue<{incoming.payload}>" for _ in output_names + ) + lines.append( + f"{indent}{lhs} = ac.route %{mapping[route.input_name]} " + f"depths [{depths}] latencies [{latencies}] {{" + ) + lines.append(f"{indent}^selector(%item: !ac.var<{incoming.payload}>):") + lines.extend(indent + line[2:] for line in emitter.lines) + lines.append( + f"{indent} ac.route.yield %{selector} : !ac.var<{selector_type}>" + ) + lines.append( + f"{indent}}} " + f"{{ac.output_names = {name_array(route.outputs)}}} : " + f"!ac.queue<{incoming.payload}> -> ({output_types})" + ) + for name, output in zip(route.outputs, output_names, strict=True): + mapping[name] = output + elif kind == "fork": + fork = item + assert isinstance(fork, ForkBinding) + incoming = by_name[fork.input_name] + output_names = [ + name if not path else f"{name}__local" for name in fork.outputs + ] + lhs = ", ".join(f"%{name}" for name in output_names) + depths = ", ".join(str(fork.depth) for _ in output_names) + latencies = ", ".join(str(fork.latency) for _ in output_names) + output_types = ", ".join( + f"!ac.queue<{incoming.payload}>" for _ in output_names + ) + lines.append( + f"{indent}{lhs} = ac.fork %{mapping[fork.input_name]} " + f"depths [{depths}] latencies [{latencies}] " + f"{{ac.output_names = {name_array(fork.outputs)}}} : " + f"!ac.queue<{incoming.payload}> -> ({output_types})" + ) + for name, output in zip(fork.outputs, output_names, strict=True): + mapping[name] = output + elif kind == "feedback": + feedback = item + assert isinstance(feedback, FeedbackBinding) + incoming = by_name[feedback.input_name] + emitter = _ExpressionEmitter( + payloads, feedback.argument, incoming.payload + ) + condition, condition_type = emitter.emit(feedback.condition) + update, update_type = emitter.emit(feedback.update) + if condition_type != "i1" or update_type != incoming.payload: + raise QueueFrontendError( + "ACPY-QUEUE-007: while condition must be bool and update " + "must preserve Queue payload" + ) + output = ( + feedback.output_name + if not path + else f"{feedback.output_name}__local" + ) + lines.append( + f"{indent}%{output} = ac.feedback %{mapping[feedback.input_name]} " + f"depth {feedback.depth} latency {feedback.latency} " + f"max_iterations {feedback.max_iterations} {{" + ) + lines.append(f"{indent}^body(%item: !ac.var<{incoming.payload}>):") + lines.extend(indent + line[2:] for line in emitter.lines) + lines.append( + f"{indent} ac.feedback.yield %{update} continue %{condition} : " + f"!ac.var<{incoming.payload}>, !ac.var" + ) + lines.append( + f'{indent}}} {{ac.name = "{feedback.output_name}"}} : ' + f"!ac.queue<{incoming.payload}> -> " + f"!ac.queue<{incoming.payload}>" + ) + mapping[feedback.output_name] = output + elif kind == "reorder": + reorder = item + assert isinstance(reorder, ReorderBinding) + incoming = by_name[reorder.input_name] + emitter = _ExpressionEmitter( + payloads, reorder.argument, incoming.payload + ) + key, key_type = emitter.emit(reorder.key) + if not key_type.startswith("i"): + raise QueueFrontendError( + "ACPY-QUEUE-013: reorder key must lower to an integer" + ) + output = ( + reorder.output_name if not path else f"{reorder.output_name}__local" + ) + lines.append( + f"{indent}%{output} = ac.reorder " + f"%{mapping[reorder.input_name]} capacity {reorder.capacity} " + f"start {reorder.start} depth {reorder.depth} " + f"latency {reorder.latency} {{" + ) + lines.append(f"{indent}^key(%item: !ac.var<{incoming.payload}>):") + lines.extend(indent + line[2:] for line in emitter.lines) + lines.append(f"{indent} ac.reorder.yield %{key} : !ac.var<{key_type}>") + lines.append( + f'{indent}}} {{ac.name = "{reorder.output_name}"}} : ' + f"!ac.queue<{incoming.payload}> -> " + f"!ac.queue<{incoming.payload}>" + ) + mapping[reorder.output_name] = output + elif kind == "dependency": + dependency = item + assert isinstance(dependency, DependencyBinding) + incoming = by_name[dependency.input_name] + policies = ( + ("key", dependency.key), + ("waits_for", dependency.waits_for), + ("resource", dependency.resource), + ("cost", dependency.cost), + ) + emitted: list[tuple[str, str, list[str]]] = [] + for policy_name, expression in policies: + emitter = _ExpressionEmitter( + payloads, dependency.argument, incoming.payload + ) + value, value_type = emitter.emit(expression) + if not value_type.startswith("i"): + raise QueueFrontendError( + "ACPY-QUEUE-014: dependency policies must lower to integers" + ) + emitted.append((value, value_type, emitter.lines)) + if emitted[0][1] != emitted[1][1]: + raise QueueFrontendError( + "ACPY-QUEUE-014: key and waits_for types must match" + ) + output = ( + dependency.output_name + if not path + else f"{dependency.output_name}__local" + ) + lines.append( + f"{indent}%{output} = ac.dependency " + f"%{mapping[dependency.input_name]} capacity " + f"{dependency.capacity} resources {dependency.resources} " + f"no_dependency " + f"{dependency.no_dependency} depth {dependency.depth} " + f"latency {dependency.latency} key {{" + ) + for index, policy_name in enumerate( + ("key", "waits_for", "resource", "cost") + ): + if index: + lines.append(f"{indent}}} {policy_name} {{") + lines.append( + f"{indent}^{policy_name}(%item: !ac.var<{incoming.payload}>):" + ) + value, value_type, policy_lines = emitted[index] + lines.extend(indent + line[2:] for line in policy_lines) + lines.append( + f"{indent} ac.dependency.yield %{value} : " + f"!ac.var<{value_type}>" + ) + lines.append( + f'{indent}}} {{ac.name = "{dependency.output_name}"}} : ' + f"!ac.queue<{incoming.payload}> -> " + f"!ac.queue<{incoming.payload}>" + ) + mapping[dependency.output_name] = output + elif kind == "credit": + credit = item + assert isinstance(credit, CreditBinding) + incoming = by_name[credit.input_name] + emitter = _ExpressionEmitter( + payloads, credit.argument, incoming.payload + ) + cost, cost_type = emitter.emit(credit.cost) + if not cost_type.startswith("i"): + raise QueueFrontendError( + "ACPY-QUEUE-016: credit cost must lower to an integer" + ) + output = ( + credit.output_name if not path else f"{credit.output_name}__local" + ) + lines.append( + f"{indent}%{output} = ac.credit " + f"%{mapping[credit.input_name]} credits {credit.credits} " + f"depth {credit.depth} latency {credit.latency} cost {{" + ) + lines.append(f"{indent}^cost(%item: !ac.var<{incoming.payload}>):") + lines.extend(indent + line[2:] for line in emitter.lines) + lines.append( + f"{indent} ac.credit.yield %{cost} : !ac.var<{cost_type}>" + ) + lines.append( + f'{indent}}} {{ac.name = "{credit.output_name}"}} : ' + f"!ac.queue<{incoming.payload}> -> " + f"!ac.queue<{incoming.payload}>" + ) + mapping[credit.output_name] = output + elif kind == "memory_request": + memory = item + assert isinstance(memory, MemoryRequestBinding) + incoming = by_name[memory.input_name] + instance = next( + value + for value in program.memory_instances + if value.name == memory.instance + ) + policies = ( + ("address", memory.address), + ("write", memory.write), + ("data", memory.data), + ) + emitted: list[tuple[str, str, list[str]]] = [] + for policy_name, expression in policies: + emitter = _ExpressionEmitter( + payloads, memory.argument, incoming.payload + ) + value, value_type = emitter.emit(expression) + emitted.append((value, value_type, emitter.lines)) + if not emitted[0][1].startswith("i"): + raise QueueFrontendError( + "ACPY-QUEUE-015: memory address must lower to an integer" + ) + if emitted[1][1] != "i1": + raise QueueFrontendError( + "ACPY-QUEUE-015: memory write must lower to bool" + ) + if emitted[2][1] != instance.data_type: + raise QueueFrontendError( + "ACPY-QUEUE-015: memory data must match result_field" + ) + output = ( + memory.output_name if not path else f"{memory.output_name}__local" + ) + lines.append( + f"{indent}%{output} = ac.memory.request @{memory.instance}, " + f"%{mapping[memory.input_name]} ordinal " + f"{memory_ordinals[(memory.instance, memory.output_name)]} " + f'result_field "{memory.result_field}" ' + f"depth {memory.depth} address {{" + ) + for index, policy_name in enumerate(("address", "write", "data")): + if index: + lines.append(f"{indent}}} {policy_name} {{") + lines.append( + f"{indent}^{policy_name}(%item: !ac.var<{incoming.payload}>):" + ) + value, value_type, policy_lines = emitted[index] + lines.extend(indent + line[2:] for line in policy_lines) + lines.append( + f"{indent} ac.memory.yield %{value} : !ac.var<{value_type}>" + ) + lines.append( + f'{indent}}} {{ac.endpoint_path = "' + f'{"/" + "/".join((*memory.scope, memory.output_name))}", ' + f'ac.name = "{memory.output_name}"}} : ' + f"!ac.queue<{incoming.payload}> -> " + f"!ac.queue<{incoming.payload}>" + ) + mapping[memory.output_name] = output + elif kind == "atomic": + atomic = item + assert isinstance(atomic, AtomicBinding) + members = [by_name[name] for name in atomic.queues] + inputs = [ + effective_input.get(queue.name, queue.input_name) + for queue in members + ] + if any(input_name is None for input_name in inputs): + raise QueueFrontendError( + "ACPY-QUEUE-009: atomic input resolution failed" + ) + input_names = [str(input_name) for input_name in inputs] + outputs = [ + queue.name if not path else f"{queue.name}__local" + for queue in members + ] + lhs = ", ".join(f"%{name}" for name in outputs) + operands = ", ".join( + f"%{mapping[input_name]}" for input_name in input_names + ) + depths = ", ".join(str(queue.depth) for queue in members) + latencies = ", ".join(str(queue.latency) for queue in members) + input_types = ", ".join( + f"!ac.queue<{payload_by_queue[input_name]}>" + for input_name in input_names + ) + output_types = ", ".join( + f"!ac.queue<{queue.payload}>" for queue in members + ) + lines.append( + f"{indent}{lhs} = ac.transform {operands} depths [{depths}] " + f"latencies [{latencies}] {{" + ) + arguments = ", ".join( + f"%item{index}: !ac.var<{payload_by_queue[input_name]}>" + for index, input_name in enumerate(input_names) + ) + lines.append(f"{indent}^transform({arguments}):") + yielded: list[str] = [] + for index, queue in enumerate(members): + assert queue.argument is not None and queue.expression is not None + emitter = _ExpressionEmitter( + payloads, + queue.argument, + payload_by_queue[input_names[index]], + root_name=f"item{index}", + prefix=f"a{index}_", + ) + result, result_type = emitter.emit(queue.expression) + if result_type != queue.payload: + raise QueueFrontendError( + "ACPY-QUEUE-009: atomic lambda result type mismatch" + ) + lines.extend(indent + line[2:] for line in emitter.lines) + yielded.append(f"%{result}") + lines.append( + f"{indent} ac.transform.yield {', '.join(yielded)} : " + + ", ".join(f"!ac.var<{queue.payload}>" for queue in members) + ) + names_attr = name_array(tuple(queue.name for queue in members)) + lines.append( + f"{indent}}} {{ac.output_names = {names_attr}}} : " + f"({input_types}) -> ({output_types})" + ) + for queue, output in zip(members, outputs, strict=True): + mapping[queue.name] = output + elif kind == "merge": + merge = item + assert isinstance(merge, MergeBinding) + output = merge.output if not path else f"{merge.output}__local" + operands = ", ".join(f"%{mapping[name]}" for name in merge.inputs) + input_types = ", ".join( + f"!ac.queue<{by_name[name].payload}>" for name in merge.inputs + ) + payload = by_name[merge.output].payload + lines.append( + f'{indent}%{output} = ac.merge {operands} policy "{merge.policy}" ' + f"depth {merge.depth} latency {merge.latency} " + f'{{ac.name = "{merge.output}"}} : ' + f"({input_types}) -> !ac.queue<{payload}>" + ) + mapping[merge.output] = output + elif kind == "expect": + expectation = item + assert isinstance(expectation, ExpectBinding) + queue = by_name[expectation.queue] + emitter = _ExpressionEmitter( + payloads, expectation.argument, queue.payload + ) + condition, condition_type = emitter.emit(expectation.predicate) + if condition_type != "i1": + raise QueueFrontendError( + "ACPY-QUEUE-021: expect predicate must lower to bool" + ) + lines.append( + f"{indent}ac.expect %{mapping[expectation.queue]} message " + f"{json.dumps(expectation.message)} {{" + ) + lines.append(f"{indent}^predicate(%item: !ac.var<{queue.payload}>):") + lines.extend(indent + line[2:] for line in emitter.lines) + lines.append(f"{indent} ac.expect.yield %{condition} : !ac.var") + lines.append( + f'{indent}}} {{ac.name = "expect_{expectation.order}"}} : ' + f"!ac.queue<{queue.payload}>" + ) + elif kind == "observe": + observation = item + assert isinstance(observation, ObservationBinding) + queue = by_name[observation.queue] + lines.append( + f"{indent}ac.observe %{mapping[observation.queue]} name " + f'"{observation.name}" : !ac.queue<{queue.payload}>' + ) + else: + sink_binding = item + assert isinstance(sink_binding, SinkBinding) + queue = by_name[sink_binding.queue] + lines.append( + f"{indent}ac.sink %{mapping[sink_binding.queue]} " + f'{{ac.name = "sink_{sink_binding.order}"}} : ' + f"!ac.queue<{queue.payload}>" + ) + + def render_scope( + scope: ScopeBinding, parent_mapping: dict[str, str], indent: str + ) -> None: + inputs, outputs = scope_io(scope.path) + result_names = [ + name if len(scope.path) == 1 else f"{name}__inner" for name in outputs + ] + lhs = ( + "" + if not result_names + else ", ".join(f"%{name}" for name in result_names) + " = " + ) + operands = ", ".join(f"%{parent_mapping[name]}" for name in inputs) + input_types = ", ".join( + f"!ac.queue<{payload_by_queue[name]}>" for name in inputs + ) + output_types = ", ".join( + f"!ac.queue<{payload_by_queue[name]}>" for name in outputs + ) + lines.append(f"{indent}{lhs}ac.scope @{scope.name}({operands}) {{") + local_mapping = dict(parent_mapping) + if inputs: + args = ", ".join( + f"%{name}__in: !ac.queue<{payload_by_queue[name]}>" for name in inputs + ) + lines.append(f"{indent}^body({args}):") + for name in inputs: + local_mapping[name] = f"{name}__in" + else: + lines.append(f"{indent}^body:") + render_items(scope.path, local_mapping, indent + " ") + yielded = ", ".join(f"%{local_mapping[name]}" for name in outputs) + yield_types = ", ".join( + f"!ac.queue<{payload_by_queue[name]}>" for name in outputs + ) + lines.append( + f"{indent} ac.scope.yield" + + (f" {yielded} : {yield_types}" if outputs else "") + ) + result_signature = output_types if len(outputs) == 1 else f"({output_types})" + lines.append(f"{indent}}} : ({input_types}) -> {result_signature}") + for name, result in zip(outputs, result_names, strict=True): + parent_mapping[name] = result + + render_items((), {}, " ") + lines.append("}") + return "\n".join(lines) + "\n" + + +def lower_queue_source( + text: str, + system: str, + static_arguments: Mapping[str, StaticValue] | None = None, + specialization_fingerprint: str | None = None, +) -> str: + return lower_queue_program( + parse_queue_program( + text, + system, + static_arguments=static_arguments, + specialization_fingerprint=specialization_fingerprint, + ) + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_resolve.py b/components/agentic-circuit/src/agentic_circuit/_resolve.py new file mode 100644 index 00000000..d0ea6832 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_resolve.py @@ -0,0 +1,101 @@ +"""Exact component port and result inference for normalized calls.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + +from ._diagnostics import SourceSpan +from ._schemas import ComponentSchema +from ._static_eval import StaticValue + + +ValueCategory = Literal["static", "flow", "endpoint", "resource", "result"] + + +@dataclass(frozen=True, slots=True) +class ValueVersion: + source_name: str + version: int + category: ValueCategory + type_key: str + producer: str | None + ownership: Literal["owned", "borrowed", "shared"] = "borrowed" + + @property + def name(self) -> str: + return f"{self.source_name}#{self.version}" + + +@dataclass(frozen=True, slots=True) +class PortBinding: + port: str + value: ValueVersion + binding_kind: Literal["flow", "endpoint", "resource_ref"] + role: str + + +@dataclass(frozen=True, slots=True) +class ResultBinding: + result: str + value: ValueVersion + + +@dataclass(frozen=True, slots=True) +class UnresolvedCall: + entity_key: str + instance_name: str + static_arguments: tuple[tuple[str, StaticValue], ...] + port_values: tuple[tuple[str, ValueVersion], ...] + result_values: tuple[ValueVersion, ...] + source: SourceSpan + + +@dataclass(frozen=True, slots=True) +class ResolvedCall: + entity_key: str + schema: ComponentSchema + instance_name: str + static_arguments: tuple[tuple[str, StaticValue], ...] + inputs: tuple[PortBinding, ...] + results: tuple[ResultBinding, ...] + source: SourceSpan + + +class ResolutionError(ValueError): + """Raised when exact port or result inference fails.""" + + +def resolve_call(call: UnresolvedCall, schema: ComponentSchema) -> ResolvedCall: + port_values = dict(call.port_values) + if set(port_values) != {port.name for port in schema.ports}: + raise ResolutionError("ACPY-CALL-003: exact port binding failed") + expected_categories = { + "flow": "flow", + "endpoint": "endpoint", + "resource_ref": "resource", + } + inputs: list[PortBinding] = [] + for port in schema.ports: + value = port_values[port.name] + expected = expected_categories[port.binding_kind] + if value.category != expected: + raise ResolutionError( + f"ACPY-CALL-004: port {port.name!r} expects {expected}" + ) + inputs.append(PortBinding(port.name, value, port.binding_kind, port.role)) + if len(call.result_values) != len(schema.results): + raise ResolutionError("ACPY-CALL-005: result shape does not match schema") + results = tuple( + ResultBinding(result.name, value) + for result, value in zip(schema.results, call.result_values, strict=True) + ) + return ResolvedCall( + entity_key=call.entity_key, + schema=schema, + instance_name=call.instance_name, + static_arguments=call.static_arguments, + inputs=tuple(inputs), + results=results, + source=call.source, + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_resources.py b/components/agentic-circuit/src/agentic_circuit/_resources.py new file mode 100644 index 00000000..1bef7df4 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_resources.py @@ -0,0 +1,193 @@ +"""Static protocol, queue, resource, and address records.""" + +from __future__ import annotations + +from dataclasses import dataclass +from itertools import combinations +from typing import TypeAlias + +from ._types import ResourceRef + + +class FrontendRuleError(ValueError): + def __init__(self, code: str, message: str) -> None: + self.code = code + self.message = message + super().__init__(f"{code}: {message}") + + +def _nonempty(value: object, field: str, code: str) -> str: + if type(value) is not str or not value: + raise FrontendRuleError(code, f"{field} must be a non-empty string") + return value + + +@dataclass(frozen=True, slots=True) +class ProtocolContract: + identity: str + producer_role: str + consumer_role: str + payload_type: str + time_domain: str + + def __post_init__(self) -> None: + for field in ( + "identity", + "producer_role", + "consumer_role", + "payload_type", + "time_domain", + ): + _nonempty(getattr(self, field), field, "ACPY-PROTOCOL-004") + if self.producer_role == self.consumer_role: + raise FrontendRuleError( + "ACPY-PROTOCOL-004", "producer and consumer roles must differ" + ) + + +def verify_protocol_roles( + contract: ProtocolContract, producer_role: str, consumer_role: str +) -> None: + if ( + producer_role != contract.producer_role + or consumer_role != contract.consumer_role + or producer_role == consumer_role + ): + raise FrontendRuleError( + "ACPY-PROTOCOL-004", + f"roles must be {contract.producer_role!r} and {contract.consumer_role!r}", + ) + + +@dataclass(frozen=True, slots=True) +class QueueSpec: + name: str + payload_type: str + protocol: str + depth: int + time_domain: str + + +def queue( + name: str, + *, + payload_type: str, + protocol: str, + depth: int, + time_domain: str = "default", +) -> QueueSpec: + _nonempty(name, "queue name", "ACPY-RESOURCE-002") + _nonempty(payload_type, "payload type", "ACPY-RESOURCE-002") + _nonempty(protocol, "protocol", "ACPY-RESOURCE-002") + _nonempty(time_domain, "time domain", "ACPY-RESOURCE-002") + if type(depth) is not int or depth <= 0: + raise FrontendRuleError( + "ACPY-RESOURCE-002", "queue depth must be a positive static integer" + ) + return QueueSpec(name, payload_type, protocol, depth, time_domain) + + +@dataclass(frozen=True, slots=True) +class AddressSpaceSpec: + name: str + width: int + unit: str = "byte" + + +def address_space(name: str, *, width: int, unit: str = "byte") -> AddressSpaceSpec: + _nonempty(name, "address-space name", "ACPY-ADDRESS-001") + _nonempty(unit, "address-space unit", "ACPY-ADDRESS-001") + if type(width) is not int or width <= 0: + raise FrontendRuleError( + "ACPY-ADDRESS-001", "address width must be a positive static integer" + ) + return AddressSpaceSpec(name, width, unit) + + +@dataclass(frozen=True, slots=True) +class AddressMapEntry: + start: int + end: int + target: ResourceRef[object, object] + priority: int + + @property + def range(self) -> tuple[int, int]: + return (self.start, self.end) + + +AddressEntryLike: TypeAlias = ( + AddressMapEntry | tuple[object, object, object, object] +) + + +@dataclass(frozen=True, slots=True) +class AddressMapSpec: + space: AddressSpaceSpec + entries: tuple[AddressMapEntry, ...] + + +def _entry(value: AddressEntryLike, space: AddressSpaceSpec) -> AddressMapEntry: + if isinstance(value, AddressMapEntry): + entry = value + elif type(value) is tuple and len(value) == 4: + entry = AddressMapEntry(value[0], value[1], value[2], value[3]) + else: + raise FrontendRuleError( + "ACPY-ADDRESS-001", "address-map entry must have four fields" + ) + if type(entry.start) is not int or type(entry.end) is not int: + raise FrontendRuleError( + "ACPY-STATIC-002", "address bounds must be static integers" + ) + if entry.start < 0 or entry.end <= entry.start: + raise FrontendRuleError( + "ACPY-ADDRESS-001", "address range must be finite and non-empty" + ) + if entry.end.bit_length() > space.width: + raise FrontendRuleError( + "ACPY-ADDRESS-001", "address range exceeds its declared space" + ) + if not isinstance(entry.target, ResourceRef): + raise FrontendRuleError( + "ACPY-ADDRESS-001", "address target must be a ResourceRef" + ) + if type(entry.priority) is not int or entry.priority < 0: + raise FrontendRuleError( + "ACPY-ADDRESS-001", "address priority must be a non-negative integer" + ) + return entry + + +def verify_address_map( + entries: tuple[AddressMapEntry, ...], +) -> tuple[AddressMapEntry, ...]: + ordered = tuple( + sorted( + entries, + key=lambda item: ( + item.start, + item.end, + item.priority, + item.target.stable_name, + ), + ) + ) + for left, right in combinations(ordered, 2): + overlap = max(left.start, right.start) < min(left.end, right.end) + if overlap and left.priority == right.priority: + raise FrontendRuleError( + "ACPY-ADDRESS-003", "ambiguous equal-priority address overlap" + ) + return ordered + + +def address_map( + space: AddressSpaceSpec, *entries: AddressEntryLike +) -> AddressMapSpec: + if not isinstance(space, AddressSpaceSpec): + raise FrontendRuleError( + "ACPY-ADDRESS-001", "address map requires a declared address space" + ) + normalized = tuple(_entry(entry, space) for entry in entries) + return AddressMapSpec(space, verify_address_map(normalized)) diff --git a/components/agentic-circuit/src/agentic_circuit/_run.py b/components/agentic-circuit/src/agentic_circuit/_run.py new file mode 100644 index 00000000..8ff700bf --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_run.py @@ -0,0 +1,633 @@ +"""Immutable run bundles and runtime-harness execution.""" + +from __future__ import annotations + +import json +import os +import re +import signal +import shutil +import stat +import subprocess +import tempfile +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from typing import Literal, NoReturn + +from ._canonical_json import sha256_bytes +from ._commands.build import BuildPublication +from ._diagnostics import Diagnostic +from ._staging import ArtifactStage + + +TerminationKind = Literal["complete", "incomplete", "any"] +RunStatus = Literal["completed", "incomplete", "failed"] +_DOMAIN = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$") +_FINGERPRINT = re.compile(r"^sha256:[0-9a-f]{64}$") +_MANIFEST_KEYS = frozenset( + { + "schema", + "version", + "contract_epoch", + "build_manifest", + "trace", + "seed", + "output_directory", + "deadlock_window", + "max_ticks", + "max_domain_cycles", + "stats_format", + "event_log", + "termination_expectation", + } +) +_RESULT_KEYS = frozenset( + { + "schema", + "version", + "contract_epoch", + "run_manifest", + "status", + "termination_reason", + "simulated_ticks", + "domain_cycles", + "event_count", + "trace_position", + "outputs", + "validation", + } +) +_COMPLETED_REASONS = frozenset({"trace_drained", "declared_model_termination"}) +_INCOMPLETE_REASONS = frozenset( + { + "max_ticks", + "max_domain_cycles", + "max_events", + "max_deltas_per_tick", + "max_trace_records", + "max_validation_work", + "interrupted", + } +) +_FAILED_REASONS = frozenset( + {"deadlock", "invariant_violation", "trace_error", "runtime_error"} +) + + +@dataclass(frozen=True, slots=True) +class RunOptions: + trace: Path + output_directory: Path + seed: int + deadlock_window: int | None + max_ticks: int | None + max_domain_cycles: tuple[tuple[str, int], ...] + stats_format: Literal["json"] + event_log: Literal["disabled", "jsonl"] + termination_kind: TerminationKind + + +@dataclass(frozen=True, slots=True) +class RunPublication: + directory: Path + manifest: Path + result: Path + status: RunStatus + termination_reason: str + exit_code: int + + +class RunFailure(Exception): + def __init__(self, exit_code: int, diagnostic: Diagnostic): + super().__init__(diagnostic.message) + self.exit_code = exit_code + self.diagnostic = diagnostic + + +def _failure(exit_code: int, code: str, message: str) -> NoReturn: + raise RunFailure( + exit_code, + Diagnostic(stage="runtime", code=code, severity="error", message=message), + ) + + +def _normalized_relative(value: object, label: str) -> str: + if type(value) is not str: + _failure(5, "ACRUN-PREFLIGHT-001", f"{label} must be a relative path") + path = PurePosixPath(value) + if ( + path.is_absolute() + or not path.parts + or any(part in ("", ".", "..") for part in path.parts) + or "\\" in value + ): + _failure(5, "ACRUN-PREFLIGHT-001", f"{label} is not normalized") + return value + + +def _pairs(pairs: list[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate object name {key!r}") + result[key] = value + return result + + +def _json_document(data: bytes, label: str) -> dict[str, object]: + try: + value = json.loads(data.decode("utf-8"), object_pairs_hook=_pairs) + except (UnicodeError, json.JSONDecodeError, ValueError) as error: + _failure(5, "ACRUN-PREFLIGHT-001", f"{label} is invalid JSON: {error}") + if type(value) is not dict: + _failure(5, "ACRUN-PREFLIGHT-001", f"{label} must be a JSON object") + return value + + +def _regular_file(path: Path, label: str) -> bytes: + try: + if path.is_symlink() or not path.is_file(): + raise OSError("not a regular file") + return path.read_bytes() + except OSError as error: + _failure(5, "ACRUN-PREFLIGHT-001", f"cannot read {label}: {error}") + + +def _file_hash(value: object, label: str) -> tuple[str, str]: + if type(value) is not dict or set(value) != {"path", "sha256"}: + _failure(5, "ACRUN-PREFLIGHT-001", f"{label} is not a file identity") + path = _normalized_relative(value["path"], f"{label}.path") + fingerprint = value["sha256"] + if type(fingerprint) is not str or not _FINGERPRINT.fullmatch(fingerprint): + _failure(5, "ACRUN-PREFLIGHT-001", f"{label}.sha256 is invalid") + return path, fingerprint + + +def _positive_optional(value: object, label: str) -> int | None: + if value is None: + return None + if type(value) is not int or value < 1 or value > (1 << 64) - 1: + _failure(5, "ACRUN-PREFLIGHT-001", f"{label} must be null or positive") + return value + + +def _canonical_bytes(value: dict[str, object]) -> bytes: + return ( + json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + "\n" + ).encode("utf-8") + + +def _validate_trace(data: bytes) -> None: + document = _json_document(data, "trace") + if ( + set(document) + != {"schema", "version", "contract_epoch", "metadata", "records"} + or document.get("schema") != "pto-trace" + or document.get("version") != "0.1" + or document.get("contract_epoch") != "0.3" + or type(document.get("metadata")) is not dict + or type(document.get("records")) is not list + ): + _failure(5, "ACTRACE-SCHEMA-001", "trace has an invalid closed envelope") + + +def create_run_manifest(build: BuildPublication, options: RunOptions) -> bytes: + if type(options.seed) is not int or not 0 <= options.seed <= (1 << 64) - 1: + _failure(2, "ACRUN-INPUT-001", "seed must be an unsigned 64-bit integer") + if options.deadlock_window is not None and ( + type(options.deadlock_window) is not int + or not 1 <= options.deadlock_window <= (1 << 64) - 1 + ): + _failure(2, "ACRUN-INPUT-001", "deadlock window must be positive") + if options.max_ticks is not None and ( + type(options.max_ticks) is not int + or not 1 <= options.max_ticks <= (1 << 64) - 1 + ): + _failure(2, "ACRUN-INPUT-001", "max ticks must be positive") + domains: dict[str, int] = {} + for name, maximum in options.max_domain_cycles: + if ( + type(name) is not str + or not _DOMAIN.fullmatch(name) + or type(maximum) is not int + or not 1 <= maximum <= (1 << 64) - 1 + or name in domains + ): + _failure(2, "ACRUN-INPUT-001", "domain bounds must be unique and positive") + domains[name] = maximum + if ( + options.stats_format != "json" + or options.event_log not in ("disabled", "jsonl") + or options.termination_kind not in ("complete", "incomplete", "any") + ): + _failure(2, "ACRUN-INPUT-001", "run output or expectation is invalid") + build_bytes = _regular_file(build.manifest, "build manifest") + trace_bytes = _regular_file(options.trace, "trace") + _validate_trace(trace_bytes) + document: dict[str, object] = { + "schema": "agentic-circuit-run-manifest", + "version": "0.1", + "contract_epoch": "0.3", + "build_manifest": { + "path": "build-manifest.json", + "sha256": sha256_bytes(build_bytes), + }, + "trace": { + "path": "trace.json", + "schema": "pto-trace", + "version": "0.1", + "sha256": sha256_bytes(trace_bytes), + }, + "seed": options.seed, + "output_directory": "runtime-result", + "deadlock_window": options.deadlock_window, + "max_ticks": options.max_ticks, + "max_domain_cycles": domains, + "stats_format": options.stats_format, + "event_log": options.event_log, + "termination_expectation": { + "kind": options.termination_kind, + "reason": None, + }, + } + return _canonical_bytes(document) + + +def _build_executable( + root: Path, build_document: dict[str, object] +) -> tuple[str, bytes, int]: + artifacts = build_document.get("artifacts") + if type(artifacts) is not list: + _failure(5, "ACRUN-PREFLIGHT-001", "build artifacts are invalid") + candidates = [ + item + for item in artifacts + if type(item) is dict and item.get("kind") == "executable" + ] + if len(candidates) != 1: + _failure(5, "ACRUN-PREFLIGHT-001", "build must name one executable") + candidate = candidates[0] + if set(candidate) != {"path", "kind", "sha256"}: + _failure(5, "ACRUN-PREFLIGHT-001", "build executable identity is invalid") + path = _normalized_relative(candidate["path"], "build executable.path") + fingerprint = candidate["sha256"] + if type(fingerprint) is not str or not _FINGERPRINT.fullmatch(fingerprint): + _failure(5, "ACRUN-PREFLIGHT-001", "build executable hash is invalid") + source = root.joinpath(*PurePosixPath(path).parts) + data = _regular_file(source, "build executable") + if sha256_bytes(data) != fingerprint: + _failure(5, "ACRUN-PREFLIGHT-001", "build executable hash does not match") + mode = stat.S_IMODE(source.stat().st_mode) + if not mode & stat.S_IXUSR: + _failure(5, "ACRUN-PREFLIGHT-001", "build executable is not executable") + return path, data, mode + + +def _verify_manifest(data: bytes) -> dict[str, object]: + document = _json_document(data, "run manifest") + if ( + set(document) != _MANIFEST_KEYS + or document.get("schema") != "agentic-circuit-run-manifest" + or document.get("version") != "0.1" + or document.get("contract_epoch") != "0.3" + ): + _failure(5, "ACRUN-PREFLIGHT-001", "run manifest has an invalid envelope") + _file_hash(document.get("build_manifest"), "build_manifest") + trace = document.get("trace") + if type(trace) is not dict or set(trace) != {"path", "schema", "version", "sha256"}: + _failure(5, "ACRUN-PREFLIGHT-001", "trace identity is invalid") + _normalized_relative(trace.get("path"), "trace.path") + if ( + trace.get("schema") != "pto-trace" + or trace.get("version") != "0.1" + or type(trace.get("sha256")) is not str + or not _FINGERPRINT.fullmatch(trace["sha256"]) + ): + _failure(5, "ACRUN-PREFLIGHT-001", "trace identity is invalid") + seed = document.get("seed") + if type(seed) is not int or not 0 <= seed <= (1 << 64) - 1: + _failure(5, "ACRUN-PREFLIGHT-001", "seed is invalid") + _normalized_relative(document.get("output_directory"), "output_directory") + _positive_optional(document.get("deadlock_window"), "deadlock_window") + _positive_optional(document.get("max_ticks"), "max_ticks") + domain_limits = document.get("max_domain_cycles") + if type(domain_limits) is not dict or any( + type(name) is not str + or not _DOMAIN.fullmatch(name) + or type(maximum) is not int + or not 1 <= maximum <= (1 << 64) - 1 + for name, maximum in domain_limits.items() + ): + _failure(5, "ACRUN-PREFLIGHT-001", "domain cycle limits are invalid") + if document.get("stats_format") != "json" or document.get("event_log") not in ( + "disabled", + "jsonl", + ): + _failure(5, "ACRUN-PREFLIGHT-001", "runtime output format is invalid") + expectation = document.get("termination_expectation") + if ( + type(expectation) is not dict + or set(expectation) != {"kind", "reason"} + or expectation.get("kind") not in ("complete", "incomplete", "any") + or expectation.get("reason") + not in ( + None, + "trace_drained", + "max_ticks", + "max_domain_cycles", + "declared_model_termination", + ) + ): + _failure(5, "ACRUN-PREFLIGHT-001", "termination expectation is invalid") + reason = expectation.get("reason") + kind = expectation.get("kind") + if ( + kind == "complete" + and reason not in (None, "trace_drained", "declared_model_termination") + ) or ( + kind == "incomplete" + and reason not in (None, "max_ticks", "max_domain_cycles") + ): + _failure(5, "ACRUN-PREFLIGHT-001", "termination expectation is inconsistent") + if data != _canonical_bytes(document): + _failure(5, "ACRUN-PREFLIGHT-001", "run manifest is not canonical") + return document + + +def _uint64(value: object) -> bool: + return type(value) is int and 0 <= value <= (1 << 64) - 1 + + +def _verify_run_result_impl( + stage: Path, + manifest_bytes: bytes, + return_code: int, + expected_outputs: frozenset[str], +) -> tuple[RunStatus, str]: + result_path = stage / "run-result.json" + data = _regular_file(result_path, "run result") + document = _json_document(data, "run result") + if ( + set(document) != _RESULT_KEYS + or document.get("schema") != "agentic-circuit-run-result" + or document.get("version") != "0.1" + or document.get("contract_epoch") != "0.3" + ): + _failure(6, "ACRUN-RESULT-001", "run result has an invalid envelope") + path, fingerprint = _file_hash(document.get("run_manifest"), "run_manifest") + if path != "run-manifest.json" or fingerprint != sha256_bytes(manifest_bytes): + _failure(6, "ACRUN-RESULT-001", "run result references another manifest") + status = document.get("status") + reason = document.get("termination_reason") + expected_codes = {"completed": 0, "incomplete": 7, "failed": 6} + if type(status) is not str or status not in expected_codes or type(reason) is not str: + _failure(6, "ACRUN-RESULT-001", "run result status is invalid") + allowed_reasons = { + "completed": _COMPLETED_REASONS, + "incomplete": _INCOMPLETE_REASONS, + "failed": _FAILED_REASONS, + } + if reason not in allowed_reasons[status]: + _failure(6, "ACRUN-RESULT-001", "run result reason disagrees with status") + if return_code != expected_codes[status]: + _failure(6, "ACRUN-RESULT-001", "runtime exit and result status disagree") + if not _uint64(document.get("simulated_ticks")) or not _uint64( + document.get("event_count") + ): + _failure(6, "ACRUN-RESULT-001", "run counters are invalid") + domain_cycles = document.get("domain_cycles") + if type(domain_cycles) is not dict or any( + type(name) is not str or not _DOMAIN.fullmatch(name) or not _uint64(count) + for name, count in domain_cycles.items() + ): + _failure(6, "ACRUN-RESULT-001", "domain cycle counters are invalid") + trace_position = document.get("trace_position") + if ( + type(trace_position) is not dict + or set(trace_position) + != {"next_record_index", "last_committed_sequence_id"} + or not _uint64(trace_position.get("next_record_index")) + or ( + trace_position.get("last_committed_sequence_id") is not None + and not _uint64(trace_position.get("last_committed_sequence_id")) + ) + ): + _failure(6, "ACRUN-RESULT-001", "trace position is invalid") + validation = document.get("validation") + if type(validation) is not dict or set(validation) != {"status", "report_sha256"}: + _failure(6, "ACRUN-RESULT-001", "validation result is invalid") + validation_status = validation.get("status") + report_hash = validation.get("report_sha256") + if validation_status not in ("passed", "failed", "not_run") or ( + (validation_status == "not_run" and report_hash is not None) + or ( + validation_status != "not_run" + and ( + type(report_hash) is not str + or not _FINGERPRINT.fullmatch(report_hash) + ) + ) + ): + _failure(6, "ACRUN-RESULT-001", "validation identity is invalid") + outputs = document.get("outputs") + if type(outputs) is not list: + _failure(6, "ACRUN-RESULT-001", "run outputs are invalid") + paths: list[str] = [] + output_hashes: dict[str, str] = {} + for item in outputs: + relative, output_hash = _file_hash(item, "run output") + output_data = _regular_file( + stage.joinpath(*PurePosixPath(relative).parts), "run output" + ) + if sha256_bytes(output_data) != output_hash: + _failure(6, "ACRUN-RESULT-001", "run output hash does not match") + paths.append(relative) + output_hashes[relative] = output_hash + if paths != sorted(set(paths)) or set(paths) != expected_outputs: + _failure(6, "ACRUN-RESULT-001", "run outputs are not canonical") + if report_hash != output_hashes.get("validation-report.json"): + _failure(6, "ACRUN-RESULT-001", "validation report hash does not match") + found: set[str] = set() + for candidate in stage.rglob("*"): + if candidate.is_symlink(): + _failure(6, "ACRUN-RESULT-001", "runtime result contains a symlink") + if candidate.is_file(): + found.add(candidate.relative_to(stage).as_posix()) + elif not candidate.is_dir(): + _failure(6, "ACRUN-RESULT-001", "runtime result is not a regular file set") + if found != expected_outputs | {"run-result.json"}: + _failure(6, "ACRUN-RESULT-001", "runtime result stage is not closed") + if data != _canonical_bytes(document): + _failure(6, "ACRUN-RESULT-001", "run result is not canonical") + return status, reason + + +def _verify_run_result( + stage: Path, + manifest_bytes: bytes, + return_code: int, + expected_outputs: frozenset[str], +) -> tuple[RunStatus, str]: + try: + return _verify_run_result_impl( + stage, manifest_bytes, return_code, expected_outputs + ) + except RunFailure as error: + if error.exit_code == 6: + raise + _failure(6, "ACRUN-RESULT-001", error.diagnostic.message) + + +def _execute_bundle( + *, + source_root: Path, + manifest_bytes: bytes, + output_directory: Path, +) -> RunPublication: + manifest = _verify_manifest(manifest_bytes) + build_path, build_hash = _file_hash(manifest["build_manifest"], "build_manifest") + build_bytes = _regular_file( + source_root.joinpath(*PurePosixPath(build_path).parts), "build manifest" + ) + if sha256_bytes(build_bytes) != build_hash: + _failure(5, "ACRUN-PREFLIGHT-001", "build manifest hash does not match") + build_document = _json_document(build_bytes, "build manifest") + executable_path, executable_bytes, executable_mode = _build_executable( + source_root, build_document + ) + trace = manifest["trace"] + assert isinstance(trace, dict) + trace_path = _normalized_relative(trace["path"], "trace.path") + trace_bytes = _regular_file( + source_root.joinpath(*PurePosixPath(trace_path).parts), "trace" + ) + if sha256_bytes(trace_bytes) != trace["sha256"]: + _failure(5, "ACRUN-PREFLIGHT-001", "trace hash does not match") + _validate_trace(trace_bytes) + if output_directory.exists() and ( + output_directory.is_symlink() + or not output_directory.is_dir() + or any(output_directory.iterdir()) + ): + _failure(5, "ACRUN-PUBLISH-001", "run output directory is not empty") + + runtime_files = ["run-result.json", "stats.json", "validation-report.json"] + if manifest["event_log"] == "jsonl": + runtime_files.append("events.jsonl") + input_paths = {build_path, executable_path, trace_path, "run-manifest.json"} + if len(input_paths) != 4 or input_paths & set(runtime_files): + _failure(5, "ACRUN-PREFLIGHT-001", "run bundle input paths collide") + result_directory = PurePosixPath(str(manifest["output_directory"])) + bundle_paths = input_paths | set(runtime_files) + if any( + result_directory == PurePosixPath(path) + or result_directory in PurePosixPath(path).parents + or PurePosixPath(path) in result_directory.parents + for path in bundle_paths + ): + _failure(5, "ACRUN-PREFLIGHT-001", "runtime result stage collides with bundle") + expected = tuple( + sorted( + { + build_path, + executable_path, + trace_path, + "run-manifest.json", + *runtime_files, + } + ) + ) + with ArtifactStage(output_directory, expected=expected) as stage: + assert stage.path is not None + stage.write_bytes(build_path, build_bytes) + stage.write_bytes(executable_path, executable_bytes) + stage.path.joinpath(*PurePosixPath(executable_path).parts).chmod(executable_mode) + stage.write_bytes(trace_path, trace_bytes) + stage.write_bytes("run-manifest.json", manifest_bytes) + result_stage = stage.path / str(manifest["output_directory"]) + completed = subprocess.run( + [ + os.fspath(stage.path.joinpath(*PurePosixPath(executable_path).parts)), + "--run-manifest", + os.fspath(stage.path / "run-manifest.json"), + "--run-result-stage", + os.fspath(result_stage), + ], + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + timeout=None, + ) + if completed.returncode == 5: + message = completed.stderr.decode("utf-8", errors="replace").strip() + _failure(5, "ACRUN-PREFLIGHT-001", message or "runtime preflight failed") + if completed.returncode in (-signal.SIGINT, 130): + _failure(130, "ACRUN-INTERRUPTED-001", "runtime execution was interrupted") + if completed.returncode not in (0, 6, 7): + _failure(6, "ACRUN-RUNTIME-001", "runtime execution failed") + status, reason = _verify_run_result( + result_stage, + manifest_bytes, + completed.returncode, + frozenset(runtime_files) - {"run-result.json"}, + ) + for relative in runtime_files: + source = result_stage / relative + destination = stage.path / relative + os.replace(source, destination) + shutil.rmtree(result_stage) + stage.commit() + return RunPublication( + directory=output_directory, + manifest=output_directory / "run-manifest.json", + result=output_directory / "run-result.json", + status=status, + termination_reason=reason, + exit_code=completed.returncode, + ) + + +def execute_run(publication: BuildPublication, options: RunOptions) -> RunPublication: + manifest_bytes = create_run_manifest(publication, options) + build_bytes = _regular_file(publication.manifest, "build manifest") + build_document = _json_document(build_bytes, "build manifest") + executable_path, executable_bytes, executable_mode = _build_executable( + publication.directory, build_document + ) + if publication.executable.resolve() != publication.directory.joinpath( + *PurePosixPath(executable_path).parts + ).resolve(): + _failure(5, "ACRUN-PREFLIGHT-001", "build publication executable disagrees") + output = options.output_directory.resolve() + output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory( + prefix=".agentic-run-input-", dir=output.parent + ) as temporary: + source_root = Path(temporary) + source_root.joinpath("build-manifest.json").write_bytes(build_bytes) + executable = source_root.joinpath(*PurePosixPath(executable_path).parts) + executable.parent.mkdir(parents=True, exist_ok=True) + executable.write_bytes(executable_bytes) + executable.chmod(executable_mode) + source_root.joinpath("trace.json").write_bytes( + _regular_file(options.trace, "trace") + ) + return _execute_bundle( + source_root=source_root, + manifest_bytes=manifest_bytes, + output_directory=output, + ) + + +def replay_run(manifest_path: Path, output_directory: Path) -> RunPublication: + if manifest_path.is_symlink(): + _failure(5, "ACRUN-PREFLIGHT-001", "run manifest must not be a symlink") + resolved = manifest_path.resolve() + manifest_bytes = _regular_file(resolved, "run manifest") + return _execute_bundle( + source_root=resolved.parent, + manifest_bytes=manifest_bytes, + output_directory=output_directory.resolve(), + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_schemas.py b/components/agentic-circuit/src/agentic_circuit/_schemas.py new file mode 100644 index 00000000..42b3e4fc --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_schemas.py @@ -0,0 +1,671 @@ +"""Closed component schemas and their generated Python call signatures.""" + +from __future__ import annotations + +import inspect +import json +import re +from dataclasses import dataclass +from pathlib import Path +from types import MappingProxyType +from typing import Literal + +from ._canonical_json import JsonValue, canonical_json_bytes, sha256_bytes +from ._static_eval import validate_ijson_value +from ._types import SymbolicValue + + +Availability = Literal["available", "declared_unavailable"] +_TOP_LEVEL_KEYS = { + "schema_kind", + "schema_version", + "contract_epoch", + "canonical_name", + "family", + "provider_namespace", + "stability", + "cpp_binding", + "static_parameters", + "bindings", + "results", + "resources", + "address_behavior", + "protocol_contracts", + "effect", + "activation", + "observation", + "schema_fingerprint", +} +_NESTED_KEYS = { + "cpp_binding": { + "header", + "symbol", + "language", + "concept", + "toolchain_target", + "functional_policy", + }, + "static_parameters": { + "name", + "acir_type", + "required", + "default", + "constraint", + "cpp_mapping", + }, + "bindings": { + "name", + "binding_kind", + "acir_type", + "direction", + "role", + "cardinality", + "linearity", + "ownership", + "delegation", + "result_mapping", + }, + "results": {"name", "acir_type", "source_binding", "linearity", "ownership"}, + "resources": { + "name", + "class", + "capacity", + "lanes", + "issue_width", + "initiation_interval", + "latency_policy", + "reservation_owner", + "transaction_classes", + "statistics", + }, + "address_behavior": { + "consumes", + "produces", + "ranges", + "translation", + "routing", + "unmapped", + }, + "protocol_contracts": { + "protocol", + "role", + "ordering", + "delivery", + "correlation", + "completion", + "failure", + }, + "effect": { + "kind", + "requirements", + "guarantees", + "observable_effects", + "failure_behavior", + }, + "activation": {"sources", "predicate", "wakeup_contract", "quiescence"}, + "observation": {"probes", "counters", "gauges", "histograms"}, +} +_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +_QUALIFIED_NAME = re.compile( + r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$" +) +_NAMESPACE_NAME = re.compile( + r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$" +) + + +class SchemaError(ValueError): + """Raised when a catalog or component schema violates the frozen contract.""" + + +def _object_pairs(pairs: list[tuple[str, JsonValue]]) -> dict[str, JsonValue]: + result: dict[str, JsonValue] = {} + for key, value in pairs: + if key in result: + raise SchemaError(f"duplicate JSON field {key!r}") + result[key] = value + return result + + +def _load_json(path: Path) -> dict[str, JsonValue]: + try: + value = json.loads( + path.read_text(encoding="utf-8"), + object_pairs_hook=_object_pairs, + parse_constant=lambda token: (_ for _ in ()).throw( + SchemaError(f"non-finite JSON number {token}") + ), + ) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise SchemaError(f"cannot load schema {path}: {error}") from error + if type(value) is not dict: + raise SchemaError(f"schema {path} must contain an object") + return value + + +def _exact_keys(value: object, expected: set[str], context: str) -> dict[str, JsonValue]: + if type(value) is not dict: + raise SchemaError(f"{context} must be an object") + record = value + if set(record) != expected: + raise SchemaError(f"{context} has unknown or missing fields") + return record + + +def _record_list(value: object, keys: set[str], context: str) -> list[dict[str, JsonValue]]: + if type(value) is not list: + raise SchemaError(f"{context} must be an array") + return [ + _exact_keys(item, keys, f"{context}[{index}]") + for index, item in enumerate(value) + ] + + +def _string(value: object, context: str, *, qualified: bool = False) -> str: + pattern = _QUALIFIED_NAME if qualified else _NAME + if type(value) is not str or not pattern.fullmatch(value): + raise SchemaError(f"{context} is invalid") + return value + + +def _nonempty_string(value: object, context: str) -> str: + if type(value) is not str or not value: + raise SchemaError(f"{context} must be a non-empty string") + return value + + +def _enum(value: object, allowed: set[str], context: str) -> str: + if type(value) is not str or value not in allowed: + raise SchemaError(f"{context} is invalid") + return value + + +def _string_list(value: object, context: str) -> list[str]: + if type(value) is not list or any(type(item) is not str for item in value): + raise SchemaError(f"{context} must be an array of strings") + if len(value) != len(set(value)): + raise SchemaError(f"{context} must contain unique strings") + return value + + +def _positive_integer(value: object, context: str, *, allow_zero: bool = False) -> int: + minimum = 0 if allow_zero else 1 + if type(value) is not int or value < minimum: + raise SchemaError(f"{context} must be an integer >= {minimum}") + return value + + +def _validate_component_fields(record: dict[str, JsonValue], name: str) -> None: + _string(record["canonical_name"], f"{name}.canonical_name", qualified=True) + _string(record["family"], f"{name}.family") + provider_namespace = record["provider_namespace"] + if ( + type(provider_namespace) is not str + or not _NAMESPACE_NAME.fullmatch(provider_namespace) + ): + raise SchemaError(f"{name}.provider_namespace is invalid") + _enum(record["stability"], {"experimental", "provisional", "stable"}, f"{name}.stability") + + cpp_binding = record["cpp_binding"] + if cpp_binding is not None: + binding = _exact_keys( + cpp_binding, _NESTED_KEYS["cpp_binding"], f"{name}.cpp_binding" + ) + for field in ("header", "symbol", "concept", "toolchain_target"): + _nonempty_string(binding[field], f"{name}.cpp_binding.{field}") + if binding["language"] != "c++20": + raise SchemaError(f"{name}.cpp_binding.language is invalid") + _enum( + binding["functional_policy"], + {"required", "optional", "none"}, + f"{name}.cpp_binding.functional_policy", + ) + + for index, parameter in enumerate( + _record_list( + record["static_parameters"], + _NESTED_KEYS["static_parameters"], + f"{name}.static_parameters", + ) + ): + context = f"{name}.static_parameters[{index}]" + _string(parameter["name"], f"{context}.name") + _nonempty_string(parameter["acir_type"], f"{context}.acir_type") + if type(parameter["required"]) is not bool: + raise SchemaError(f"{context}.required must be boolean") + if parameter["constraint"] is not None: + _nonempty_string(parameter["constraint"], f"{context}.constraint") + _enum( + parameter["cpp_mapping"], + {"template_argument", "constexpr_argument", "constructor_constant"}, + f"{context}.cpp_mapping", + ) + + for index, binding in enumerate( + _record_list(record["bindings"], _NESTED_KEYS["bindings"], f"{name}.bindings") + ): + context = f"{name}.bindings[{index}]" + _string(binding["name"], f"{context}.name") + _enum(binding["binding_kind"], {"flow", "endpoint", "resource_ref"}, f"{context}.binding_kind") + _nonempty_string(binding["acir_type"], f"{context}.acir_type") + _enum(binding["direction"], {"in", "out", "inout"}, f"{context}.direction") + _nonempty_string(binding["role"], f"{context}.role") + cardinality = binding["cardinality"] + if type(cardinality) is int: + _positive_integer(cardinality, f"{context}.cardinality", allow_zero=True) + else: + _nonempty_string(cardinality, f"{context}.cardinality") + _enum(binding["linearity"], {"linear", "affine", "unrestricted"}, f"{context}.linearity") + _enum(binding["ownership"], {"owned", "borrowed", "shared"}, f"{context}.ownership") + _enum(binding["delegation"], {"forbidden", "allowed", "required"}, f"{context}.delegation") + if binding["result_mapping"] is not None: + _string(binding["result_mapping"], f"{context}.result_mapping") + + for index, result in enumerate( + _record_list(record["results"], _NESTED_KEYS["results"], f"{name}.results") + ): + context = f"{name}.results[{index}]" + _string(result["name"], f"{context}.name") + _nonempty_string(result["acir_type"], f"{context}.acir_type") + if result["source_binding"] is not None: + _string(result["source_binding"], f"{context}.source_binding") + _enum(result["linearity"], {"linear", "affine", "unrestricted"}, f"{context}.linearity") + _enum(result["ownership"], {"owned", "borrowed", "shared"}, f"{context}.ownership") + + for index, resource in enumerate( + _record_list(record["resources"], _NESTED_KEYS["resources"], f"{name}.resources") + ): + context = f"{name}.resources[{index}]" + _string(resource["name"], f"{context}.name") + for field in ("class", "latency_policy", "reservation_owner"): + _nonempty_string(resource[field], f"{context}.{field}") + capacity = resource["capacity"] + if type(capacity) is int: + _positive_integer(capacity, f"{context}.capacity", allow_zero=True) + else: + _nonempty_string(capacity, f"{context}.capacity") + for field in ("lanes", "issue_width", "initiation_interval"): + _positive_integer(resource[field], f"{context}.{field}") + _string_list(resource["transaction_classes"], f"{context}.transaction_classes") + _string_list(resource["statistics"], f"{context}.statistics") + + address = record["address_behavior"] + if address is not None: + address = _exact_keys( + address, _NESTED_KEYS["address_behavior"], f"{name}.address_behavior" + ) + for field in ("consumes", "produces", "ranges"): + _string_list(address[field], f"{name}.address_behavior.{field}") + for field in ("translation", "routing", "unmapped"): + _nonempty_string(address[field], f"{name}.address_behavior.{field}") + + for index, protocol in enumerate( + _record_list( + record["protocol_contracts"], + _NESTED_KEYS["protocol_contracts"], + f"{name}.protocol_contracts", + ) + ): + for field in _NESTED_KEYS["protocol_contracts"]: + _nonempty_string(protocol[field], f"{name}.protocol_contracts[{index}].{field}") + + effect = _exact_keys(record["effect"], _NESTED_KEYS["effect"], f"{name}.effect") + effect_kind = _enum(effect["kind"], {"pure", "stateful"}, f"{name}.effect.kind") + for field in ("requirements", "guarantees", "observable_effects"): + _string_list(effect[field], f"{name}.effect.{field}") + _nonempty_string(effect["failure_behavior"], f"{name}.effect.failure_behavior") + + activation = record["activation"] + if effect_kind == "pure" and activation is not None: + raise SchemaError(f"{name}.activation must be null for a pure component") + if effect_kind != "pure" and activation is None: + raise SchemaError(f"{name}.activation is required for a stateful component") + if activation is not None: + activation = _exact_keys( + activation, _NESTED_KEYS["activation"], f"{name}.activation" + ) + _string_list(activation["sources"], f"{name}.activation.sources") + _nonempty_string(activation["predicate"], f"{name}.activation.predicate") + _string_list( + activation["wakeup_contract"], f"{name}.activation.wakeup_contract" + ) + _nonempty_string(activation["quiescence"], f"{name}.activation.quiescence") + + observation = _exact_keys( + record["observation"], _NESTED_KEYS["observation"], f"{name}.observation" + ) + for field in ("probes", "counters", "gauges", "histograms"): + _string_list(observation[field], f"{name}.observation.{field}") + + +@dataclass(frozen=True, slots=True) +class PortSchema: + name: str + binding_kind: Literal["flow", "endpoint", "resource_ref"] + acir_type: str + direction: Literal["in", "out", "inout"] + role: str + cardinality: int | str + + +@dataclass(frozen=True, slots=True) +class ResultSchema: + name: str + acir_type: str + source_binding: str | None + ownership: Literal["owned", "borrowed", "shared"] = "owned" + + +@dataclass(frozen=True, slots=True) +class ParameterSchema: + name: str + acir_type: str + required: bool + default: JsonValue + constraint: str | None + + +@dataclass(frozen=True, slots=True) +class ComponentSchema: + identity: str + fingerprint: str + ports: tuple[PortSchema, ...] + results: tuple[ResultSchema, ...] + parameters: tuple[ParameterSchema, ...] + availability: Availability + effect_kind: Literal["pure", "stateful"] = "stateful" + external_binding: str | None = None + + +@dataclass(frozen=True, slots=True) +class PendingCall: + schema: ComponentSchema + arguments: tuple[tuple[str, object], ...] + + +def _component_schema( + record: dict[str, JsonValue], availability: Availability, expected_name: str, + *, external: bool = False, +) -> ComponentSchema: + _exact_keys(record, _TOP_LEVEL_KEYS, expected_name) + if ( + record["schema_kind"] != "agentic-circuit-component" + or record["schema_version"] != "0.1" + or record["contract_epoch"] != "0.3" + or record["canonical_name"] != expected_name + ): + raise SchemaError(f"component identity mismatch for {expected_name}") + + _validate_component_fields(record, expected_name) + + cpp_binding = record["cpp_binding"] + if cpp_binding is not None: + _exact_keys(cpp_binding, _NESTED_KEYS["cpp_binding"], f"{expected_name}.cpp_binding") + parameter_records = _record_list( + record["static_parameters"], + _NESTED_KEYS["static_parameters"], + f"{expected_name}.static_parameters", + ) + binding_records = _record_list( + record["bindings"], _NESTED_KEYS["bindings"], f"{expected_name}.bindings" + ) + result_records = _record_list( + record["results"], _NESTED_KEYS["results"], f"{expected_name}.results" + ) + _record_list( + record["resources"], _NESTED_KEYS["resources"], f"{expected_name}.resources" + ) + address_behavior = record["address_behavior"] + if address_behavior is not None: + _exact_keys( + address_behavior, + _NESTED_KEYS["address_behavior"], + f"{expected_name}.address_behavior", + ) + _record_list( + record["protocol_contracts"], + _NESTED_KEYS["protocol_contracts"], + f"{expected_name}.protocol_contracts", + ) + _exact_keys(record["effect"], _NESTED_KEYS["effect"], f"{expected_name}.effect") + activation = record["activation"] + if activation is not None: + _exact_keys( + activation, _NESTED_KEYS["activation"], f"{expected_name}.activation" + ) + _exact_keys( + record["observation"], + _NESTED_KEYS["observation"], + f"{expected_name}.observation", + ) + + digest_record = dict(record) + fingerprint = digest_record.pop("schema_fingerprint") + if type(fingerprint) is not str: + raise SchemaError(f"component fingerprint is invalid for {expected_name}") + try: + computed = sha256_bytes(canonical_json_bytes(digest_record)) + except ValueError as error: + raise SchemaError(f"component contains invalid JSON for {expected_name}") from error + if fingerprint != computed: + raise SchemaError(f"component fingerprint mismatch for {expected_name}") + + parameters: list[ParameterSchema] = [] + for parameter in parameter_records: + name = parameter["name"] + acir_type = parameter["acir_type"] + required = parameter["required"] + constraint = parameter["constraint"] + if ( + type(name) is not str + or type(acir_type) is not str + or type(required) is not bool + or constraint is not None + and type(constraint) is not str + ): + raise SchemaError(f"invalid static parameter in {expected_name}") + parameters.append( + ParameterSchema(name, acir_type, required, parameter["default"], constraint) + ) + + ports: list[PortSchema] = [] + for binding in binding_records: + if not all( + type(binding[key]) is str + for key in ("name", "binding_kind", "acir_type", "direction", "role") + ) or type(binding["cardinality"]) not in (int, str): + raise SchemaError(f"invalid binding in {expected_name}") + ports.append( + PortSchema( + binding["name"], + binding["binding_kind"], + binding["acir_type"], + binding["direction"], + binding["role"], + binding["cardinality"], + ) + ) + + results: list[ResultSchema] = [] + for result in result_records: + source_binding = result["source_binding"] + if ( + type(result["name"]) is not str + or type(result["acir_type"]) is not str + or source_binding is not None + and type(source_binding) is not str + ): + raise SchemaError(f"invalid result in {expected_name}") + results.append( + ResultSchema( + result["name"], + result["acir_type"], + source_binding, + result["ownership"], + ) + ) + + names = [port.name for port in ports] + [parameter.name for parameter in parameters] + if len(names) != len(set(names)) or "name" in names: + raise SchemaError(f"component signature names collide for {expected_name}") + return ComponentSchema( + identity=expected_name, + fingerprint=fingerprint, + ports=tuple(ports), + results=tuple(results), + parameters=tuple(parameters), + availability=availability, + effect_kind=record["effect"]["kind"], + external_binding=( + expected_name.replace(".", "_") + "_binding" if external else None + ), + ) + + +def signature_for(schema: ComponentSchema) -> inspect.Signature: + parameters: list[inspect.Parameter] = [ + inspect.Parameter(port.name, inspect.Parameter.POSITIONAL_OR_KEYWORD) + for port in schema.ports + ] + for parameter in schema.parameters: + default = inspect.Parameter.empty if parameter.required else parameter.default + parameters.append( + inspect.Parameter( + parameter.name, inspect.Parameter.KEYWORD_ONLY, default=default + ) + ) + parameters.append( + inspect.Parameter("name", inspect.Parameter.KEYWORD_ONLY, default=None) + ) + return inspect.Signature(parameters) + + +class ComponentCallable: + def __init__(self, schema: ComponentSchema) -> None: + self.schema = schema + self.__signature__ = signature_for(schema) + self.__name__ = schema.identity.rsplit(".", 1)[-1] + self.__qualname__ = schema.identity + + def __repr__(self) -> str: + return f"ComponentCallable({self.schema.identity!r})" + + def __call__(self, *args: object, **kwargs: object) -> PendingCall: + try: + bound = self.__signature__.bind(*args, **kwargs) + except TypeError as error: + raise TypeError(f"ACPY-CALL-003: {error}") from error + bound.apply_defaults() + for port in self.schema.ports: + if not isinstance(bound.arguments[port.name], SymbolicValue): + raise TypeError( + f"ACPY-CALL-003: binding {port.name!r} requires a symbolic value" + ) + for parameter in self.schema.parameters: + try: + validate_ijson_value(bound.arguments[parameter.name]) + except ValueError as error: + raise TypeError( + f"ACPY-CALL-003: parameter {parameter.name!r} is not static" + ) from error + instance_name = bound.arguments["name"] + if instance_name is not None and ( + type(instance_name) is not str or not instance_name + ): + raise TypeError("ACPY-CALL-003: instance name must be a non-empty string") + return PendingCall(self.schema, tuple(bound.arguments.items())) + + +class SchemaRegistry: + def __init__(self, schemas: dict[str, ComponentSchema]) -> None: + self._schemas = MappingProxyType(dict(schemas)) + + @classmethod + def from_catalog(cls, catalog_path: Path, repository: Path) -> "SchemaRegistry": + root = repository.resolve(strict=True) + catalog = _load_json(catalog_path.resolve(strict=True)) + _exact_keys( + catalog, + {"catalog", "version", "contract_epoch", "entries"}, + "stdlib catalog", + ) + if ( + catalog["catalog"] != "ac" + or catalog["version"] != "0.1" + or catalog["contract_epoch"] != "0.3" + ): + raise SchemaError("stdlib catalog identity must be ac@0.1") + entries = _record_list( + catalog["entries"], + {"canonical_name", "availability", "schema_path", "schema_fingerprint"}, + "stdlib catalog entries", + ) + names = [entry["canonical_name"] for entry in entries] + if any(type(name) is not str for name in names) or names != sorted(names) or len( + names + ) != len(set(names)): + raise SchemaError("stdlib catalog names must be unique and ordered") + + schemas: dict[str, ComponentSchema] = {} + for entry in entries: + name = entry["canonical_name"] + availability = entry["availability"] + schema_path = entry["schema_path"] + if availability not in ("available", "declared_unavailable"): + raise SchemaError(f"invalid availability for {name}") + if type(schema_path) is not str: + raise SchemaError(f"invalid schema path for {name}") + path = (root / schema_path).resolve(strict=True) + try: + path.relative_to(root / "schemas" / "stdlib") + except ValueError as error: + raise SchemaError(f"component schema escapes stdlib root for {name}") from error + schema = _component_schema(_load_json(path), availability, name) + if entry["schema_fingerprint"] != schema.fingerprint: + raise SchemaError(f"catalog fingerprint mismatch for {name}") + schemas[name] = schema + return cls(schemas) + + def with_component_roots(self, roots: tuple[Path, ...]) -> "SchemaRegistry": + schemas = dict(self._schemas) + resolved_roots = sorted(path.resolve() for path in roots) + for root in resolved_roots: + if not root.exists(): + continue + if not root.is_dir(): + raise SchemaError(f"component root is not a directory: {root}") + for path in sorted(root.rglob("*.component.json")): + record = _load_json(path) + identity = record.get("canonical_name") + if type(identity) is not str: + raise SchemaError(f"component schema {path} has no identity") + schema = _component_schema( + record, "available", identity, external=True + ) + if identity in schemas: + raise SchemaError(f"duplicate component schema {identity}") + schemas[identity] = schema + return SchemaRegistry(schemas) + + def schema(self, identity: str) -> ComponentSchema: + try: + return self._schemas[identity] + except KeyError as error: + raise KeyError(f"ACPY-CALL-001: unknown component {identity!r}") from error + + def callable(self, identity: str) -> ComponentCallable: + schema = self.schema(identity) + if schema.availability != "available": + raise LookupError(f"ACPY-CALL-001: {identity} is declared unavailable") + return ComponentCallable(schema) + + def candidates(self, short_name: str) -> tuple[ComponentSchema, ...]: + return tuple( + schema + for identity, schema in sorted(self._schemas.items()) + if identity.rsplit(".", 1)[-1] == short_name + and schema.availability == "available" + ) + + def __len__(self) -> int: + return len(self._schemas) diff --git a/components/agentic-circuit/src/agentic_circuit/_scopes.py b/components/agentic-circuit/src/agentic_circuit/_scopes.py new file mode 100644 index 00000000..9ef0013e --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_scopes.py @@ -0,0 +1,92 @@ +"""Minimal strong-scope capture and escape outlining.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ._normalize import NormalizedProgram +from ._resolve import ResolvedCall, ValueVersion + + +class ScopeError(ValueError): + """Raised when a strong-scope ownership boundary is invalid.""" + + +@dataclass(frozen=True, slots=True) +class CaptureBinding: + value: ValueVersion + + +@dataclass(frozen=True, slots=True) +class EscapeBinding: + value: ValueVersion + + +@dataclass(frozen=True, slots=True) +class OutlinedScope: + key: str + name: str + parent: str | None + captures: tuple[CaptureBinding, ...] + escapes: tuple[EscapeBinding, ...] + body: tuple[ResolvedCall, ...] + + +def outline_scopes(program: NormalizedProgram) -> tuple[OutlinedScope, ...]: + calls = {call.entity_key: call for call in program.calls} + outlined: list[OutlinedScope] = [] + for region in program.scopes: + internal = set(region.call_keys) + internal_values = set(region.value_names) + body = tuple(calls[key] for key in region.call_keys) + captures: list[CaptureBinding] = [] + captured_names: set[str] = set() + for call in body: + for binding in call.inputs: + value = binding.value + if value.producer not in internal and value.name not in captured_names: + captured_names.add(value.name) + captures.append(CaptureBinding(value)) + + escaped_names: set[str] = set() + for call in program.calls: + if call.entity_key in internal: + continue + for binding in call.inputs: + if ( + binding.value.producer in internal + or binding.value.name in internal_values + ): + escaped_names.add(binding.value.name) + for value in program.returns: + if value.producer in internal or value.name in internal_values: + escaped_names.add(value.name) + escapes = tuple( + EscapeBinding(value) + for value in program.values + if value.name in escaped_names + ) + invalid = next( + ( + binding.value + for binding in escapes + if binding.value.category == "resource" + and binding.value.ownership == "owned" + ), + None, + ) + if invalid is not None: + raise ScopeError( + f"ACPY-SCOPE-004: owned resource {invalid.source_name!r} escapes scope {region.name!r}" + ) + outlined.append( + OutlinedScope( + key=region.key, + name=region.name, + parent=region.parent, + captures=tuple(captures), + escapes=escapes, + body=body, + ) + ) + return tuple(outlined) diff --git a/components/agentic-circuit/src/agentic_circuit/_source.py b/components/agentic-circuit/src/agentic_circuit/_source.py new file mode 100644 index 00000000..11f5c3bb --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_source.py @@ -0,0 +1,148 @@ +"""Normalized Python source loading and lexical definition indexing.""" + +from __future__ import annotations + +import ast +from dataclasses import dataclass +from pathlib import Path +from typing import TypeAlias + +from ._canonical_json import sha256_bytes +from ._diagnostics import SourceSpan + + +DefinitionNode: TypeAlias = ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef + + +class SourceCaptureError(ValueError): + """Raised when a source file cannot enter the portable frontend.""" + + +@dataclass(frozen=True, slots=True) +class DefinitionSite: + qualified_name: str + name: str + lexical_index: int + span: SourceSpan + decorator_names: tuple[str, ...] + node: DefinitionNode + + +@dataclass(frozen=True, slots=True) +class SourceUnit: + path: str + sha256: str + text: str + tree: ast.Module + definitions: tuple[DefinitionSite, ...] + + +def _decorator_name(node: ast.expr) -> str: + if isinstance(node, ast.Call): + return _decorator_name(node.func) + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + prefix = _decorator_name(node.value) + return f"{prefix}.{node.attr}" if prefix else node.attr + return "" + + +def _source_span(node: DefinitionNode, path: str) -> SourceSpan: + end_line = getattr(node, "end_lineno", None) + end_column = getattr(node, "end_col_offset", None) + if end_line is None or end_column is None: + raise SourceCaptureError(f"unmatchable definition span for {node.name!r}") + starts = [(node.lineno, node.col_offset)] + starts.extend( + (decorator.lineno, max(0, decorator.col_offset - 1)) + for decorator in node.decorator_list + ) + start_line, start_column = min(starts) + return SourceSpan( + file=path, + start_line=start_line, + start_column=start_column + 1, + end_line=end_line, + end_column=end_column + 1, + ) + + +class _DefinitionIndexer(ast.NodeVisitor): + def __init__(self, path: str) -> None: + self._path = path + self._scopes: list[tuple[str, bool]] = [] + self._seen: set[str] = set() + self.sites: list[DefinitionSite] = [] + + def _qualified_name(self, name: str) -> str: + parts: list[str] = [] + for scope_name, is_function in self._scopes: + parts.append(scope_name) + if is_function: + parts.append("") + parts.append(name) + return ".".join(parts) + + def _visit_definition(self, node: DefinitionNode, *, is_function: bool) -> None: + qualified_name = self._qualified_name(node.name) + if qualified_name in self._seen: + raise SourceCaptureError( + f"duplicate definition {qualified_name!r} in {self._path}" + ) + self._seen.add(qualified_name) + self.sites.append( + DefinitionSite( + qualified_name=qualified_name, + name=node.name, + lexical_index=len(self.sites), + span=_source_span(node, self._path), + decorator_names=tuple( + _decorator_name(decorator) for decorator in node.decorator_list + ), + node=node, + ) + ) + self._scopes.append((node.name, is_function)) + self.generic_visit(node) + self._scopes.pop() + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + self._visit_definition(node, is_function=True) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + self._visit_definition(node, is_function=True) + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + self._visit_definition(node, is_function=False) + + +def index_definitions(tree: ast.Module, path: str) -> tuple[DefinitionSite, ...]: + indexer = _DefinitionIndexer(path) + indexer.visit(tree) + return tuple(indexer.sites) + + +def load_source_unit(entry: Path, workspace: Path) -> SourceUnit: + root = workspace.resolve(strict=True) + source = entry.resolve(strict=True) + try: + relative = source.relative_to(root).as_posix() + except ValueError as error: + raise SourceCaptureError( + f"source {source} is outside workspace {root}" + ) from error + + raw = source.read_bytes() + try: + text = raw.decode("utf-8") + except UnicodeDecodeError as error: + raise SourceCaptureError(f"source {relative!r} is not UTF-8") from error + tree = ast.parse(text, filename=relative, type_comments=True) + return SourceUnit( + path=relative, + sha256=sha256_bytes(raw), + text=text, + tree=tree, + definitions=index_definitions(tree, relative), + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_staging.py b/components/agentic-circuit/src/agentic_circuit/_staging.py new file mode 100644 index 00000000..b7ef1f9a --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_staging.py @@ -0,0 +1,132 @@ +"""Contained sibling staging for atomic artifact publication.""" + +from __future__ import annotations + +import os +import shutil +import tempfile +from pathlib import Path, PurePosixPath +from types import TracebackType +from typing import Iterable + + +def _relative_path(value: str) -> PurePosixPath: + path = PurePosixPath(value) + if ( + path.is_absolute() + or not path.parts + or any(part in ("", ".", "..") for part in path.parts) + ): + raise ValueError(f"artifact path is not a contained relative path: {value!r}") + return path + + +class ArtifactStage: + """Stage a closed file set beside its destination and publish transactionally.""" + + __slots__ = ("destination", "expected", "path", "committed") + + def __init__(self, destination: Path, *, expected: Iterable[str]): + self.destination = destination.resolve() + self.expected = tuple(sorted({_relative_path(item) for item in expected})) + self.path: Path | None = None + self.committed = False + + def __enter__(self) -> "ArtifactStage": + self.destination.parent.mkdir(parents=True, exist_ok=True) + self.path = Path( + tempfile.mkdtemp( + prefix=".agentic-stage-", dir=self.destination.parent + ) + ) + return self + + def __exit__( + self, + exception_type: type[BaseException] | None, + exception: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self.path is not None and self.path.exists(): + shutil.rmtree(self.path) + + def _open_path(self, relative: str) -> Path: + if self.path is None: + raise RuntimeError("artifact stage has not been entered") + normalized = _relative_path(relative) + target = self.path.joinpath(*normalized.parts) + target.parent.mkdir(parents=True, exist_ok=True) + return target + + def write_bytes(self, relative: str, data: bytes) -> None: + if type(data) is not bytes: + raise TypeError("staged artifact data must be bytes") + self._open_path(relative).write_bytes(data) + + def write_text(self, relative: str, text: str) -> None: + if type(text) is not str: + raise TypeError("staged artifact text must be a string") + self.write_bytes(relative, text.encode("utf-8")) + + def _verify(self) -> None: + if self.path is None: + raise RuntimeError("artifact stage has not been entered") + found: list[PurePosixPath] = [] + for candidate in sorted(self.path.rglob("*")): + if candidate.is_dir(): + continue + if not candidate.is_file() or candidate.is_symlink(): + raise ValueError("artifact stages may contain regular files only") + found.append(PurePosixPath(candidate.relative_to(self.path).as_posix())) + if tuple(found) != self.expected: + raise ValueError("staged artifact set does not match the declared file set") + + def verify(self) -> None: + """Validate the staged closed file set without publishing it.""" + + self._verify() + + def commit(self, *, allow_replace: Iterable[str] = ()) -> None: + self._verify() + assert self.path is not None + replace = {_relative_path(item) for item in allow_replace} + if not replace.issubset(set(self.expected)): + raise ValueError("replacement permission names an undeclared artifact") + for relative in self.expected: + target = self.destination.joinpath(*relative.parts) + if target.exists() and relative not in replace: + raise FileExistsError(f"artifact already exists: {relative.as_posix()}") + + backup_root = self.path / ".backup" + published: list[PurePosixPath] = [] + backed_up: list[PurePosixPath] = [] + try: + self.destination.mkdir(parents=True, exist_ok=True) + for relative in self.expected: + source = self.path.joinpath(*relative.parts) + target = self.destination.joinpath(*relative.parts) + target.parent.mkdir(parents=True, exist_ok=True) + if target.exists(): + backup = backup_root.joinpath(*relative.parts) + backup.parent.mkdir(parents=True, exist_ok=True) + os.replace(target, backup) + backed_up.append(relative) + os.replace(source, target) + published.append(relative) + except BaseException: + for relative in reversed(published): + target = self.destination.joinpath(*relative.parts) + source = self.path.joinpath(*relative.parts) + source.parent.mkdir(parents=True, exist_ok=True) + if target.exists(): + os.replace(target, source) + for relative in reversed(backed_up): + backup = backup_root.joinpath(*relative.parts) + target = self.destination.joinpath(*relative.parts) + target.parent.mkdir(parents=True, exist_ok=True) + if backup.exists(): + os.replace(backup, target) + raise + if backup_root.exists(): + shutil.rmtree(backup_root) + self.committed = True diff --git a/components/agentic-circuit/src/agentic_circuit/_static_eval.py b/components/agentic-circuit/src/agentic_circuit/_static_eval.py new file mode 100644 index 00000000..ee628a2a --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_static_eval.py @@ -0,0 +1,347 @@ +"""Closed interpreter for deterministic elaboration-time expressions.""" + +from __future__ import annotations + +import ast +import math +import operator +from collections.abc import Callable, Iterator, Mapping +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import TypeAlias + + +StaticScalar: TypeAlias = None | bool | int | float | str + +_MAX_SAFE_INTEGER = (1 << 53) - 1 +_MAX_EXPANSION = 10_000 + + +class StaticEvalError(ValueError): + """Raised when an expression has no portable static meaning.""" + + +@dataclass(frozen=True, slots=True) +class FrozenMap(Mapping[str, "StaticValue"]): + entries: tuple[tuple[str, StaticValue], ...] + + def __getitem__(self, key: str) -> StaticValue: + for candidate, value in self.entries: + if candidate == key: + return value + raise KeyError(key) + + def __iter__(self) -> Iterator[str]: + return (key for key, _ in self.entries) + + def __len__(self) -> int: + return len(self.entries) + + +StaticValue: TypeAlias = StaticScalar | tuple["StaticValue", ...] | FrozenMap +StaticHelper: TypeAlias = Callable[..., StaticValue] + + +@dataclass(frozen=True, slots=True) +class StaticEnvironment: + values: Mapping[str, StaticValue] + helpers: Mapping[str, StaticHelper] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__(self, "values", MappingProxyType(dict(self.values))) + object.__setattr__(self, "helpers", MappingProxyType(dict(self.helpers))) + + +def _check_scalar(value: StaticScalar) -> StaticScalar: + if isinstance(value, int) and not isinstance(value, bool): + if not -_MAX_SAFE_INTEGER <= value <= _MAX_SAFE_INTEGER: + raise StaticEvalError("integer is outside the portable I-JSON range") + if isinstance(value, float) and not math.isfinite(value): + raise StaticEvalError("static floating-point values must be finite") + return value + + +def validate_ijson_value(value: StaticValue) -> None: + if value is None or isinstance(value, (bool, int, float, str)): + _check_scalar(value) + return + if isinstance(value, tuple): + for item in value: + validate_ijson_value(item) + return + if isinstance(value, FrozenMap): + for _, item in value.entries: + validate_ijson_value(item) + return + raise StaticEvalError(f"unsupported static value {type(value).__name__}") + + +class _StaticEvaluator(ast.NodeVisitor): + _binary = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.FloorDiv: operator.floordiv, + ast.Mod: operator.mod, + } + _unary = {ast.UAdd: operator.pos, ast.USub: operator.neg, ast.Not: operator.not_} + _comparison = { + ast.Eq: operator.eq, + ast.NotEq: operator.ne, + ast.Lt: operator.lt, + ast.LtE: operator.le, + ast.Gt: operator.gt, + ast.GtE: operator.ge, + } + + def __init__(self, environment: StaticEnvironment) -> None: + self._values = dict(environment.values) + self._helpers = environment.helpers + self._expansions = 0 + + def generic_visit(self, node: ast.AST): + raise StaticEvalError(f"unsupported static syntax: {type(node).__name__}") + + def visit_Constant(self, node: ast.Constant) -> StaticValue: + value = node.value + if value is None or isinstance(value, (bool, int, float, str)): + return _check_scalar(value) + raise StaticEvalError(f"unsupported literal {type(value).__name__}") + + def visit_Name(self, node: ast.Name) -> StaticValue: + try: + return self._values[node.id] + except KeyError as error: + raise StaticEvalError(f"unknown static name {node.id!r}") from error + + def visit_Tuple(self, node: ast.Tuple) -> StaticValue: + return tuple(self.visit(item) for item in node.elts) + + def visit_List(self, node: ast.List) -> StaticValue: + return tuple(self.visit(item) for item in node.elts) + + def visit_Dict(self, node: ast.Dict) -> StaticValue: + entries: dict[str, StaticValue] = {} + for key_node, value_node in zip(node.keys, node.values, strict=True): + if key_node is None: + raise StaticEvalError("dictionary unpacking is not static") + key = self.visit(key_node) + if not isinstance(key, str): + raise StaticEvalError("static map keys must be strings") + if key in entries: + raise StaticEvalError(f"duplicate static map key {key!r}") + entries[key] = self.visit(value_node) + return FrozenMap(tuple(sorted(entries.items()))) + + def visit_Attribute(self, node: ast.Attribute) -> StaticValue: + value = self.visit(node.value) + if isinstance(value, FrozenMap): + try: + return value[node.attr] + except KeyError as error: + raise StaticEvalError(f"unknown static field {node.attr!r}") from error + raise StaticEvalError("static attribute access requires a frozen record") + + def visit_BinOp(self, node: ast.BinOp) -> StaticValue: + function = self._binary.get(type(node.op)) + if function is None: + raise StaticEvalError(f"unsupported static operator {type(node.op).__name__}") + left = self.visit(node.left) + right = self.visit(node.right) + if type(left) not in (int, float, str, tuple) or type(right) not in ( + int, + float, + str, + tuple, + ): + raise StaticEvalError("static arithmetic operands are incompatible") + try: + result = function(left, right) + except (ArithmeticError, TypeError) as error: + raise StaticEvalError("static arithmetic failed") from error + validate_ijson_value(result) + return result + + def visit_UnaryOp(self, node: ast.UnaryOp) -> StaticValue: + function = self._unary.get(type(node.op)) + if function is None: + raise StaticEvalError(f"unsupported static unary operator {type(node.op).__name__}") + value = self.visit(node.operand) + try: + result = function(value) + except TypeError as error: + raise StaticEvalError("static unary operation failed") from error + validate_ijson_value(result) + return result + + def visit_BoolOp(self, node: ast.BoolOp) -> StaticValue: + values = [self.visit(value) for value in node.values] + if any(type(value) is not bool for value in values): + raise StaticEvalError("static boolean operators require booleans") + return all(values) if isinstance(node.op, ast.And) else any(values) + + def visit_Compare(self, node: ast.Compare) -> StaticValue: + left = self.visit(node.left) + for operation, comparator in zip(node.ops, node.comparators, strict=True): + right = self.visit(comparator) + function = self._comparison.get(type(operation)) + if function is None: + raise StaticEvalError( + f"unsupported static comparison {type(operation).__name__}" + ) + try: + matches = function(left, right) + except TypeError as error: + raise StaticEvalError("static comparison operands are incompatible") from error + if not matches: + return False + left = right + return True + + def visit_IfExp(self, node: ast.IfExp) -> StaticValue: + condition = self.visit(node.test) + if type(condition) is not bool: + raise StaticEvalError("static condition must be boolean") + return self.visit(node.body if condition else node.orelse) + + def visit_Subscript(self, node: ast.Subscript) -> StaticValue: + value = self.visit(node.value) + index = self.visit(node.slice) + if isinstance(value, FrozenMap) and isinstance(index, str): + return value[index] + if isinstance(value, (tuple, str)) and type(index) is int: + try: + return value[index] + except IndexError as error: + raise StaticEvalError("static index is out of range") from error + raise StaticEvalError("unsupported static subscript") + + def _range(self, node: ast.Call) -> tuple[StaticValue, ...]: + if node.keywords: + raise StaticEvalError("range does not accept keywords here") + arguments = [self.visit(argument) for argument in node.args] + if not 1 <= len(arguments) <= 3 or any(type(item) is not int for item in arguments): + raise StaticEvalError("range requires one to three integer arguments") + if len(arguments) == 1: + start, stop, step = 0, arguments[0], 1 + elif len(arguments) == 2: + start, stop, step = arguments[0], arguments[1], 1 + else: + start, stop, step = arguments + if step <= 0: + raise StaticEvalError("range step must be positive") + bounded_range = range(start, stop, step) + try: + length = len(bounded_range) + except OverflowError as error: + raise StaticEvalError("static expansion exceeds the maximum") from error + self._consume_expansion(length) + result = tuple(bounded_range) + return result + + def _consume_expansion(self, amount: int) -> None: + self._expansions += amount + if self._expansions > _MAX_EXPANSION: + raise StaticEvalError("static expansion exceeds the maximum") + + def visit_Call(self, node: ast.Call) -> StaticValue: + if not isinstance(node.func, ast.Name): + raise StaticEvalError("unapproved call in static expression") + name = node.func.id + if name == "range": + return self._range(node) + if name in ("tuple", "list"): + if len(node.args) != 1 or node.keywords: + raise StaticEvalError(f"{name} requires one positional argument") + value = self.visit(node.args[0]) + if not isinstance(value, tuple): + raise StaticEvalError(f"{name} requires a static iterable") + return value + helper = self._helpers.get(name) + if helper is None: + raise StaticEvalError(f"unapproved call {name!r} in static expression") + arguments = [self.visit(argument) for argument in node.args] + keywords = {keyword.arg: self.visit(keyword.value) for keyword in node.keywords} + if None in keywords: + raise StaticEvalError("static helper keyword unpacking is forbidden") + result = helper(*arguments, **keywords) + validate_ijson_value(result) + return result + + def _bind_target(self, target: ast.expr, value: StaticValue) -> None: + if isinstance(target, ast.Name): + self._values[target.id] = value + return + if isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, tuple): + if len(target.elts) != len(value): + raise StaticEvalError("comprehension target shape does not match") + for child, item in zip(target.elts, value, strict=True): + self._bind_target(child, item) + return + raise StaticEvalError("unsupported comprehension target") + + def _target_names(self, target: ast.expr) -> set[str]: + if isinstance(target, ast.Name): + return {target.id} + if isinstance(target, (ast.Tuple, ast.List)): + names: set[str] = set() + for child in target.elts: + names.update(self._target_names(child)) + return names + raise StaticEvalError("unsupported comprehension target") + + def _comprehend( + self, element: ast.expr, generators: list[ast.comprehension], index: int = 0 + ) -> list[StaticValue]: + if index == len(generators): + self._consume_expansion(1) + return [self.visit(element)] + generator = generators[index] + if generator.is_async: + raise StaticEvalError("async comprehensions are forbidden") + iterable = self.visit(generator.iter) + if not isinstance(iterable, tuple): + raise StaticEvalError("comprehension iterable must be statically bounded") + output: list[StaticValue] = [] + for item in iterable: + self._bind_target(generator.target, item) + include = True + for condition_node in generator.ifs: + condition = self.visit(condition_node) + if type(condition) is not bool: + raise StaticEvalError("comprehension condition must be boolean") + if not condition: + include = False + break + if include: + output.extend(self._comprehend(element, generators, index + 1)) + return output + + def _evaluate_comprehension( + self, element: ast.expr, generators: list[ast.comprehension] + ) -> tuple[StaticValue, ...]: + names: set[str] = set() + for generator in generators: + names.update(self._target_names(generator.target)) + missing = object() + previous = {name: self._values.get(name, missing) for name in names} + try: + return tuple(self._comprehend(element, generators)) + finally: + for name, value in previous.items(): + if value is missing: + self._values.pop(name, None) + else: + self._values[name] = value + + def visit_GeneratorExp(self, node: ast.GeneratorExp) -> StaticValue: + return self._evaluate_comprehension(node.elt, node.generators) + + def visit_ListComp(self, node: ast.ListComp) -> StaticValue: + return self._evaluate_comprehension(node.elt, node.generators) + + +def evaluate_static(node: ast.AST, environment: StaticEnvironment) -> StaticValue: + value = _StaticEvaluator(environment).visit(node) + validate_ijson_value(value) + return value diff --git a/components/agentic-circuit/src/agentic_circuit/_types.py b/components/agentic-circuit/src/agentic_circuit/_types.py new file mode 100644 index 00000000..571f5f98 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_types.py @@ -0,0 +1,104 @@ +"""Public annotation categories and frontend-only symbolic values.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Generic, Never, TypeVar + + +T = TypeVar("T") +P = TypeVar("P") +InterfaceT = TypeVar("InterfaceT") +R = TypeVar("R") + + +@dataclass(frozen=True, slots=True) +class ScalarType: + width: int + signed: bool = False + + +u1 = ScalarType(1) +u2 = ScalarType(2) +u4 = ScalarType(4) +u8 = ScalarType(8) +u16 = ScalarType(16) +u32 = ScalarType(32) +u64 = ScalarType(64) +s8 = ScalarType(8, True) +s16 = ScalarType(16, True) +s32 = ScalarType(32, True) +s64 = ScalarType(64, True) + + +class Static(Generic[T]): + """Mark an elaboration-time specialization parameter.""" + + +# The Queue frontend uses the lower-case spelling to make the +# specialization boundary read like a value category rather than a runtime +# container. Keep ``Static`` available for the existing component frontend; +# both annotations have the same runtime origin. +const = Static + + +class Flow(Generic[T, P]): + """Describe a typed logical dataflow edge using protocol ``P``.""" + + +class Endpoint(Generic[InterfaceT, R]): + """Describe interface ``I`` bound in role ``R``.""" + + +@dataclass(frozen=True, slots=True, eq=False) +class SymbolicValue: + """Frontend identity for an architecture value without a Python value.""" + + stable_name: str + annotation: object + + def __repr__(self) -> str: + return f"SymbolicValue({self.stable_name!r})" + + def _reject(self, operation: str) -> Never: + raise TypeError( + f"ACPY-STATIC-002: {self.stable_name!r} cannot be used for {operation}" + ) + + def __bool__(self) -> Never: + return self._reject("truth testing") + + def __int__(self) -> Never: + return self._reject("integer conversion") + + def __hash__(self) -> Never: + return self._reject("hashing") + + def __iter__(self) -> Never: + return self._reject("iteration") + + def __eq__(self, other: object) -> Never: + return self._reject("equality") + + def __ne__(self, other: object) -> Never: + return self._reject("equality") + + +@dataclass(frozen=True, slots=True, eq=False, repr=False) +class ResourceRef(SymbolicValue, Generic[T, R]): + """A typed resource capability bound in a declared role.""" + + role: object + + @property + def resource_type(self) -> object: + return self.annotation + + def __repr__(self) -> str: + return f"ResourceRef({self.stable_name!r})" + + +def _test_symbolic(stable_name: str, annotation: object) -> SymbolicValue: + """Create a symbolic value for contract tests without elaboration state.""" + + return SymbolicValue(stable_name=stable_name, annotation=annotation) diff --git a/components/agentic-circuit/src/agentic_circuit/_validate.py b/components/agentic-circuit/src/agentic_circuit/_validate.py new file mode 100644 index 00000000..20ce4f7d --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_validate.py @@ -0,0 +1,266 @@ +"""Context-sensitive validation for the portable Python subset.""" + +from __future__ import annotations + +import ast +from dataclasses import dataclass + +from ._definitions import DefinitionKind +from ._diagnostics import Diagnostic, DiagnosticBag, SourceSpan +from ._source import DefinitionSite + + +@dataclass(frozen=True, slots=True) +class ValidationContext: + definition_kind: DefinitionKind + static_names: frozenset[str] + symbolic_names: frozenset[str] + approved_helpers: frozenset[str] + + +@dataclass(frozen=True, slots=True) +class ProcessValidationIssue: + code: str + message: str + node: ast.AST + + +def _span(path: str, node: ast.AST) -> SourceSpan: + line = getattr(node, "lineno", 1) + column = getattr(node, "col_offset", 0) + end_line = getattr(node, "end_lineno", line) + end_column = getattr(node, "end_col_offset", column) + 1 + return SourceSpan(path, line, column + 1, end_line, end_column) + + +class _Validator: + _forbidden_calls = { + "eval", + "exec", + "open", + "getattr", + "setattr", + "compile", + "__import__", + } + _forbidden_expressions = ( + ast.Await, + ast.Lambda, + ast.NamedExpr, + ast.Set, + ast.SetComp, + ast.Yield, + ast.YieldFrom, + ) + + def __init__( + self, path: str, context: ValidationContext, diagnostics: DiagnosticBag + ) -> None: + self._path = path + self._context = context + self._diagnostics = diagnostics + + def _error(self, code: str, message: str, node: ast.AST) -> None: + self._diagnostics.add( + Diagnostic( + stage="python-validation", + code=code, + severity="error", + message=message, + source=_span(self._path, node), + ) + ) + + def _symbolic_operand(self, node: ast.AST) -> ast.Name | None: + matches = [ + candidate + for candidate in ast.walk(node) + if isinstance(candidate, ast.Name) + and candidate.id in self._context.symbolic_names + ] + return min(matches, key=lambda item: (item.lineno, item.col_offset), default=None) + + def _require_static_control(self, node: ast.expr) -> None: + operand = self._symbolic_operand(node) + if operand is not None: + self._error( + "ACPY-STATIC-002", + f"symbolic value {operand.id!r} cannot control Python execution", + operand, + ) + self._validate_expression(node) + + def _validate_target(self, node: ast.expr) -> None: + if isinstance(node, ast.Name): + return + if isinstance(node, (ast.Tuple, ast.List)): + for item in node.elts: + self._validate_target(item) + return + self._error("ACPY-SYNTAX-001", "mutation target is not portable", node) + + def _validate_expression(self, node: ast.AST) -> None: + for candidate in ast.walk(node): + if isinstance(candidate, self._forbidden_expressions): + self._error( + "ACPY-SYNTAX-001", + f"{type(candidate).__name__} is not supported", + candidate, + ) + elif isinstance(candidate, ast.Call): + if isinstance(candidate.func, ast.Name): + if candidate.func.id in self._forbidden_calls: + self._error( + "ACPY-SYNTAX-001", + f"call to {candidate.func.id!r} is not portable", + candidate, + ) + elif isinstance(candidate.func, ast.Attribute): + self._error( + "ACPY-SYNTAX-001", + "qualified or reflective call target is not portable", + candidate, + ) + else: + self._error( + "ACPY-SYNTAX-001", "dynamic call target is not portable", candidate + ) + + def validate_statement(self, statement: ast.stmt) -> None: + if isinstance(statement, (ast.Assign, ast.AnnAssign)): + targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target] + for target in targets: + self._validate_target(target) + value = statement.value + if value is not None: + self._validate_expression(value) + return + if isinstance(statement, ast.Expr): + self._validate_expression(statement.value) + return + if isinstance(statement, ast.Return): + if statement.value is not None: + self._validate_expression(statement.value) + return + if isinstance(statement, ast.If): + self._require_static_control(statement.test) + self.validate_statements(statement.body) + self.validate_statements(statement.orelse) + return + if isinstance(statement, ast.For): + self._validate_target(statement.target) + self._require_static_control(statement.iter) + self.validate_statements(statement.body) + self.validate_statements(statement.orelse) + return + if isinstance(statement, ast.With): + valid_scope = all( + isinstance(item.context_expr, ast.Call) + and isinstance(item.context_expr.func, ast.Name) + and item.context_expr.func.id == "scope" + and item.optional_vars is None + for item in statement.items + ) + if not valid_scope: + self._error( + "ACPY-SYNTAX-001", "only with scope(static_name) is supported", statement + ) + for item in statement.items: + self._require_static_control(item.context_expr) + self.validate_statements(statement.body) + return + if isinstance(statement, ast.Assert): + self._require_static_control(statement.test) + if statement.msg is not None: + self._validate_expression(statement.msg) + return + if isinstance(statement, ast.Pass): + return + self._error( + "ACPY-SYNTAX-001", + f"{type(statement).__name__} is not supported in portable definitions", + statement, + ) + + def validate_statements(self, statements: list[ast.stmt]) -> None: + for statement in statements: + self.validate_statement(statement) + + +def validate_definition( + site: DefinitionSite, + context: ValidationContext, + diagnostics: DiagnosticBag, +) -> None: + if isinstance(site.node, ast.AsyncFunctionDef): + diagnostics.add( + Diagnostic( + stage="python-validation", + code="ACPY-SYNTAX-001", + severity="error", + message="async definitions are not portable", + source=site.span, + ) + ) + return + if not isinstance(site.node, ast.FunctionDef): + diagnostics.add( + Diagnostic( + stage="python-validation", + code="ACPY-SYNTAX-001", + severity="error", + message="selected definition must be a function", + source=site.span, + ) + ) + return + _Validator(site.span.file, context, diagnostics).validate_statements(site.node.body) + + +def validate_process_site(site: DefinitionSite) -> tuple[ProcessValidationIssue, ...]: + """Validate syntax that can never be represented by a portable process CFG.""" + + node = site.node + if isinstance(node, ast.AsyncFunctionDef): + return ( + ProcessValidationIssue( + "ACPY-PROCESS-002", + "Python coroutines are not a process runtime mechanism", + node, + ), + ) + if not isinstance(node, ast.FunctionDef): + return ( + ProcessValidationIssue( + "ACPY-PROCESS-002", "a process must decorate a function", node + ), + ) + + issues: list[ProcessValidationIssue] = [] + for candidate in ast.walk(node): + if isinstance(candidate, (ast.Yield, ast.YieldFrom, ast.Await)): + issues.append( + ProcessValidationIssue( + "ACPY-PROCESS-002", + "Python generators and await are not process suspension points", + candidate, + ) + ) + elif isinstance(candidate, (ast.Try, ast.Raise)): + issues.append( + ProcessValidationIssue( + "ACPY-PROCESS-004", + "exception-driven process control is not portable", + candidate, + ) + ) + return tuple( + sorted( + issues, + key=lambda issue: ( + getattr(issue.node, "lineno", 0), + getattr(issue.node, "col_offset", 0), + issue.code, + ), + ) + ) diff --git a/components/agentic-circuit/src/agentic_circuit/_workspace.py b/components/agentic-circuit/src/agentic_circuit/_workspace.py new file mode 100644 index 00000000..3316c592 --- /dev/null +++ b/components/agentic-circuit/src/agentic_circuit/_workspace.py @@ -0,0 +1,284 @@ +"""Closed workspace discovery and TOML configuration.""" + +from __future__ import annotations + +import re +import tomllib +from dataclasses import dataclass +from pathlib import Path +from typing import Literal, NoReturn + +from ._canonical_json import JsonValue, validate_ijson_value +from ._diagnostics import Diagnostic + + +_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$") +_TOP_LEVEL = frozenset( + {"contract_epoch", "project", "providers", "build", "run", "diagnostics"} +) + + +class UserInputError(Exception): + """A stable user-facing configuration or command failure.""" + + def __init__(self, diagnostic: Diagnostic): + super().__init__(diagnostic.message) + self.diagnostic = diagnostic + + +@dataclass(frozen=True, slots=True) +class WorkspaceConfig: + root: Path + project_name: str + project_version: str + contract_epoch: str + architecture: Path + default_system: str + standard_library_providers: tuple[str, ...] + build_profile: Literal["fast", "validated", "custom"] + compiler: str + standard_library: str + component_roots: tuple[Path, ...] + protocol_roots: tuple[Path, ...] + trace_roots: tuple[Path, ...] + build_root: Path + default_trace: Path | None + default_run_inputs: tuple[tuple[str, JsonValue], ...] + diagnostic_format: Literal["text", "json", "jsonl"] + instrumentation_layers: tuple[str, ...] + + +def _fail(code: str, message: str) -> NoReturn: + raise UserInputError( + Diagnostic( + stage="workspace", + code=code, + severity="error", + message=message, + ) + ) + + +def _closed_table( + value: object, + *, + section: str, + allowed: frozenset[str], + required: frozenset[str] = frozenset(), +) -> dict[str, object]: + if type(value) is not dict: + _fail("ACPY-CONFIG-002", f"[{section}] must be a table") + table: dict[str, object] = value + unknown = sorted(set(table) - allowed) + missing = sorted(required - set(table)) + if unknown: + _fail( + "ACPY-CONFIG-002", + f"[{section}] contains unknown key {unknown[0]!r}", + ) + if missing: + _fail( + "ACPY-CONFIG-002", + f"[{section}] is missing required key {missing[0]!r}", + ) + return table + + +def _string(table: dict[str, object], key: str, section: str) -> str: + value = table[key] + if type(value) is not str or not value: + _fail("ACPY-CONFIG-002", f"[{section}].{key} must be a non-empty string") + return value + + +def _string_list( + table: dict[str, object], key: str, section: str +) -> tuple[str, ...]: + value = table[key] + if type(value) is not list or not all(type(item) is str and item for item in value): + _fail( + "ACPY-CONFIG-002", f"[{section}].{key} must be an array of strings" + ) + result = tuple(value) + if len(set(result)) != len(result): + _fail("ACPY-CONFIG-004", f"[{section}].{key} contains a duplicate") + return result + + +def _workspace_path(root: Path, value: str, label: str) -> Path: + candidate = Path(value) + if candidate.is_absolute(): + _fail("ACPY-CONFIG-003", f"{label} must be workspace-relative") + resolved = (root / candidate).resolve() + if not resolved.is_relative_to(root): + _fail("ACPY-CONFIG-003", f"{label} escapes the workspace") + return resolved + + +def _workspace_paths( + root: Path, table: dict[str, object], key: str, section: str +) -> tuple[Path, ...]: + return tuple( + _workspace_path(root, value, f"[{section}].{key}") + for value in _string_list(table, key, section) + ) + + +def load_workspace(manifest: Path) -> WorkspaceConfig: + """Load exactly one manifest without current-directory discovery.""" + + manifest = manifest.resolve() + if not manifest.is_file(): + _fail("ACPY-CONFIG-001", f"workspace manifest not found: {manifest}") + try: + with manifest.open("rb") as source: + document = tomllib.load(source) + except (OSError, tomllib.TOMLDecodeError) as error: + _fail("ACPY-CONFIG-002", f"workspace manifest is invalid: {error}") + unknown = sorted(set(document) - _TOP_LEVEL) + missing = sorted(_TOP_LEVEL - set(document)) + if unknown: + _fail("ACPY-CONFIG-002", f"workspace contains unknown section {unknown[0]!r}") + if missing: + _fail("ACPY-CONFIG-002", f"workspace is missing {missing[0]!r}") + + epoch = document["contract_epoch"] + if epoch != "0.3": + _fail("ACPY-CONFIG-005", "contract_epoch must equal 0.3") + root = manifest.parent.resolve() + + project = _closed_table( + document["project"], + section="project", + allowed=frozenset({"name", "version", "architecture", "system"}), + required=frozenset({"name", "version", "architecture", "system"}), + ) + project_name = _string(project, "name", "project") + project_version = _string(project, "version", "project") + default_system = _string(project, "system", "project") + if not _NAME.fullmatch(project_name) or not _NAME.fullmatch(default_system): + _fail("ACPY-CONFIG-002", "project and system names use invalid syntax") + architecture = _workspace_path( + root, _string(project, "architecture", "project"), "[project].architecture" + ) + + providers = _closed_table( + document["providers"], + section="providers", + allowed=frozenset({"standard_library"}), + required=frozenset({"standard_library"}), + ) + provider_names = _string_list(providers, "standard_library", "providers") + if any(name != "ac" for name in provider_names): + _fail("ACPY-CONFIG-006", "unknown standard-library provider") + + build = _closed_table( + document["build"], + section="build", + allowed=frozenset( + { + "profile", + "compiler", + "standard_library", + "component_roots", + "protocol_roots", + "build_root", + "instrumentation_layers", + } + ), + required=frozenset( + { + "profile", + "compiler", + "standard_library", + "component_roots", + "protocol_roots", + "build_root", + "instrumentation_layers", + } + ), + ) + profile = _string(build, "profile", "build") + if profile not in ("fast", "validated", "custom"): + _fail("ACPY-CONFIG-002", "[build].profile is not supported") + compiler = _string(build, "compiler", "build") + standard_library = _string(build, "standard_library", "build") + if standard_library not in ("libc++", "libstdc++"): + _fail("ACPY-CONFIG-002", "[build].standard_library is not supported") + component_roots = _workspace_paths(root, build, "component_roots", "build") + protocol_roots = _workspace_paths(root, build, "protocol_roots", "build") + if set(component_roots) & set(protocol_roots): + _fail("ACPY-CONFIG-004", "component and protocol roots overlap") + build_root = _workspace_path( + root, _string(build, "build_root", "build"), "[build].build_root" + ) + instrumentation = _string_list(build, "instrumentation_layers", "build") + + run = _closed_table( + document["run"], + section="run", + allowed=frozenset({"trace_roots", "default_trace", "inputs"}), + required=frozenset({"trace_roots", "inputs"}), + ) + trace_roots = _workspace_paths(root, run, "trace_roots", "run") + default_trace = ( + _workspace_path( + root, + _string(run, "default_trace", "run"), + "[run].default_trace", + ) + if "default_trace" in run + else None + ) + inputs = run["inputs"] + if type(inputs) is not dict or not all(type(key) is str for key in inputs): + _fail("ACPY-CONFIG-002", "[run].inputs must be a string-keyed table") + try: + validate_ijson_value(inputs) + except ValueError as error: + _fail("ACPY-CONFIG-002", f"[run].inputs is invalid: {error}") + default_run_inputs = tuple(sorted(inputs.items())) + + diagnostics = _closed_table( + document["diagnostics"], + section="diagnostics", + allowed=frozenset({"format"}), + required=frozenset({"format"}), + ) + diagnostic_format = _string(diagnostics, "format", "diagnostics") + if diagnostic_format not in ("text", "json", "jsonl"): + _fail("ACPY-CONFIG-002", "[diagnostics].format is not supported") + + return WorkspaceConfig( + root=root, + project_name=project_name, + project_version=project_version, + contract_epoch="0.3", + architecture=architecture, + default_system=default_system, + standard_library_providers=provider_names, + build_profile=profile, + compiler=compiler, + standard_library=standard_library, + component_roots=component_roots, + protocol_roots=protocol_roots, + trace_roots=trace_roots, + build_root=build_root, + default_trace=default_trace, + default_run_inputs=default_run_inputs, + diagnostic_format=diagnostic_format, + instrumentation_layers=instrumentation, + ) + + +def discover_workspace(start: Path) -> WorkspaceConfig: + """Search from *start* upward for the nearest workspace manifest.""" + + resolved = start.resolve() + if resolved.is_file(): + resolved = resolved.parent + for directory in (resolved, *resolved.parents): + candidate = directory / "agentic-circuit.toml" + if candidate.is_file(): + return load_workspace(candidate) + _fail("ACPY-CONFIG-001", "workspace configuration not found") diff --git a/components/agentic-circuit/test/ACIR/address-map-canonical-order.mlir b/components/agentic-circuit/test/ACIR/address-map-canonical-order.mlir new file mode 100644 index 00000000..ab7c277a --- /dev/null +++ b/components/agentic-circuit/test/ACIR/address-map-canonical-order.mlir @@ -0,0 +1,42 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt_public %t/a.mlir > %t/a.out +// RUN: %acir_opt_public %t/b.mlir > %t/b.out +// RUN: diff %t/a.out %t/b.out +// RUN: %acir_opt_public %t/a.out > %t/a.roundtrip +// RUN: diff %t/a.out %t/a.roundtrip + +//--- a.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.address_space @source width 8 unit "byte" id "source" path "source" + ac.address_space @target_a width 8 unit "byte" id "target_a" path "target_a" + ac.address_space @target_b width 8 unit "byte" id "target_b" path "target_b" + ac.address_map @map source @source entries [ + {base = 0 : i64, size = 16 : i64, target = @target_b, offset = 8 : i64, + permissions = ["execute", "write"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 1 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @target_a, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}} + ] default {kind = "unmapped"} + ac.return + } +} + +//--- b.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.address_space @source width 8 unit "byte" id "source" path "source" + ac.address_space @target_a width 8 unit "byte" id "target_a" path "target_a" + ac.address_space @target_b width 8 unit "byte" id "target_b" path "target_b" + ac.address_map @map source @source entries [ + {base = 0 : i64, size = 16 : i64, target = @target_a, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @target_b, offset = 8 : i64, + permissions = ["write", "execute"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 1 : i64}} + ] default {kind = "unmapped"} + ac.return + } +} diff --git a/components/agentic-circuit/test/ACIR/address-selectors-valid.mlir b/components/agentic-circuit/test/ACIR/address-selectors-valid.mlir new file mode 100644 index 00000000..0927cd56 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/address-selectors-valid.mlir @@ -0,0 +1,57 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "A", fields = [{name = "tag", type = i8}]}> : () -> () + "ac.transaction"() <{sym_name = "B", fields = [{name = "tag", type = i8}]}> : () -> () + }) : () -> () + ac.module @M() parameters {} graph { + ac.address_space @space width 8 unit "byte" id "space" path "space" + ac.address_map @class_split source @space entries [ + {base = 0 : i64, size = 32 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [@types::@A]}, + {base = 0 : i64, size = 32 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [@types::@B]} + ] default {kind = "unmapped"} + ac.address_map @mixed_disjoint source @space entries [ + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 2 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}} + ] default {kind = "unmapped"} + ac.address_map @mixed_priority source @space entries [ + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], priority = 2 : i64, + interleave = {granularity = 2 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], priority = 1 : i64, + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 1 : i64}} + ] default {kind = "unmapped"} + ac.address_map @general_fast_forward source @space entries [ + {base = 24 : i64, size = 62 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 2 : i64, banks = 7 : i64, bank = 0 : i64}}, + {base = 48 : i64, size = 98 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 8 : i64, banks = 8 : i64, bank = 5 : i64}} + ] default {kind = "unmapped"} + ac.address_map @general_fast_reverse source @space entries [ + {base = 48 : i64, size = 98 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 8 : i64, banks = 8 : i64, bank = 5 : i64}}, + {base = 24 : i64, size = 62 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 2 : i64, banks = 7 : i64, bank = 0 : i64}} + ] default {kind = "unmapped"} + ac.return + } +} + +// CHECK: ac.address_map @class_split +// CHECK: ac.address_map @mixed_disjoint +// CHECK: ac.address_map @mixed_priority +// CHECK: ac.address_map @general_fast_forward +// CHECK: ac.address_map @general_fast_reverse diff --git a/components/agentic-circuit/test/ACIR/address-time-invalid.mlir b/components/agentic-circuit/test/ACIR/address-time-invalid.mlir new file mode 100644 index 00000000..9ba99722 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/address-time-invalid.mlir @@ -0,0 +1,600 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/period-zero.mlir 2>&1 | %FileCheck %s --check-prefix=PERIOD +// RUN: %not %acir_opt %t/phase-negative.mlir 2>&1 | %FileCheck %s --check-prefix=PHASE +// RUN: %not %acir_opt %t/tick-overflow.mlir 2>&1 | %FileCheck %s --check-prefix=TICK-OVERFLOW +// RUN: %not %acir_opt %t/domain-cycle.mlir 2>&1 | %FileCheck %s --check-prefix=DOMAIN-CYCLE +// RUN: %not %acir_opt %t/missing-bridge.mlir 2>&1 | %FileCheck %s --check-prefix=BRIDGE +// RUN: %not %acir_opt %t/width.mlir 2>&1 | %FileCheck %s --check-prefix=WIDTH +// RUN: %not %acir_opt %t/address-cycle.mlir 2>&1 | %FileCheck %s --check-prefix=ADDRESS-CYCLE +// RUN: %not %acir_opt %t/range-overflow.mlir 2>&1 | %FileCheck %s --check-prefix=RANGE +// RUN: %not %acir_opt %t/overlap.mlir 2>&1 | %FileCheck %s --check-prefix=OVERLAP +// RUN: %not %acir_opt %t/overlap.mlir > /dev/null 2> %t/overlap.first +// RUN: %not %acir_opt %t/overlap.mlir > /dev/null 2> %t/overlap.second +// RUN: diff %t/overlap.first %t/overlap.second +// RUN: %not %acir_opt %t/equal-priority.mlir 2>&1 | %FileCheck %s --check-prefix=PRIORITY +// RUN: %not %acir_opt %t/interleave.mlir 2>&1 | %FileCheck %s --check-prefix=INTERLEAVE +// RUN: %not %acir_opt %t/default.mlir 2>&1 | %FileCheck %s --check-prefix=DEFAULT +// RUN: %not %acir_opt %t/address-segment.mlir 2>&1 | %FileCheck %s --check-prefix=ADDRESS-SEGMENT +// RUN: %not %acir_opt %t/address-unit.mlir 2>&1 | %FileCheck %s --check-prefix=ADDRESS-UNIT +// RUN: %not %acir_opt %t/address-layout.mlir 2>&1 | %FileCheck %s --check-prefix=ADDRESS-LAYOUT +// RUN: %not %acir_opt %t/address-parent-pair.mlir 2>&1 | %FileCheck %s --check-prefix=PARENT-PAIR +// RUN: %not %acir_opt %t/address-parent.mlir 2>&1 | %FileCheck %s --check-prefix=PARENT +// RUN: %not %acir_opt %t/address-translation.mlir 2>&1 | %FileCheck %s --check-prefix=TRANSLATION +// RUN: %not %acir_opt %t/address-width-translation.mlir 2>&1 | %FileCheck %s --check-prefix=TRANSLATED-WIDTH +// RUN: %not %acir_opt %t/map-name.mlir 2>&1 | %FileCheck %s --check-prefix=MAP-NAME +// RUN: %not %acir_opt %t/map-source.mlir 2>&1 | %FileCheck %s --check-prefix=MAP-SOURCE +// RUN: %not %acir_opt %t/source-width.mlir 2>&1 | %FileCheck %s --check-prefix=SOURCE-WIDTH +// RUN: %not %acir_opt %t/target-width.mlir 2>&1 | %FileCheck %s --check-prefix=TARGET-WIDTH +// RUN: %not %acir_opt %t/permissions.mlir 2>&1 | %FileCheck %s --check-prefix=PERMISSIONS +// RUN: %not %acir_opt %t/interleave-stripe.mlir 2>&1 | %FileCheck %s --check-prefix=INTERLEAVE-STRIPE +// RUN: %not %acir_opt %t/default-target.mlir 2>&1 | %FileCheck %s --check-prefix=DEFAULT-TARGET +// RUN: %not %acir_opt %t/time-name.mlir 2>&1 | %FileCheck %s --check-prefix=TIME-NAME +// RUN: %not %acir_opt %t/time-parent.mlir 2>&1 | %FileCheck %s --check-prefix=TIME-PARENT +// RUN: %not %acir_opt %t/bridge-schema.mlir 2>&1 | %FileCheck %s --check-prefix=BRIDGE-SCHEMA +// RUN: %not %acir_opt %t/address-self.mlir 2>&1 | %FileCheck %s --check-prefix=ADDRESS-SELF +// RUN: %not %acir_opt %t/map-target.mlir 2>&1 | %FileCheck %s --check-prefix=MAP-TARGET +// RUN: %not %acir_opt %t/map-key.mlir 2>&1 | %FileCheck %s --check-prefix=MAP-KEY +// RUN: %not %acir_opt %t/map-unknown-key.mlir 2>&1 | %FileCheck %s --check-prefix=MAP-UNKNOWN-KEY +// RUN: %not %acir_opt %t/map-class.mlir 2>&1 | %FileCheck %s --check-prefix=MAP-CLASS +// RUN: %not %acir_opt %t/target-overflow.mlir 2>&1 | %FileCheck %s --check-prefix=TARGET-OVERFLOW +// RUN: %not %acir_opt %t/time-self.mlir 2>&1 | %FileCheck %s --check-prefix=TIME-SELF +// RUN: %not %acir_opt %t/noncanonical-rational.mlir 2>&1 | %FileCheck %s --check-prefix=RATIONAL +// RUN: %not %acir_opt %t/fractional-no-alignment.mlir 2>&1 | %FileCheck %s --check-prefix=NO-ALIGNMENT +// RUN: %not %acir_opt %t/fractional-bad-alignment.mlir 2>&1 | %FileCheck %s --check-prefix=BAD-ALIGNMENT +// RUN: %not %acir_opt %t/zero-size.mlir 2>&1 | %FileCheck %s --check-prefix=ZERO-SIZE +// RUN: %not %acir_opt %t/interleave-alignment.mlir 2>&1 | %FileCheck %s --check-prefix=INTERLEAVE-ALIGNMENT +// RUN: %not %acir_opt %t/mixed-interleave-overlap.mlir 2>&1 | %FileCheck %s --check-prefix=MIXED-INTERLEAVE-OVERLAP +// RUN: %not %acir_opt %t/relation-limit.mlir 2>&1 | %FileCheck %s --check-prefix=RELATION-LIMIT +// RUN: %not %acir_opt %t/different-granularity-overlap.mlir 2>&1 | %FileCheck %s --check-prefix=DIFFERENT-GRANULARITY-OVERLAP + +//--- period-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.time_domain"() <{sym_name = "t", period = 0 : i64, phase = 0 : i64, tick_scale = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PERIOD: period must be positive global ticks + +//--- phase-negative.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.time_domain"() <{sym_name = "t", period = 1 : i64, phase = -1 : i64, tick_scale = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PHASE: phase must be non-negative global ticks + +//--- tick-overflow.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.time_domain"() <{sym_name = "t", period = 9223372036854775807 : i64, phase = 1 : i64, tick_scale = 4294967297 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TICK-OVERFLOW: tick scale exceeds implementation capability + +//--- domain-cycle.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Bridge", function_type = () -> (), static_params = {}}> ({ + "ac.return"() : () -> () + }) : () -> () + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Bridge, sym_name = "x", stable_id = "x", path = "x", static_args = {}}> : () -> () + "ac.time_domain"() <{sym_name = "a", period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64, parent = @b, bridge = {kind = "explicit", owner = @x}}> : () -> () + "ac.time_domain"() <{sym_name = "b", period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64, parent = @a, bridge = {kind = "explicit", owner = @x}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DOMAIN-CYCLE: time-domain parent cycle + +//--- missing-bridge.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.time_domain"() <{sym_name = "a", period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64}> : () -> () + "ac.time_domain"() <{sym_name = "b", period = 2 : i64, phase = 0 : i64, tick_scale = 1 : i64, parent = @a}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// BRIDGE: cross-domain parent relation requires explicit bridge metadata + +//--- width.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 65 : i64, address_unit = "byte"}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// WIDTH: address width must be in [1, 64] + +//--- address-cycle.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte", parent = @b, translation = {numerator = 1 : i64, denominator = 1 : i64, offset = 0 : i64}}> : () -> () + "ac.address_space"() <{sym_name = "b", stable_id = "b", path = "b", address_width = 32 : i64, address_unit = "byte", parent = @a, translation = {numerator = 1 : i64, denominator = 1 : i64, offset = 0 : i64}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ADDRESS-CYCLE: address-space parent cycle + +//--- range-overflow.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 64 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = -1 : i64, size = 2 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = []}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// RANGE: address interval exceeds source address width + +//--- overlap.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 8 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = []}, {base = 4 : i64, size = 8 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = []}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// OVERLAP: overlapping selector intersections require explicit distinct priorities + +//--- equal-priority.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 8 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = [], priority = 1 : i64}, {base = 4 : i64, size = 8 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = [], priority = 1 : i64}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PRIORITY: overlapping selector intersections have equal priority + +//--- interleave.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 256 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = [], interleave = {granularity = 64 : i64, banks = 4 : i64, bank = 4 : i64}}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// INTERLEAVE: interleave bank must be in [0, banks) + +//--- default.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [], default_behavior = {kind = "drop"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DEFAULT: default behavior requires exact unmapped or target schema + +//--- address-segment.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a.b", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ADDRESS-SEGMENT: address-space name, stable id, and path must be stable local segments + +//--- address-unit.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "word"}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ADDRESS-UNIT: address unit must be exactly 'byte' or 'bit' + +//--- address-layout.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte", data_layout = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ADDRESS-LAYOUT: data layout hook must implement DataLayoutSpecInterface + +//--- address-parent-pair.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte", parent = @b}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PARENT-PAIR: parent address space and translation must appear together + +//--- address-parent.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte", parent = @missing, translation = {numerator = 1 : i64, denominator = 1 : i64, offset = 0 : i64}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PARENT: parent address space '@missing' is unresolved + +//--- address-translation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "parent", stable_id = "parent", path = "parent", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_space"() <{sym_name = "child", stable_id = "child", path = "child", address_width = 16 : i64, address_unit = "byte", parent = @parent, translation = {numerator = 0 : i64, denominator = 1 : i64, offset = 0 : i64}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TRANSLATION: translation values must be unsigned signless i64 with positive ratio + +//--- address-width-translation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "parent", stable_id = "parent", path = "parent", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_space"() <{sym_name = "child", stable_id = "child", path = "child", address_width = 8 : i64, address_unit = "byte", parent = @parent, translation = {numerator = 2 : i64, denominator = 1 : i64, offset = 0 : i64}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TRANSLATED-WIDTH: translated child address range exceeds parent address width + +//--- map-name.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_map"() <{sym_name = "bad.name", source = @a, entries = [], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MAP-NAME: address-map name must be one stable local segment + +//--- map-source.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_map"() <{sym_name = "m", source = @missing, entries = [], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MAP-SOURCE: source address space '@missing' is unresolved + +//--- source-width.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 4 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 8 : i64, size = 9 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = []}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// SOURCE-WIDTH: address interval exceeds source address width + +//--- target-width.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "source", stable_id = "source", path = "source", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_space"() <{sym_name = "target", stable_id = "target", path = "target", address_width = 4 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @source, entries = [{base = 0 : i64, size = 2 : i64, target = @target, offset = 15 : i64, permissions = ["read"], classes = []}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TARGET-WIDTH: address target range exceeds target address width + +//--- permissions.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 1 : i64, target = @a, offset = 0 : i64, permissions = ["write", "write"], classes = []}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PERMISSIONS: address-map permissions must be unique read/write/execute values + +//--- interleave-stripe.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 16 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 255 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = [], interleave = {granularity = 2 : i64, banks = -1 : i64, bank = 0 : i64}}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// INTERLEAVE-STRIPE: interleave geometry exceeds unsigned i64 + +//--- default-target.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [], default_behavior = {kind = "target", target = @missing}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DEFAULT-TARGET: default target behavior is unresolved + +//--- time-name.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.time_domain"() <{sym_name = "bad.name", period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TIME-NAME: time-domain name must be one stable local segment + +//--- time-parent.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Bridge", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Bridge, sym_name = "x", stable_id = "x", path = "x", static_args = {}}> : () -> () + "ac.time_domain"() <{sym_name = "t", period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64, parent = @missing, bridge = {kind = "explicit", owner = @x}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TIME-PARENT: parent time domain '@missing' is unresolved + +//--- bridge-schema.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Bridge", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Bridge, sym_name = "x", stable_id = "x", path = "x", static_args = {}}> : () -> () + "ac.time_domain"() <{sym_name = "parent", period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64}> : () -> () + "ac.time_domain"() <{sym_name = "child", period = 2 : i64, phase = 0 : i64, tick_scale = 1 : i64, parent = @parent, bridge = {kind = "implicit", owner = @x}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// BRIDGE-SCHEMA: bridge requires exact {kind = "explicit", owner = local symbol} schema + +//--- address-self.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte", parent = @a, translation = {numerator = 1 : i64, denominator = 1 : i64, offset = 0 : i64}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ADDRESS-SELF: address-space parent cycle: @a -> @a + +//--- map-target.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 1 : i64, target = @missing, offset = 0 : i64, permissions = ["read"], classes = []}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MAP-TARGET: address-map target '@missing' is unresolved + +//--- map-key.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 1 : i64, target = @a, offset = 0 : i64, permissions = ["read"]}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MAP-KEY: address-map entry is missing 'classes' + +//--- map-unknown-key.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 1 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = [], extra = true}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MAP-UNKNOWN-KEY: unknown address-map entry key 'extra' + +//--- map-class.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 32 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 1 : i64, target = @a, offset = 0 : i64, permissions = ["read"], classes = [@missing]}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MAP-CLASS: address-map transaction class '@missing' is unresolved + +//--- target-overflow.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 64 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 0 : i64, size = 2 : i64, target = @a, offset = -1 : i64, permissions = ["read"], classes = []}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TARGET-OVERFLOW: address target range exceeds target address width + +//--- time-self.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Bridge", function_type = () -> (), static_params = {}}> ({ + "ac.return"() : () -> () + }) : () -> () + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Bridge, sym_name = "x", stable_id = "x", path = "x", static_args = {}}> : () -> () + "ac.time_domain"() <{sym_name = "t", period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64, parent = @t, bridge = {kind = "explicit", owner = @x}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TIME-SELF: time-domain parent cycle: @t -> @t + +//--- noncanonical-rational.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "parent", stable_id = "parent", path = "parent", address_width = 16 : i64, address_unit = "byte"}> : () -> () + "ac.address_space"() <{sym_name = "child", stable_id = "child", path = "child", address_width = 8 : i64, address_unit = "byte", parent = @parent, translation = {numerator = 2 : i64, denominator = 2 : i64, offset = 0 : i64}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// RATIONAL: translation rational must be in canonical reduced form + +//--- fractional-no-alignment.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "parent", stable_id = "parent", path = "parent", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_space"() <{sym_name = "child", stable_id = "child", path = "child", address_width = 8 : i64, address_unit = "byte", parent = @parent, translation = {numerator = 1 : i64, denominator = 2 : i64, offset = 0 : i64}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// NO-ALIGNMENT: fractional translation requires exact positive alignment proving divisibility + +//--- fractional-bad-alignment.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "parent", stable_id = "parent", path = "parent", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_space"() <{sym_name = "child", stable_id = "child", path = "child", address_width = 8 : i64, address_unit = "byte", parent = @parent, translation = {numerator = 1 : i64, denominator = 2 : i64, offset = 0 : i64, alignment = 1 : i64}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// BAD-ALIGNMENT: fractional translation requires exact positive alignment proving divisibility + +//--- zero-size.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 64 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = -1 : i64, size = 0 : i64, target = @a, offset = -1 : i64, permissions = ["read"], classes = []}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ZERO-SIZE: address-map entry size must be positive + +//--- interleave-alignment.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "a", stable_id = "a", path = "a", address_width = 16 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @a, entries = [{base = 1 : i64, size = 256 : i64, target = @a, offset = 1 : i64, permissions = ["read"], classes = [], interleave = {granularity = 64 : i64, banks = 4 : i64, bank = 0 : i64}}], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// INTERLEAVE-ALIGNMENT: interleave offset must be aligned to granularity + +//--- mixed-interleave-overlap.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "space", stable_id = "space", path = "space", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @space, entries = [ + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}} + ], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MIXED-INTERLEAVE-OVERLAP: overlapping selector intersections require explicit distinct priorities + +//--- different-granularity-overlap.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "space", stable_id = "space", path = "space", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @space, entries = [ + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 2 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 1 : i64}} + ], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DIFFERENT-GRANULARITY-OVERLAP: overlapping selector intersections require explicit distinct priorities + +//--- relation-limit.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.address_space"() <{sym_name = "space", stable_id = "space", path = "space", address_width = 8 : i64, address_unit = "byte"}> : () -> () + "ac.address_map"() <{sym_name = "m", source = @space, entries = [ + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 2 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}}, + {base = 0 : i64, size = 16 : i64, target = @space, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 2 : i64}} + ], default_behavior = {kind = "unmapped"}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// RELATION-LIMIT: general mixed interleave analysis exceeds ACIR limit 256 diff --git a/components/agentic-circuit/test/ACIR/address-time-valid.mlir b/components/agentic-circuit/test/ACIR/address-time-valid.mlir new file mode 100644 index 00000000..d08564a9 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/address-time-valid.mlir @@ -0,0 +1,63 @@ +// RUN: %acir_opt_public %s | %FileCheck %s +// RUN: %acir_opt_public %s | %acir_opt_public | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Bridge() parameters {} graph { ac.return } + ac.module @Top() parameters {} graph { + ac.instance @cdc of @Bridge() static {} id "cdc" path "cdc" : () -> () + ac.time_domain @global period 1 phase 0 scale 1 + ac.time_domain @core period 2 phase 1 scale 2 parent @global + bridge {kind = "explicit", owner = @cdc} + ac.time_domain @late period 9223372036854775807 phase 9223372036854775807 scale 2 + ac.address_space @physical width 48 unit "byte" id "physical" path "physical" + layout #dlti.dl_spec<> + ac.address_space @virtual width 32 unit "byte" id "virtual" path "virtual" + parent @physical translate {numerator = 1 : i64, denominator = 1 : i64, offset = 4096 : i64} + ac.address_space @bits width 19 unit "bit" id "bits" path "bits" + ac.address_space @bytes width 16 unit "byte" id "bytes" path "bytes" + parent @bits translate {numerator = 8 : i64, denominator = 1 : i64, offset = 0 : i64} + ac.address_space @small width 8 unit "byte" id "small" path "small" + ac.address_space @wide width 16 unit "byte" id "wide" path "wide" + parent @small translate {numerator = 1 : i64, denominator = 256 : i64, + offset = 0 : i64, alignment = 256 : i64} + ac.address_space @half width 7 unit "byte" id "half" path "half" + ac.address_space @aligned width 8 unit "byte" id "aligned" path "aligned" + parent @half translate {numerator = 1 : i64, denominator = 2 : i64, + offset = 0 : i64, alignment = 2 : i64} + ac.address_space @full width 64 unit "byte" id "full" path "full" + ac.address_map @map source @virtual entries [ + {base = 0 : i64, size = 4096 : i64, target = @physical, offset = 0 : i64, + permissions = ["read", "write"], classes = [], priority = 1 : i64}, + {base = 4096 : i64, size = 4096 : i64, target = @physical, offset = 8192 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 64 : i64, banks = 4 : i64, bank = 0 : i64}} + ] default {kind = "unmapped"} + ac.address_map @upper source @full entries [ + {base = -9223372036854775808 : i64, size = -9223372036854775808 : i64, + target = @full, offset = 0 : i64, permissions = ["read"], classes = [], + priority = -1 : i64} + ] default {kind = "unmapped"} + ac.address_map @permission_split source @small entries [ + {base = 0 : i64, size = 16 : i64, target = @small, offset = 0 : i64, + permissions = ["read"], classes = []}, + {base = 0 : i64, size = 16 : i64, target = @small, offset = 0 : i64, + permissions = ["write"], classes = []} + ] default {kind = "unmapped"} + ac.address_space @bank_target width 6 unit "byte" id "bank_target" path "bank_target" + ac.address_map @bank_split source @small entries [ + {base = 0 : i64, size = 256 : i64, target = @bank_target, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 0 : i64}}, + {base = 0 : i64, size = 256 : i64, target = @bank_target, offset = 0 : i64, + permissions = ["read"], classes = [], + interleave = {granularity = 1 : i64, banks = 4 : i64, bank = 1 : i64}} + ] default {kind = "unmapped"} + ac.return + } +} + +// CHECK: ac.time_domain @core +// CHECK: ac.address_space @virtual +// CHECK: ac.address_map @map +// CHECK: ac.address_map @upper +// CHECK: ac.address_map @bank_split diff --git a/components/agentic-circuit/test/ACIR/barrier-invalid.mlir b/components/agentic-circuit/test/ACIR/barrier-invalid.mlir new file mode 100644 index 00000000..fac771dd --- /dev/null +++ b/components/agentic-circuit/test/ACIR/barrier-invalid.mlir @@ -0,0 +1,41 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/arity.mlir 2>&1 | %FileCheck %s --check-prefix=ARITY +// RUN: %not %acir_opt %t/type.mlir 2>&1 | %FileCheck %s --check-prefix=TYPE +// RUN: %not %acir_opt %t/duplicate.mlir 2>&1 | %FileCheck %s --check-prefix=DUPLICATE +// RUN: %not %acir_opt %t/depth.mlir 2>&1 | %FileCheck %s --check-prefix=DEPTH + +// ARITY: error: 'ac.barrier' op requires at least two input queues +// TYPE: error: 'ac.barrier' op output queue 1 must match its input queue type +// DUPLICATE: error: 'ac.barrier' op input queue operands must be unique +// DEPTH: error: 'ac.barrier' op output depths must match results and be positive + +//--- arity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.barrier %input depths [1] latencies [1] + : (!ac.queue) -> (!ac.queue) +} + +//--- type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %left = ac.source depth 1 latency 1 : !ac.queue + %right = ac.source depth 1 latency 1 : !ac.queue + %left_ready, %bad = ac.barrier %left, %right depths [1, 1] latencies [1, 1] + : (!ac.queue, !ac.queue) -> (!ac.queue, !ac.queue) +} + +//--- duplicate.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %left, %right = ac.barrier %input, %input depths [1, 1] latencies [1, 1] + : (!ac.queue, !ac.queue) -> (!ac.queue, !ac.queue) +} + +//--- depth.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %left = ac.source depth 1 latency 1 : !ac.queue + %right = ac.source depth 1 latency 1 : !ac.queue + %left_ready, %right_ready = ac.barrier %left, %right + depths [1] latencies [1, 1] + : (!ac.queue, !ac.queue) -> (!ac.queue, !ac.queue) +} diff --git a/components/agentic-circuit/test/ACIR/barrier.mlir b/components/agentic-circuit/test/ACIR/barrier.mlir new file mode 100644 index 00000000..4e3a66de --- /dev/null +++ b/components/agentic-circuit/test/ACIR/barrier.mlir @@ -0,0 +1,18 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %left = ac.source depth 1 latency 1 : !ac.queue + %right = ac.source depth 1 latency 1 : !ac.queue + %left_ready, %right_ready = ac.barrier %left, %right + depths [2, 3] latencies [1, 1] + : (!ac.queue, !ac.queue) + -> (!ac.queue, !ac.queue) + ac.sink %left_ready : !ac.queue + ac.sink %right_ready : !ac.queue +} + +// CHECK: ac.barrier +// CHECK: depths [2, 3] latencies [1, 1] diff --git a/components/agentic-circuit/test/ACIR/broadcast-invalid.mlir b/components/agentic-circuit/test/ACIR/broadcast-invalid.mlir new file mode 100644 index 00000000..2180a47d --- /dev/null +++ b/components/agentic-circuit/test/ACIR/broadcast-invalid.mlir @@ -0,0 +1,34 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/one-output.mlir 2>&1 | %FileCheck %s --check-prefix=ONE-OUTPUT +// RUN: %not %acir_opt %t/type.mlir 2>&1 | %FileCheck %s --check-prefix=TYPE +// RUN: %not %acir_opt %t/depth.mlir 2>&1 | %FileCheck %s --check-prefix=DEPTH +// RUN: %not %acir_opt %t/latency.mlir 2>&1 | %FileCheck %s --check-prefix=LATENCY + +// ONE-OUTPUT: error: 'ac.broadcast' op requires at least two output queues +// TYPE: error: 'ac.broadcast' op output queue 1 must match input queue type +// DEPTH: error: 'ac.broadcast' op output depths must be positive +// LATENCY: error: 'ac.broadcast' op output latencies must be positive + +//--- one-output.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %only = ac.broadcast %input depths [1] latencies [1] : !ac.queue -> (!ac.queue) +} + +//--- type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %left, %right = ac.broadcast %input depths [1, 1] latencies [1, 1] : !ac.queue -> (!ac.queue, !ac.queue) +} + +//--- depth.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %left, %right = ac.broadcast %input depths [1, 0] latencies [1, 1] : !ac.queue -> (!ac.queue, !ac.queue) +} + +//--- latency.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %left, %right = ac.broadcast %input depths [1, 1] latencies [1, 0] : !ac.queue -> (!ac.queue, !ac.queue) +} diff --git a/components/agentic-circuit/test/ACIR/broadcast.mlir b/components/agentic-circuit/test/ACIR/broadcast.mlir new file mode 100644 index 00000000..25e39c94 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/broadcast.mlir @@ -0,0 +1,13 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %left, %right = ac.broadcast %input depths [1, 1] latencies [1, 1] : !ac.queue -> (!ac.queue, !ac.queue) + ac.sink %left : !ac.queue + ac.sink %right : !ac.queue +} + +// CHECK: %[[BROADCAST:.*]]:2 = ac.broadcast %[[INPUT:.*]] depths [1, 1] latencies [1, 1] +// CHECK: ac.sink %[[BROADCAST]]#0 +// CHECK: ac.sink %[[BROADCAST]]#1 diff --git a/components/agentic-circuit/test/ACIR/canonical-entrypoint.mlir b/components/agentic-circuit/test/ACIR/canonical-entrypoint.mlir new file mode 100644 index 00000000..ab2484bb --- /dev/null +++ b/components/agentic-circuit/test/ACIR/canonical-entrypoint.mlir @@ -0,0 +1,69 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt %t/generic.mlir > /dev/null +// RUN: %not %acir_opt_public %t/generic.mlir 2>&1 | %FileCheck %s --check-prefix=GENERIC +// RUN: %acir_opt_public %t/canonical.mlir | %FileCheck %s --check-prefix=CANONICAL +// RUN: %acir_opt %t/canonical.mlir --emit-bytecode -o %t/canonical.mlirbc +// RUN: %acir_opt_public %t/canonical.mlirbc > /dev/null +// RUN: %acir_opt %t/internal-provider.mlir > /dev/null +// RUN: %not %acir_opt_public %t/internal-provider.mlir 2>&1 | %FileCheck %s --check-prefix=PROVIDER +// RUN: %not %acir_opt_public %t/escaped-ac.mlir 2>&1 | %FileCheck %s --check-prefix=ESCAPED-AC +// RUN: %not %acir_opt_public %t/mixed-escaped-ac.mlir 2>&1 | %FileCheck %s --check-prefix=ESCAPED-AC +// RUN: %not %acir_opt_public %t/escaped-acsim.mlir 2>&1 | %FileCheck %s --check-prefix=ESCAPED-AC +// RUN: %not %acir_opt_public %t/escaped-non-ac.mlir 2>&1 | %FileCheck %s --check-prefix=NON-AC --implicit-check-not=internal-only +// RUN: %not %acir_opt_public %t/malformed-escape.mlir 2>&1 | %FileCheck %s --check-prefix=MALFORMED + +//--- generic.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.return"() : () -> () + }) : () -> () +} +// GENERIC: generic ACIR operation spelling is internal-only + +//--- canonical.mlir +module attributes {ac.contract_epoch = "0.3"} { + // A quoted ACIR-like string is data, not a generic operation spelling. + ac.module @Top() parameters {label = "ac.fake"} graph { + ac.return + } +} +// CANONICAL: ac.module @Top + +//--- internal-provider.mlir +module attributes {ac.contract_epoch = "0.3"} { + ac.module.extern @Leaf : () -> () parameters {} + implementation {registry = "cpp", name = "Leaf"} +} +// PROVIDER: structural provider 'cpp:Leaf' is not registered + +//--- escaped-ac.mlir +module attributes {ac.contract_epoch = "0.3"} { + "\61c.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.return"() : () -> () + }) : () -> () +} +// ESCAPED-AC: generic ACIR operation spelling is internal-only + +//--- mixed-escaped-ac.mlir +module attributes {ac.contract_epoch = "0.3"} { + "\61\63.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.return"() : () -> () + }) : () -> () +} + +//--- escaped-acsim.mlir +module attributes {ac.contract_epoch = "0.3"} { + "\61csim.fake"() : () -> () +} + +//--- escaped-non-ac.mlir +module attributes {ac.contract_epoch = "0.3"} { + "\62c.fake"() : () -> () +} +// NON-AC: error: + +//--- malformed-escape.mlir +module attributes {ac.contract_epoch = "0.3"} { + "\6Gc.module"() : () -> () +} +// MALFORMED: malformed quoted operation name escape diff --git a/components/agentic-circuit/test/ACIR/collections-invalid.mlir b/components/agentic-circuit/test/ACIR/collections-invalid.mlir new file mode 100644 index 00000000..e9aefff2 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/collections-invalid.mlir @@ -0,0 +1,165 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/negative-shape.mlir 2>&1 | %FileCheck %s --check-prefix=NEGATIVE +// RUN: %not %acir_opt %t/wrong-cardinality.mlir 2>&1 | %FileCheck %s --check-prefix=CARDINALITY +// RUN: %not %acir_opt %t/heterogeneous-shape.mlir 2>&1 | %FileCheck %s --check-prefix=HETERO +// RUN: %not %acir_opt %t/bad-permutation.mlir 2>&1 | %FileCheck %s --check-prefix=PERMUTE +// RUN: %not %acir_opt %t/duplicate-owned-path.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-OWNED +// RUN: %not %acir_opt %t/too-large.mlir 2>&1 | %FileCheck %s --check-prefix=TOO-LARGE +// RUN: %not %acir_opt %t/view-overflow.mlir 2>&1 | %FileCheck %s --check-prefix=VIEW-OVERFLOW +// RUN: %not %acir_opt %t/view-provenance.mlir 2>&1 | %FileCheck %s --check-prefix=PROVENANCE +// RUN: %not %acir_opt %t/bad-select.mlir 2>&1 | %FileCheck %s --check-prefix=SELECT +// RUN: %not %acir_opt %t/bad-slice.mlir 2>&1 | %FileCheck %s --check-prefix=SLICE +// RUN: %not %acir_opt %t/bad-concat.mlir 2>&1 | %FileCheck %s --check-prefix=CONCAT +// RUN: %not %acir_opt %t/bad-zip.mlir 2>&1 | %FileCheck %s --check-prefix=ZIP +// RUN: %not %acir_opt %t/bad-elementwise.mlir 2>&1 | %FileCheck %s --check-prefix=ELEMENTWISE + +//--- negative-shape.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.array"() <{definition = @Leaf, sym_name = "a", stable_id = "a", path = "a", shape = array, static_args = []}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// NEGATIVE: array shape dimensions must be non-negative + +//--- wrong-cardinality.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.array"() <{definition = @Leaf, sym_name = "a", stable_id = "a", path = "a", shape = array, static_args = [{}]}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// CARDINALITY: one concrete static argument set per lexicographically ordered element + +//--- heterogeneous-shape.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "A", function_type = (i32) -> i32, static_params = {}, implementation = {registry = "cpp", name = "A"}}> : () -> () + "ac.module.extern"() <{sym_name = "B", function_type = (i64) -> i32, static_params = {}, implementation = {registry = "cpp", name = "B"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + %x:2 = "ac.instances"(%a, %b) <{sym_name = "collection", stable_id = "collection", path = "collection", definitions = [@A, @B], names = ["a", "b"], stable_ids = ["a", "b"], paths = ["a", "b"], interface = (i32) -> i32, static_args = [{}, {}]}> : (i32, i32) -> (i32, i32) + "ac.return"(%x#0, %x#1) : (i32, i32) -> () + }) : () -> () +} +// HETERO: does not implement the exact declared common interface + +//--- bad-permutation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Pair", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + "ac.return"(%a, %b) : (i32, i32) -> () + }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + %source:2 = "ac.instance"(%a, %b) <{definition = @Pair, sym_name = "pair", stable_id = "pair", path = "pair", static_args = {}}> : (i32, i32) -> (i32, i32) + %x:2 = "ac.view"(%source#0, %source#1) <{sym_name = "view", kind = "permutation", source_producers = [@pair], source_shapes = [array], indices = array, shape = array}> : (i32, i32) -> (i32, i32) + "ac.return"(%x#0, %x#1) : (i32, i32) -> () + }) : () -> () +} +// PERMUTE: permutation indices must be an in-bounds bijection + +//--- duplicate-owned-path.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = true}> : () -> () + "ac.module.extern"() <{sym_name = "A", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "A"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instances"() <{sym_name = "collection", stable_id = "collection", path = "collection", definitions = [@A, @A], names = ["a", "b"], stable_ids = ["a", "b"], paths = ["same", "same"], interface = () -> (), static_args = [{}, {}]}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DUP-OWNED: collection paths must be stable, unique parent-relative segments + +//--- too-large.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.array"() <{definition = @Leaf, sym_name = "huge", stable_id = "huge", path = "huge", shape = array, static_args = []}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// TOO-LARGE: array cardinality exceeds static elaboration bound 1048576 + +//--- view-overflow.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.view"() <{sym_name = "view", kind = "concat", source_producers = [@missing], source_shapes = [array], axis = 0 : i64, indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// VIEW-OVERFLOW: view cardinality overflows 64 bits + +//--- view-provenance.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Identity", function_type = (i32) -> i32, static_params = {}}> ({ ^bb0(%x : i32): "ac.return"(%x) : (i32) -> () }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg : i32): + %source = "ac.instance"(%arg) <{definition = @Identity, sym_name = "source", stable_id = "source", path = "source", static_args = {}}> : (i32) -> i32 + %x = "ac.view"(%arg) <{sym_name = "view", kind = "permutation", source_producers = [@source], source_shapes = [array], indices = array, shape = array}> : (i32) -> i32 + "ac.return"(%x) : (i32) -> () + }) : () -> () +} +// PROVENANCE: each source must be the complete result group of its declared structural producer + +//--- bad-select.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Pair", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ ^bb0(%a : i32, %b : i32): "ac.return"(%a, %b) : (i32, i32) -> () }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32, i32) -> i32, static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + %source:2 = "ac.instance"(%a, %b) <{definition = @Pair, sym_name = "pair", stable_id = "pair", path = "pair", static_args = {}}> : (i32, i32) -> (i32, i32) + %x = "ac.view"(%source#0, %source#1) <{sym_name = "view", kind = "select", source_producers = [@pair], source_shapes = [array], indices = array, shape = array}> : (i32, i32) -> i32 + "ac.return"(%x) : (i32) -> () + }) : () -> () +} +// SELECT: select coordinate is out of bounds + +//--- bad-slice.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Pair", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ ^bb0(%a : i32, %b : i32): "ac.return"(%a, %b) : (i32, i32) -> () }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + %source:2 = "ac.instance"(%a, %b) <{definition = @Pair, sym_name = "pair", stable_id = "pair", path = "pair", static_args = {}}> : (i32, i32) -> (i32, i32) + %x:2 = "ac.view"(%source#0, %source#1) <{sym_name = "view", kind = "slice", source_producers = [@pair], source_shapes = [array], indices = array, shape = array}> : (i32, i32) -> (i32, i32) + "ac.return"(%x#0, %x#1) : (i32, i32) -> () + }) : () -> () +} +// SLICE: slice bounds are invalid + +//--- bad-concat.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Pair", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ ^bb0(%a : i32, %b : i32): "ac.return"(%a, %b) : (i32, i32) -> () }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32, i32) -> (), static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + %left:2 = "ac.instance"(%a, %b) <{definition = @Pair, sym_name = "left", stable_id = "left", path = "left", static_args = {}}> : (i32, i32) -> (i32, i32) + %right:2 = "ac.instance"(%a, %b) <{definition = @Pair, sym_name = "right", stable_id = "right", path = "right", static_args = {}}> : (i32, i32) -> (i32, i32) + %x:4 = "ac.view"(%left#0, %left#1, %right#0, %right#1) <{sym_name = "view", kind = "concat", source_producers = [@left, @right], source_shapes = [array, array], axis = 1 : i64, indices = array, shape = array}> : (i32, i32, i32, i32) -> (i32, i32, i32, i32) + "ac.return"() : () -> () + }) : () -> () +} +// CONCAT: concat axis is out of bounds + +//--- bad-zip.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Pair", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ ^bb0(%a : i32, %b : i32): "ac.return"(%a, %b) : (i32, i32) -> () }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32, i32) -> (), static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + %left:2 = "ac.instance"(%a, %b) <{definition = @Pair, sym_name = "left", stable_id = "left", path = "left", static_args = {}}> : (i32, i32) -> (i32, i32) + %right:2 = "ac.instance"(%a, %b) <{definition = @Pair, sym_name = "right", stable_id = "right", path = "right", static_args = {}}> : (i32, i32) -> (i32, i32) + %x:4 = "ac.view"(%left#0, %left#1, %right#0, %right#1) <{sym_name = "view", kind = "zip", source_producers = [@left, @right], source_shapes = [array, array], indices = array, shape = array}> : (i32, i32, i32, i32) -> (i32, i32, i32, i32) + "ac.return"() : () -> () + }) : () -> () +} +// ZIP: zip result shape must append source count + +//--- bad-elementwise.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Pair", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ ^bb0(%a : i32, %b : i32): "ac.return"(%a, %b) : (i32, i32) -> () }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32, i32) -> (), static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + %source:2 = "ac.instance"(%a, %b) <{definition = @Pair, sym_name = "pair", stable_id = "pair", path = "pair", static_args = {}}> : (i32, i32) -> (i32, i32) + %x:2 = "ac.view"(%source#0, %source#1) <{sym_name = "view", kind = "elementwise", source_producers = [@pair], source_shapes = [array], indices = array, shape = array}> : (i32, i32) -> (i32, i32) + "ac.return"() : () -> () + }) : () -> () +} +// ELEMENTWISE: elementwise requires at least two sources diff --git a/components/agentic-circuit/test/ACIR/collections-valid.mlir b/components/agentic-circuit/test/ACIR/collections-valid.mlir new file mode 100644 index 00000000..34068a8f --- /dev/null +++ b/components/agentic-circuit/test/ACIR/collections-valid.mlir @@ -0,0 +1,29 @@ +// RUN: %acir_opt %s | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.system"() <{sym_name = "collections", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = true}> : () -> () + "ac.module"() <{sym_name = "Leaf", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + "ac.module"() <{sym_name = "Leaf2", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i32, i32) -> (i32, i32), static_params = {}}> ({ + ^bb0(%a : i32, %b : i32): + %x:2 = "ac.array"(%a, %b) <{definition = @Leaf, sym_name = "lanes", stable_id = "lanes", path = "lanes", shape = array, static_args = [{}, {}]}> : (i32, i32) -> (i32, i32) + %y:2 = "ac.instances"(%x#0, %x#1) <{sym_name = "collection", stable_id = "collection", path = "collection", definitions = [@Leaf, @Leaf2], names = ["a", "b"], stable_ids = ["a", "b"], paths = ["mix_a", "mix_b"], interface = (i32) -> i32, static_args = [{}, {}]}> : (i32, i32) -> (i32, i32) + %z:2 = "ac.view"(%y#0, %y#1) <{sym_name = "z", kind = "permutation", source_producers = [@collection], source_shapes = [array], indices = array, shape = array}> : (i32, i32) -> (i32, i32) + %selected = "ac.view"(%y#0, %y#1) <{sym_name = "selected", kind = "select", source_producers = [@collection], source_shapes = [array], indices = array, shape = array}> : (i32, i32) -> i32 + %slice:2 = "ac.view"(%y#0, %y#1) <{sym_name = "slice", kind = "slice", source_producers = [@collection], source_shapes = [array], indices = array, shape = array}> : (i32, i32) -> (i32, i32) + %concat:4 = "ac.view"(%x#0, %x#1, %y#0, %y#1) <{sym_name = "concat", kind = "concat", source_producers = [@lanes, @collection], source_shapes = [array, array], axis = 0 : i64, indices = array, shape = array}> : (i32, i32, i32, i32) -> (i32, i32, i32, i32) + %zip:4 = "ac.view"(%x#0, %x#1, %y#0, %y#1) <{sym_name = "zip", kind = "zip", source_producers = [@lanes, @collection], source_shapes = [array, array], indices = array, shape = array}> : (i32, i32, i32, i32) -> (i32, i32, i32, i32) + %bound:4 = "ac.view"(%x#0, %x#1, %y#0, %y#1) <{sym_name = "bound", kind = "elementwise", source_producers = [@lanes, @collection], source_shapes = [array, array], indices = array, shape = array}> : (i32, i32, i32, i32) -> (i32, i32, i32, i32) + "ac.return"(%z#0, %z#1) : (i32, i32) -> () + }) : () -> () +} + +// CHECK: ac.array +// CHECK: ac.instances +// CHECK: ac.view diff --git a/components/agentic-circuit/test/ACIR/contract-epoch.mlir b/components/agentic-circuit/test/ACIR/contract-epoch.mlir new file mode 100644 index 00000000..62bfd74d --- /dev/null +++ b/components/agentic-circuit/test/ACIR/contract-epoch.mlir @@ -0,0 +1,20 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt %t/valid.mlir -o /dev/null +// RUN: %not %acir_opt %t/legacy.mlir 2>&1 | %FileCheck %s --check-prefix=LEGACY +// RUN: %not %acir_opt %t/missing.mlir 2>&1 | %FileCheck %s --check-prefix=MISSING + +//--- valid.mlir +module attributes {ac.contract_epoch = "0.3"} { +} + +//--- legacy.mlir +module attributes {ac.contract_epoch = "0.1"} { +} + +// LEGACY: expected top-level 'ac.contract_epoch' string attribute equal to "0.3" + +//--- missing.mlir +module { +} + +// MISSING: expected top-level 'ac.contract_epoch' string attribute equal to "0.3" diff --git a/components/agentic-circuit/test/ACIR/contracts-invalid.mlir b/components/agentic-circuit/test/ACIR/contracts-invalid.mlir new file mode 100644 index 00000000..ce557d96 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/contracts-invalid.mlir @@ -0,0 +1,90 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/probe-dataflow.mlir 2>&1 | %FileCheck %s --check-prefix=PROBE +// RUN: %not %acir_opt %t/instrumentation-result.mlir 2>&1 | %FileCheck %s --check-prefix=INSTRUMENT +// RUN: %not %acir_opt %t/bad-stat.mlir 2>&1 | %FileCheck %s --check-prefix=STAT +// RUN: %not %acir_opt %t/monitor-effect.mlir 2>&1 | %FileCheck %s --check-prefix=MONITOR +// RUN: %not %acir_opt %t/static-assert.mlir 2>&1 | %FileCheck %s --check-prefix=STATIC-ASSERT +// RUN: %not %acir_opt %t/require-non-boolean.mlir 2>&1 | %FileCheck %s --check-prefix=REQUIRE-TYPE +// RUN: %not %acir_opt %t/ensure-non-boolean.mlir 2>&1 | %FileCheck %s --check-prefix=ENSURE-TYPE + +//--- probe-dataflow.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + %one = arith.constant 1 : i64 + %v = ac.probe @queue kind "queue" : i32 + ac.schedule @worker %v after %one : i32 + ac.yield_sim + } + ac.return + } +} +// PROBE: probe result may only feed observation operations + +//--- instrumentation-result.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + ac.instrumentation @bad { + %value = arith.constant 1 : i64 + ac.schedule @worker %value after %value : i64 + } + ac.yield_sim + } + ac.return + } +} +// INSTRUMENT: instrumentation may contain only removable observation operations + +//--- bad-stat.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.stat @bad kind "average" + ac.return + } +} +// STAT: kind must be 'counter', 'gauge', 'histogram', or 'event_log' + +//--- monitor-effect.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(i32) parameters {} graph { + ^bb0(%v : i32): + ac.process @p kind "monitor" captures(%v : i32) { + ^bb0(%captured : i32): + %accepted = ac.try_send @queue %captured : i32 + ac.yield_sim + } + ac.return + } +} +// MONITOR: monitor process cannot perform functional state effects + +//--- static-assert.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(i1) parameters {} graph { + ^bb0(%condition : i1): + ac.assert %condition, "runtime only" + ac.return + } +} +// STATIC-ASSERT: operation is not legal in an ac.module structural Graph region + +//--- require-non-boolean.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + %bad = arith.constant 1 : i32 + ac.require %bad, "non-boolean require" + ac.return + } +} +// REQUIRE-TYPE: error: use of value '%bad' expects different type than prior uses: 'i1' vs 'i32' + +//--- ensure-non-boolean.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + %bad = arith.constant 1 : i32 + ac.ensure %bad, "non-boolean ensure" + ac.return + } +} +// ENSURE-TYPE: error: use of value '%bad' expects different type than prior uses: 'i1' vs 'i32' diff --git a/components/agentic-circuit/test/ACIR/contracts-valid.mlir b/components/agentic-circuit/test/ACIR/contracts-valid.mlir new file mode 100644 index 00000000..52ca27ad --- /dev/null +++ b/components/agentic-circuit/test/ACIR/contracts-valid.mlir @@ -0,0 +1,58 @@ +// RUN: %acir_opt_public %s | %FileCheck %s +// RUN: %acir_opt_public %s | %acir_opt_public | %FileCheck %s +// RUN: %acir_opt_public --pass-pipeline='builtin.module(canonicalize,cse)' %s | %FileCheck %s --check-prefix=EFFECTS + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @fifo { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i64 action "offer" + ac.transition from @idle to @done on @push transfer true retain false guard {} + } + ac.module @Observed(i1) parameters {} graph { + ^bb0(%static_condition : i1): + ac.queue @queue payload i64 entries 8 ordering "fifo" protocol @fifo + ownership "exclusive" id "queue" path "queue" + ac.require %static_condition, "static capacity contract" + ac.ensure %static_condition, "static topology contract" + ac.stat @cycles kind "counter" + ac.stat @occupancy kind "gauge" + ac.stat @latency kind "histogram" + ac.stat @events kind "event_log" + ac.process @monitor kind "monitor" { + %true = arith.constant true + %one = arith.constant 1 : i64 + ac.require %true, "capacity contract" + ac.ensure %true, "latency contract" + ac.assert %true, "runtime contract" + %observed = ac.probe @queue kind "queue" : i64 + ac.stat.add @cycles %one : i64 + ac.instrumentation @debug { + ac.stat.add @occupancy %observed : i64 + } + ac.yield_sim + } + ac.return + } +} + +// CHECK: ac.require %{{.*}}, "static capacity contract" +// CHECK: ac.ensure %{{.*}}, "static topology contract" +// CHECK: ac.stat @cycles kind "counter" +// CHECK: ac.stat @occupancy kind "gauge" +// CHECK: ac.stat @latency kind "histogram" +// CHECK: ac.stat @events kind "event_log" +// CHECK: ac.require +// CHECK: ac.ensure +// CHECK: ac.assert +// CHECK: ac.probe +// CHECK: ac.stat.add +// CHECK: ac.instrumentation @debug +// EFFECTS: ac.require +// EFFECTS: ac.ensure +// EFFECTS: ac.assert +// EFFECTS: ac.probe +// EFFECTS: ac.stat.add +// EFFECTS: ac.yield_sim diff --git a/components/agentic-circuit/test/ACIR/control-feedback-invalid.mlir b/components/agentic-circuit/test/ACIR/control-feedback-invalid.mlir new file mode 100644 index 00000000..a7030bb2 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/control-feedback-invalid.mlir @@ -0,0 +1,65 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/merge-count.mlir 2>&1 | %FileCheck %s --check-prefix=MERGE-COUNT +// RUN: %not %acir_opt %t/merge-type.mlir 2>&1 | %FileCheck %s --check-prefix=MERGE-TYPE +// RUN: %not %acir_opt %t/merge-policy.mlir 2>&1 | %FileCheck %s --check-prefix=MERGE-POLICY +// RUN: %not %acir_opt %t/feedback-latency.mlir 2>&1 | %FileCheck %s --check-prefix=FEEDBACK-LATENCY +// RUN: %not %acir_opt %t/feedback-value.mlir 2>&1 | %FileCheck %s --check-prefix=FEEDBACK-VALUE +// RUN: %not %acir_opt %t/feedback-condition.mlir 2>&1 | %FileCheck %s --check-prefix=FEEDBACK-CONDITION + +// MERGE-COUNT: error: 'ac.merge' op requires at least two input queues +// MERGE-TYPE: error: 'ac.merge' op input queue 1 must match output queue type +// MERGE-POLICY: error: 'ac.merge' op policy must be 'round_robin' or 'priority' +// FEEDBACK-LATENCY: error: 'ac.feedback' op depth, latency, and max_iterations must be positive +// FEEDBACK-VALUE: error: 'ac.feedback' op yielded value must match queue payload Var +// FEEDBACK-CONDITION: error: 'ac.feedback' op continue value must be !ac.var + +//--- merge-count.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.merge %input policy "round_robin" depth 1 latency 1 : (!ac.queue) -> !ac.queue +} + +//--- merge-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %left = ac.source depth 1 latency 1 : !ac.queue + %right = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.merge %left, %right policy "round_robin" depth 1 latency 1 : (!ac.queue, !ac.queue) -> !ac.queue +} + +//--- merge-policy.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %left = ac.source depth 1 latency 1 : !ac.queue + %right = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.merge %left, %right policy "random" depth 1 latency 1 : (!ac.queue, !ac.queue) -> !ac.queue +} + +//--- feedback-latency.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.feedback %input depth 1 latency 0 max_iterations 8 { + ^body(%item: !ac.var): + %continue = ac.var.constant false as !ac.var + ac.feedback.yield %item continue %continue : !ac.var, !ac.var + } : !ac.queue -> !ac.queue +} + +//--- feedback-value.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.feedback %input depth 1 latency 1 max_iterations 8 { + ^body(%item: !ac.var): + %value = ac.var.constant 1 : i32 as !ac.var + %continue = ac.var.constant false as !ac.var + ac.feedback.yield %value continue %continue : !ac.var, !ac.var + } : !ac.queue -> !ac.queue +} + +//--- feedback-condition.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.feedback %input depth 1 latency 1 max_iterations 8 { + ^body(%item: !ac.var): + %continue = ac.var.constant 1 : i64 as !ac.var + ac.feedback.yield %item continue %continue : !ac.var, !ac.var + } : !ac.queue -> !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/control-feedback.mlir b/components/agentic-circuit/test/ACIR/control-feedback.mlir new file mode 100644 index 00000000..9a1ca3db --- /dev/null +++ b/components/agentic-circuit/test/ACIR/control-feedback.mlir @@ -0,0 +1,22 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %left = ac.source depth 2 latency 1 : !ac.queue + %right = ac.source depth 2 latency 1 : !ac.queue + %merged = ac.merge %left, %right policy "round_robin" depth 2 latency 1 : (!ac.queue, !ac.queue) -> !ac.queue + %done = ac.feedback %merged depth 1 latency 1 max_iterations 8 { + ^body(%item: !ac.var): + %one = ac.var.constant 1 : i64 as !ac.var + %next = ac.var.add %item, %one : !ac.var + %continue = ac.var.constant false as !ac.var + ac.feedback.yield %next continue %continue : !ac.var, !ac.var + } : !ac.queue -> !ac.queue + ac.sink %done : !ac.queue +} + +// CHECK: ac.merge +// CHECK: policy "round_robin" depth 2 latency 1 +// CHECK: ac.feedback +// CHECK: max_iterations 8 +// CHECK: ac.feedback.yield diff --git a/components/agentic-circuit/test/ACIR/credit-invalid.mlir b/components/agentic-circuit/test/ACIR/credit-invalid.mlir new file mode 100644 index 00000000..96e41d19 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/credit-invalid.mlir @@ -0,0 +1,37 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/credits.mlir 2>&1 | %FileCheck %s --check-prefix=CREDITS +// RUN: %not %acir_opt %t/cost.mlir 2>&1 | %FileCheck %s --check-prefix=COST +// RUN: %not %acir_opt %t/effect.mlir 2>&1 | %FileCheck %s --check-prefix=EFFECT + +// CREDITS: error: 'ac.credit' op credits, depth, and latency must be positive +// COST: error: 'ac.credit' op cost must yield an integer Var with width at most 64 +// EFFECT: error: 'ac.credit' op cost operation 'ac.assert' must be pure + +//--- credits.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.credit %input credits 0 depth 1 latency 1 cost { + ^cost(%item: !ac.var): ac.credit.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- cost.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.credit %input credits 1 depth 1 latency 1 cost { + ^cost(%item: !ac.var): + %wide = ac.var.constant 1 : i128 as !ac.var + ac.credit.yield %wide : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- effect.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.credit %input credits 1 depth 1 latency 1 cost { + ^cost(%item: !ac.var): + %condition = arith.constant true + ac.assert %condition, "illegal" + ac.credit.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/credit.mlir b/components/agentic-circuit/test/ACIR/credit.mlir new file mode 100644 index 00000000..2e3a17f3 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/credit.mlir @@ -0,0 +1,17 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %issued = ac.source depth 4 latency 1 : !ac.queue + %completed = ac.credit %issued credits 4 depth 4 latency 1 cost { + ^cost(%item: !ac.var): + ac.credit.yield %item : !ac.var + } : !ac.queue -> !ac.queue + ac.sink %completed : !ac.queue +} + +// CHECK: ac.credit +// CHECK: credits 4 depth 4 latency 1 cost +// CHECK: ac.credit.yield diff --git a/components/agentic-circuit/test/ACIR/declarations-invalid.mlir b/components/agentic-circuit/test/ACIR/declarations-invalid.mlir new file mode 100644 index 00000000..55906580 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/declarations-invalid.mlir @@ -0,0 +1,67 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/duplicate-symbol.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-SYMBOL +// RUN: %not %acir_opt %t/duplicate-field.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-FIELD +// RUN: %not %acir_opt %t/duplicate-enumerant.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-ENUM +// RUN: %not %acir_opt %t/unresolved-field.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED +// RUN: %not %acir_opt %t/wrong-kind.mlir 2>&1 | %FileCheck %s --check-prefix=WRONG-KIND +// RUN: %not %acir_opt %t/recursive.mlir 2>&1 | %FileCheck %s --check-prefix=RECURSIVE +// RUN: %not %acir_opt %t/alias-target.mlir 2>&1 | %FileCheck %s --check-prefix=ALIAS + +// DUP-SYMBOL: error: {{.*}}redefinition of symbol named 'Item' +// DUP-FIELD: error: {{.*}}duplicate field 'x' +// DUP-ENUM: error: {{.*}}duplicate enumerant 'read' +// UNRESOLVED: error: {{.*}}unresolved named data type '@types::@Missing' +// WRONG-KIND: error: {{.*}}named type '@types::@Mode' requires ac.struct but resolves to ac.enum +// RECURSIVE: error: {{.*}}unbounded value recursion through '@Node' +// ALIAS: error: {{.*}}unresolved named data type '@types::@Missing' + +//--- duplicate-symbol.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Item", fields = []}> : () -> () + "ac.transaction"() <{sym_name = "Item", fields = []}> : () -> () + }) : () -> () +} + +//--- duplicate-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Pair", fields = [{name = "x", type = i8}, {name = "x", type = i16}]}> : () -> () + }) : () -> () +} + +//--- duplicate-enumerant.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.enum"() <{sym_name = "Mode", enumerants = ["read", "read"]}> : () -> () + }) : () -> () +} + +//--- unresolved-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Holder", fields = [{name = "item", type = !ac.struct<@types::@Missing>}]}> : () -> () + }) : () -> () +} + +//--- wrong-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.enum"() <{sym_name = "Mode", enumerants = ["read"]}> : () -> () + "ac.transaction"() <{sym_name = "Holder", fields = [{name = "mode", type = !ac.struct<@types::@Mode>}]}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 1 : i64}>} : () -> () +} + +//--- recursive.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Node", fields = [{name = "next", type = !ac.transaction<@types::@Node>}]}> : () -> () + }) : () -> () +} + +//--- alias-target.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.type_alias"() <{sym_name = "MissingAlias", target = !ac.struct<@types::@Missing>}> : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/declarations-valid.mlir b/components/agentic-circuit/test/ACIR/declarations-valid.mlir new file mode 100644 index 00000000..6f7a677b --- /dev/null +++ b/components/agentic-circuit/test/ACIR/declarations-valid.mlir @@ -0,0 +1,26 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.type_alias"() <{sym_name = "Word", target = i32}> : () -> () + "ac.struct"() <{sym_name = "Header", fields = [{name = "opcode", type = i8}, {name = "tag", type = i16}]}> : () -> () + "ac.enum"() <{sym_name = "Opcode", enumerants = ["read", "write"]}> : () -> () + "ac.union"() <{sym_name = "Payload", fields = [{name = "kind", type = i8}, {name = "word", type = i32}], discriminator = "kind"}> : () -> () + "ac.packet"() <{sym_name = "Request", fields = [{name = "opcode", type = i8}, {name = "payload", type = i32}]}> : () -> () + "ac.transaction"() <{sym_name = "Dma", fields = [{name = "request", type = !ac.packet<@types::@Request>}, {name = "tag", type = i16}]}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec< + !ac.struct<@types::@Header> = {abi_alignment = 2 : i64, endianness = "little", preferred_alignment = 2 : i64, size = 4 : i64}, + !ac.enum<@types::@Opcode> = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 1 : i64}, + !ac.union<@types::@Payload> = {abi_alignment = 4 : i64, endianness = "little", preferred_alignment = 4 : i64, size = 4 : i64}, + !ac.packet<@types::@Request> = {abi_alignment = 4 : i64, endianness = "little", preferred_alignment = 4 : i64, serialization_width = 8 : i64, size = 8 : i64} + >} : () -> () +} + +// CHECK: ac.type_scope @types +// CHECK: "ac.type_alias" +// CHECK: ac.struct @Header +// CHECK: "ac.enum" +// CHECK: "ac.union" +// CHECK: "ac.packet" +// CHECK: ac.transaction @Dma diff --git a/components/agentic-circuit/test/ACIR/dependency-invalid.mlir b/components/agentic-circuit/test/ACIR/dependency-invalid.mlir new file mode 100644 index 00000000..ccc7650a --- /dev/null +++ b/components/agentic-circuit/test/ACIR/dependency-invalid.mlir @@ -0,0 +1,123 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/capacity.mlir 2>&1 | %FileCheck %s --check-prefix=CAPACITY +// RUN: %not %acir_opt %t/type.mlir 2>&1 | %FileCheck %s --check-prefix=TYPE +// RUN: %not %acir_opt %t/policy-type.mlir 2>&1 | %FileCheck %s --check-prefix=POLICY-TYPE +// RUN: %not %acir_opt %t/no-dependency.mlir 2>&1 | %FileCheck %s --check-prefix=NO-DEPENDENCY +// RUN: %not %acir_opt %t/resources.mlir 2>&1 | %FileCheck %s --check-prefix=RESOURCES +// RUN: %not %acir_opt %t/cost.mlir 2>&1 | %FileCheck %s --check-prefix=COST +// RUN: %not %acir_opt %t/effect.mlir 2>&1 | %FileCheck %s --check-prefix=EFFECT + +// CAPACITY: error: 'ac.dependency' op capacity, resources, depth, and latency must be positive +// TYPE: error: 'ac.dependency' op output queue must match input queue type +// POLICY-TYPE: error: 'ac.dependency' op key and waits_for must use the same integer Var type +// NO-DEPENDENCY: error: 'ac.dependency' op no_dependency must fit dependency width +// RESOURCES: error: 'ac.dependency' op resources must fit resource width +// COST: error: 'ac.dependency' op cost must yield an integer Var with width at most 64 +// EFFECT: error: 'ac.dependency' op key operation 'ac.assert' must be pure + +//--- capacity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.dependency %input capacity 0 resources 1 no_dependency 255 depth 1 latency 1 key { + ^key(%item: !ac.var): ac.dependency.yield %item : !ac.var + } waits_for { + ^waits_for(%item: !ac.var): ac.dependency.yield %item : !ac.var + } resource { + ^resource(%item: !ac.var): ac.dependency.yield %item : !ac.var + } cost { + ^cost(%item: !ac.var): ac.dependency.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.dependency %input capacity 4 resources 1 no_dependency 255 depth 1 latency 1 key { + ^key(%item: !ac.var): ac.dependency.yield %item : !ac.var + } waits_for { + ^waits_for(%item: !ac.var): ac.dependency.yield %item : !ac.var + } resource { + ^resource(%item: !ac.var): ac.dependency.yield %item : !ac.var + } cost { + ^cost(%item: !ac.var): ac.dependency.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- policy-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.dependency %input capacity 4 resources 1 no_dependency 255 depth 1 latency 1 key { + ^key(%item: !ac.var): ac.dependency.yield %item : !ac.var + } waits_for { + ^waits_for(%item: !ac.var): + %wide = ac.var.constant 0 : i16 as !ac.var + ac.dependency.yield %wide : !ac.var + } resource { + ^resource(%item: !ac.var): ac.dependency.yield %item : !ac.var + } cost { + ^cost(%item: !ac.var): ac.dependency.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- no-dependency.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.dependency %input capacity 4 resources 1 no_dependency 16 depth 1 latency 1 key { + ^key(%item: !ac.var): ac.dependency.yield %item : !ac.var + } waits_for { + ^waits_for(%item: !ac.var): ac.dependency.yield %item : !ac.var + } resource { + ^resource(%item: !ac.var): ac.dependency.yield %item : !ac.var + } cost { + ^cost(%item: !ac.var): ac.dependency.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- cost.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.dependency %input capacity 4 resources 1 no_dependency 255 depth 1 latency 1 key { + ^key(%item: !ac.var): ac.dependency.yield %item : !ac.var + } waits_for { + ^waits_for(%item: !ac.var): ac.dependency.yield %item : !ac.var + } resource { + ^resource(%item: !ac.var): ac.dependency.yield %item : !ac.var + } cost { + ^cost(%item: !ac.var): + %wide = ac.var.constant 0 : i128 as !ac.var + ac.dependency.yield %wide : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- resources.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.dependency %input capacity 4 resources 3 no_dependency 255 depth 1 latency 1 key { + ^key(%item: !ac.var): ac.dependency.yield %item : !ac.var + } waits_for { + ^waits_for(%item: !ac.var): ac.dependency.yield %item : !ac.var + } resource { + ^resource(%item: !ac.var): + %zero = ac.var.constant 0 : i1 as !ac.var + ac.dependency.yield %zero : !ac.var + } cost { + ^cost(%item: !ac.var): ac.dependency.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- effect.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.dependency %input capacity 4 resources 1 no_dependency 255 depth 1 latency 1 key { + ^key(%item: !ac.var): + %condition = arith.constant true + ac.assert %condition, "illegal" + ac.dependency.yield %item : !ac.var + } waits_for { + ^waits_for(%item: !ac.var): ac.dependency.yield %item : !ac.var + } resource { + ^resource(%item: !ac.var): ac.dependency.yield %item : !ac.var + } cost { + ^cost(%item: !ac.var): ac.dependency.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/dependency.mlir b/components/agentic-circuit/test/ACIR/dependency.mlir new file mode 100644 index 00000000..a8ec8221 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/dependency.mlir @@ -0,0 +1,29 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 8 latency 1 : !ac.queue + %completed = ac.dependency %input capacity 16 resources 4 no_dependency 255 depth 8 latency 1 key { + ^key(%item: !ac.var): + ac.dependency.yield %item : !ac.var + } waits_for { + ^waits_for(%item: !ac.var): + %none = ac.var.constant 255 : i8 as !ac.var + ac.dependency.yield %none : !ac.var + } resource { + ^resource(%item: !ac.var): + %zero = ac.var.constant 0 : i2 as !ac.var + ac.dependency.yield %zero : !ac.var + } cost { + ^cost(%item: !ac.var): + %one = ac.var.constant 1 : i8 as !ac.var + ac.dependency.yield %one : !ac.var + } : !ac.queue -> !ac.queue + ac.sink %completed : !ac.queue +} + +// CHECK: ac.dependency +// CHECK: capacity 16 resources 4 no_dependency 255 depth 8 latency 1 +// CHECK-COUNT-4: ac.dependency.yield diff --git a/components/agentic-circuit/test/ACIR/dialect-smoke.mlir b/components/agentic-circuit/test/ACIR/dialect-smoke.mlir new file mode 100644 index 00000000..caa7581c --- /dev/null +++ b/components/agentic-circuit/test/ACIR/dialect-smoke.mlir @@ -0,0 +1,30 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt --show-dialects | %FileCheck %s --check-prefix=DIALECTS +// RUN: %acir_opt %t/canonical.mlir | %FileCheck %s --check-prefix=CANONICAL +// RUN: %acir_opt --pass-pipeline='builtin.module(verify-ac-file)' %t/canonical.mlir | %FileCheck %s --check-prefix=CANONICAL +// RUN: %not %acir_opt %t/missing-epoch.mlir 2>&1 | %FileCheck %s --check-prefix=MISSING +// RUN: %not %acir_opt %t/wrong-epoch.mlir 2>&1 | %FileCheck %s --check-prefix=WRONG +// RUN: %not %acir_opt %t/unknown-ac-op.mlir 2>&1 | %FileCheck %s --check-prefix=UNKNOWN-AC + +// DIALECTS: Available Dialects: ac,acsim,arith,builtin,cf,dlti,func,index,scf +// CANONICAL: module attributes {ac.contract_epoch = "0.3"} +// MISSING: error: expected top-level 'ac.contract_epoch' string attribute equal to "0.3" +// WRONG: error: expected top-level 'ac.contract_epoch' string attribute equal to "0.3" +// UNKNOWN-AC: error: unregistered operation 'ac.unknown' + +//--- canonical.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { +} + +//--- missing-epoch.mlir +builtin.module { +} + +//--- wrong-epoch.mlir +builtin.module attributes {ac.contract_epoch = "0.1"} { +} + +//--- unknown-ac-op.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.unknown"() : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/expect-invalid.mlir b/components/agentic-circuit/test/ACIR/expect-invalid.mlir new file mode 100644 index 00000000..59903e8a --- /dev/null +++ b/components/agentic-circuit/test/ACIR/expect-invalid.mlir @@ -0,0 +1,25 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/message.mlir 2>&1 | %FileCheck %s --check-prefix=MESSAGE +// RUN: %not %acir_opt %t/predicate.mlir 2>&1 | %FileCheck %s --check-prefix=PREDICATE + +// MESSAGE: error: 'ac.expect' op message must be non-empty +// PREDICATE: error: 'ac.expect' op predicate must terminate with an i1 ac.expect.yield + +//--- message.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + ac.expect %input message "" { + ^predicate(%item: !ac.var): + %true = ac.var.constant true as !ac.var + ac.expect.yield %true : !ac.var + } : !ac.queue +} + +//--- predicate.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + ac.expect %input message "bad" { + ^predicate(%item: !ac.var): + ac.expect.yield %item : !ac.var + } : !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/expect.mlir b/components/agentic-circuit/test/ACIR/expect.mlir new file mode 100644 index 00000000..4b264d8e --- /dev/null +++ b/components/agentic-circuit/test/ACIR/expect.mlir @@ -0,0 +1,17 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + ac.expect %input message "positive" { + ^predicate(%item: !ac.var): + %zero = ac.var.constant 0 : i8 as !ac.var + %positive = ac.var.cmp "sgt" %item, %zero : !ac.var -> !ac.var + ac.expect.yield %positive : !ac.var + } : !ac.queue + ac.sink %input : !ac.queue +} + +// CHECK: ac.expect +// CHECK: message "positive" +// CHECK: ac.expect.yield diff --git a/components/agentic-circuit/test/ACIR/firing-invalid.mlir b/components/agentic-circuit/test/ACIR/firing-invalid.mlir new file mode 100644 index 00000000..374b1335 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/firing-invalid.mlir @@ -0,0 +1,88 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/unlisted.mlir 2>&1 | %FileCheck %s --check-prefix=UNLISTED +// RUN: %not %acir_opt %t/duplicate-pop.mlir 2>&1 | %FileCheck %s --check-prefix=DUPLICATE-POP +// RUN: %not %acir_opt %t/duplicate-push.mlir 2>&1 | %FileCheck %s --check-prefix=DUPLICATE-PUSH +// RUN: %not %acir_opt %t/effectless.mlir 2>&1 | %FileCheck %s --check-prefix=EFFECTLESS +// RUN: %not %acir_opt %t/payload.mlir 2>&1 | %FileCheck %s --check-prefix=PAYLOAD +// RUN: %not %acir_opt %t/peek-payload.mlir 2>&1 | %FileCheck %s --check-prefix=PEEK-PAYLOAD +// RUN: %not %acir_opt %t/foreign-effect.mlir 2>&1 | %FileCheck %s --check-prefix=FOREIGN-EFFECT + +// UNLISTED: error: 'ac.firing' op queue effect references an unlisted firing operand +// DUPLICATE-POP: error: 'ac.firing' op queue may be popped at most once per firing +// DUPLICATE-PUSH: error: 'ac.firing' op queue may be pushed at most once per firing +// EFFECTLESS: error: 'ac.firing' op requires at least one queue state effect +// PAYLOAD: error: 'ac.queue.push' op value must be '!ac.var' +// PEEK-PAYLOAD: error: 'ac.queue.peek' op result must be '!ac.var' +// FOREIGN-EFFECT: error: 'ac.firing' op body operation 'ac.assert' is not a queue effect or pure computation + +//--- unlisted.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + %output = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.firing (%input) { + %item = ac.queue.pop %input : !ac.queue -> !ac.var + ac.queue.push %output, %item : !ac.queue, !ac.var + ac.firing.yield + } : (!ac.queue) +} + +//--- duplicate-pop.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.firing (%input) { + %first = ac.queue.pop %input : !ac.queue -> !ac.var + %second = ac.queue.pop %input : !ac.queue -> !ac.var + ac.firing.yield + } : (!ac.queue) +} + +//--- duplicate-push.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %output = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.firing (%output) { + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var + ac.queue.push %output, %value : !ac.queue, !ac.var + ac.queue.push %output, %value : !ac.queue, !ac.var + ac.firing.yield + } : (!ac.queue) +} + +//--- effectless.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.firing (%input) { + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var + ac.firing.yield + } : (!ac.queue) +} + +//--- payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %output = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.firing (%output) { + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var + ac.queue.push %output, %value : !ac.queue, !ac.var + ac.firing.yield + } : (!ac.queue) +} + +//--- foreign-effect.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.firing (%input) { + %item = ac.queue.pop %input : !ac.queue -> !ac.var + %condition = arith.constant true + ac.assert %condition, "foreign effect" + ac.firing.yield + } : (!ac.queue) +} + +//--- peek-payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.firing (%input) { + %item = ac.queue.peek %input : !ac.queue -> !ac.var + %consumed = ac.queue.pop %input : !ac.queue -> !ac.var + ac.firing.yield + } : (!ac.queue) +} diff --git a/components/agentic-circuit/test/ACIR/firing.mlir b/components/agentic-circuit/test/ACIR/firing.mlir new file mode 100644 index 00000000..e47e1411 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/firing.mlir @@ -0,0 +1,21 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + %output = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.firing (%input, %output) { + %item = ac.queue.peek %input : !ac.queue -> !ac.var + %consumed = ac.queue.pop %input : !ac.queue -> !ac.var + ac.queue.push %output, %consumed : !ac.queue, !ac.var + ac.firing.yield + } : (!ac.queue, !ac.queue) +} + +// CHECK: ac.firing(%[[INPUT:.*]], %[[OUTPUT:.*]]) +// CHECK: %[[ITEM:.*]] = ac.queue.peek %[[INPUT]] : !ac.queue -> !ac.var +// CHECK: %[[CONSUMED:.*]] = ac.queue.pop %[[INPUT]] : !ac.queue -> !ac.var +// CHECK: ac.queue.push %[[OUTPUT]], %[[CONSUMED]] : !ac.queue, !ac.var +// CHECK: ac.firing.yield diff --git a/components/agentic-circuit/test/ACIR/fork-invalid.mlir b/components/agentic-circuit/test/ACIR/fork-invalid.mlir new file mode 100644 index 00000000..46498f46 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/fork-invalid.mlir @@ -0,0 +1,18 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/count.mlir 2>&1 | %FileCheck %s --check-prefix=COUNT +// RUN: %not %acir_opt %t/depth.mlir 2>&1 | %FileCheck %s --check-prefix=DEPTH + +// COUNT: error: 'ac.fork' op requires at least two output queues +// DEPTH: error: 'ac.fork' op output depths must match results and be positive + +//--- count.mlir +module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %only = ac.fork %input depths [1] latencies [1] : !ac.queue -> (!ac.queue) +} + +//--- depth.mlir +module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %left, %right = ac.fork %input depths [1, 0] latencies [1, 1] : !ac.queue -> (!ac.queue, !ac.queue) +} diff --git a/components/agentic-circuit/test/ACIR/fork.mlir b/components/agentic-circuit/test/ACIR/fork.mlir new file mode 100644 index 00000000..e8c0ec60 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/fork.mlir @@ -0,0 +1,10 @@ +// RUN: %acir_opt %s | %FileCheck %s + +module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 2 latency 1 : !ac.queue + %left, %right = ac.fork %input depths [2, 3] latencies [1, 2] : !ac.queue -> (!ac.queue, !ac.queue) + ac.sink %left : !ac.queue + ac.sink %right : !ac.queue +} + +// CHECK: ac.fork {{.*}} depths [2, 3] latencies [1, 2] diff --git a/components/agentic-circuit/test/ACIR/guarantees-review-r1-invalid.mlir b/components/agentic-circuit/test/ACIR/guarantees-review-r1-invalid.mlir new file mode 100644 index 00000000..564f59c1 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/guarantees-review-r1-invalid.mlir @@ -0,0 +1,52 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/string-kind.mlir 2>&1 | %FileCheck %s --check-prefix=STRING +// RUN: %not %acir_opt %t/stable.mlir 2>&1 | %FileCheck %s --check-prefix=STABLE +// RUN: %not %acir_opt %t/inflight.mlir 2>&1 | %FileCheck %s --check-prefix=INFLIGHT +// RUN: %not %acir_opt %t/correlation.mlir 2>&1 | %FileCheck %s --check-prefix=CORRELATION +// RUN: %not %acir_opt %t/custom.mlir 2>&1 | %FileCheck %s --check-prefix=CUSTOM + +// STRING: unsupported backpressure value '' +// STABLE: stable_pending requires a boolean value +// INFLIGHT: max_inflight requires a positive i64 value +// CORRELATION: correlation requires a non-empty field name +// CUSTOM: custom_backpressure requires a non-empty declarative contract + +//--- string-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "backpressure", value = 1 : i64}> : () -> () + }) : () -> () +} + +//--- stable.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = 1 : i64}> : () -> () + }) : () -> () +} + +//--- inflight.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "max_inflight", value = "many"}> : () -> () + }) : () -> () +} + +//--- correlation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "correlation", value = 1 : i64}> : () -> () + }) : () -> () +} + +//--- custom.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "custom_backpressure", value = true}> : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/hierarchy-invalid.mlir b/components/agentic-circuit/test/ACIR/hierarchy-invalid.mlir new file mode 100644 index 00000000..236a1a0c --- /dev/null +++ b/components/agentic-circuit/test/ACIR/hierarchy-invalid.mlir @@ -0,0 +1,276 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/no-selected.mlir 2>&1 | %FileCheck %s --check-prefix=NO-SELECTED +// RUN: %not %acir_opt %t/bad-ref.mlir 2>&1 | %FileCheck %s --check-prefix=BAD-REF +// RUN: %not %acir_opt %t/bad-call.mlir 2>&1 | %FileCheck %s --check-prefix=BAD-CALL +// RUN: %not %acir_opt %t/bad-return.mlir 2>&1 | %FileCheck %s --check-prefix=BAD-RETURN +// RUN: %not %acir_opt %t/duplicate-path.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-PATH +// RUN: %not %acir_opt %t/dynamic-param.mlir 2>&1 | %FileCheck %s --check-prefix=DYNAMIC +// RUN: %not %acir_opt %t/generic-binding.mlir 2>&1 | %FileCheck %s --check-prefix=BINDING +// RUN: %not %acir_opt %t/private-export.mlir 2>&1 | %FileCheck %s --check-prefix=PRIVATE +// RUN: %not %acir_opt %t/missing-static-arg.mlir 2>&1 | %FileCheck %s --check-prefix=STATIC-ARG +// RUN: %not %acir_opt %t/unresolved-static-symbol.mlir 2>&1 | %FileCheck %s --check-prefix=STATIC-SYMBOL +// RUN: %not %acir_opt %t/unknown-generator.mlir 2>&1 | %FileCheck %s --check-prefix=UNKNOWN-GENERATOR +// RUN: %not %acir_opt %t/nonzero-epoch.mlir 2>&1 | %FileCheck %s --check-prefix=EPOCH +// RUN: %not %acir_opt %t/unresolved-workload.mlir 2>&1 | %FileCheck %s --check-prefix=WORKLOAD +// RUN: %not %acir_opt %t/direct-recursion.mlir 2>&1 | %FileCheck %s --check-prefix=DIRECT-RECURSION +// RUN: %not %acir_opt %t/mutual-recursion.mlir 2>&1 | %FileCheck %s --check-prefix=MUTUAL-RECURSION +// RUN: %not %acir_opt %t/absolute-local-path.mlir 2>&1 | %FileCheck %s --check-prefix=LOCAL-PATH +// RUN: %not %acir_opt %t/duplicate-id.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-ID +// RUN: %not %acir_opt %t/mutual-recursion.mlir > /dev/null 2> %t/mutual.first +// RUN: %not %acir_opt %t/mutual-recursion.mlir > /dev/null 2> %t/mutual.second +// RUN: diff %t/mutual.first %t/mutual.second +// RUN: %not %acir_opt %t/static-arg-type.mlir 2>&1 | %FileCheck %s --check-prefix=STATIC-TYPE +// RUN: %not %acir_opt %t/orphan-instance.mlir 2>&1 | %FileCheck %s --check-prefix=ORPHAN +// RUN: %not %acir_opt %t/nested-module.mlir 2>&1 | %FileCheck %s --check-prefix=NESTED-MODULE +// RUN: %not %acir_opt %t/two-selected.mlir 2>&1 | %FileCheck %s --check-prefix=TWO-SELECTED +// RUN: %not %acir_opt %t/unknown-provider.mlir 2>&1 | %FileCheck %s --check-prefix=PROVIDER +// RUN: %not %acir_opt %t/extern-root.mlir 2>&1 | %FileCheck %s --check-prefix=EXTERN-ROOT +// RUN: %not %acir_opt %t/negative-seed.mlir 2>&1 | %FileCheck %s --check-prefix=SEED +// RUN: %not %acir_opt %t/bad-result-schema.mlir 2>&1 | %FileCheck %s --check-prefix=RESULT-SCHEMA +// RUN: %not %acir_opt %t/bad-instrumentation.mlir 2>&1 | %FileCheck %s --check-prefix=INSTRUMENTATION +// RUN: %not %acir_opt %t/static-array.mlir 2>&1 | %FileCheck %s --check-prefix=STATIC-ARRAY +// RUN: %not %acir_opt %t/bad-unit.mlir 2>&1 | %FileCheck %s --check-prefix=BAD-UNIT +// RUN: %not %acir_opt %t/bad-seed-type.mlir 2>&1 | %FileCheck %s --check-prefix=SEED-TYPE +// RUN: %not %acir_opt %t/unresolved-instrumentation.mlir 2>&1 | %FileCheck %s --check-prefix=INSTRUMENTATION-REF + +//--- no-selected.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = false}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () +} +// NO-SELECTED: ACIR file requires exactly one selected ac.system, found 0 + +//--- bad-ref.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.system"() <{sym_name = "s", root = @Missing, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = true}> : () -> () +} +// BAD-REF: selected root must resolve to a materialized ac.module + +//--- bad-call.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Leaf", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%x : i32): + "ac.return"(%x) : (i32) -> () + }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = (i64) -> i64, static_params = {}}> ({ + ^bb0(%x : i64): + %v = "ac.instance"(%x) <{definition = @Leaf, sym_name = "x", stable_id = "x", path = "x", static_args = {}}> : (i64) -> i64 + "ac.return"(%v) : (i64) -> () + }) : () -> () +} +// BAD-CALL: operand types do not match module signature + +//--- bad-return.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = (i32) -> i64, static_params = {}}> ({ + ^bb0(%x : i32): + "ac.return"(%x) : (i32) -> () + }) : () -> () +} +// BAD-RETURN: operand types and count must exactly match module results + +//--- duplicate-path.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = true}> : () -> () + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Leaf, sym_name = "a", stable_id = "a", path = "same", static_args = {}}> : () -> () + "ac.instance"() <{definition = @Leaf, sym_name = "b", stable_id = "b", path = "same", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DUP-PATH: duplicate local structural path 'same' + +//--- dynamic-param.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {gain = 1.0 : f32}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () +} +// DYNAMIC: static parameters must contain only concrete builtin static values + +//--- generic-binding.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.generated"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}, generator = {registry = "generic", name = "fallback"}}> : () -> () +} +// BINDING: requires exact registered {registry, name} metadata + +//--- private-export.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = (!ac.resource_token<@owned>) -> !ac.resource_token<@owned>, static_params = {}}> ({ + ^bb0(%token : !ac.resource_token<@owned>): + "ac.return"(%token) : (!ac.resource_token<@owned>) -> () + }) : () -> () +} +// PRIVATE: private ownership handle cannot be exported from ac.module + +//--- missing-static-arg.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {width = 8 : i64}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Leaf, sym_name = "leaf", stable_id = "leaf", path = "leaf", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// STATIC-ARG: static argument names must exactly match definition parameters + +//--- unresolved-static-symbol.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {target = @Missing}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () +} +// STATIC-SYMBOL: unresolved static symbol reference '@Missing' + +//--- unknown-generator.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.generated"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}, generator = {registry = "unknown", name = "Leaf"}}> : () -> () +} +// UNKNOWN-GENERATOR: generated module requires registered registry 'ac' + +//--- nonzero-epoch.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Top", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Top"}}> : () -> () + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 1 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = true}> : () -> () +} +// EPOCH: global tick epoch must be exactly 0 + +//--- unresolved-workload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", primary_workload = @Top::@missing, seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = true}> : () -> () +} +// WORKLOAD: primary workload '@Top::@missing' is unresolved + +//--- direct-recursion.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Loop", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Loop, sym_name = "self", stable_id = "self", path = "self", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DIRECT-RECURSION: recursive module instantiation cycle: @Loop -> @Loop + +//--- mutual-recursion.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "A", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @B, sym_name = "b", stable_id = "b", path = "b", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () + "ac.module"() <{sym_name = "B", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @A, sym_name = "a", stable_id = "a", path = "a", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MUTUAL-RECURSION: recursive module instantiation cycle: @A -> @B -> @A + +//--- absolute-local-path.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Leaf, sym_name = "leaf", stable_id = "leaf", path = "root.leaf", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// LOCAL-PATH: path must be stable local segments + +//--- duplicate-id.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = true}> : () -> () + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Leaf, sym_name = "a", stable_id = "same", path = "a", static_args = {}}> : () -> () + "ac.instance"() <{definition = @Leaf, sym_name = "b", stable_id = "same", path = "b", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DUP-ID: duplicate local structural stable id 'same' + +//--- static-arg-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Leaf", function_type = () -> (), static_params = {width = 8 : i64}, implementation = {registry = "cpp", name = "Leaf"}}> : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Leaf, sym_name = "leaf", stable_id = "leaf", path = "leaf", static_args = {width = 8 : i32}}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// STATIC-TYPE: static argument 'width' must match parameter attribute type 'i64' + +//--- orphan-instance.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Leaf", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.instance"() <{definition = @Leaf, sym_name = "x", stable_id = "x", path = "x", static_args = {}}> : () -> () +} +// ORPHAN: must be a direct child of the unique ac.module Graph block + +//--- nested-module.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.module"() <{sym_name = "Nested", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + }) : () -> () +} +// NESTED-MODULE: must be a direct child of the outer builtin.module + +//--- two-selected.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.system"() <{sym_name = "a", root = @Top, root_name = "a", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "a", format = "json"}, selected = true}> : () -> () + "ac.system"() <{sym_name = "b", root = @Top, root_name = "b", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "b", format = "json"}, selected = true}> : () -> () +} +// TWO-SELECTED: exactly one selected ac.system, found 2 + +//--- unknown-provider.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Ext", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "not_registered"}}> : () -> () +} +// PROVIDER: structural provider 'cpp:not_registered' is not registered + +//--- extern-root.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module.extern"() <{sym_name = "Ext", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Ext"}}> : () -> () + "ac.system"() <{sym_name = "s", root = @Ext, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "x", format = "json"}, selected = true}> : () -> () +} +// EXTERN-ROOT: selected root must resolve to a materialized ac.module + +//--- negative-seed.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = -1 : i64}, instrumentation = [], result_schema = {id = "x", format = "json"}, selected = true}> : () -> () +} +// SEED: fixed seed value must be a non-negative signless i64 + +//--- bad-result-schema.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [], result_schema = {id = "", format = "text"}, selected = true}> : () -> () +} +// RESULT-SCHEMA: result schema requires exact {id = non-empty string, format = "json"} + +//--- bad-instrumentation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = ["trace"], result_schema = {id = "x", format = "json"}, selected = true}> : () -> () +} +// INSTRUMENTATION: instrumentation entries must be symbol references + +//--- static-array.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {bad = [1 : i64]}}> ({ "ac.return"() : () -> () }) : () -> () +} +// STATIC-ARRAY: static parameters must contain only concrete builtin static values + +//--- bad-unit.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {latency = {value = 1 : i64, unit = "ns", extra = true}}}> ({ "ac.return"() : () -> () }) : () -> () +} +// BAD-UNIT: static parameters must contain only concrete builtin static values + +//--- bad-seed-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : ui32}, instrumentation = [], result_schema = {id = "x", format = "json"}, selected = true}> : () -> () +} +// SEED-TYPE: seed policy requires exact {kind = "fixed", value = signless i64} schema + +//--- unresolved-instrumentation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.system"() <{sym_name = "s", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 0 : i64}, instrumentation = [@Top::@missing::@trace], result_schema = {id = "x", format = "json"}, selected = true}> : () -> () +} +// INSTRUMENTATION-REF: instrumentation reference '@Top::@missing::@trace' does not resolve to ac.instrumentation diff --git a/components/agentic-circuit/test/ACIR/hierarchy-valid.mlir b/components/agentic-circuit/test/ACIR/hierarchy-valid.mlir new file mode 100644 index 00000000..f453d09c --- /dev/null +++ b/components/agentic-circuit/test/ACIR/hierarchy-valid.mlir @@ -0,0 +1,71 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.system"() <{sym_name = "soc", root = @Top, root_name = "root", tick_epoch = 0 : i64, tick_unit = "cycle", primary_workload = @Top::@workload, seed_policy = {kind = "fixed", value = 7 : i64}, instrumentation = [@Top::@workload::@trace], result_schema = {id = "default", format = "json"}, selected = true}> : () -> () + "ac.system"() <{sym_name = "leaf_harness", root = @Leaf, root_name = "leaf", tick_epoch = 0 : i64, tick_unit = "cycle", seed_policy = {kind = "fixed", value = 9 : i64}, instrumentation = [], result_schema = {id = "default", format = "json"}, selected = false}> : () -> () + + "ac.module"() <{sym_name = "Top", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + %0 = "ac.instance"(%arg0) <{definition = @Leaf, sym_name = "child", stable_id = "child", path = "child", static_args = {}}> : (i32) -> i32 + "ac.instance"() <{definition = @Reusable, sym_name = "left", stable_id = "left", path = "left", static_args = {}}> : () -> () + "ac.instance"() <{definition = @Reusable, sym_name = "right", stable_id = "right", path = "right", static_args = {}}> : () -> () + ac.process @workload kind "workload" captures(%arg0 : i32) { + ^bb0(%capture : i32): + ac.instrumentation @trace {} + ac.yield_sim + } + "ac.return"(%0) : (i32) -> () + }) : () -> () + + "ac.module"() <{sym_name = "Leaf", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + ac.process @state kind "control" { ac.yield_sim } + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + + // Static symbol references inside modules and structural arguments resolve + // in the enclosing builtin.module symbol table. + "ac.module"() <{sym_name = "Parameterized", function_type = () -> (), static_params = {target = @Leaf}}> ({ + "ac.return"() : () -> () + }) : () -> () + "ac.module"() <{sym_name = "StaticRefs", function_type = () -> (), static_params = {target = @Leaf}}> ({ + "ac.instance"() <{definition = @Parameterized, sym_name = "one", stable_id = "one", path = "one", static_args = {target = @Leaf}}> : () -> () + "ac.array"() <{definition = @Parameterized, sym_name = "array", stable_id = "array", path = "array", shape = array, static_args = [{target = @Leaf}]}> : () -> () + "ac.instances"() <{sym_name = "many", stable_id = "many", path = "many", definitions = [@Parameterized], names = ["item"], stable_ids = ["item"], paths = ["item"], interface = () -> (), static_args = [{target = @Leaf}]}> : () -> () + "ac.return"() : () -> () + }) : () -> () + + "ac.module.extern"() <{sym_name = "Ext", function_type = (i32) -> i32, static_params = {}, implementation = {registry = "cpp", name = "Ext"}}> : () -> () + "ac.module.generated"() <{sym_name = "Gen", function_type = (i32) -> i32, static_params = {}, generator = {registry = "ac", name = "Gen"}}> : () -> () + + // One reusable definition may be instantiated by multiple parents. Its + // relative child segment expands independently below each ownership path. + "ac.module"() <{sym_name = "Reusable", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Empty, sym_name = "leaf", stable_id = "leaf", path = "leaf", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () + "ac.module"() <{sym_name = "ReuseHarness", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Reusable, sym_name = "left", stable_id = "left", path = "left", static_args = {}}> : () -> () + "ac.instance"() <{definition = @Reusable, sym_name = "right", stable_id = "right", path = "right", static_args = {}}> : () -> () + "ac.return"() : () -> () + }) : () -> () + "ac.module.extern"() <{sym_name = "Empty", function_type = () -> (), static_params = {}, implementation = {registry = "cpp", name = "Empty"}}> : () -> () + + // Graph-region SSA may use a value before its textual definition and cycle + // when the instantiated module contributes an explicit state boundary. + "ac.module"() <{sym_name = "DataCycle", function_type = () -> i32, static_params = {}}> ({ + %a = "ac.instance"(%b) <{definition = @Leaf, sym_name = "left", stable_id = "data-left", path = "left", static_args = {}}> : (i32) -> i32 + %b = "ac.instance"(%a) <{definition = @Leaf, sym_name = "right", stable_id = "data-right", path = "right", static_args = {}}> : (i32) -> i32 + "ac.return"(%a) : (i32) -> () + }) : () -> () +} + +// CHECK: ac.system +// CHECK-SAME: workload @Top::@workload +// CHECK-SAME: instrumentation [@Top::@workload::@trace] +// CHECK: ac.module +// CHECK: ac.instance +// CHECK: ac.return +// CHECK: ac.module.extern +// CHECK: ac.module.generated diff --git a/components/agentic-circuit/test/ACIR/interfaces-invalid.mlir b/components/agentic-circuit/test/ACIR/interfaces-invalid.mlir new file mode 100644 index 00000000..f9432e55 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/interfaces-invalid.mlir @@ -0,0 +1,233 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/duplicate-role.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-ROLE +// RUN: %not %acir_opt %t/unresolved-dual.mlir 2>&1 | %FileCheck %s --check-prefix=DUAL +// RUN: %not %acir_opt %t/asymmetric-dual.mlir 2>&1 | %FileCheck %s --check-prefix=ASYMMETRIC +// RUN: %not %acir_opt %t/unresolved-port-role.mlir 2>&1 | %FileCheck %s --check-prefix=PORT-ROLE +// RUN: %not %acir_opt %t/invalid-payload.mlir 2>&1 | %FileCheck %s --check-prefix=PAYLOAD +// RUN: %not %acir_opt %t/unresolved-protocol.mlir 2>&1 | %FileCheck %s --check-prefix=PROTOCOL +// RUN: %not %acir_opt %t/channel-outside.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL +// RUN: %not %acir_opt %t/unknown-interface.mlir 2>&1 | %FileCheck %s --check-prefix=INTERFACE +// RUN: %not %acir_opt %t/unknown-endpoint-role.mlir 2>&1 | %FileCheck %s --check-prefix=ENDPOINT-ROLE +// RUN: %not %acir_opt %t/endpoint-cardinality.mlir 2>&1 | %FileCheck %s --check-prefix=CARDINALITY +// RUN: %not %acir_opt %t/flow-protocol.mlir 2>&1 | %FileCheck %s --check-prefix=FLOW +// RUN: %not %acir_opt %t/duplicate-port.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-PORT +// RUN: %not %acir_opt %t/bad-cardinality.mlir 2>&1 | %FileCheck %s --check-prefix=ROLE-CARDINALITY +// RUN: %not %acir_opt %t/self-dual.mlir 2>&1 | %FileCheck %s --check-prefix=SELF-DUAL +// RUN: %not %acir_opt %t/non-channel-port.mlir 2>&1 | %FileCheck %s --check-prefix=PORT-TYPE +// RUN: %not %acir_opt %t/flow-cardinality.mlir 2>&1 | %FileCheck %s --check-prefix=FLOW-CARDINALITY +// RUN: %not %acir_opt %t/unresolved-port-target.mlir 2>&1 | %FileCheck %s --check-prefix=PORT-TARGET +// RUN: %not %acir_opt %t/nondual-port.mlir 2>&1 | %FileCheck %s --check-prefix=PORT-DUAL +// RUN: %not %acir_opt %t/protocol-payload-mismatch.mlir 2>&1 | %FileCheck %s --check-prefix=PROTOCOL-PAYLOAD +// RUN: %not %acir_opt %t/deterministic.mlir 2> %t/diag-one +// RUN: %not %acir_opt %t/deterministic.mlir 2> %t/diag-two +// RUN: diff %t/diag-one %t/diag-two +// RUN: %FileCheck %s --check-prefix=DETERMINISTIC < %t/diag-one + +// DUP-ROLE: redefinition of symbol named 'a' +// DUAL: unresolved dual role '@missing' +// ASYMMETRIC: role duality must be symmetric +// PORT-ROLE: unresolved port source role '@missing' +// PAYLOAD: channel payload type must be a normative ACIR value type +// PROTOCOL: unresolved channel protocol '@missing' +// CHANNEL: channel type is only permitted in an ac.interface channel declaration +// INTERFACE: unresolved endpoint interface '@Missing' +// ENDPOINT-ROLE: endpoint role '@missing' is not a member of interface '@I' +// CARDINALITY: exclusive endpoint value has more than one structural use +// FLOW: unresolved flow protocol '@missing' +// DUP-PORT: redefinition of symbol named 'x' +// ROLE-CARDINALITY: unsupported role cardinality 'many' +// SELF-DUAL: role cannot be its own dual +// PORT-TYPE: port type must be !ac.channel +// FLOW-CARDINALITY: flow value has more than one functional use +// PORT-TARGET: unresolved port target role '@missing' +// PORT-DUAL: port source and target roles must be dual +// PROTOCOL-PAYLOAD: channel payload 'i16' from mapped protocol role '@sender' to '@receiver' does not match any carrier event in protocol '@p' +// DETERMINISTIC: unresolved port source role '@first_missing' + +//--- duplicate-role.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "a", dual = @a, cardinality = "exclusive"}> : () -> () + }) : () -> () +} + +//--- unresolved-dual.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @missing, cardinality = "exclusive"}> : () -> () + }) : () -> () +} + +//--- asymmetric-dual.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @b, cardinality = "exclusive"}> : () -> () + }) : () -> () +} + +//--- unresolved-port-role.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({"ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> ()}) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @missing, to = @b, protocol_from = @missing, protocol_to = @b}> : () -> () + }) : () -> () +} + +//--- invalid-payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({"ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> ()}) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, @p>, from = @a, to = @b, protocol_from = @a, protocol_to = @b}> : () -> () + }) : () -> () +} + +//--- unresolved-protocol.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @b, protocol_from = @a, protocol_to = @b}> : () -> () + }) : () -> () +} + +//--- channel-outside.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.channel +} + +//--- unknown-interface.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.endpoint<@Missing, @a> +} + +//--- unknown-endpoint-role.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + }) : () -> () + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.endpoint<@I, @missing> +} + +//--- endpoint-cardinality.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + }) : () -> () + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.endpoint<@I, @a> + %a = "builtin.unrealized_conversion_cast"(%x) : (!ac.endpoint<@I, @a>) -> i1 + %b = "builtin.unrealized_conversion_cast"(%x) : (!ac.endpoint<@I, @a>) -> i1 +} + +//--- flow-protocol.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.flow +} + +//--- duplicate-port.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "forward", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.event"() <{sym_name = "reverse", from = @b, to = @a, payload = i8, action = "notify"}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @b, protocol_from = @a, protocol_to = @b}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @b, to = @a, protocol_from = @b, protocol_to = @a}> : () -> () + }) : () -> () +} + +//--- bad-cardinality.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "many"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + }) : () -> () +} + +//--- self-dual.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @a, cardinality = "exclusive"}> : () -> () + }) : () -> () +} + +//--- non-channel-port.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = i8, from = @a, to = @b, protocol_from = @a, protocol_to = @b}> : () -> () + }) : () -> () +} + +//--- flow-cardinality.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "send", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + }) : () -> () + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.flow + %a = "builtin.unrealized_conversion_cast"(%x) : (!ac.flow) -> i1 + %b = "builtin.unrealized_conversion_cast"(%x) : (!ac.flow) -> i1 +} + +//--- deterministic.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({"ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> ()}) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @first_missing, to = @second_missing, protocol_from = @first_missing, protocol_to = @second_missing}> : () -> () + }) : () -> () +} + +//--- unresolved-port-target.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({"ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> ()}) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @missing, protocol_from = @a, protocol_to = @missing}> : () -> () + }) : () -> () +} + +//--- nondual-port.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({"ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> ()}) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "c", dual = @d, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "d", dual = @c, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @c, protocol_from = @a, protocol_to = @c}> : () -> () + }) : () -> () +} + +//--- protocol-payload-mismatch.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "sender", dual = @receiver, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "receiver", dual = @sender, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "send", from = @sender, to = @receiver, payload = i8, action = "notify"}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @b, protocol_from = @sender, protocol_to = @receiver}> : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/interfaces-review-r1-invalid.mlir b/components/agentic-circuit/test/ACIR/interfaces-review-r1-invalid.mlir new file mode 100644 index 00000000..bf52a6bc --- /dev/null +++ b/components/agentic-circuit/test/ACIR/interfaces-review-r1-invalid.mlir @@ -0,0 +1,135 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/flow-mismatch.mlir 2>&1 | %FileCheck %s --check-prefix=FLOW +// RUN: %not %acir_opt %t/direction-mismatch.mlir 2>&1 | %FileCheck %s --check-prefix=DIRECTION +// RUN: %not %acir_opt %t/control-only.mlir 2>&1 | %FileCheck %s --check-prefix=CONTROL +// RUN: %not %acir_opt %t/notify-correlation.mlir 2>&1 | %FileCheck %s --check-prefix=NOTIFY-CORR +// RUN: %not %acir_opt %t/correlation-type.mlir 2>&1 | %FileCheck %s --check-prefix=CORR-TYPE +// RUN: %not %acir_opt %t/unreachable-response.mlir 2>&1 | %FileCheck %s --check-prefix=RESPONSE +// RUN: %not %acir_opt %t/unreachable-accept.mlir 2>&1 | %FileCheck %s --check-prefix=ACCEPT +// RUN: %not %acir_opt %t/unreachable-terminal.mlir 2>&1 | %FileCheck %s --check-prefix=TERMINAL + +// FLOW: flow payload 'i16' does not match any carrier event in protocol '@p' +// DIRECTION: channel payload 'i8' from mapped protocol role '@b' to '@a' does not match any carrier event in protocol '@p' +// CONTROL: channel payload 'i8' from mapped protocol role '@a' to '@b' does not match any carrier event in protocol '@p' +// NOTIFY-CORR: correlation field 'tag' is missing from reachable offer/response payload +// CORR-TYPE: correlation field 'tag' has type 'i8' but expected 'i16' +// RESPONSE: on_response completion requires a reachable response event +// ACCEPT: on_accept completion requires a reachable accept event +// TERMINAL: on_terminal_phase completion requires a reachable terminal state + +//--- flow-mismatch.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e, transfer = true}> ({}) : () -> () + }) : () -> () + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.flow +} + +//--- direction-mismatch.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e, transfer = true}> ({}) : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @b, to = @a, protocol_from = @b, protocol_to = @a}> : () -> () + }) : () -> () +} + +//--- control-only.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "accept"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({}) : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @b, protocol_from = @a, protocol_to = @b}> : () -> () + }) : () -> () +} + +//--- notify-correlation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Req", fields = [{name = "id", type = i8}]}> : () -> () + "ac.transaction"() <{sym_name = "Meta", fields = [{name = "tag", type = i8}]}> : () -> () + }) : () -> () + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = !ac.transaction<@types::@Req>, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "response", from = @b, to = @a, payload = !ac.transaction<@types::@Req>, action = "response"}> : () -> () + "ac.event"() <{sym_name = "noise", from = @a, to = @b, payload = !ac.transaction<@types::@Meta>, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @response}> ({}) : () -> () + "ac.transition"() <{source = @s, target = @s, event = @offer, transfer = true}> ({}) : () -> () + "ac.transition"() <{source = @s, target = @s, event = @noise}> ({}) : () -> () + "ac.guarantee"() <{kind = "correlation", value = "tag"}> : () -> () + }) : () -> () +} + +//--- correlation-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Req", fields = [{name = "tag", type = i16}]}> : () -> () + "ac.transaction"() <{sym_name = "Resp", fields = [{name = "tag", type = i8}]}> : () -> () + }) : () -> () + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = !ac.transaction<@types::@Req>, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "response", from = @b, to = @a, payload = !ac.transaction<@types::@Resp>, action = "response"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @response}> ({}) : () -> () + "ac.transition"() <{source = @s, target = @s, event = @offer, transfer = true}> ({}) : () -> () + "ac.guarantee"() <{kind = "correlation", value = "tag"}> : () -> () + }) : () -> () +} + +//--- unreachable-response.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "dead", initial = false, terminal = false}> : () -> () + "ac.event"() <{sym_name = "response", from = @b, to = @a, payload = i8, action = "response"}> : () -> () + "ac.transition"() <{source = @dead, target = @dead, event = @response}> ({}) : () -> () + "ac.guarantee"() <{kind = "correlation", value = "tag"}> : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_response"}> : () -> () + }) : () -> () +} + +//--- unreachable-accept.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "response", from = @b, to = @a, payload = i8, action = "response"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @response}> ({}) : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_accept"}> : () -> () + }) : () -> () +} + +//--- unreachable-terminal.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "dead", initial = false, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_terminal_phase"}> : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/interfaces-valid.mlir b/components/agentic-circuit/test/ACIR/interfaces-valid.mlir new file mode 100644 index 00000000..2e1a64a1 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/interfaces-valid.mlir @@ -0,0 +1,52 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "wire"}> ({ + "ac.role"() <{sym_name = "sender", dual = @receiver, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "receiver", dual = @sender, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "idle", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "send", from = @sender, to = @receiver, payload = i8, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "accepted", from = @receiver, to = @sender, payload = i8, action = "accept"}> : () -> () + "ac.transition"() <{source = @idle, target = @idle, event = @send, transfer = true}> ({}) : () -> () + "ac.transition"() <{source = @idle, target = @idle, event = @accepted}> ({}) : () -> () + "ac.guarantee"() <{kind = "backpressure", value = "none"}> : () -> () + "ac.guarantee"() <{kind = "ordering", value = "fifo"}> : () -> () + "ac.guarantee"() <{kind = "delivery", value = "exactly_once"}> : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_accept"}> : () -> () + "ac.guarantee"() <{kind = "max_inflight", value = 1 : i64}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "Wire"}> ({ + "ac.role"() <{sym_name = "source", dual = @sink, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "sink", dual = @source, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "data", type = !ac.channel, from = @source, to = @sink, protocol_from = @sender, protocol_to = @receiver}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "SharedWire"}> ({ + "ac.role"() <{sym_name = "source", dual = @sink, cardinality = "shared"}> : () -> () + "ac.role"() <{sym_name = "sink", dual = @source, cardinality = "shared"}> : () -> () + }) : () -> () + "ac.protocol"() <{sym_name = "exchange"}> ({ + "ac.role"() <{sym_name = "initiator", dual = @target, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "target", dual = @initiator, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "ready", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "request", from = @initiator, to = @target, payload = i8, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "reply", from = @target, to = @initiator, payload = i16, action = "response"}> : () -> () + "ac.transition"() <{source = @ready, target = @ready, event = @request, transfer = true}> ({}) : () -> () + "ac.transition"() <{source = @ready, target = @ready, event = @reply}> ({}) : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "Exchange"}> ({ + "ac.role"() <{sym_name = "initiator", dual = @target, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "target", dual = @initiator, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "request", type = !ac.channel, from = @initiator, to = @target, protocol_from = @initiator, protocol_to = @target}> : () -> () + "ac.port"() <{sym_name = "reply", type = !ac.channel, from = @target, to = @initiator, protocol_from = @target, protocol_to = @initiator}> : () -> () + }) : () -> () + %endpoint = "builtin.unrealized_conversion_cast"() : () -> !ac.endpoint<@Wire, @source> + %shared = "builtin.unrealized_conversion_cast"() : () -> !ac.endpoint<@SharedWire, @source> + %use0 = "builtin.unrealized_conversion_cast"(%shared) : (!ac.endpoint<@SharedWire, @source>) -> i1 + %use1 = "builtin.unrealized_conversion_cast"(%shared) : (!ac.endpoint<@SharedWire, @source>) -> i1 +} + +// CHECK: ac.interface +// CHECK: ac.port +// CHECK-SAME: !ac.channel +// CHECK: !ac.endpoint<@Wire, @source> diff --git a/components/agentic-circuit/test/ACIR/memory-invalid.mlir b/components/agentic-circuit/test/ACIR/memory-invalid.mlir new file mode 100644 index 00000000..43ee944d --- /dev/null +++ b/components/agentic-circuit/test/ACIR/memory-invalid.mlir @@ -0,0 +1,160 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/entries.mlir 2>&1 | %FileCheck %s --check-prefix=ENTRIES +// RUN: %not %acir_opt %t/latency.mlir 2>&1 | %FileCheck %s --check-prefix=LATENCY +// RUN: %not %acir_opt %t/init.mlir 2>&1 | %FileCheck %s --check-prefix=INIT +// RUN: %not %acir_opt %t/write.mlir 2>&1 | %FileCheck %s --check-prefix=WRITE +// RUN: %not %acir_opt %t/sibling.mlir 2>&1 | %FileCheck %s --check-prefix=OUTSIDE +// RUN: %not %acir_opt %t/prefix.mlir 2>&1 | %FileCheck %s --check-prefix=OUTSIDE +// RUN: %not %acir_opt %t/parent.mlir 2>&1 | %FileCheck %s --check-prefix=OUTSIDE +// RUN: %not %acir_opt %t/shadowed-instance.mlir 2>&1 | %FileCheck %s --check-prefix=SHADOWED + +// ENTRIES: error: 'ac.memory.instance' op entries and latency must be positive +// LATENCY: error: 'ac.memory.instance' op entries and latency must be positive +// INIT: error: 'ac.memory.instance' op memory init must be zero +// WRITE: error: 'ac.memory.request' op write must yield !ac.var +// OUTSIDE: error: 'ac.memory.request' op memory instance is outside the request scope ancestry +// SHADOWED: error: 'ac.memory.instance' op must have at least one memory.request endpoint + +//--- entries.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.memory.instance @bad data i16 entries 0 init 0 latency 1 owner "/" stable_id "memory/bad" +} + +//--- latency.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.memory.instance @bad data i16 entries 16 init 0 latency 0 owner "/" stable_id "memory/bad" +} + +//--- init.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.memory.instance @bad data i16 entries 16 init 1 latency 1 owner "/" stable_id "memory/bad" +} + +//--- write.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.type_scope @types { + ac.struct @Request fields [{name = "address", type = i8}, {name = "write", type = i1}, {name = "data", type = i16}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 2 : i64, endianness = "little", preferred_alignment = 2 : i64, size = 4 : i64}>} + ac.memory.instance @sram data i16 entries 16 init 0 latency 1 owner "/" stable_id "memory/sram" + %input = ac.source depth 1 latency 1 : !ac.queue> + %bad = ac.memory.request @sram, %input ordinal 0 result_field "data" depth 1 address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var + } write { + ^write(%item: !ac.var>): + %bad_write = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %bad_write : !ac.var + } data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var + } {ac.endpoint_path = "/bad", ac.name = "bad"} : !ac.queue> -> !ac.queue> +} + +//--- sibling.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.type_scope @types { + ac.struct @Request fields [{name = "address", type = i8}, {name = "write", type = i1}, {name = "data", type = i16}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 2 : i64, endianness = "little", preferred_alignment = 2 : i64, size = 4 : i64}>} + ac.memory.instance @sram data i16 entries 16 init 0 latency 1 owner "/owner" stable_id "memory/owner/sram" + ac.scope @owner() { ac.scope.yield } : () -> () + ac.scope @sibling() { + %input = ac.source depth 1 latency 1 : !ac.queue> + %response = ac.memory.request @sram, %input ordinal 0 result_field "data" depth 1 address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var + } write { + ^write(%item: !ac.var>): + %write = ac.var.get %item field "write" : !ac.var> -> !ac.var + ac.memory.yield %write : !ac.var + } data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var + } {ac.endpoint_path = "/sibling/response", ac.name = "response"} : !ac.queue> -> !ac.queue> + ac.sink %response : !ac.queue> + ac.scope.yield + } : () -> () +} + +//--- prefix.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.type_scope @types { + ac.struct @Request fields [{name = "address", type = i8}, {name = "write", type = i1}, {name = "data", type = i16}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 2 : i64, endianness = "little", preferred_alignment = 2 : i64, size = 4 : i64}>} + ac.memory.instance @sram data i16 entries 16 init 0 latency 1 owner "/owner" stable_id "memory/owner/sram" + ac.scope @owner() { ac.scope.yield } : () -> () + ac.scope @owner_prefix() { + %input = ac.source depth 1 latency 1 : !ac.queue> + %response = ac.memory.request @sram, %input ordinal 0 result_field "data" depth 1 address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var + } write { + ^write(%item: !ac.var>): + %write = ac.var.get %item field "write" : !ac.var> -> !ac.var + ac.memory.yield %write : !ac.var + } data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var + } {ac.endpoint_path = "/owner_prefix/response", ac.name = "response"} : !ac.queue> -> !ac.queue> + ac.sink %response : !ac.queue> + ac.scope.yield + } : () -> () +} + +//--- parent.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.type_scope @types { + ac.struct @Request fields [{name = "address", type = i8}, {name = "write", type = i1}, {name = "data", type = i16}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 2 : i64, endianness = "little", preferred_alignment = 2 : i64, size = 4 : i64}>} + ac.memory.instance @sram data i16 entries 16 init 0 latency 1 owner "/owner" stable_id "memory/owner/sram" + ac.scope @owner() { ac.scope.yield } : () -> () + %input = ac.source depth 1 latency 1 : !ac.queue> + %response = ac.memory.request @sram, %input ordinal 0 result_field "data" depth 1 address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var + } write { + ^write(%item: !ac.var>): + %write = ac.var.get %item field "write" : !ac.var> -> !ac.var + ac.memory.yield %write : !ac.var + } data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var + } {ac.endpoint_path = "/response", ac.name = "response"} : !ac.queue> -> !ac.queue> + ac.sink %response : !ac.queue> +} + +//--- shadowed-instance.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.memory.instance @sram data i16 entries 16 init 0 latency 1 owner "/" stable_id "memory/sram" + builtin.module @nested attributes {ac.contract_epoch = "0.3"} { + ac.type_scope @types { + ac.struct @Request fields [{name = "address", type = i8}, {name = "write", type = i1}, {name = "data", type = i16}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 2 : i64, endianness = "little", preferred_alignment = 2 : i64, size = 4 : i64}>} + ac.memory.instance @sram data i16 entries 16 init 0 latency 1 owner "/inner" stable_id "memory/inner/sram" + ac.scope @inner() { + %input = ac.source depth 1 latency 1 : !ac.queue> + %response = ac.memory.request @sram, %input ordinal 0 result_field "data" depth 1 address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var + } write { + ^write(%item: !ac.var>): + %write = ac.var.get %item field "write" : !ac.var> -> !ac.var + ac.memory.yield %write : !ac.var + } data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var + } {ac.endpoint_path = "/inner/response", ac.name = "response"} : !ac.queue> -> !ac.queue> + ac.sink %response : !ac.queue> + ac.scope.yield + } : () -> () + } +} diff --git a/components/agentic-circuit/test/ACIR/memory.mlir b/components/agentic-circuit/test/ACIR/memory.mlir new file mode 100644 index 00000000..ba4fe5d9 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/memory.mlir @@ -0,0 +1,69 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.type_scope @types { + ac.struct @Request fields [{name = "address", type = i8}, {name = "write", type = i1}, {name = "data", type = i16}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 2 : i64, endianness = "little", preferred_alignment = 2 : i64, size = 4 : i64}>} + ac.memory.instance @sram data i16 entries 16 init 0 latency 3 owner "/" stable_id "memory/sram" + ac.memory.instance @owned data i16 entries 16 init 0 latency 2 owner "/owner" stable_id "memory/owner/owned" + %input = ac.source depth 4 latency 1 : !ac.queue> + %response = ac.memory.request @sram, %input ordinal 0 result_field "data" depth 4 address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var + } write { + ^write(%item: !ac.var>): + %write = ac.var.get %item field "write" : !ac.var> -> !ac.var + ac.memory.yield %write : !ac.var + } data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var + } {ac.endpoint_path = "/response", ac.name = "response"} : !ac.queue> -> !ac.queue> + ac.sink %response : !ac.queue> + ac.scope @owner() { + %same_input = ac.source depth 1 latency 1 : !ac.queue> + %same_response = ac.memory.request @owned, %same_input ordinal 0 result_field "data" depth 1 address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var + } write { + ^write(%item: !ac.var>): + %write = ac.var.get %item field "write" : !ac.var> -> !ac.var + ac.memory.yield %write : !ac.var + } data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var + } {ac.endpoint_path = "/owner/same_response", ac.name = "same_response"} : !ac.queue> -> !ac.queue> + ac.sink %same_response : !ac.queue> + ac.scope @child() { + %child_input = ac.source depth 1 latency 1 : !ac.queue> + %child_response = ac.memory.request @owned, %child_input ordinal 1 result_field "data" depth 1 address { + ^address(%item: !ac.var>): + %address = ac.var.get %item field "address" : !ac.var> -> !ac.var + ac.memory.yield %address : !ac.var + } write { + ^write(%item: !ac.var>): + %write = ac.var.get %item field "write" : !ac.var> -> !ac.var + ac.memory.yield %write : !ac.var + } data { + ^data(%item: !ac.var>): + %data = ac.var.get %item field "data" : !ac.var> -> !ac.var + ac.memory.yield %data : !ac.var + } {ac.endpoint_path = "/owner/child/child_response", ac.name = "child_response"} : !ac.queue> -> !ac.queue> + ac.sink %child_response : !ac.queue> + ac.scope.yield + } : () -> () + ac.scope.yield + } : () -> () +} + +// CHECK: ac.memory.instance @sram data i16 entries 16 init 0 latency 3 +// CHECK: ac.memory.instance @owned data i16 entries 16 init 0 latency 2 owner "/owner" stable_id "memory/owner/owned" +// CHECK: ac.memory.request @sram, %{{.*}} ordinal 0 result_field "data" depth 4 address +// CHECK: ac.memory.request @owned, %{{.*}} ordinal 0 result_field "data" depth 1 address +// CHECK: ac.memory.request @owned, %{{.*}} ordinal 1 result_field "data" depth 1 address diff --git a/components/agentic-circuit/test/ACIR/observe-invalid.mlir b/components/agentic-circuit/test/ACIR/observe-invalid.mlir new file mode 100644 index 00000000..d6117c65 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/observe-invalid.mlir @@ -0,0 +1,8 @@ +// RUN: %not %acir_opt %s 2>&1 | %FileCheck %s + +// CHECK: error: 'ac.observe' op name must be non-empty + +module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 2 latency 1 : !ac.queue + ac.observe %input name "" : !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/observe.mlir b/components/agentic-circuit/test/ACIR/observe.mlir new file mode 100644 index 00000000..cc780fea --- /dev/null +++ b/components/agentic-circuit/test/ACIR/observe.mlir @@ -0,0 +1,11 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 2 latency 1 : !ac.queue + ac.observe %input name "head" : !ac.queue + ac.sink %input : !ac.queue +} + +// CHECK: ac.observe {{.*}} name "head" +// CHECK: ac.sink diff --git a/components/agentic-circuit/test/ACIR/ownership-order-review-r2.mlir b/components/agentic-circuit/test/ACIR/ownership-order-review-r2.mlir new file mode 100644 index 00000000..3f628beb --- /dev/null +++ b/components/agentic-circuit/test/ACIR/ownership-order-review-r2.mlir @@ -0,0 +1,104 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt %t/forward.mlir > %t/forward.out +// RUN: %acir_opt %t/reverse.mlir > %t/reverse.out +// RUN: %acir_opt %t/legal-cycle.mlir > /dev/null +// RUN: %not %acir_opt %t/illegal-cycle.mlir 2>&1 | %FileCheck %s --check-prefix=CYCLE +// RUN: %not %acir_opt %t/conflict-forward.mlir 2>&1 | %FileCheck %s --check-prefix=CONFLICT +// RUN: %not %acir_opt %t/conflict-reverse.mlir 2>&1 | %FileCheck %s --check-prefix=CONFLICT + +// CONFLICT: ownership state conflict at join state '@join' +// CYCLE: ownership state conflict at join state '@start' + +//--- forward.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s0", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "s1", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "s2", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "step", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.transition"() <{source = @s0, target = @s1, event = @step}> ({}) : () -> () + "ac.transition"() <{source = @s1, target = @s2, event = @step}> ({}) : () -> () + }) : () -> () +} + +//--- reverse.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s0", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "s1", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "s2", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "step", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.transition"() <{source = @s1, target = @s2, event = @step}> ({}) : () -> () + "ac.transition"() <{source = @s0, target = @s1, event = @step}> ({}) : () -> () + }) : () -> () +} + +//--- legal-cycle.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s0", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "s1", initial = false, terminal = false}> : () -> () + "ac.event"() <{sym_name = "step", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.transition"() <{source = @s1, target = @s0, event = @step}> ({}) : () -> () + "ac.transition"() <{source = @s0, target = @s1, event = @step}> ({}) : () -> () + }) : () -> () +} + +//--- conflict-forward.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "start", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "pending", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "clear", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "join", initial = false, terminal = false}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "step", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @start, target = @pending, event = @offer, retain = true}> ({}) : () -> () + "ac.transition"() <{source = @start, target = @clear, event = @step}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @join, event = @step}> ({}) : () -> () + "ac.transition"() <{source = @clear, target = @join, event = @step}> ({}) : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + }) : () -> () +} + +//--- illegal-cycle.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "start", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "pending", initial = false, terminal = false}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "step", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @start, target = @pending, event = @offer, retain = true}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @start, event = @step}> ({}) : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + }) : () -> () +} + +//--- conflict-reverse.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "start", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "pending", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "clear", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "join", initial = false, terminal = false}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "step", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @clear, target = @join, event = @step}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @join, event = @step}> ({}) : () -> () + "ac.transition"() <{source = @start, target = @clear, event = @step}> ({}) : () -> () + "ac.transition"() <{source = @start, target = @pending, event = @offer, retain = true}> ({}) : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/port-mapping-review-r2.mlir b/components/agentic-circuit/test/ACIR/port-mapping-review-r2.mlir new file mode 100644 index 00000000..85f39fae --- /dev/null +++ b/components/agentic-circuit/test/ACIR/port-mapping-review-r2.mlir @@ -0,0 +1,89 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/missing.mlir 2>&1 | %FileCheck %s --check-prefix=MISSING +// RUN: %not %acir_opt %t/missing-to.mlir 2>&1 | %FileCheck %s --check-prefix=MISSING-TO +// RUN: %not %acir_opt %t/unresolved.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED +// RUN: %not %acir_opt %t/nondual.mlir 2>&1 | %FileCheck %s --check-prefix=NONDUAL +// RUN: %not %acir_opt %t/cardinality.mlir 2>&1 | %FileCheck %s --check-prefix=CARDINALITY + +// MISSING: requires attribute 'protocol_from' +// MISSING-TO: requires attribute 'protocol_to' +// UNRESOLVED: unresolved mapped protocol source role '@missing' +// NONDUAL: mapped protocol roles must be dual +// CARDINALITY: interface and mapped protocol roles must have matching cardinality + +//--- missing.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @b}> : () -> () + }) : () -> () +} + +//--- missing-to.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @b, protocol_from = @a}> : () -> () + }) : () -> () +} + +//--- unresolved.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "source", dual = @sink, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "sink", dual = @source, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @source, to = @sink, protocol_from = @missing, protocol_to = @b}> : () -> () + }) : () -> () +} + +//--- nondual.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "c", dual = @d, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "d", dual = @c, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @b, protocol_from = @a, protocol_to = @c}> : () -> () + }) : () -> () +} + +//--- cardinality.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "I"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "shared"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "shared"}> : () -> () + "ac.port"() <{sym_name = "x", type = !ac.channel, from = @a, to = @b, protocol_from = @a, protocol_to = @b}> : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/process-invalid.mlir b/components/agentic-circuit/test/ACIR/process-invalid.mlir new file mode 100644 index 00000000..2f850082 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/process-invalid.mlir @@ -0,0 +1,311 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/bad-kind.mlir 2>&1 | %FileCheck %s --check-prefix=KIND +// RUN: %not %acir_opt %t/no-suspend.mlir 2>&1 | %FileCheck %s --check-prefix=PROGRESS +// RUN: %not %acir_opt %t/linear-live.mlir 2>&1 | %FileCheck %s --check-prefix=LIVE +// RUN: %not %acir_opt %t/topology.mlir 2>&1 | %FileCheck %s --check-prefix=TOPOLOGY +// RUN: %not %acir_opt %t/missing-termination.mlir 2>&1 | %FileCheck %s --check-prefix=TERMINATION +// RUN: %not %acir_opt %t/capture-mismatch.mlir 2>&1 | %FileCheck %s --check-prefix=CAPTURE +// RUN: %not %acir_opt %t/result-live.mlir 2>&1 | %FileCheck %s --check-prefix=RESULT-LIVE +// RUN: %not %acir_opt %t/duplicate-owner-name.mlir 2>&1 | %FileCheck %s --check-prefix=OWNER-NAME +// RUN: %not %acir_opt %t/unstable-owner-segment.mlir 2>&1 | %FileCheck %s --check-prefix=OWNER-SEGMENT +// RUN: %not %acir_opt %t/unreachable-suspension.mlir 2>&1 | %FileCheck %s --check-prefix=BACKEDGE +// RUN: %not %acir_opt %t/for-iter-arg-live.mlir 2>&1 | %FileCheck %s --check-prefix=ITER-LIVE +// RUN: %not %acir_opt %t/if-path-live.mlir 2>&1 | %FileCheck %s --check-prefix=IF-LIVE +// RUN: %not %acir_opt %t/while-iter-arg-live.mlir 2>&1 | %FileCheck %s --check-prefix=WHILE-LIVE +// RUN: %not %acir_opt %t/malformed-scf.mlir 2>&1 | %FileCheck %s --check-prefix=MALFORMED +// RUN: /bin/sh -c '%acir_opt %t/malformed-if-arity.mlir > %t/malformed-if-arity.out 2>&1; status=$?; test $status -gt 0 && test $status -lt 128' +// RUN: %FileCheck %s --check-prefix=MALFORMED-IF-ARITY < %t/malformed-if-arity.out +// RUN: /bin/sh -c '%acir_opt %t/malformed-for-arity.mlir > %t/malformed-for-arity.out 2>&1; status=$?; test $status -gt 0 && test $status -lt 128' +// RUN: %FileCheck %s --check-prefix=MALFORMED-FOR-ARITY < %t/malformed-for-arity.out +// RUN: /bin/sh -c '%acir_opt %t/malformed-while-arity.mlir > %t/malformed-while-arity.out 2>&1; status=$?; test $status -gt 0 && test $status -lt 128' +// RUN: %FileCheck %s --check-prefix=MALFORMED-WHILE-ARITY < %t/malformed-while-arity.out +// RUN: %not %acir_opt %t/dynamic-for-no-suspend.mlir 2>&1 | %FileCheck %s --check-prefix=DYNAMIC-FOR + +//--- bad-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "thread" { ac.yield_sim } + ac.return + } +} +// KIND: kind must be 'control', 'workload', or 'monitor' + +//--- no-suspend.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + %t = arith.constant true + scf.while (%arg = %t) : (i1) -> i1 { + scf.condition(%arg) %arg : i1 + } do { + ^bb0(%arg : i1): + scf.yield %arg : i1 + } + ac.yield_sim + } + ac.return + } +} +// PROGRESS: every scf.while backedge must suspend or prove bounded progress + +//--- linear-live.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(!ac.resource_token<@r>) parameters {} graph { + ^bb0(%token : !ac.resource_token<@r>): + ac.process @p kind "control" captures(%token : !ac.resource_token<@r>) { + ^bb0(%captured : !ac.resource_token<@r>): + %c1 = arith.constant 1 : i64 + ac.await_event @events + ac.schedule @worker %captured after %c1 : !ac.resource_token<@r> + ac.yield_sim + } + ac.return + } +} +// LIVE: cannot remain live across suspension + +//--- topology.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + ac.instance @illegal of @M() static {} id "illegal" path "illegal" : () -> () + ac.yield_sim + } + ac.return + } +} +// TOPOLOGY: ac.process contains unsupported operation ac.instance + +//--- missing-termination.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { %zero = arith.constant 0 : i64 } + ac.return + } +} +// TERMINATION: body must terminate with ac.yield_sim + +//--- capture-mismatch.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(i32) parameters {} graph { + ^bb0(%value : i32): + ac.process @p kind "control" captures(%value : i32) { + ^bb0(%wrong : i64): + ac.yield_sim + } + ac.return + } +} +// CAPTURE: body arguments must exactly match capture types + +//--- result-live.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + %one = arith.constant 1 : i64 + %token, %received = ac.try_recv @tokens : !ac.resource_token<@r> + ac.await_event @events + ac.schedule @worker %token after %one : !ac.resource_token<@r> + ac.yield_sim + } + ac.return + } +} +// RESULT-LIVE: cannot remain live across suspension + +//--- duplicate-owner-name.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @state kind "control" { ac.yield_sim } + ac.stat @state kind "counter" + ac.return + } +} +// OWNER-NAME: duplicate local structural name 'state' + +//--- unstable-owner-segment.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @"bad.name" kind "control" { ac.yield_sim } + ac.return + } +} +// OWNER-SEGMENT: symbol name must be one stable hierarchy owner segment + +//--- unreachable-suspension.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + %true = arith.constant true + %false = arith.constant false + scf.while : () -> () { + scf.condition(%true) + } do { + scf.if %false { + ac.wait_until %true + } + scf.yield + } + ac.yield_sim + } + ac.return + } +} +// BACKEDGE: every scf.while backedge must suspend or prove bounded progress + +//--- for-iter-arg-live.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(!ac.resource_token<@r>) parameters {} graph { + ^bb0(%token : !ac.resource_token<@r>): + ac.process @p kind "control" captures(%token : !ac.resource_token<@r>) { + ^bb0(%captured : !ac.resource_token<@r>): + %lb = arith.constant 0 : index + %ub = arith.constant 4 : index + %step = arith.constant 1 : index + %true = arith.constant true + %result = scf.for %i = %lb to %ub step %step + iter_args(%iter = %captured) -> (!ac.resource_token<@r>) { + ac.wait_until %true + scf.yield %iter : !ac.resource_token<@r> + } + ac.yield_sim + } + ac.return + } +} +// ITER-LIVE: cannot remain live across suspension + +//--- if-path-live.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(!ac.resource_token<@r>, !ac.resource_token<@r>, i1) + parameters {} graph { + ^bb0(%token : !ac.resource_token<@r>, %worker_token : !ac.resource_token<@r>, + %condition : i1): + ac.process @worker kind "workload" + captures(%worker_token : !ac.resource_token<@r>) { + ^bb0(%value : !ac.resource_token<@r>): + ac.yield_sim + } + ac.process @p kind "control" + captures(%token, %condition : !ac.resource_token<@r>, i1) { + ^bb0(%captured : !ac.resource_token<@r>, %branch : i1): + %delay = arith.constant 1 : i64 + scf.if %branch { + ac.wait_until %branch + ac.schedule @worker %captured after %delay : !ac.resource_token<@r> + } + ac.yield_sim + } + ac.return + } +} +// IF-LIVE: cannot remain live across suspension + +//--- while-iter-arg-live.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(!ac.resource_token<@r>, i1) parameters {} graph { + ^bb0(%token : !ac.resource_token<@r>, %condition : i1): + ac.process @p kind "control" + captures(%token, %condition : !ac.resource_token<@r>, i1) { + ^bb0(%captured : !ac.resource_token<@r>, %keep_running : i1): + %result = scf.while (%iter = %captured) + : (!ac.resource_token<@r>) -> !ac.resource_token<@r> { + scf.condition(%keep_running) %iter : !ac.resource_token<@r> + } do { + ^bb0(%after : !ac.resource_token<@r>): + ac.wait_until %keep_running + scf.yield %after : !ac.resource_token<@r> + } + ac.yield_sim + } + ac.return + } +} +// WHILE-LIVE: cannot remain live across suspension + +//--- malformed-scf.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.process"() <{kind = "control", sym_name = "p"}> ({ + %true = "arith.constant"() <{value = true}> : () -> i1 + "scf.if"(%true) ({ + "ac.wait_until"(%true) : (i1) -> () + }) : (i1) -> () + "ac.yield_sim"() : () -> () + }) : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MALFORMED: malformed scf.if region must terminate with scf.yield + +//--- malformed-if-arity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.process"() <{kind = "control", sym_name = "p"}> ({ + %true = "arith.constant"() <{value = true}> : () -> i1 + %zero = "index.constant"() <{value = 0 : index}> : () -> index + %result = "scf.if"(%true) ({ + "scf.yield"() : () -> () + }, { + "scf.yield"(%zero) : (index) -> () + }) : (i1) -> index + "ac.yield_sim"() : () -> () + }) : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MALFORMED-IF-ARITY: malformed scf.if operand/result/block argument/yield arity or type mismatch + +//--- malformed-for-arity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.process"() <{kind = "control", sym_name = "p"}> ({ + %lb = "index.constant"() <{value = 0 : index}> : () -> index + %ub = "index.constant"() <{value = 4 : index}> : () -> index + %step = "index.constant"() <{value = 1 : index}> : () -> index + %seed = "index.constant"() <{value = 7 : index}> : () -> index + %result = "scf.for"(%lb, %ub, %step, %seed) ({ + ^bb0(%iter : index): + "scf.yield"(%iter) : (index) -> () + }) : (index, index, index, index) -> index + "ac.yield_sim"() : () -> () + }) : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MALFORMED-FOR-ARITY: malformed scf.for operand/result/block argument/yield arity or type mismatch + +//--- malformed-while-arity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.process"() <{kind = "control", sym_name = "p"}> ({ + %seed = "index.constant"() <{value = 7 : index}> : () -> index + %result = "scf.while"(%seed) ({ + ^bb0(%before : index): + %true = "arith.constant"() <{value = true}> : () -> i1 + "scf.condition"(%true) : (i1) -> () + }, { + ^bb0(%after : index): + "scf.yield"(%after) : (index) -> () + }) : (index) -> index + "ac.yield_sim"() : () -> () + }) : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// MALFORMED-WHILE-ARITY: malformed scf.while operand/result/block argument/yield arity or type mismatch + +//--- dynamic-for-no-suspend.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(index, index, index) parameters {} graph { + ^bb0(%lb : index, %ub : index, %step : index): + ac.process @p kind "control" + captures(%lb, %ub, %step : index, index, index) { + ^bb0(%l : index, %u : index, %s : index): + scf.for %i = %l to %u step %s { scf.yield } + ac.yield_sim + } + ac.return + } +} +// DYNAMIC-FOR: dynamic scf.for requires every reachable backedge to suspend diff --git a/components/agentic-circuit/test/ACIR/process-valid.mlir b/components/agentic-circuit/test/ACIR/process-valid.mlir new file mode 100644 index 00000000..303aa823 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/process-valid.mlir @@ -0,0 +1,81 @@ +// RUN: %acir_opt_public %s | %FileCheck %s +// RUN: %acir_opt_public %s | %acir_opt_public | %FileCheck %s +// RUN: %acir_opt_public --pass-pipeline='builtin.module(canonicalize,cse)' %s | %FileCheck %s --check-prefix=EFFECTS + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @fifo { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i32 action "offer" + ac.transition from @idle to @done on @push transfer true retain false guard {} + } + ac.module @Top(i32) parameters {} graph { + ^bb0(%arg0 : i32): + ac.time_domain @core period 1 phase 0 scale 1 + ac.queue @ready payload i32 entries 8 ordering "fifo" protocol @fifo + ownership "exclusive" id "ready" path "ready" + ac.event_queue @done payload !ac.event capacity 8 + ordering "time_then_sequence" domain @core id "done" path "done" + ac.resource @compute capacity 1 issue_width 1 ii 1 + latency {kind = "fixed", ticks = 1 : i64} + lifecycle {reservation = "propose_commit", release = "balanced", cancellation = "explicit"} + ownership "exclusive" classes [] id "compute" path "compute" + ac.process @worker kind "workload" captures(%arg0 : i32) { + ^bb0(%value : i32): + ac.yield_sim + } + ac.process @control kind "control" captures(%arg0 : i32) { + ^bb0(%capture : i32): + %c0 = arith.constant 0 : i64 + %c1 = arith.constant 1 : i64 + %idx = index.constant 0 + %ready = arith.cmpi eq, %c0, %c0 : i64 + %accepted = ac.try_send @ready %capture : i32 + %value, %received = ac.try_recv @ready : i32 + ac.schedule @worker %value after %c1 : i32 + ac.wait_until %ready + ac.wait_for @compute + ac.await_event @done + scf.if %accepted { + ac.assert %received, "receive follows accepted send" + } + ac.yield_sim + } + ac.return + } + ac.module @BranchLocal(i1) parameters {} graph { + ^bb0(%condition : i1): + ac.process @control kind "control" captures(%condition : i1) { + ^bb0(%branch : i1): + %cursor = ac.trace.open source "branch_linear" + %next, %captured, %advanced = ac.trace.next %cursor from source "branch_linear" : !ac.resource_token<@r> + scf.if %branch { + ac.wait_until %branch + } else { + %decoded = ac.trace.decode %captured : !ac.resource_token<@r> to i64 + } + ac.yield_sim + } + ac.return + } +} + +// CHECK: ac.process @control kind "control" captures(%{{.*}} : i32) +// CHECK: ac.try_send @ready +// CHECK: ac.try_recv @ready +// CHECK: ac.schedule @worker +// CHECK: ac.wait_until +// CHECK: ac.wait_for @compute +// CHECK: ac.await_event @done +// CHECK: scf.if +// CHECK: ac.yield_sim +// CHECK: ac.module @BranchLocal +// EFFECTS: ac.try_send +// EFFECTS: ac.try_recv +// EFFECTS: ac.schedule +// EFFECTS: ac.wait_until +// EFFECTS: ac.wait_for +// EFFECTS: ac.await_event +// EFFECTS: ac.yield_sim diff --git a/components/agentic-circuit/test/ACIR/protocols-invalid.mlir b/components/agentic-circuit/test/ACIR/protocols-invalid.mlir new file mode 100644 index 00000000..7a3f3d19 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/protocols-invalid.mlir @@ -0,0 +1,351 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/no-initial.mlir 2>&1 | %FileCheck %s --check-prefix=NO-INITIAL +// RUN: %not %acir_opt %t/multiple-initial.mlir 2>&1 | %FileCheck %s --check-prefix=MULTI-INITIAL +// RUN: %not %acir_opt %t/unresolved-state.mlir 2>&1 | %FileCheck %s --check-prefix=STATE +// RUN: %not %acir_opt %t/unresolved-event.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT +// RUN: %not %acir_opt %t/ambiguous.mlir 2>&1 | %FileCheck %s --check-prefix=AMBIG +// RUN: %not %acir_opt %t/duplicate-priority.mlir 2>&1 | %FileCheck %s --check-prefix=PRIORITY +// RUN: %not %acir_opt %t/impure-guard.mlir 2>&1 | %FileCheck %s --check-prefix=GUARD +// RUN: %not %acir_opt %t/bad-backpressure.mlir 2>&1 | %FileCheck %s --check-prefix=BACKPRESSURE +// RUN: %not %acir_opt %t/bad-ordering.mlir 2>&1 | %FileCheck %s --check-prefix=ORDERING +// RUN: %not %acir_opt %t/bad-delivery.mlir 2>&1 | %FileCheck %s --check-prefix=DELIVERY +// RUN: %not %acir_opt %t/bad-completion.mlir 2>&1 | %FileCheck %s --check-prefix=COMPLETION +// RUN: %not %acir_opt %t/unknown-guarantee.mlir 2>&1 | %FileCheck %s --check-prefix=UNKNOWN +// RUN: %not %acir_opt %t/unstable-pending.mlir 2>&1 | %FileCheck %s --check-prefix=STABLE +// RUN: %not %acir_opt %t/bad-max-inflight.mlir 2>&1 | %FileCheck %s --check-prefix=INFLIGHT +// RUN: %not %acir_opt %t/correlation.mlir 2>&1 | %FileCheck %s --check-prefix=CORRELATION +// RUN: %not %acir_opt %t/lost-offer.mlir 2>&1 | %FileCheck %s --check-prefix=LOST +// RUN: %not %acir_opt %t/duplicate-state.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-STATE +// RUN: %not %acir_opt %t/duplicate-event.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-EVENT +// RUN: %not %acir_opt %t/event-role.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT-ROLE +// RUN: %not %acir_opt %t/event-payload.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT-PAYLOAD +// RUN: %not %acir_opt %t/event-action.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT-ACTION +// RUN: %not %acir_opt %t/duplicate-guarantee.mlir 2>&1 | %FileCheck %s --check-prefix=DUP-GUARANTEE +// RUN: %not %acir_opt %t/retained-no-resolution.mlir 2>&1 | %FileCheck %s --check-prefix=NO-RESOLUTION +// RUN: %not %acir_opt %t/bad-correlation.mlir 2>&1 | %FileCheck %s --check-prefix=BAD-CORRELATION +// RUN: %not %acir_opt %t/missing-correlation-field.mlir 2>&1 | %FileCheck %s --check-prefix=MISSING-CORRELATION +// RUN: %not %acir_opt %t/custom-missing-contract.mlir 2>&1 | %FileCheck %s --check-prefix=CUSTOM +// RUN: %not %acir_opt %t/per-key-no-correlation.mlir 2>&1 | %FileCheck %s --check-prefix=PER-KEY +// RUN: %not %acir_opt %t/terminal-completion.mlir 2>&1 | %FileCheck %s --check-prefix=TERMINAL +// RUN: %not %acir_opt %t/unresolved-target.mlir 2>&1 | %FileCheck %s --check-prefix=TARGET +// RUN: %not %acir_opt %t/negative-priority.mlir 2>&1 | %FileCheck %s --check-prefix=NEGATIVE-PRIORITY + +// NO-INITIAL: protocol requires exactly one initial state, found 0 +// MULTI-INITIAL: protocol requires exactly one initial state, found 2 +// STATE: unresolved transition source state '@missing' +// EVENT: unresolved transition event '@missing' +// AMBIG: overlapping transitions require explicit priority +// PRIORITY: overlapping transitions require unique priority +// GUARD: guard operation 'ac.type_scope' is not in the pure expression allowlist +// BACKPRESSURE: unsupported backpressure value 'stall' +// ORDERING: unsupported ordering value 'global' +// DELIVERY: unsupported delivery value 'maybe' +// COMPLETION: unsupported completion value 'eventually' +// UNKNOWN: unknown mandatory protocol guarantee 'magic' +// STABLE: retained pending offer requires stable_pending = true +// INFLIGHT: max_inflight requires a positive i64 value +// CORRELATION: max_inflight greater than one requires correlation +// LOST: offer transition must transfer or retain ownership +// DUP-STATE: redefinition of symbol named 's' +// DUP-EVENT: redefinition of symbol named 'e' +// EVENT-ROLE: unresolved event source role '@missing' +// EVENT-PAYLOAD: event payload type must be a normative ACIR value type +// EVENT-ACTION: unsupported event action 'drop' +// DUP-GUARANTEE: duplicate protocol guarantee 'ordering' +// NO-RESOLUTION: pending ownership reaches state '@pending' with no outgoing transition +// BAD-CORRELATION: correlation requires a non-empty field name +// MISSING-CORRELATION: correlation field 'missing' is missing from reachable offer/response payload +// CUSTOM: custom backpressure requires a custom_backpressure declaration +// PER-KEY: per_key ordering requires correlation +// TERMINAL: on_terminal_phase completion requires a reachable terminal state +// TARGET: unresolved transition target state '@missing' +// NEGATIVE-PRIORITY: transition priority must be a non-negative i64 value + +//--- no-initial.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = false, terminal = true}> : () -> () + }) : () -> () +} + +//--- multiple-initial.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "a", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "b", initial = true, terminal = false}> : () -> () + }) : () -> () +} + +//--- unresolved-state.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.transition"() <{source = @missing, target = @s, event = @e}> ({}) : () -> () + }) : () -> () +} + +//--- unresolved-event.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @missing}> ({}) : () -> () + }) : () -> () +} + +//--- ambiguous.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({}) : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({}) : () -> () + }) : () -> () +} + +//--- duplicate-priority.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e, priority = 1 : i64}> ({}) : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e, priority = 1 : i64}> ({}) : () -> () + }) : () -> () +} + +//--- impure-guard.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({ + "ac.type_scope"() <{sym_name = "side_effect"}> ({}) : () -> () + }) : () -> () + }) : () -> () +} + +//--- bad-backpressure.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "backpressure", value = "stall"}> : () -> () + }) : () -> () +} + +//--- bad-ordering.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "ordering", value = "global"}> : () -> () + }) : () -> () +} + +//--- bad-delivery.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "delivery", value = "maybe"}> : () -> () + }) : () -> () +} + +//--- bad-completion.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "completion", value = "eventually"}> : () -> () + }) : () -> () +} + +//--- unknown-guarantee.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "magic", value = "unknown"}> : () -> () + }) : () -> () +} + +//--- unstable-pending.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e, retain = true}> ({}) : () -> () + }) : () -> () +} + +//--- bad-max-inflight.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "max_inflight", value = 0 : i64}> : () -> () + }) : () -> () +} + +//--- correlation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "max_inflight", value = 2 : i64}> : () -> () + }) : () -> () +} + +//--- lost-offer.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({}) : () -> () + }) : () -> () +} + +//--- duplicate-state.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "s", initial = false, terminal = true}> : () -> () + }) : () -> () +} + +//--- duplicate-event.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.event"() <{sym_name = "e", from = @b, to = @a, payload = i8, action = "notify"}> : () -> () + }) : () -> () +} + +//--- event-role.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @missing, to = @b, payload = i8, action = "notify"}> : () -> () + }) : () -> () +} + +//--- event-payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = !ac.endpoint<@I, @a>, action = "notify"}> : () -> () + }) : () -> () +} + +//--- event-action.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "drop"}> : () -> () + }) : () -> () +} + +//--- duplicate-guarantee.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "ordering", value = "fifo"}> : () -> () + "ac.guarantee"() <{kind = "ordering", value = "unordered"}> : () -> () + }) : () -> () +} + +//--- retained-no-resolution.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "pending", initial = false, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.transition"() <{source = @s, target = @pending, event = @e, retain = true}> ({}) : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + }) : () -> () +} + +//--- bad-correlation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "correlation", value = ""}> : () -> () + }) : () -> () +} + +//--- missing-correlation-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "tag", type = i8}]}> : () -> () + }) : () -> () + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = !ac.transaction<@types::@T>, action = "offer"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e, transfer = true}> ({}) : () -> () + "ac.guarantee"() <{kind = "correlation", value = "missing"}> : () -> () + }) : () -> () +} + +//--- custom-missing-contract.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "backpressure", value = "custom"}> : () -> () + }) : () -> () +} + +//--- per-key-no-correlation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.guarantee"() <{kind = "ordering", value = "per_key"}> : () -> () + }) : () -> () +} + +//--- terminal-completion.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_terminal_phase"}> : () -> () + }) : () -> () +} + +//--- unresolved-target.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @missing, event = @e}> ({}) : () -> () + }) : () -> () +} + +//--- negative-priority.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e, priority = -1 : i64}> ({}) : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/protocols-review-r1-invalid.mlir b/components/agentic-circuit/test/ACIR/protocols-review-r1-invalid.mlir new file mode 100644 index 00000000..528537f7 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/protocols-review-r1-invalid.mlir @@ -0,0 +1,206 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/transfer-retain.mlir 2>&1 | %FileCheck %s --check-prefix=TRANSFER-RETAIN +// RUN: %not %acir_opt %t/orphan-transfer.mlir 2>&1 | %FileCheck %s --check-prefix=ORPHAN +// RUN: %not %acir_opt %t/retry-no-retain.mlir 2>&1 | %FileCheck %s --check-prefix=RETRY +// RUN: %not %acir_opt %t/drop-branch.mlir 2>&1 | %FileCheck %s --check-prefix=DROP +// RUN: %not %acir_opt %t/pending-terminal.mlir 2>&1 | %FileCheck %s --check-prefix=PENDING-TERMINAL +// RUN: %not %acir_opt %t/conflicting-join.mlir 2>&1 | %FileCheck %s --check-prefix=JOIN +// RUN: %not %acir_opt %t/scf-guard.mlir 2>&1 | %FileCheck %s --check-prefix=SCF +// RUN: %not %acir_opt %t/unknown-pure-guard.mlir 2>&1 | %FileCheck %s --check-prefix=UNKNOWN-GUARD +// RUN: %not %acir_opt %t/container-guard.mlir 2>&1 | %FileCheck %s --check-prefix=CONTAINER-GUARD +// RUN: %not %acir_opt %t/interface-child.mlir 2>&1 | %FileCheck %s --check-prefix=INTERFACE-CHILD +// RUN: %not %acir_opt %t/protocol-child.mlir 2>&1 | %FileCheck %s --check-prefix=PROTOCOL-CHILD +// RUN: %not %acir_opt %t/dual-cardinality.mlir 2>&1 | %FileCheck %s --check-prefix=DUAL-CARDINALITY +// RUN: %not %acir_opt %t/event-same-role.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT-DIRECTION +// RUN: %not %acir_opt %t/event-target.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT-TARGET + +// TRANSFER-RETAIN: transition cannot both transfer and retain ownership +// ORPHAN: ownership transfer requires a pending offer +// RETRY: retry transition must retain the pending offer +// DROP: pending ownership reaches state '@drop' with no outgoing transition +// PENDING-TERMINAL: terminal state '@done' is reachable with pending ownership +// JOIN: ownership state conflict at join state '@join' +// SCF: guard operation 'scf.yield' is not in the pure expression allowlist +// UNKNOWN-GUARD: guard operation 'builtin.unrealized_conversion_cast' is not in the pure expression allowlist +// CONTAINER-GUARD: guard operation 'ac.type_scope' is not in the pure expression allowlist +// INTERFACE-CHILD: interface body only permits ac.role and ac.port, found ac.state +// PROTOCOL-CHILD: protocol body contains unsupported operation ac.port +// DUAL-CARDINALITY: dual roles must have matching cardinality +// EVENT-DIRECTION: event source and target roles must differ +// EVENT-TARGET: unresolved event target role '@missing' + +//--- transfer-retain.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @offer, transfer = true, retain = true}> ({}) : () -> () + }) : () -> () +} + +//--- orphan-transfer.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "done", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "accept", from = @b, to = @a, payload = i8, action = "accept"}> : () -> () + "ac.transition"() <{source = @s, target = @done, event = @accept, transfer = true}> ({}) : () -> () + }) : () -> () +} + +//--- retry-no-retain.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "idle", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "pending", initial = false, terminal = false}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "retry", from = @a, to = @b, payload = i8, action = "retry"}> : () -> () + "ac.transition"() <{source = @idle, target = @pending, event = @offer, retain = true}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @pending, event = @retry}> ({}) : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + }) : () -> () +} + +//--- drop-branch.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "idle", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "pending", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "drop", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "done", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "cancel", from = @a, to = @b, payload = i8, action = "cancel"}> : () -> () + "ac.event"() <{sym_name = "tick", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @idle, target = @pending, event = @offer, retain = true}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @done, event = @cancel}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @drop, event = @tick}> ({}) : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + }) : () -> () +} + +//--- pending-terminal.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "idle", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "done", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.transition"() <{source = @idle, target = @done, event = @offer, retain = true}> ({}) : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + }) : () -> () +} + +//--- conflicting-join.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "idle", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "join", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "done", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "offer", from = @a, to = @b, payload = i8, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "tick", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.event"() <{sym_name = "cancel", from = @a, to = @b, payload = i8, action = "cancel"}> : () -> () + "ac.transition"() <{source = @idle, target = @join, event = @offer, retain = true}> ({}) : () -> () + "ac.transition"() <{source = @idle, target = @join, event = @tick}> ({}) : () -> () + "ac.transition"() <{source = @join, target = @done, event = @cancel}> ({}) : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + }) : () -> () +} + +//--- scf-guard.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "tick", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @tick}> ({ + %true = "arith.constant"() <{value = true}> : () -> i1 + "scf.if"(%true) ({ + "scf.yield"() : () -> () + }, { + "scf.yield"() : () -> () + }) : (i1) -> () + }) : () -> () + }) : () -> () +} + +//--- unknown-pure-guard.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "tick", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @tick}> ({ + %x = "builtin.unrealized_conversion_cast"() : () -> i1 + }) : () -> () + }) : () -> () +} + +//--- container-guard.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "tick", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @tick}> ({ + "ac.type_scope"() <{sym_name = "bad"}> ({}) : () -> () + }) : () -> () + }) : () -> () +} + +//--- interface-child.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + "ac.state"() <{sym_name = "bad", initial = true, terminal = false}> : () -> () + }) : () -> () +} + +//--- protocol-child.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.port"() <{sym_name = "bad", type = i8, from = @a, to = @b, protocol_from = @a, protocol_to = @b}> : () -> () + }) : () -> () +} + +//--- dual-cardinality.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "shared"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + }) : () -> () +} + +//--- event-same-role.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @a, payload = i8, action = "notify"}> : () -> () + }) : () -> () +} + +//--- event-target.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @missing, payload = i8, action = "notify"}> : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/protocols-valid.mlir b/components/agentic-circuit/test/ACIR/protocols-valid.mlir new file mode 100644 index 00000000..5a84c548 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/protocols-valid.mlir @@ -0,0 +1,73 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Message", fields = [{name = "tag", type = i16}]}> : () -> () + "ac.transaction"() <{sym_name = "Req", fields = [{name = "id", type = i16}, {name = "data", type = i8}]}> : () -> () + "ac.transaction"() <{sym_name = "Resp", fields = [{name = "id", type = i16}, {name = "status", type = i1}]}> : () -> () + }) : () -> () + "ac.protocol"() <{sym_name = "handshake"}> ({ + "ac.role"() <{sym_name = "producer", dual = @consumer, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "consumer", dual = @producer, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "idle", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "pending", initial = false, terminal = false}> : () -> () + "ac.state"() <{sym_name = "done", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "offer", from = @producer, to = @consumer, payload = i32, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "accept", from = @consumer, to = @producer, payload = i32, action = "accept"}> : () -> () + "ac.event"() <{sym_name = "cancel", from = @producer, to = @consumer, payload = i32, action = "cancel"}> : () -> () + "ac.event"() <{sym_name = "reject", from = @consumer, to = @producer, payload = i32, action = "reject"}> : () -> () + "ac.event"() <{sym_name = "retry", from = @producer, to = @consumer, payload = i32, action = "retry"}> : () -> () + "ac.transition"() <{source = @idle, target = @pending, event = @offer, retain = true}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @done, event = @accept, transfer = true}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @done, event = @cancel}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @done, event = @reject}> ({}) : () -> () + "ac.transition"() <{source = @pending, target = @pending, event = @retry, retain = true}> ({}) : () -> () + "ac.guarantee"() <{kind = "backpressure", value = "accept"}> : () -> () + "ac.guarantee"() <{kind = "ordering", value = "fifo"}> : () -> () + "ac.guarantee"() <{kind = "delivery", value = "exactly_once"}> : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_accept"}> : () -> () + "ac.guarantee"() <{kind = "stable_pending", value = true}> : () -> () + "ac.guarantee"() <{kind = "max_inflight", value = 1 : i64}> : () -> () + }) : () -> () + + "ac.protocol"() <{sym_name = "correlated"}> ({ + "ac.role"() <{sym_name = "requester", dual = @responder, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "responder", dual = @requester, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "active", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "request", from = @requester, to = @responder, payload = !ac.transaction<@types::@Req>, action = "offer"}> : () -> () + "ac.event"() <{sym_name = "response", from = @responder, to = @requester, payload = !ac.transaction<@types::@Resp>, action = "response"}> : () -> () + "ac.transition"() <{source = @active, target = @active, event = @request, transfer = true}> ({}) : () -> () + "ac.transition"() <{source = @active, target = @active, event = @response}> ({ + %zero = "arith.constant"() <{value = 0 : i16}> : () -> i16 + %idx = "index.constant"() <{value = 0 : index}> : () -> index + %message = "ac.record.create"(%zero) <{field_names = ["tag"]}> : (i16) -> !ac.transaction<@types::@Message> + %tag = "ac.record.get"(%message) <{field = "tag"}> : (!ac.transaction<@types::@Message>) -> i16 + }) : () -> () + "ac.guarantee"() <{kind = "backpressure", value = "credit"}> : () -> () + "ac.guarantee"() <{kind = "ordering", value = "per_key"}> : () -> () + "ac.guarantee"() <{kind = "delivery", value = "exactly_once"}> : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_response"}> : () -> () + "ac.guarantee"() <{kind = "max_inflight", value = 4 : i64}> : () -> () + "ac.guarantee"() <{kind = "correlation", value = "id"}> : () -> () + }) : () -> () + + "ac.protocol"() <{sym_name = "terminal_completion"}> ({ + "ac.state"() <{sym_name = "start", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "done", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "finish", from = @producer, to = @consumer, payload = i1, action = "notify"}> : () -> () + "ac.role"() <{sym_name = "producer", dual = @consumer, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "consumer", dual = @producer, cardinality = "exclusive"}> : () -> () + "ac.transition"() <{source = @start, target = @done, event = @finish}> ({}) : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_terminal_phase"}> : () -> () + }) : () -> () + + %flow = "builtin.unrealized_conversion_cast"() : () -> !ac.flow +} + +// CHECK: ac.protocol +// CHECK: ac.role +// CHECK: ac.state +// CHECK: ac.event +// CHECK: ac.transition +// CHECK: ac.guarantee diff --git a/components/agentic-circuit/test/ACIR/queue-var-types-invalid.mlir b/components/agentic-circuit/test/ACIR/queue-var-types-invalid.mlir new file mode 100644 index 00000000..0f4e77e5 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/queue-var-types-invalid.mlir @@ -0,0 +1,86 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/queue-of-var.mlir 2>&1 | %FileCheck %s --check-prefix=QUEUE-OF-VAR +// RUN: %not %acir_opt %t/var-of-queue.mlir 2>&1 | %FileCheck %s --check-prefix=VAR-OF-QUEUE +// RUN: %not %acir_opt %t/queue-of-function.mlir 2>&1 | %FileCheck %s --check-prefix=QUEUE-OF-FUNCTION +// RUN: %not %acir_opt %t/var-of-function.mlir 2>&1 | %FileCheck %s --check-prefix=VAR-OF-FUNCTION +// RUN: %not %acir_opt %t/queue-of-list.mlir 2>&1 | %FileCheck %s --check-prefix=QUEUE-OF-LIST +// RUN: %not %acir_opt %t/var-of-list.mlir 2>&1 | %FileCheck %s --check-prefix=VAR-OF-LIST +// RUN: %not %acir_opt %t/array-zero.mlir 2>&1 | %FileCheck %s --check-prefix=ARRAY-ZERO +// RUN: %not %acir_opt %t/array-payload.mlir 2>&1 | %FileCheck %s --check-prefix=ARRAY-PAYLOAD +// RUN: %not %acir_opt %t/map-duplicate.mlir 2>&1 | %FileCheck %s --check-prefix=MAP-DUPLICATE +// RUN: %not %acir_opt %t/map-order.mlir 2>&1 | %FileCheck %s --check-prefix=MAP-ORDER +// RUN: %not %acir_opt %t/set-zero.mlir 2>&1 | %FileCheck %s --check-prefix=SET-ZERO +// RUN: %not %acir_opt %t/set-payload.mlir 2>&1 | %FileCheck %s --check-prefix=SET-PAYLOAD + +// QUEUE-OF-VAR: error: queue payload must be an immutable ACIR value type +// VAR-OF-QUEUE: error: var payload must be an immutable ACIR value type +// QUEUE-OF-FUNCTION: error: queue payload must be an immutable ACIR value type +// VAR-OF-FUNCTION: error: var payload must be an immutable ACIR value type +// QUEUE-OF-LIST: error: queue payload must be an immutable ACIR value type +// VAR-OF-LIST: error: var payload must be an immutable ACIR value type +// ARRAY-ZERO: error: array length must be positive +// ARRAY-PAYLOAD: error: array element must be a queue, var, or static collection +// MAP-DUPLICATE: error: map keys must be non-empty and strictly lexicographic +// MAP-ORDER: error: map keys must be non-empty and strictly lexicographic +// SET-ZERO: error: set length must be positive +// SET-PAYLOAD: error: set element must be a queue, var, or static collection + +//--- queue-of-var.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.queue> +} + +//--- var-of-queue.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.var> +} + +//--- queue-of-function.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.queue<(i32) -> i32> +} + +//--- var-of-function.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.var<(i32) -> i32> +} + +//--- queue-of-list.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.queue> +} + +//--- var-of-list.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.var> +} + +//--- array-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.array<0 x !ac.queue> +} + +//--- array-payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.array<2 x i32> +} + +//--- map-duplicate.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.map<["lane", "lane"], !ac.queue> +} + +//--- map-order.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.map<["right", "left"], !ac.queue> +} + +//--- set-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.set<0 x !ac.var> +} + +//--- set-payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.set<2 x i32> +} diff --git a/components/agentic-circuit/test/ACIR/queue-var-types.mlir b/components/agentic-circuit/test/ACIR/queue-var-types.mlir new file mode 100644 index 00000000..f0226940 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/queue-var-types.mlir @@ -0,0 +1,27 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +// This phase-branch fixture uses the active file epoch while the +// replacement contract is built in verified slices. The final hard-break +// checkpoint updates every artifact and fixture to epoch 0.3 together. +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.var + "builtin.unrealized_conversion_cast"() : () -> !ac.var> + "builtin.unrealized_conversion_cast"() : () -> !ac.queue + "builtin.unrealized_conversion_cast"() : () -> !ac.queue> + "builtin.unrealized_conversion_cast"() : () -> !ac.array<4 x !ac.queue> + "builtin.unrealized_conversion_cast"() : () -> !ac.map<["cube", "scalar", "vector"], !ac.queue> + "builtin.unrealized_conversion_cast"() : () -> !ac.set<4 x !ac.var> + "builtin.unrealized_conversion_cast"() : () -> !ac.array<2 x !ac.map<["left", "right"], !ac.queue>>> +} + +// CHECK: !ac.var +// CHECK: !ac.var> +// CHECK: !ac.queue +// CHECK: !ac.queue> +// CHECK: !ac.array<4 x !ac.queue> +// CHECK: !ac.map<["cube", "scalar", "vector"], !ac.queue> +// CHECK: !ac.set<4 x !ac.var> +// CHECK: !ac.array<2 x !ac.map<["left", "right"], !ac.queue>>> diff --git a/components/agentic-circuit/test/ACIR/records-invalid.mlir b/components/agentic-circuit/test/ACIR/records-invalid.mlir new file mode 100644 index 00000000..7f52862b --- /dev/null +++ b/components/agentic-circuit/test/ACIR/records-invalid.mlir @@ -0,0 +1,85 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/missing-field.mlir 2>&1 | %FileCheck %s --check-prefix=MISSING +// RUN: %not %acir_opt %t/create-type.mlir 2>&1 | %FileCheck %s --check-prefix=CREATE-TYPE +// RUN: %not %acir_opt %t/get-type.mlir 2>&1 | %FileCheck %s --check-prefix=GET-TYPE +// RUN: %not %acir_opt %t/with-identity.mlir 2>&1 | %FileCheck %s --check-prefix=IDENTITY +// RUN: %not %acir_opt %t/serialize-kind.mlir 2>&1 | %FileCheck %s --check-prefix=SERIALIZE-KIND +// RUN: %not %acir_opt %t/serialize-width.mlir 2>&1 | %FileCheck %s --check-prefix=SERIALIZE-WIDTH +// RUN: %not %acir_opt %t/deserialize-identity.mlir 2>&1 | %FileCheck %s --check-prefix=DESERIALIZE-ID + +// MISSING: error: {{.*}}record.create fields must exactly match declaration +// CREATE-TYPE: error: {{.*}}field 'x' expects 'i8' but received 'i16' +// GET-TYPE: error: {{.*}}field 'x' has type 'i8' but operation returns 'i16' +// IDENTITY: error: {{.*}}record.with must preserve record identity +// SERIALIZE-KIND: error: {{.*}}packet.serialize requires a packet operand +// SERIALIZE-WIDTH: error: {{.*}}serialized byte vector width must equal packet serialization width 4 +// DESERIALIZE-ID: error: {{.*}}packet.deserialize result identity does not match serialization contract + +//--- missing-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Pair", fields = [{name = "x", type = i8}, {name = "y", type = i8}]}> : () -> () + }) : () -> () + %x = "builtin.unrealized_conversion_cast"() : () -> i8 + %v = "ac.record.create"(%x) <{field_names = ["x"]}> : (i8) -> !ac.transaction<@types::@Pair> +} + +//--- create-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "One", fields = [{name = "x", type = i8}]}> : () -> () + }) : () -> () + %x = "builtin.unrealized_conversion_cast"() : () -> i16 + %v = "ac.record.create"(%x) <{field_names = ["x"]}> : (i16) -> !ac.transaction<@types::@One> +} + +//--- get-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "One", fields = [{name = "x", type = i8}]}> : () -> () + }) : () -> () + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.transaction<@types::@One> + %x = "ac.record.get"(%v) <{field = "x"}> : (!ac.transaction<@types::@One>) -> i16 +} + +//--- with-identity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "A", fields = [{name = "x", type = i8}]}> : () -> () + "ac.transaction"() <{sym_name = "B", fields = [{name = "x", type = i8}]}> : () -> () + }) : () -> () + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.transaction<@types::@A> + %x = "builtin.unrealized_conversion_cast"() : () -> i8 + %bad = "ac.record.with"(%v, %x) <{field = "x"}> : (!ac.transaction<@types::@A>, i8) -> !ac.transaction<@types::@B> +} + +//--- serialize-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = []}> : () -> () + }) : () -> () + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.transaction<@types::@T> + %bytes = "ac.packet.serialize"(%v) <{packet = @types::@T}> : (!ac.transaction<@types::@T>) -> !ac.vector<1 x i8> +} + +//--- serialize-width.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.packet"() <{sym_name = "P", fields = []}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, serialization_width = 4 : i64, size = 8 : i64}>} : () -> () + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.packet<@types::@P> + %bytes = "ac.packet.serialize"(%v) <{packet = @types::@P}> : (!ac.packet<@types::@P>) -> !ac.vector<8 x i8> +} + +//--- deserialize-identity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.packet"() <{sym_name = "P", fields = []}> : () -> () + "ac.packet"() <{sym_name = "Q", fields = []}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec< + !ac.packet<@types::@P> = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, serialization_width = 4 : i64, size = 4 : i64}, + !ac.packet<@types::@Q> = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, serialization_width = 4 : i64, size = 4 : i64} + >} : () -> () + %bytes = "builtin.unrealized_conversion_cast"() : () -> !ac.vector<4 x i8> + %v = "ac.packet.deserialize"(%bytes) <{packet = @types::@P}> : (!ac.vector<4 x i8>) -> !ac.packet<@types::@Q> +} diff --git a/components/agentic-circuit/test/ACIR/records-valid.mlir b/components/agentic-circuit/test/ACIR/records-valid.mlir new file mode 100644 index 00000000..4727613d --- /dev/null +++ b/components/agentic-circuit/test/ACIR/records-valid.mlir @@ -0,0 +1,32 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "Header", fields = [{name = "opcode", type = i8}, {name = "tag", type = i16}]}> : () -> () + "ac.packet"() <{sym_name = "Request", fields = [{name = "opcode", type = i8}, {name = "payload", type = i32}]}> : () -> () + "ac.transaction"() <{sym_name = "Dma", fields = [{name = "request", type = !ac.packet<@types::@Request>}, {name = "tag", type = i16}]}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec< + !ac.struct<@types::@Header> = {abi_alignment = 2 : i64, endianness = "little", preferred_alignment = 2 : i64, size = 4 : i64}, + !ac.packet<@types::@Request> = {abi_alignment = 4 : i64, endianness = "little", preferred_alignment = 4 : i64, serialization_width = 8 : i64, size = 8 : i64} + >} : () -> () + + %opcode = "builtin.unrealized_conversion_cast"() : () -> i8 + %tag = "builtin.unrealized_conversion_cast"() : () -> i16 + %payload = "builtin.unrealized_conversion_cast"() : () -> i32 + %header = "ac.record.create"(%opcode, %tag) <{field_names = ["opcode", "tag"]}> : (i8, i16) -> !ac.struct<@types::@Header> + %got = "ac.record.get"(%header) <{field = "opcode"}> : (!ac.struct<@types::@Header>) -> i8 + %updated = "ac.record.with"(%header, %got) <{field = "opcode"}> : (!ac.struct<@types::@Header>, i8) -> !ac.struct<@types::@Header> + %packet = "ac.record.create"(%opcode, %payload) <{field_names = ["opcode", "payload"]}> : (i8, i32) -> !ac.packet<@types::@Request> + %packet2 = "ac.record.with"(%packet, %payload) <{field = "payload"}> : (!ac.packet<@types::@Request>, i32) -> !ac.packet<@types::@Request> + %bytes = "ac.packet.serialize"(%packet2) <{packet = @types::@Request}> : (!ac.packet<@types::@Request>) -> !ac.vector<8 x i8> + %copy = "ac.packet.deserialize"(%bytes) <{packet = @types::@Request}> : (!ac.vector<8 x i8>) -> !ac.packet<@types::@Request> + %tx = "ac.record.create"(%copy, %tag) <{field_names = ["request", "tag"]}> : (!ac.packet<@types::@Request>, i16) -> !ac.transaction<@types::@Dma> +} + +// CHECK: "ac.record.create" +// CHECK: "ac.record.get" +// CHECK: "ac.record.with" +// CHECK: "ac.packet.serialize" +// CHECK: "ac.packet.deserialize" +// CHECK-SAME: !ac.packet<@types::@Request> diff --git a/components/agentic-circuit/test/ACIR/reorder-invalid.mlir b/components/agentic-circuit/test/ACIR/reorder-invalid.mlir new file mode 100644 index 00000000..9971b0ba --- /dev/null +++ b/components/agentic-circuit/test/ACIR/reorder-invalid.mlir @@ -0,0 +1,60 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/capacity.mlir 2>&1 | %FileCheck %s --check-prefix=CAPACITY +// RUN: %not %acir_opt %t/type.mlir 2>&1 | %FileCheck %s --check-prefix=TYPE +// RUN: %not %acir_opt %t/key.mlir 2>&1 | %FileCheck %s --check-prefix=KEY +// RUN: %not %acir_opt %t/start-width.mlir 2>&1 | %FileCheck %s --check-prefix=START-WIDTH +// RUN: %not %acir_opt %t/effect.mlir 2>&1 | %FileCheck %s --check-prefix=EFFECT + +// CAPACITY: error: 'ac.reorder' op capacity, depth, and latency must be positive +// TYPE: error: 'ac.reorder' op output queue must match input queue type +// KEY: error: 'ac.reorder' op key must be an integer Var with width at most 64 +// START-WIDTH: error: 'ac.reorder' op start must fit key width +// EFFECT: error: 'ac.reorder' op key operation 'ac.assert' must be pure + +//--- capacity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.reorder %input capacity 0 start 0 depth 1 latency 1 { + ^key(%item: !ac.var): + ac.reorder.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.reorder %input capacity 4 start 0 depth 1 latency 1 { + ^key(%item: !ac.var): + ac.reorder.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- key.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.reorder %input capacity 4 start 0 depth 1 latency 1 { + ^key(%item: !ac.var): + %wide = ac.var.constant 0 : i128 as !ac.var + ac.reorder.yield %wide : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- effect.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.reorder %input capacity 4 start 0 depth 1 latency 1 { + ^key(%item: !ac.var): + %condition = arith.constant true + ac.assert %condition, "illegal" + ac.reorder.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} + +//--- start-width.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.reorder %input capacity 4 start 256 depth 1 latency 1 { + ^key(%item: !ac.var): + ac.reorder.yield %item : !ac.var + } : !ac.queue -> !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/reorder.mlir b/components/agentic-circuit/test/ACIR/reorder.mlir new file mode 100644 index 00000000..fc096b58 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/reorder.mlir @@ -0,0 +1,17 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 8 latency 1 : !ac.queue + %ordered = ac.reorder %input capacity 16 start 0 depth 4 latency 1 { + ^key(%item: !ac.var): + ac.reorder.yield %item : !ac.var + } : !ac.queue -> !ac.queue + ac.sink %ordered : !ac.queue +} + +// CHECK: ac.reorder +// CHECK: capacity 16 start 0 depth 4 latency 1 +// CHECK: ac.reorder.yield diff --git a/components/agentic-circuit/test/ACIR/resources-forward-malformed-invalid.mlir b/components/agentic-circuit/test/ACIR/resources-forward-malformed-invalid.mlir new file mode 100644 index 00000000..8324c6f7 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/resources-forward-malformed-invalid.mlir @@ -0,0 +1,52 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt_public %t/queue-before-protocol.mlir 2>&1 | %FileCheck %s --check-prefix=ORDERING +// RUN: %not %acir_opt_public %t/protocol-before-queue.mlir 2>&1 | %FileCheck %s --check-prefix=CORRELATION + +//--- queue-before-protocol.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.queue @ready payload i32 entries 8 ordering "fifo" protocol @p + ownership "exclusive" id "ready" path "ready" + ac.return + } + ac.protocol @p { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i32 action "offer" + ac.transition from @idle to @done on @push transfer true retain false guard {} + ac.guarantee "ordering" = 1 : i64 + ac.guarantee "delivery" = "exactly_once" + ac.guarantee "completion" = "on_terminal_phase" + ac.guarantee "backpressure" = "capacity" + ac.guarantee "stable_pending" = true + ac.guarantee "max_inflight" = 1 : i64 + } +} +// ORDERING: protocol ordering guarantee must be a string + +//--- protocol-before-queue.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @p { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i32 action "offer" + ac.transition from @idle to @done on @push transfer true retain false guard {} + ac.guarantee "ordering" = "per_key" + ac.guarantee "correlation" = 1 : i64 + ac.guarantee "delivery" = "exactly_once" + ac.guarantee "completion" = "on_terminal_phase" + ac.guarantee "backpressure" = "capacity" + ac.guarantee "stable_pending" = true + ac.guarantee "max_inflight" = 1 : i64 + } + ac.module @Top() parameters {} graph { + ac.queue @ready payload i32 entries 8 ordering "per_key" protocol @p + ownership "exclusive" id "ready" path "ready" + ac.return + } +} +// CORRELATION: protocol correlation guarantee must be a non-empty string diff --git a/components/agentic-circuit/test/ACIR/resources-invalid.mlir b/components/agentic-circuit/test/ACIR/resources-invalid.mlir new file mode 100644 index 00000000..5d4d6ad7 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/resources-invalid.mlir @@ -0,0 +1,348 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/queue-zero.mlir 2>&1 | %FileCheck %s --check-prefix=QUEUE-ZERO +// RUN: %not %acir_opt %t/queue-watermarks.mlir 2>&1 | %FileCheck %s --check-prefix=WATERMARKS +// RUN: %not %acir_opt %t/queue-protocol.mlir 2>&1 | %FileCheck %s --check-prefix=PROTOCOL +// RUN: %not %acir_opt %t/event-unstable.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT-ORDER +// RUN: %not %acir_opt %t/event-domain.mlir 2>&1 | %FileCheck %s --check-prefix=DOMAIN +// RUN: %not %acir_opt %t/resource-width.mlir 2>&1 | %FileCheck %s --check-prefix=ISSUE +// RUN: %not %acir_opt %t/resource-ii.mlir 2>&1 | %FileCheck %s --check-prefix=II +// RUN: %not %acir_opt %t/resource-latency.mlir 2>&1 | %FileCheck %s --check-prefix=LATENCY +// RUN: %not %acir_opt %t/resource-lifecycle.mlir 2>&1 | %FileCheck %s --check-prefix=LIFECYCLE +// RUN: %not %acir_opt %t/resource-arbiter.mlir 2>&1 | %FileCheck %s --check-prefix=ARBITER +// RUN: %not %acir_opt %t/resource-class.mlir 2>&1 | %FileCheck %s --check-prefix=CLASS +// RUN: %not %acir_opt %t/duplicate-owner.mlir 2>&1 | %FileCheck %s --check-prefix=OWNER +// RUN: %not %acir_opt %t/orphan.mlir 2>&1 | %FileCheck %s --check-prefix=PLACEMENT +// RUN: %not %acir_opt %t/queue-bytes.mlir 2>&1 | %FileCheck %s --check-prefix=BYTES +// RUN: %not %acir_opt %t/queue-order.mlir 2>&1 | %FileCheck %s --check-prefix=QUEUE-ORDER +// RUN: %not %acir_opt %t/queue-owner.mlir 2>&1 | %FileCheck %s --check-prefix=QUEUE-OWNER +// RUN: %not %acir_opt %t/queue-payload.mlir 2>&1 | %FileCheck %s --check-prefix=QUEUE-PAYLOAD +// RUN: %not %acir_opt %t/owner-segment.mlir 2>&1 | %FileCheck %s --check-prefix=OWNER-SEGMENT +// RUN: %not %acir_opt %t/delay.mlir 2>&1 | %FileCheck %s --check-prefix=DELAY +// RUN: %not %acir_opt %t/event-capacity.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT-CAPACITY +// RUN: %not %acir_opt %t/event-payload.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT-PAYLOAD +// RUN: %not %acir_opt %t/resource-capacity.mlir 2>&1 | %FileCheck %s --check-prefix=RESOURCE-CAPACITY +// RUN: %not %acir_opt %t/resource-kind.mlir 2>&1 | %FileCheck %s --check-prefix=RESOURCE-KIND +// RUN: %not %acir_opt %t/resource-symbol-latency.mlir 2>&1 | %FileCheck %s --check-prefix=SYMBOL-LATENCY +// RUN: %not %acir_opt %t/resource-ownership.mlir 2>&1 | %FileCheck %s --check-prefix=RESOURCE-OWNERSHIP +// RUN: %not %acir_opt %t/exclusive-arbiter.mlir 2>&1 | %FileCheck %s --check-prefix=EXCLUSIVE-ARBITER +// RUN: %not %acir_opt %t/duplicate-class.mlir 2>&1 | %FileCheck %s --check-prefix=DUPLICATE-CLASS +// RUN: %not %acir_opt %t/queue-schema.mlir 2>&1 | %FileCheck %s --check-prefix=QUEUE-SCHEMA +// RUN: %not %acir_opt %t/resource-latency-schema.mlir 2>&1 | %FileCheck %s --check-prefix=LATENCY-SCHEMA +// RUN: %not %acir_opt %t/resource-arbiter-kind.mlir 2>&1 | %FileCheck %s --check-prefix=ARBITER-KIND +// RUN: %not %acir_opt %t/fifo-weakened.mlir 2>&1 | %FileCheck %s --check-prefix=FIFO-WEAKENED +// RUN: %not %acir_opt %t/per-key-no-correlation.mlir 2>&1 | %FileCheck %s --check-prefix=PER-KEY-CORRELATION + +//--- queue-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 0 : i64, ordering = "fifo", protocol = @p, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// QUEUE-ZERO: entry capacity must be positive + +//--- queue-watermarks.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 8 : i64, ordering = "fifo", protocol = @p, ownership = "exclusive", watermarks = {low = 7 : i64, high = 7 : i64}, delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// WATERMARKS: watermarks require 0 <= low < high <= entry capacity + +//--- queue-protocol.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 8 : i64, ordering = "fifo", protocol = @missing, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PROTOCOL: endpoint protocol '@missing' is unresolved + +//--- event-unstable.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.event_queue"() <{sym_name = "e", stable_id = "e", path = "e", payload = !ac.event, capacity = 4 : i64, ordering = "time_only", time_domain = @clock, delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// EVENT-ORDER: ordering must be exactly 'time_then_sequence' + +//--- event-domain.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.event_queue"() <{sym_name = "e", stable_id = "e", path = "e", payload = !ac.event, capacity = 4 : i64, ordering = "time_then_sequence", time_domain = @clock, delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DOMAIN: time domain '@clock' is unresolved + +//--- resource-width.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 2 : i64, issue_width = 3 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ISSUE: issue width must be in [1, capacity] + +//--- resource-ii.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 2 : i64, issue_width = 1 : i64, initiation_interval = 0 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// II: initiation interval must be at least one global tick + +//--- resource-latency.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 2 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 0 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// LATENCY: fixed latency ticks must be positive + +//--- resource-lifecycle.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 2 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "eager", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// LIFECYCLE: lifecycle requires exact reservation/release/cancellation schema + +//--- resource-arbiter.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 2 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "shared", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ARBITER: shared or contested resource requires one arbitration owner + +//--- resource-class.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 2 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [@missing], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// CLASS: transaction class '@missing' is unresolved + +//--- duplicate-owner.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "a", stable_id = "same", path = "a", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.resource"() <{sym_name = "b", stable_id = "same", path = "b", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// OWNER: duplicate local structural stable id 'same' + +//--- orphan.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 1 : i64, ordering = "fifo", protocol = @p, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () +} +// PLACEMENT: must be a direct child of the unique ac.module Graph block + +//--- queue-bytes.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 1 : i64, byte_capacity = -1 : i64, ordering = "fifo", protocol = @p, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// BYTES: byte capacity must be positive when present + +//--- queue-order.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 1 : i64, ordering = "unordered", protocol = @p, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// QUEUE-ORDER: ordering must be 'fifo' or 'per_key' + +//--- queue-owner.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 1 : i64, ordering = "fifo", protocol = @p, ownership = "shared", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// QUEUE-OWNER: queue ownership must be exactly 'exclusive' + +//--- queue-payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = !ac.endpoint<@I, @r>, entry_capacity = 1 : i64, ordering = "fifo", protocol = @p, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// QUEUE-PAYLOAD: queue payload must be a normative ACIR value type + +//--- owner-segment.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "bad.name", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// OWNER-SEGMENT: owner name, stable id, and path must be stable local segments + +//--- delay.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 0 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DELAY: stateful declaration delay_ticks must be exactly one positive tick + +//--- event-capacity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.event_queue"() <{sym_name = "e", stable_id = "e", path = "e", payload = !ac.event, capacity = -1 : i64, ordering = "time_then_sequence", time_domain = @clock, delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// EVENT-CAPACITY: event queue capacity must be positive + +//--- event-payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.event_queue"() <{sym_name = "e", stable_id = "e", path = "e", payload = i32, capacity = 1 : i64, ordering = "time_then_sequence", time_domain = @clock, delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// EVENT-PAYLOAD: event queue payload must be an exact !ac.event type + +//--- resource-capacity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = -1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// RESOURCE-CAPACITY: resource capacity must be positive + +//--- resource-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "dynamic"}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// RESOURCE-KIND: latency model kind must be 'fixed' or 'symbol' + +//--- resource-symbol-latency.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "symbol", ref = @missing}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// SYMBOL-LATENCY: symbol latency model reference is unresolved + +//--- resource-ownership.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "public", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// RESOURCE-OWNERSHIP: resource ownership must be exclusive, shared, or contested + +//--- exclusive-arbiter.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", arbitration_owner = @x, transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// EXCLUSIVE-ARBITER: exclusive resource cannot declare an arbitration owner + +//--- duplicate-class.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = []}> : () -> () + }) : () -> () + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [@types::@T, @types::@T], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// DUPLICATE-CLASS: duplicate transaction class + +//--- queue-schema.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "start", initial = true, terminal = false}> : () -> () + "ac.state"() <{sym_name = "done", initial = false, terminal = true}> : () -> () + "ac.event"() <{sym_name = "finish", from = @a, to = @b, payload = i64, action = "notify"}> : () -> () + "ac.transition"() <{source = @start, target = @done, event = @finish}> ({}) : () -> () + "ac.guarantee"() <{kind = "completion", value = "on_terminal_phase"}> : () -> () + }) : () -> () + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 1 : i64, ordering = "fifo", protocol = @p, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// QUEUE-SCHEMA: queue payload does not match endpoint protocol schema + +//--- resource-latency-schema.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64, extra = true}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "exclusive", transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// LATENCY-SCHEMA: fixed latency model requires exact kind/ticks schema + +//--- resource-arbiter-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.time_domain"() <{sym_name = "clock", period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64}> : () -> () + "ac.resource"() <{sym_name = "r", stable_id = "r", path = "r", capacity = 1 : i64, issue_width = 1 : i64, initiation_interval = 1 : i64, latency_model = {kind = "fixed", ticks = 1 : i64}, lifecycle = {reservation = "propose_commit", release = "balanced", cancellation = "explicit"}, ownership = "shared", arbitration_owner = @clock, transaction_classes = [], delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ARBITER-KIND: arbitration owner '@clock' is unresolved + +//--- fifo-weakened.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i32, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({}) : () -> () + "ac.guarantee"() <{kind = "ordering", value = "fifo"}> : () -> () + }) : () -> () + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 1 : i64, ordering = "per_key", protocol = @p, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// FIFO-WEAKENED: queue ordering 'per_key' weakens protocol ordering 'fifo' + +//--- per-key-no-correlation.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i32, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({}) : () -> () + "ac.guarantee"() <{kind = "ordering", value = "unordered"}> : () -> () + }) : () -> () + "ac.module"() <{sym_name = "M", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 1 : i64, ordering = "per_key", protocol = @p, ownership = "exclusive", delay_ticks = 1 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PER-KEY-CORRELATION: per_key queue storage requires protocol correlation semantics diff --git a/components/agentic-circuit/test/ACIR/resources-valid.mlir b/components/agentic-circuit/test/ACIR/resources-valid.mlir new file mode 100644 index 00000000..9ef6865b --- /dev/null +++ b/components/agentic-circuit/test/ACIR/resources-valid.mlir @@ -0,0 +1,51 @@ +// RUN: %acir_opt_public %s | %FileCheck %s +// RUN: %acir_opt_public %s | %acir_opt_public | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @fifo { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @pending initial false terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i32 action "offer" + ac.event @accept from @receiver to @sender payload i32 action "accept" + ac.event @cancel from @sender to @receiver payload i32 action "cancel" + ac.transition from @idle to @pending on @push transfer false retain true guard {} + ac.transition from @pending to @done on @accept transfer true retain false guard {} + ac.transition from @pending to @done on @cancel transfer false retain false guard {} + ac.guarantee "ordering" = "unordered" + ac.guarantee "delivery" = "exactly_once" + ac.guarantee "completion" = "on_accept" + ac.guarantee "backpressure" = "capacity" + ac.guarantee "stable_pending" = true + ac.guarantee "max_inflight" = 1 : i64 + } + ac.interface @QueueLink { + ac.role @source dual @sink cardinality "exclusive" + ac.role @sink dual @source cardinality "exclusive" + ac.port @data : !ac.channel from @source to @sink + protocol_roles @sender to @receiver + } + ac.module @Top() parameters {} graph { + ac.time_domain @core period 2 phase 0 scale 1 + ac.queue @ready payload i32 entries 8 bytes 64 ordering "fifo" protocol @fifo + ownership "exclusive" id "ready" path "ready" watermarks {low = 2 : i64, high = 6 : i64} + ac.event_queue @done payload !ac.event capacity 16 ordering "time_then_sequence" + domain @core id "done" path "done" + ac.instance @scheduler of @Arb() static {} id "scheduler" path "scheduler" : () -> () + ac.resource @compute capacity 4 issue_width 2 ii 1 + latency {kind = "fixed", ticks = 3 : i64} + lifecycle {reservation = "propose_commit", release = "balanced", cancellation = "explicit"} + ownership "shared" arbiter @scheduler classes [] + id "compute" path "compute" + ac.return + } + ac.module @Arb() parameters {} graph { ac.return } +} + +// CHECK: ac.interface @QueueLink +// CHECK: ac.port @data +// CHECK: ac.queue @ready +// CHECK: ac.event_queue @done +// CHECK: ac.resource @compute diff --git a/components/agentic-circuit/test/ACIR/review-r1-invalid.mlir b/components/agentic-circuit/test/ACIR/review-r1-invalid.mlir new file mode 100644 index 00000000..0238c425 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/review-r1-invalid.mlir @@ -0,0 +1,214 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/declaration-placement.mlir 2>&1 | %FileCheck %s --check-prefix=PLACEMENT +// RUN: %not %acir_opt %t/flat-external.mlir 2>&1 | %FileCheck %s --check-prefix=FLAT +// RUN: %not %acir_opt %t/function-field.mlir 2>&1 | %FileCheck %s --check-prefix=FUNCTION +// RUN: %not %acir_opt %t/channel-field.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL +// RUN: %not %acir_opt %t/capability-field.mlir 2>&1 | %FileCheck %s --check-prefix=CAPABILITY +// RUN: %not %acir_opt %t/none-field.mlir 2>&1 | %FileCheck %s --check-prefix=NONE +// RUN: %not %acir_opt %t/list-bound-missing.mlir 2>&1 | %FileCheck %s --check-prefix=BOUND-MISSING +// RUN: %not %acir_opt %t/list-bound-zero.mlir 2>&1 | %FileCheck %s --check-prefix=BOUND-ZERO +// RUN: %not %acir_opt %t/list-bound-inconsistent.mlir 2>&1 | %FileCheck %s --check-prefix=BOUND-INCONSISTENT +// RUN: %not %acir_opt %t/layout-missing.mlir 2>&1 | %FileCheck %s --check-prefix=LAYOUT-MISSING +// RUN: %not %acir_opt %t/layout-invalid.mlir 2>&1 | %FileCheck %s --check-prefix=LAYOUT-INVALID +// RUN: %not %acir_opt %t/packet-width-missing.mlir 2>&1 | %FileCheck %s --check-prefix=PACKET-WIDTH-MISSING +// RUN: %not %acir_opt %t/union-discriminator-missing.mlir 2>&1 | %FileCheck %s --check-prefix=UNION-MISSING +// RUN: %not %acir_opt %t/get-field.mlir 2>&1 | %FileCheck %s --check-prefix=GET-FIELD +// RUN: %not %acir_opt %t/get-kind.mlir 2>&1 | %FileCheck %s --check-prefix=GET-KIND +// RUN: %not %acir_opt %t/with-field.mlir 2>&1 | %FileCheck %s --check-prefix=WITH-FIELD +// RUN: %not %acir_opt %t/with-kind.mlir 2>&1 | %FileCheck %s --check-prefix=WITH-KIND +// RUN: %not %acir_opt %t/with-value.mlir 2>&1 | %FileCheck %s --check-prefix=WITH-VALUE +// RUN: %not %acir_opt %t/serialize-identity.mlir 2>&1 | %FileCheck %s --check-prefix=SER-ID +// RUN: %not %acir_opt %t/serialize-unresolved.mlir 2>&1 | %FileCheck %s --check-prefix=SER-UNRESOLVED +// RUN: %not %acir_opt %t/serialize-element.mlir 2>&1 | %FileCheck %s --check-prefix=SER-ELEMENT +// RUN: %not %acir_opt %t/deserialize-unresolved.mlir 2>&1 | %FileCheck %s --check-prefix=DESER-UNRESOLVED +// RUN: %not %acir_opt %t/deserialize-width.mlir 2>&1 | %FileCheck %s --check-prefix=DESER-WIDTH + +// PLACEMENT: error: {{.*}}named data declarations must be direct children of ac.type_scope +// FLAT: error: {{.*}}named data references require a qualified symbol such as '@types::@S' +// FUNCTION: error: {{.*}}field 'bad' has non-value type +// CHANNEL: error: {{.*}}field 'bad' has non-value type +// CAPABILITY: error: {{.*}}field 'bad' has non-value type +// NONE: error: {{.*}}field 'bad' has non-value type +// BOUND-MISSING: error: {{.*}}list field 'items' requires a finite positive max_length +// BOUND-ZERO: error: {{.*}}list field 'items' requires a finite positive max_length +// BOUND-INCONSISTENT: error: {{.*}}non-list field 'value' cannot declare max_length +// LAYOUT-MISSING: error: {{.*}}missing DLTI layout entry for '!ac.struct<@types::@S>' +// LAYOUT-INVALID: error: {{.*}}layout entry requires positive size/alignment and explicit endianness +// PACKET-WIDTH-MISSING: error: packet layout entry requires positive serialization_width +// UNION-MISSING: error: {{.*}}union discriminator 'missing' does not name a field +// GET-FIELD: error: {{.*}}unknown record field 'missing' +// GET-KIND: error: {{.*}}record.get requires a record-like operand +// WITH-FIELD: error: {{.*}}unknown record field 'missing' +// WITH-KIND: error: {{.*}}record.with requires a record-like operand +// WITH-VALUE: error: {{.*}}field 'value' expects 'i32' but received 'i8' +// SER-ID: error: {{.*}}packet.serialize identity does not match packet operand +// SER-UNRESOLVED: error: {{.*}}packet.serialize packet declaration is unresolved +// SER-ELEMENT: error: {{.*}}packet.serialize result must be an i8 byte vector +// DESER-UNRESOLVED: error: {{.*}}packet.deserialize packet declaration is unresolved +// DESER-WIDTH: error: {{.*}}serialized byte vector width must equal packet serialization width 8 + +//--- declaration-placement.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.struct"() <{sym_name = "S", fields = []}> : () -> () +} + +//--- flat-external.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.struct<@S> + %x = "ac.record.get"(%v) <{field = "x"}> : (!ac.struct<@S>) -> i8 +} + +//--- function-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "S", fields = [{name = "bad", type = (i8) -> i8}]}> : () -> () + }) : () -> () +} + +//--- channel-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "S", fields = [{name = "bad", type = !ac.channel}]}> : () -> () + }) : () -> () +} + +//--- capability-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "bad", type = !ac.resource_token<@r>}]}> : () -> () + }) : () -> () +} + +//--- none-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "bad", type = none}]}> : () -> () + }) : () -> () +} + +//--- list-bound-missing.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "S", fields = [{name = "items", type = !ac.list}]}> : () -> () + }) : () -> () +} + +//--- list-bound-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "S", fields = [{name = "items", type = !ac.list, max_length = 0 : i64}]}> : () -> () + }) : () -> () +} + +//--- list-bound-inconsistent.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "S", fields = [{name = "value", type = i8, max_length = 4 : i64}]}> : () -> () + }) : () -> () +} + +//--- layout-missing.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "S", fields = []}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec<>} : () -> () +} + +//--- layout-invalid.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "S", fields = []}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 0 : i64, endianness = "middle", preferred_alignment = 0 : i64, size = 0 : i64}>} : () -> () +} + +//--- packet-width-missing.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.packet"() <{sym_name = "P", fields = []}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 8 : i64}>} : () -> () +} + +//--- union-discriminator-missing.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.union"() <{sym_name = "U", fields = [{name = "tag", type = i8}], discriminator = "missing"}> : () -> () + }) : () -> () +} + +//--- get-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "value", type = i32}]}> : () -> () + }) : () -> () + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.transaction<@types::@T> + %x = "ac.record.get"(%v) <{field = "missing"}> : (!ac.transaction<@types::@T>) -> i32 +} + +//--- get-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %v = "builtin.unrealized_conversion_cast"() : () -> i32 + %x = "ac.record.get"(%v) <{field = "x"}> : (i32) -> i8 +} + +//--- with-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "value", type = i32}]}> : () -> () + }) : () -> () + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.transaction<@types::@T> + %x = "builtin.unrealized_conversion_cast"() : () -> i32 + %r = "ac.record.with"(%v, %x) <{field = "missing"}> : (!ac.transaction<@types::@T>, i32) -> !ac.transaction<@types::@T> +} + +//--- with-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %v = "builtin.unrealized_conversion_cast"() : () -> i32 + %x = "builtin.unrealized_conversion_cast"() : () -> i8 + %r = "ac.record.with"(%v, %x) <{field = "x"}> : (i32, i8) -> i32 +} + +//--- with-value.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "value", type = i32}]}> : () -> () + }) : () -> () + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.transaction<@types::@T> + %x = "builtin.unrealized_conversion_cast"() : () -> i8 + %r = "ac.record.with"(%v, %x) <{field = "value"}> : (!ac.transaction<@types::@T>, i8) -> !ac.transaction<@types::@T> +} + +//--- serialize-identity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.packet<@types::@P> + %r = "ac.packet.serialize"(%v) <{packet = @types::@Q}> : (!ac.packet<@types::@P>) -> !ac.vector<8 x i8> +} + +//--- serialize-unresolved.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.packet<@types::@Missing> + %r = "ac.packet.serialize"(%v) <{packet = @types::@Missing}> : (!ac.packet<@types::@Missing>) -> !ac.vector<8 x i8> +} + +//--- serialize-element.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.packet"() <{sym_name = "P", fields = []}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, serialization_width = 8 : i64, size = 8 : i64}>} : () -> () + %v = "builtin.unrealized_conversion_cast"() : () -> !ac.packet<@types::@P> + %r = "ac.packet.serialize"(%v) <{packet = @types::@P}> : (!ac.packet<@types::@P>) -> !ac.vector<8 x i16> +} + +//--- deserialize-unresolved.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %b = "builtin.unrealized_conversion_cast"() : () -> !ac.vector<8 x i8> + %r = "ac.packet.deserialize"(%b) <{packet = @types::@Missing}> : (!ac.vector<8 x i8>) -> !ac.packet<@types::@Missing> +} + +//--- deserialize-width.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.packet"() <{sym_name = "P", fields = []}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, serialization_width = 8 : i64, size = 8 : i64}>} : () -> () + %b = "builtin.unrealized_conversion_cast"() : () -> !ac.vector<4 x i8> + %r = "ac.packet.deserialize"(%b) <{packet = @types::@P}> : (!ac.vector<4 x i8>) -> !ac.packet<@types::@P> +} diff --git a/components/agentic-circuit/test/ACIR/review-r1-valid.mlir b/components/agentic-circuit/test/ACIR/review-r1-valid.mlir new file mode 100644 index 00000000..54a900c6 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/review-r1-valid.mlir @@ -0,0 +1,32 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "S", fields = [{name = "value", type = i32}]}> : () -> () + "ac.enum"() <{sym_name = "E", enumerants = ["a", "b"]}> : () -> () + "ac.union"() <{sym_name = "U", fields = [{name = "tag", type = !ac.enum<@types::@E>}, {name = "value", type = i32}], discriminator = "tag"}> : () -> () + "ac.packet"() <{sym_name = "P", fields = [{name = "items", type = !ac.list, max_length = 8 : i64}]}> : () -> () + "ac.transaction"() <{sym_name = "T", fields = [{name = "packet", type = !ac.packet<@types::@P>}]}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec< + !ac.struct<@types::@S> = {abi_alignment = 4 : i64, endianness = "little", preferred_alignment = 4 : i64, size = 4 : i64}, + !ac.packet<@types::@P> = {abi_alignment = 8 : i64, endianness = "big", preferred_alignment = 8 : i64, serialization_width = 8 : i64, size = 16 : i64}, + !ac.enum<@types::@E> = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 1 : i64}, + !ac.union<@types::@U> = {abi_alignment = 4 : i64, endianness = "little", preferred_alignment = 4 : i64, size = 4 : i64} + >} : () -> () + + %i32 = "builtin.unrealized_conversion_cast"() : () -> i32 + %s = "ac.record.create"(%i32) <{field_names = ["value"]}> : (i32) -> !ac.struct<@types::@S> + %value = "ac.record.get"(%s) <{field = "value"}> : (!ac.struct<@types::@S>) -> i32 + %s2 = "ac.record.with"(%s, %value) <{field = "value"}> : (!ac.struct<@types::@S>, i32) -> !ac.struct<@types::@S> + %packet = "builtin.unrealized_conversion_cast"() : () -> !ac.packet<@types::@P> + %bytes = "ac.packet.serialize"(%packet) <{packet = @types::@P}> : (!ac.packet<@types::@P>) -> !ac.vector<8 x i8> + %copy = "ac.packet.deserialize"(%bytes) <{packet = @types::@P}> : (!ac.vector<8 x i8>) -> !ac.packet<@types::@P> +} + +// CHECK: dlti.dl_spec +// CHECK: !ac.struct<@types::@S> +// CHECK: !ac.packet<@types::@P> +// CHECK: serialization_width = 8 : i64 +// CHECK: "ac.packet.serialize" +// CHECK-SAME: packet = @types::@P diff --git a/components/agentic-circuit/test/ACIR/review-r2-invalid.mlir b/components/agentic-circuit/test/ACIR/review-r2-invalid.mlir new file mode 100644 index 00000000..9d82db03 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/review-r2-invalid.mlir @@ -0,0 +1,128 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/malformed-field.mlir 2>&1 | %FileCheck %s --check-prefix=MALFORMED +// RUN: %not %acir_opt %t/missing-name.mlir 2>&1 | %FileCheck %s --check-prefix=MISSING-NAME +// RUN: %not %acir_opt %t/missing-type.mlir 2>&1 | %FileCheck %s --check-prefix=MISSING-TYPE +// RUN: %not %acir_opt %t/wrong-typed-non-list-bound.mlir 2>&1 | %FileCheck %s --check-prefix=WRONG-BOUND +// RUN: %not %acir_opt %t/wrong-typed-list-bound.mlir 2>&1 | %FileCheck %s --check-prefix=WRONG-LIST-BOUND +// RUN: %not %acir_opt %t/noncanonical-list-bound.mlir 2>&1 | %FileCheck %s --check-prefix=NONCANONICAL-LIST-BOUND +// RUN: %not %acir_opt %t/nested-list-no-bound.mlir 2>&1 | %FileCheck %s --check-prefix=NESTED-LIST +// RUN: %not %acir_opt %t/union-discriminator-type.mlir 2>&1 | %FileCheck %s --check-prefix=UNION-TYPE +// RUN: %not %acir_opt %t/create-unresolved.mlir 2>&1 | %FileCheck %s --check-prefix=CREATE-UNRESOLVED +// RUN: %not %acir_opt %t/create-non-record.mlir 2>&1 | %FileCheck %s --check-prefix=CREATE-NON-RECORD +// RUN: %not %acir_opt %t/create-field-order.mlir 2>&1 | %FileCheck %s --check-prefix=CREATE-ORDER +// RUN: %not %acir_opt %t/serialize-flat-attr.mlir 2>&1 | %FileCheck %s --check-prefix=SERIALIZE-FLAT +// RUN: %not %acir_opt %t/deserialize-flat-attr.mlir 2>&1 | %FileCheck %s --check-prefix=DESERIALIZE-FLAT +// RUN: %not %acir_opt %t/deserialize-element.mlir 2>&1 | %FileCheck %s --check-prefix=DESERIALIZE-ELEMENT + +// MALFORMED: error: {{.*}}field metadata requires string 'name' and type 'type' +// MISSING-NAME: error: {{.*}}field metadata requires string 'name' and type 'type' +// MISSING-TYPE: error: {{.*}}field metadata requires string 'name' and type 'type' +// WRONG-BOUND: error: {{.*}}non-list field 'value' cannot declare max_length +// WRONG-LIST-BOUND: error: {{.*}}list field 'items' requires a finite positive max_length +// NONCANONICAL-LIST-BOUND: error: {{.*}}list field 'items' requires a finite positive max_length +// NESTED-LIST: error: {{.*}}list field 'items' requires a finite positive max_length +// UNION-TYPE: error: {{.*}}union discriminator 'tag' must name an integer or enum field +// CREATE-UNRESOLVED: error: {{.*}}record.create result must resolve to a record declaration +// CREATE-NON-RECORD: error: {{.*}}record.create result must resolve to a record declaration +// CREATE-ORDER: error: {{.*}}record.create fields must exactly match declaration +// SERIALIZE-FLAT: error: {{.*}}named data references require a qualified symbol such as '@types::@S' +// DESERIALIZE-FLAT: error: {{.*}}named data references require a qualified symbol such as '@types::@S' +// DESERIALIZE-ELEMENT: error: {{.*}}packet.deserialize operand must be an i8 byte vector + +//--- malformed-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = ["oops"]}> : () -> () + }) : () -> () +} + +//--- missing-name.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{type = i8}]}> : () -> () + }) : () -> () +} + +//--- missing-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "value"}]}> : () -> () + }) : () -> () +} + +//--- wrong-typed-non-list-bound.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "value", type = i8, max_length = "oops"}]}> : () -> () + }) : () -> () +} + +//--- wrong-typed-list-bound.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "items", type = !ac.list, max_length = "oops"}]}> : () -> () + }) : () -> () +} + +//--- nested-list-no-bound.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "items", type = !ac.optional>>}]}> : () -> () + }) : () -> () +} + +//--- noncanonical-list-bound.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "items", type = !ac.list, max_length = 4 : i32}]}> : () -> () + }) : () -> () +} + +//--- union-discriminator-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.union"() <{sym_name = "U", fields = [{name = "tag", type = f32}], discriminator = "tag"}> : () -> () + }) : () -> () +} + +//--- create-unresolved.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %value = "builtin.unrealized_conversion_cast"() : () -> i8 + %record = "ac.record.create"(%value) <{field_names = ["value"]}> : (i8) -> !ac.transaction<@types::@Missing> +} + +//--- create-non-record.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %record = "ac.record.create"() <{field_names = []}> : () -> i8 +} + +//--- create-field-order.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "T", fields = [{name = "first", type = i8}, {name = "second", type = i8}]}> : () -> () + }) : () -> () + %first = "builtin.unrealized_conversion_cast"() : () -> i8 + %second = "builtin.unrealized_conversion_cast"() : () -> i8 + %record = "ac.record.create"(%first, %second) <{field_names = ["second", "first"]}> : (i8, i8) -> !ac.transaction<@types::@T> +} + +//--- serialize-flat-attr.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %packet = "builtin.unrealized_conversion_cast"() : () -> !ac.packet<@types::@P> + %bytes = "ac.packet.serialize"(%packet) <{packet = @P}> : (!ac.packet<@types::@P>) -> !ac.vector<8 x i8> +} + +//--- deserialize-flat-attr.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %bytes = "builtin.unrealized_conversion_cast"() : () -> !ac.vector<8 x i8> + %packet = "ac.packet.deserialize"(%bytes) <{packet = @P}> : (!ac.vector<8 x i8>) -> !ac.packet<@types::@P> +} + +//--- deserialize-element.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.packet"() <{sym_name = "P", fields = []}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, serialization_width = 8 : i64, size = 8 : i64}>} : () -> () + %bytes = "builtin.unrealized_conversion_cast"() : () -> !ac.vector<8 x i16> + %packet = "ac.packet.deserialize"(%bytes) <{packet = @types::@P}> : (!ac.vector<8 x i16>) -> !ac.packet<@types::@P> +} diff --git a/components/agentic-circuit/test/ACIR/route-invalid.mlir b/components/agentic-circuit/test/ACIR/route-invalid.mlir new file mode 100644 index 00000000..8cee04e1 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/route-invalid.mlir @@ -0,0 +1,36 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/outputs.mlir 2>&1 | %FileCheck %s --check-prefix=OUTPUTS +// RUN: %not %acir_opt %t/selector.mlir 2>&1 | %FileCheck %s --check-prefix=SELECTOR +// RUN: %not %acir_opt %t/payload.mlir 2>&1 | %FileCheck %s --check-prefix=PAYLOAD + +// OUTPUTS: error: 'ac.route' op requires at least two output queues +// SELECTOR: error: 'ac.route' op selector must be an integer or enum Var +// PAYLOAD: error: 'ac.route' op output queue 1 must match input queue type + +//--- outputs.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %only = ac.route %input depths [1] latencies [1] { + ^selector(%item: !ac.var): + ac.route.yield %item : !ac.var + } : !ac.queue -> (!ac.queue) +} + +//--- selector.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %left, %right = ac.route %input depths [1, 1] latencies [1, 1] { + ^selector(%item: !ac.var): + %bad = "builtin.unrealized_conversion_cast"() : () -> !ac.var> + ac.route.yield %bad : !ac.var> + } : !ac.queue -> (!ac.queue, !ac.queue) +} + +//--- payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %left, %right = ac.route %input depths [1, 1] latencies [1, 1] { + ^selector(%item: !ac.var): + ac.route.yield %item : !ac.var + } : !ac.queue -> (!ac.queue, !ac.queue) +} diff --git a/components/agentic-circuit/test/ACIR/route.mlir b/components/agentic-circuit/test/ACIR/route.mlir new file mode 100644 index 00000000..960561dd --- /dev/null +++ b/components/agentic-circuit/test/ACIR/route.mlir @@ -0,0 +1,14 @@ +// RUN: %acir_opt %s | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %left, %right = ac.route %input depths [2, 2] latencies [1, 1] { + ^selector(%item: !ac.var): + ac.route.yield %item : !ac.var + } : !ac.queue -> (!ac.queue, !ac.queue) + ac.sink %left : !ac.queue + ac.sink %right : !ac.queue +} + +// CHECK: ac.route +// CHECK: ac.route.yield diff --git a/components/agentic-circuit/test/ACIR/runtime-references-invalid.mlir b/components/agentic-circuit/test/ACIR/runtime-references-invalid.mlir new file mode 100644 index 00000000..eb1d9e3a --- /dev/null +++ b/components/agentic-circuit/test/ACIR/runtime-references-invalid.mlir @@ -0,0 +1,147 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/unresolved-send.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED-SEND +// RUN: %not %acir_opt %t/wrong-send-kind.mlir 2>&1 | %FileCheck %s --check-prefix=SEND-KIND +// RUN: %not %acir_opt %t/unresolved-schedule.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED-SCHEDULE +// RUN: %not %acir_opt %t/unresolved-wait.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED-WAIT +// RUN: %not %acir_opt %t/unresolved-event.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED-EVENT +// RUN: %not %acir_opt %t/unresolved-probe.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED-PROBE +// RUN: %not %acir_opt %t/unresolved-stat.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED-STAT +// RUN: %not %acir_opt %t/schedule-type.mlir 2>&1 | %FileCheck %s --check-prefix=SCHEDULE-TYPE +// RUN: %not %acir_opt %t/probe-kind.mlir 2>&1 | %FileCheck %s --check-prefix=PROBE-KIND +// RUN: %not %acir_opt %t/stat-kind.mlir 2>&1 | %FileCheck %s --check-prefix=STAT-KIND + +//--- unresolved-send.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + %v = arith.constant 1 : i32 + %ok = ac.try_send @missing %v : i32 + ac.yield_sim + } + ac.return + } +} +// UNRESOLVED-SEND: unresolved runtime target '@missing' + +//--- wrong-send-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.stat @not_a_queue kind "counter" + ac.process @p kind "control" { + %v = arith.constant 1 : i32 + %ok = ac.try_send @not_a_queue %v : i32 + ac.yield_sim + } + ac.return + } +} +// SEND-KIND: runtime target '@not_a_queue' must resolve to ac.queue + +//--- unresolved-schedule.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + %v = arith.constant 1 : i32 + %delay = arith.constant 1 : i64 + ac.schedule @missing %v after %delay : i32 + ac.yield_sim + } + ac.return + } +} +// UNRESOLVED-SCHEDULE: unresolved runtime target '@missing' + +//--- unresolved-wait.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + ac.wait_for @missing + ac.yield_sim + } + ac.return + } +} +// UNRESOLVED-WAIT: unresolved runtime target '@missing' + +//--- unresolved-event.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "control" { + ac.await_event @missing + ac.yield_sim + } + ac.return + } +} +// UNRESOLVED-EVENT: unresolved runtime target '@missing' + +//--- unresolved-probe.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "monitor" { + %v = ac.probe @missing kind "queue" : i64 + ac.yield_sim + } + ac.return + } +} +// UNRESOLVED-PROBE: unresolved runtime target '@missing' + +//--- unresolved-stat.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "monitor" { + %v = arith.constant 1 : i64 + ac.stat.add @missing %v : i64 + ac.yield_sim + } + ac.return + } +} +// UNRESOLVED-STAT: unresolved runtime target '@missing' + +//--- schedule-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(i64) parameters {} graph { + ^bb0(%input : i64): + ac.process @worker kind "workload" captures(%input : i64) { + ^bb0(%value : i64): + ac.yield_sim + } + ac.process @p kind "control" { + %v = arith.constant 1 : i32 + %delay = arith.constant 1 : i64 + ac.schedule @worker %v after %delay : i32 + ac.yield_sim + } + ac.return + } +} +// SCHEDULE-TYPE: must match the target process's single capture type + +//--- probe-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.stat @state kind "counter" + ac.process @p kind "monitor" { + %v = ac.probe @state kind "queue" : i64 + ac.yield_sim + } + ac.return + } +} +// PROBE-KIND: runtime target '@state' must resolve to ac.queue + +//--- stat-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @target kind "monitor" { ac.yield_sim } + ac.process @p kind "monitor" { + %v = arith.constant 1 : i64 + ac.stat.add @target %v : i64 + ac.yield_sim + } + ac.return + } +} +// STAT-KIND: runtime target '@target' must resolve to ac.stat diff --git a/components/agentic-circuit/test/ACIR/scope-invalid.mlir b/components/agentic-circuit/test/ACIR/scope-invalid.mlir new file mode 100644 index 00000000..5b404f1f --- /dev/null +++ b/components/agentic-circuit/test/ACIR/scope-invalid.mlir @@ -0,0 +1,48 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/arg-count.mlir 2>&1 | %FileCheck %s --check-prefix=ARG-COUNT +// RUN: %not %acir_opt %t/arg-type.mlir 2>&1 | %FileCheck %s --check-prefix=ARG-TYPE +// RUN: %not %acir_opt %t/yield-count.mlir 2>&1 | %FileCheck %s --check-prefix=YIELD-COUNT +// RUN: %not %acir_opt %t/yield-type.mlir 2>&1 | %FileCheck %s --check-prefix=YIELD-TYPE + +// ARG-COUNT: error: 'ac.scope' op body argument count must match input queue count +// ARG-TYPE: error: 'ac.scope' op body argument 0 must match input queue type +// YIELD-COUNT: error: 'ac.scope' op yielded queue count must match result count +// YIELD-TYPE: error: 'ac.scope' op yielded queue 0 must match result type + +//--- arg-count.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %output = ac.scope @s(%input) { + ^body: + ac.scope.yield %input : !ac.queue + } : (!ac.queue) -> !ac.queue +} + +//--- arg-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %output = ac.scope @s(%input) { + ^body(%borrowed: !ac.queue): + %local = ac.source depth 4 latency 1 : !ac.queue + ac.scope.yield %local : !ac.queue + } : (!ac.queue) -> !ac.queue +} + +//--- yield-count.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %output = ac.scope @s(%input) { + ^body(%borrowed: !ac.queue): + ac.scope.yield + } : (!ac.queue) -> !ac.queue +} + +//--- yield-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %output = ac.scope @s(%input) { + ^body(%borrowed: !ac.queue): + %local = ac.source depth 4 latency 1 : !ac.queue + ac.scope.yield %local : !ac.queue + } : (!ac.queue) -> !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/scope.mlir b/components/agentic-circuit/test/ACIR/scope.mlir new file mode 100644 index 00000000..8eae5578 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/scope.mlir @@ -0,0 +1,21 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %output = ac.scope @frontend(%input) { + ^body(%borrowed: !ac.queue): + %local = ac.transform %borrowed depths [8] latencies [1] { + ^transform(%item: !ac.var): + ac.transform.yield %item : !ac.var + } : (!ac.queue) -> !ac.queue + ac.scope.yield %local : !ac.queue + } : (!ac.queue) -> !ac.queue + ac.sink %output : !ac.queue +} + +// CHECK: %[[OUTPUT:.*]] = ac.scope @frontend(%[[INPUT:.*]]) +// CHECK: ^bb0(%[[BORROWED:.*]]: !ac.queue): +// CHECK: ac.transform %[[BORROWED]] +// CHECK: ac.scope.yield %{{.*}} : !ac.queue +// CHECK: ac.sink %[[OUTPUT]] diff --git a/components/agentic-circuit/test/ACIR/select-invalid.mlir b/components/agentic-circuit/test/ACIR/select-invalid.mlir new file mode 100644 index 00000000..8ff88159 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/select-invalid.mlir @@ -0,0 +1,39 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/arity.mlir 2>&1 | %FileCheck %s --check-prefix=ARITY +// RUN: %not %acir_opt %t/type.mlir 2>&1 | %FileCheck %s --check-prefix=TYPE +// RUN: %not %acir_opt %t/key.mlir 2>&1 | %FileCheck %s --check-prefix=KEY + +// ARITY: error: 'ac.select' op requires one control and at least two data queues +// TYPE: error: 'ac.select' op data input queue 1 must match output queue type +// KEY: error: 'ac.select' op key must yield an integer Var with width at most 64 + +//--- arity.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %control = ac.source depth 1 latency 1 : !ac.queue + %data = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.select %control, %data depth 1 latency 1 key { + ^key(%item: !ac.var): ac.select.yield %item : !ac.var + } : (!ac.queue, !ac.queue) -> !ac.queue +} + +//--- type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %control = ac.source depth 1 latency 1 : !ac.queue + %left = ac.source depth 1 latency 1 : !ac.queue + %right = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.select %control, %left, %right depth 1 latency 1 key { + ^key(%item: !ac.var): ac.select.yield %item : !ac.var + } : (!ac.queue, !ac.queue, !ac.queue) -> !ac.queue +} + +//--- key.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %control = ac.source depth 1 latency 1 : !ac.queue + %left = ac.source depth 1 latency 1 : !ac.queue + %right = ac.source depth 1 latency 1 : !ac.queue + %bad = ac.select %control, %left, %right depth 1 latency 1 key { + ^key(%item: !ac.var): + %wide = ac.var.constant 0 : i128 as !ac.var + ac.select.yield %wide : !ac.var + } : (!ac.queue, !ac.queue, !ac.queue) -> !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/select.mlir b/components/agentic-circuit/test/ACIR/select.mlir new file mode 100644 index 00000000..d58d57bd --- /dev/null +++ b/components/agentic-circuit/test/ACIR/select.mlir @@ -0,0 +1,23 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.type_scope @types { + ac.struct @Control fields [{name = "route", type = i1}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 1 : i64}>} + %control = ac.source depth 1 latency 1 : !ac.queue> + %left = ac.source depth 1 latency 1 : !ac.queue + %right = ac.source depth 1 latency 1 : !ac.queue + %selected = ac.select %control, %left, %right depth 2 latency 1 key { + ^key(%item: !ac.var>): + %route = ac.var.get %item field "route" : !ac.var> -> !ac.var + ac.select.yield %route : !ac.var + } : (!ac.queue>, !ac.queue, !ac.queue) -> !ac.queue + ac.sink %selected : !ac.queue +} + +// CHECK: ac.select +// CHECK: depth 2 latency 1 key +// CHECK: ac.select.yield diff --git a/components/agentic-circuit/test/ACIR/source-sink-invalid.mlir b/components/agentic-circuit/test/ACIR/source-sink-invalid.mlir new file mode 100644 index 00000000..d3da4dbe --- /dev/null +++ b/components/agentic-circuit/test/ACIR/source-sink-invalid.mlir @@ -0,0 +1,18 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/depth-zero.mlir 2>&1 | %FileCheck %s --check-prefix=DEPTH-ZERO +// RUN: %not %acir_opt %t/latency-zero.mlir 2>&1 | %FileCheck %s --check-prefix=LATENCY-ZERO + +// DEPTH-ZERO: error: 'ac.source' op depth must be positive +// LATENCY-ZERO: error: 'ac.source' op latency must be positive + +//--- depth-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %source = ac.source depth 0 latency 1 : !ac.queue + ac.sink %source : !ac.queue +} + +//--- latency-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %source = ac.source depth 4 latency 0 : !ac.queue + ac.sink %source : !ac.queue +} diff --git a/components/agentic-circuit/test/ACIR/source-sink.mlir b/components/agentic-circuit/test/ACIR/source-sink.mlir new file mode 100644 index 00000000..2e0bf381 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/source-sink.mlir @@ -0,0 +1,12 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %source = ac.source depth 4 latency 1 : !ac.queue + ac.sink %source : !ac.queue +} + +// CHECK: %[[SOURCE:.*]] = ac.source depth 4 latency 1 : !ac.queue +// CHECK: ac.sink %[[SOURCE]] : !ac.queue diff --git a/components/agentic-circuit/test/ACIR/topology-review-r1-invalid.mlir b/components/agentic-circuit/test/ACIR/topology-review-r1-invalid.mlir new file mode 100644 index 00000000..ea77e76c --- /dev/null +++ b/components/agentic-circuit/test/ACIR/topology-review-r1-invalid.mlir @@ -0,0 +1,55 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/optional-flow-result.mlir 2>&1 | %FileCheck %s --check-prefix=OPTIONAL +// RUN: %not %acir_opt %t/list-endpoint-result.mlir 2>&1 | %FileCheck %s --check-prefix=LIST +// RUN: %not %acir_opt %t/vector-resource-result.mlir 2>&1 | %FileCheck %s --check-prefix=VECTOR +// RUN: %not %acir_opt %t/event-token-result.mlir 2>&1 | %FileCheck %s --check-prefix=EVENT +// RUN: %not %acir_opt %t/nested-channel-result.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL +// RUN: %not %acir_opt %t/block-argument.mlir 2>&1 | %FileCheck %s --check-prefix=BLOCK +// RUN: %not %acir_opt %t/property.mlir 2>&1 | %FileCheck %s --check-prefix=PROPERTY + +// OPTIONAL: topology type '!ac.flow' cannot be nested inside '!ac.optional>' +// LIST: topology type '!ac.endpoint<@Missing, @role>' cannot be nested inside '!ac.list>' +// VECTOR: topology type '!ac.resource_ref<@Resource, @role>' cannot be nested inside '!ac.vector<2 x !ac.resource_ref<@Resource, @role>>' +// EVENT: topology type '!ac.resource_token<@Resource>' cannot be nested inside '!ac.event>' +// CHANNEL: channel types cannot be nested inside value types +// BLOCK: topology type '!ac.flow' cannot be nested inside '!ac.optional>' +// PROPERTY: topology type '!ac.flow' cannot be nested inside '!ac.optional>' + +//--- optional-flow-result.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.optional> +} + +//--- list-endpoint-result.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.list> +} + +//--- vector-resource-result.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.vector<2 x !ac.resource_ref<@Resource, @role>> +} + +//--- event-token-result.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.event> +} + +//--- nested-channel-result.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %x = "builtin.unrealized_conversion_cast"() : () -> !ac.optional> +} + +//--- block-argument.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.interface"() <{sym_name = "I"}> ({ + ^bb0(%arg0: !ac.optional>): + }) : () -> () +} + +//--- property.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.type_alias"() <{sym_name = "Bad", target = !ac.optional>}> : () -> () + }) : () -> () +} diff --git a/components/agentic-circuit/test/ACIR/trace-invalid.mlir b/components/agentic-circuit/test/ACIR/trace-invalid.mlir new file mode 100644 index 00000000..5a9eb6e7 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/trace-invalid.mlir @@ -0,0 +1,231 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/forked-cursor.mlir 2>&1 | %FileCheck %s --check-prefix=CURSOR +// RUN: %not %acir_opt %t/noncursor.mlir 2>&1 | %FileCheck %s --check-prefix=TYPE +// RUN: %not %acir_opt %t/wrong-owner.mlir 2>&1 | %FileCheck %s --check-prefix=OWNER +// RUN: %not %acir_opt %t/duplicate-document-owner.mlir 2>&1 | %FileCheck %s --check-prefix=MULTI-OWNER +// RUN: %not %acir_opt %t/duplicate-cursor.mlir 2>&1 | %FileCheck %s --check-prefix=DUPLICATE-CURSOR +// RUN: %not %acir_opt %t/source-is-not-path.mlir 2>&1 | %FileCheck %s --check-prefix=SOURCE-ID +// RUN: %not %acir_opt %t/forwarded-fork.mlir 2>&1 | %FileCheck %s --check-prefix=FORWARDED-FORK +// RUN: %not %acir_opt %t/ambiguous-merge.mlir 2>&1 | %FileCheck %s --check-prefix=AMBIGUOUS +// RUN: %not %acir_opt %t/cursor-noncursor-merge.mlir 2>&1 | %FileCheck %s --check-prefix=NONCURSOR-MERGE +// RUN: %not %acir_opt %t/for-induction-cursor.mlir 2>&1 | %FileCheck %s --check-prefix=FOR-INDUCTION +// RUN: %not %acir_opt %t/decode-non-next.mlir 2>&1 | %FileCheck %s --check-prefix=DECODE-NON-NEXT +// RUN: %not %acir_opt %t/decode-not-in-process.mlir 2>&1 | %FileCheck %s --check-prefix=DECODE-NOT-PROCESS +// RUN: %not %acir_opt %t/eof-non-cursor.mlir 2>&1 | %FileCheck %s --check-prefix=EOF-TYPE +// RUN: %not %acir_opt %t/position-non-cursor.mlir 2>&1 | %FileCheck %s --check-prefix=POSITION-TYPE + +//--- forked-cursor.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "workload" { + %cursor = ac.trace.open source "input" + %next, %raw, %advanced = ac.trace.next %cursor from source "input" : i32 + %other, %raw2, %advanced2 = ac.trace.next %cursor from source "input" : i32 + ac.yield_sim + } + ac.return + } +} +// CURSOR: trace cursor provenance has more than one advancing consumer + +//--- noncursor.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "workload" { + %bad = arith.constant 0 : index + %next, %raw, %advanced = ac.trace.next %bad from source "input" : i32 + ac.yield_sim + } + ac.return + } +} +// TYPE: trace cursor must originate from ac.trace.open or ac.trace.next + +//--- wrong-owner.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "workload" { + %cursor = ac.trace.open source "input" + %next, %raw, %advanced = ac.trace.next %cursor from source "other" : i32 + ac.yield_sim + } + ac.return + } +} +// OWNER: trace cursor owner does not match 'from source' + +//--- duplicate-document-owner.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @left kind "workload" { + %cursor = ac.trace.open source "pto" + ac.yield_sim + } + ac.process @right kind "workload" { + %cursor = ac.trace.open source "pto" + ac.yield_sim + } + ac.return + } +} +// MULTI-OWNER: trace source 'pto' must have exactly one cursor owner + +//--- source-is-not-path.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @workload kind "workload" { + %cursor = ac.trace.open source "traces/model.json" + ac.yield_sim + } + ac.return + } +} +// SOURCE-ID: trace source must be one stable logical identifier segment + +//--- duplicate-cursor.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @workload kind "workload" { + %first = ac.trace.open source "pto" + %second = ac.trace.open source "pto" + ac.yield_sim + } + ac.return + } +} +// DUPLICATE-CURSOR: trace source 'pto' must have exactly one cursor owner + +//--- forwarded-fork.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(i1) parameters {} graph { + ^bb0(%condition : i1): + ac.process @p kind "workload" captures(%condition : i1) { + ^bb0(%condition_copy : i1): + %cursor = ac.trace.open source "pto" + %forwarded = scf.if %condition_copy -> index { + scf.yield %cursor : index + } else { + scf.yield %cursor : index + } + %next0, %raw0, %advanced0 = ac.trace.next %cursor from source "pto" : i32 + %next1, %raw1, %advanced1 = ac.trace.next %forwarded from source "pto" : i32 + ac.yield_sim + } + ac.return + } +} +// FORWARDED-FORK: trace cursor provenance has more than one advancing consumer + +//--- ambiguous-merge.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(i1) parameters {} graph { + ^bb0(%condition : i1): + ac.process @p kind "workload" captures(%condition : i1) { + ^bb0(%condition_copy : i1): + %left = ac.trace.open source "left" + %right = ac.trace.open source "right" + %merged = scf.if %condition_copy -> index { + scf.yield %left : index + } else { + scf.yield %right : index + } + %next, %raw, %advanced = ac.trace.next %merged from source "left" : i32 + ac.yield_sim + } + ac.return + } +} +// AMBIGUOUS: trace cursor forwarding merges distinct provenance + +//--- cursor-noncursor-merge.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(i1) parameters {} graph { + ^bb0(%condition : i1): + ac.process @p kind "workload" captures(%condition : i1) { + ^bb0(%branch : i1): + %cursor = ac.trace.open source "pto" + %ordinary = index.constant 0 + %merged = scf.if %branch -> index { + scf.yield %cursor : index + } else { + scf.yield %ordinary : index + } + %next, %raw, %advanced = ac.trace.next %merged from source "pto" : i32 + ac.yield_sim + } + ac.return + } +} +// NONCURSOR-MERGE: trace cursor forwarding merges cursor and non-cursor values + +//--- for-induction-cursor.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "workload" { + %cursor = ac.trace.open source "pto" + %lb = index.constant 0 + %ub = index.constant 4 + %step = index.constant 1 + %laundered = scf.for %i = %lb to %ub step %step + iter_args(%iter = %cursor) -> index { + scf.yield %i : index + } + %next, %raw, %advanced = ac.trace.next %laundered from source "pto" : i32 + ac.yield_sim + } + ac.return + } +} +// FOR-INDUCTION: trace cursor forwarding merges cursor and non-cursor values +// DECODE-NON-NEXT: trace.decode input must be an ac.trace.next entry +// DECODE-NOT-PROCESS: operation is not legal in an ac.module structural Graph region + +//--- decode-non-next.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "workload" { + %cursor = ac.trace.open source "input" + %next, %raw, %advanced = ac.trace.next %cursor from source "input" : i32 + %bad = arith.constant 0 : i32 + %decoded = ac.trace.decode %bad : i32 to i64 + ac.yield_sim + } + ac.return + } +} + +//--- decode-not-in-process.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + %cursor = ac.trace.open source "input" + %next, %raw, %advanced = ac.trace.next %cursor from source "input" : i32 + %decoded = ac.trace.decode %raw : i32 to i64 + ac.return + } +} + +//--- eof-non-cursor.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "workload" { + %bad = arith.constant 1 : i32 + %eof = ac.trace.eof %bad from source "pto" + ac.yield_sim + } + ac.return + } +} +// EOF-TYPE: error: use of value '%bad' expects different type than prior uses: 'index' vs 'i32' + +//--- position-non-cursor.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M() parameters {} graph { + ac.process @p kind "workload" { + %bad = arith.constant 1 : i32 + %position = ac.trace.position %bad from source "pto" + ac.yield_sim + } + ac.return + } +} +// POSITION-TYPE: error: use of value '%bad' expects different type than prior uses: 'index' vs 'i32' diff --git a/components/agentic-circuit/test/ACIR/trace-valid.mlir b/components/agentic-circuit/test/ACIR/trace-valid.mlir new file mode 100644 index 00000000..6b0dbe0d --- /dev/null +++ b/components/agentic-circuit/test/ACIR/trace-valid.mlir @@ -0,0 +1,108 @@ +// RUN: %acir_opt_public %s | %FileCheck %s +// RUN: %acir_opt_public %s | %acir_opt_public | %FileCheck %s +// RUN: %acir_opt_public --pass-pipeline='builtin.module(canonicalize,cse)' %s | %FileCheck %s --check-prefix=EFFECTS + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Trace() parameters {} graph { + ac.stat @decoded kind "counter" + ac.stat @position kind "gauge" + ac.stat @eof kind "counter" + ac.stat @observed kind "gauge" + ac.process @decoder kind "monitor" { ac.yield_sim } + ac.process @workload kind "workload" { + %cursor0 = ac.trace.open source "pto" + %cursor1, %raw, %advanced = ac.trace.next %cursor0 from source "pto" : i32 + %decoded = ac.trace.decode %raw : i32 to i64 + %position = ac.trace.position %cursor1 from source "pto" + %eof = ac.trace.eof %cursor1 from source "pto" + %observed = ac.probe @decoder kind "module" : i64 + %index_cursor0 = ac.trace.open source "index_records" + %index_cursor1, %index_raw, %index_advanced = ac.trace.next %index_cursor0 from source "index_records" : index + %index_decoded = ac.trace.decode %index_raw : index to i64 + ac.stat.add @decoded %decoded : i64 + ac.stat.add @position %position : index + ac.stat.add @eof %eof : i1 + ac.stat.add @observed %observed : i64 + ac.stat.add @decoded %index_decoded : i64 + ac.yield_sim + } + ac.return + } + ac.module @LoopTrace(i1) parameters {} graph { + ^bb0(%keep_running : i1): + ac.stat @position kind "gauge" + ac.process @workload kind "workload" captures(%keep_running : i1) { + ^bb0(%condition : i1): + %cursor0 = ac.trace.open source "runtime_loop" + %cursor1 = scf.while (%cursor = %cursor0) : (index) -> index { + scf.condition(%condition) %cursor : index + } do { + ^bb0(%cursor : index): + %next, %raw, %advanced = ac.trace.next %cursor from source "runtime_loop" : i32 + ac.wait_until %advanced + scf.yield %next : index + } + %position = ac.trace.position %cursor1 from source "runtime_loop" + ac.stat.add @position %position : index + ac.yield_sim + } + ac.return + } + ac.module @ForTrace() parameters {} graph { + ac.stat @position kind "gauge" + ac.stat @ordinary kind "gauge" + ac.process @workload kind "workload" { + %cursor0 = ac.trace.open source "for_loop" + %lb = index.constant 0 + %ub = index.constant 4 + %step = index.constant 1 + %cursor1 = scf.for %i = %lb to %ub step %step + iter_args(%cursor = %cursor0) -> index { + scf.yield %cursor : index + } + %ordinary0 = index.constant 7 + %ordinary1 = scf.for %i = %lb to %ub step %step + iter_args(%ordinary = %ordinary0) -> index { + scf.yield %ordinary : index + } + %position = ac.trace.position %cursor1 from source "for_loop" + ac.stat.add @position %position : index + ac.stat.add @ordinary %ordinary1 : index + ac.yield_sim + } + ac.return + } + ac.module @MergedTrace(i1) parameters {} graph { + ^bb0(%condition : i1): + ac.process @workload kind "workload" captures(%condition : i1) { + ^bb0(%branch : i1): + %cursor = ac.trace.open source "merged" + %merged = scf.if %branch -> index { + scf.yield %cursor : index + } else { + scf.yield %cursor : index + } + %next, %raw, %advanced = ac.trace.next %merged from source "merged" : i32 + ac.yield_sim + } + ac.return + } +} + +// CHECK: ac.trace.open source "pto" +// CHECK: ac.trace.next +// CHECK: ac.trace.decode +// CHECK: ac.trace.position +// CHECK: ac.trace.eof +// CHECK: ac.module @LoopTrace +// CHECK: scf.while +// CHECK: ac.trace.next +// CHECK: ac.module @ForTrace +// CHECK: ac.module @MergedTrace +// EFFECTS: ac.trace.open +// EFFECTS: ac.trace.next +// EFFECTS: ac.trace.position +// EFFECTS: ac.trace.eof +// EFFECTS: ac.probe +// EFFECTS: ac.stat.add +// EFFECTS: ac.yield_sim diff --git a/components/agentic-circuit/test/ACIR/transform-invalid.mlir b/components/agentic-circuit/test/ACIR/transform-invalid.mlir new file mode 100644 index 00000000..45377a2e --- /dev/null +++ b/components/agentic-circuit/test/ACIR/transform-invalid.mlir @@ -0,0 +1,83 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/no-input.mlir 2>&1 | %FileCheck %s --check-prefix=NO-INPUT +// RUN: %not %acir_opt %t/no-output.mlir 2>&1 | %FileCheck %s --check-prefix=NO-OUTPUT +// RUN: %not %acir_opt %t/depth-count.mlir 2>&1 | %FileCheck %s --check-prefix=DEPTH-COUNT +// RUN: %not %acir_opt %t/latency-zero.mlir 2>&1 | %FileCheck %s --check-prefix=LATENCY-ZERO +// RUN: %not %acir_opt %t/block-arg.mlir 2>&1 | %FileCheck %s --check-prefix=BLOCK-ARG +// RUN: %not %acir_opt %t/yield-type.mlir 2>&1 | %FileCheck %s --check-prefix=YIELD-TYPE +// RUN: %not %acir_opt %t/effectful-body.mlir 2>&1 | %FileCheck %s --check-prefix=EFFECTFUL-BODY + +// NO-INPUT: error: 'ac.transform' op requires at least one input queue +// NO-OUTPUT: error: 'ac.transform' op requires at least one output queue +// DEPTH-COUNT: error: 'ac.transform' op output depth count must match result count +// LATENCY-ZERO: error: 'ac.transform' op output latencies must be positive +// BLOCK-ARG: error: 'ac.transform' op body argument 0 must be '!ac.var' +// YIELD-TYPE: error: 'ac.transform' op yielded value 0 must be '!ac.var' +// EFFECTFUL-BODY: error: 'ac.transform' op body operation 'ac.assert' must be pure + +//--- no-input.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %output = ac.transform depths [4] latencies [1] { + ^body: + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var + ac.transform.yield %value : !ac.var + } : () -> (!ac.queue) +} + +//--- effectful-body.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + %output = ac.transform %input depths [4] latencies [1] { + ^body(%item: !ac.var): + %condition = arith.constant true + ac.assert %condition, "effect is illegal" + ac.transform.yield %item : !ac.var + } : (!ac.queue) -> (!ac.queue) +} + +//--- no-output.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + ac.transform %input depths [] latencies [] { + ^body(%item: !ac.var): + ac.transform.yield + } : (!ac.queue) -> () +} + +//--- depth-count.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + %output = ac.transform %input depths [] latencies [1] { + ^body(%item: !ac.var): + ac.transform.yield %item : !ac.var + } : (!ac.queue) -> (!ac.queue) +} + +//--- latency-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + %output = ac.transform %input depths [4] latencies [0] { + ^body(%item: !ac.var): + ac.transform.yield %item : !ac.var + } : (!ac.queue) -> (!ac.queue) +} + +//--- block-arg.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + %output = ac.transform %input depths [4] latencies [1] { + ^body(%item: !ac.var): + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var + ac.transform.yield %value : !ac.var + } : (!ac.queue) -> (!ac.queue) +} + +//--- yield-type.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + %output = ac.transform %input depths [4] latencies [1] { + ^body(%item: !ac.var): + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var + ac.transform.yield %value : !ac.var + } : (!ac.queue) -> (!ac.queue) +} diff --git a/components/agentic-circuit/test/ACIR/transform.mlir b/components/agentic-circuit/test/ACIR/transform.mlir new file mode 100644 index 00000000..da3a3988 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/transform.mlir @@ -0,0 +1,18 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + %input = "builtin.unrealized_conversion_cast"() : () -> !ac.queue + %output = ac.transform %input depths [4] latencies [1] { + ^body(%item: !ac.var): + ac.transform.yield %item : !ac.var + } : (!ac.queue) -> (!ac.queue) +} + +// CHECK: %[[INPUT:.*]] = unrealized_conversion_cast to !ac.queue +// CHECK: %[[OUTPUT:.*]] = ac.transform %[[INPUT]] depths [4] latencies [1] +// CHECK: ^bb0(%[[ITEM:.*]]: !ac.var): +// CHECK: ac.transform.yield %[[ITEM]] : !ac.var +// CHECK: (!ac.queue) -> !ac.queue diff --git a/components/agentic-circuit/test/ACIR/types-invalid.mlir b/components/agentic-circuit/test/ACIR/types-invalid.mlir new file mode 100644 index 00000000..91c26fbb --- /dev/null +++ b/components/agentic-circuit/test/ACIR/types-invalid.mlir @@ -0,0 +1,148 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/vector-zero.mlir 2>&1 | %FileCheck %s --check-prefix=VECTOR-ZERO +// RUN: %not %acir_opt %t/vector-negative.mlir 2>&1 | %FileCheck %s --check-prefix=VECTOR-NEGATIVE +// RUN: %not %acir_opt %t/vector-minimum.mlir 2>&1 | %FileCheck %s --check-prefix=VECTOR-MINIMUM +// RUN: %not %acir_opt %t/vector-overflow.mlir 2>&1 | %FileCheck %s --check-prefix=VECTOR-OVERFLOW +// RUN: %not %acir_opt %t/vector-underflow.mlir 2>&1 | %FileCheck %s --check-prefix=VECTOR-UNDERFLOW +// RUN: %not %acir_opt %t/channel-nested.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL-NESTED +// RUN: %not %acir_opt %t/channel-standalone.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL-STANDALONE +// RUN: %not %acir_opt %t/channel-tuple.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL-TUPLE +// RUN: %not %acir_opt %t/channel-function.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL-FUNCTION +// RUN: %not %acir_opt %t/channel-type-attr.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL-TYPE-ATTR +// RUN: %not %acir_opt %t/channel-composite-attr.mlir 2>&1 | %FileCheck %s --check-prefix=CHANNEL-COMPOSITE-ATTR +// RUN: %not %acir_opt %t/rate-numerator.mlir 2>&1 | %FileCheck %s --check-prefix=RATE-NUMERATOR +// RUN: %not %acir_opt %t/rate-denominator.mlir 2>&1 | %FileCheck %s --check-prefix=RATE-DENOMINATOR +// RUN: %not %acir_opt %t/duration-data-unit.mlir 2>&1 | %FileCheck %s --check-prefix=DURATION-DATA +// RUN: %not %acir_opt %t/unknown-unit.mlir 2>&1 | %FileCheck %s --check-prefix=UNKNOWN-UNIT +// RUN: %not %acir_opt %t/malformed-named.mlir 2>&1 | %FileCheck %s --check-prefix=MALFORMED-NAMED +// RUN: %not %acir_opt %t/malformed-aggregate.mlir 2>&1 | %FileCheck %s --check-prefix=MALFORMED-AGGREGATE +// RUN: %not %acir_opt %t/malformed-topology.mlir 2>&1 | %FileCheck %s --check-prefix=MALFORMED-TOPOLOGY +// RUN: %not %acir_opt %t/union-non-symbol.mlir 2>&1 | %FileCheck %s --check-prefix=UNION-PARAM +// RUN: %not %acir_opt %t/address-non-symbol.mlir 2>&1 | %FileCheck %s --check-prefix=ADDRESS-PARAM + +// VECTOR-ZERO: error: vector length must be positive +// VECTOR-NEGATIVE: error: vector length must be positive +// VECTOR-MINIMUM: error: vector length must be positive +// VECTOR-OVERFLOW: error: failed to parse ACIR_VectorType parameter 'length' +// VECTOR-UNDERFLOW: error: failed to parse ACIR_VectorType parameter 'length' +// CHANNEL-NESTED: error: channel types cannot be nested inside value types +// CHANNEL-STANDALONE: error: channel type is only permitted in an ac.interface channel declaration +// CHANNEL-TUPLE: topology type '!ac.channel' cannot be nested inside 'tuple>>' +// CHANNEL-FUNCTION: channel type is only permitted in an ac.interface channel declaration +// CHANNEL-TYPE-ATTR: error: channel type is only permitted in an ac.interface channel declaration +// CHANNEL-COMPOSITE-ATTR: topology type '!ac.channel' cannot be nested inside 'tuple>' +// RATE-NUMERATOR: error: rate numerator must be a data unit +// RATE-DENOMINATOR: error: rate denominator must be a time unit +// DURATION-DATA: error: duration requires a time unit +// UNKNOWN-UNIT: error: failed to parse ACIR_DurationType parameter 'unit' +// MALFORMED-NAMED: error: failed to parse ACIR_StructType parameter 'name' +// MALFORMED-AGGREGATE: error: failed to parse ACIR_OptionalType parameter 'elementType' +// MALFORMED-TOPOLOGY: error: expected ',' + +//--- vector-zero.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.vector<0 x i8> +} + +//--- vector-negative.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.vector<-2 x i8> +} + +//--- vector-overflow.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.vector<9223372036854775808 x i8> +} + +//--- vector-minimum.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.vector<-9223372036854775808 x i8> +} + +//--- vector-underflow.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.vector<-9223372036854775809 x i8> +} + +//--- channel-nested.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.optional> +} + +//--- channel-standalone.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.channel +} + +//--- channel-tuple.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> tuple>> +} + +//--- channel-function.mlir +builtin.module attributes { + ac.contract_epoch = "0.3", + test.signature = (i8) -> !ac.channel +} { +} + +//--- channel-type-attr.mlir +builtin.module attributes { + ac.contract_epoch = "0.3", + test.type = !ac.channel +} { +} + +//--- channel-composite-attr.mlir +builtin.module attributes { + ac.contract_epoch = "0.3", + test.types = [tuple>] +} { +} + +//--- rate-numerator.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.rate +} + +//--- rate-denominator.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.rate +} + +//--- duration-data-unit.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.duration +} + +//--- unknown-unit.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.duration +} + +//--- malformed-named.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.struct +} + +//--- malformed-aggregate.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.optional<> +} + +//--- malformed-topology.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.flow +} + +//--- union-non-symbol.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.union +} +// UNION-PARAM: error: invalid kind of attribute specified + +//--- address-non-symbol.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !ac.address +} +// ADDRESS-PARAM: error: invalid kind of attribute specified diff --git a/components/agentic-circuit/test/ACIR/types-valid.mlir b/components/agentic-circuit/test/ACIR/types-valid.mlir new file mode 100644 index 00000000..f64bec81 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/types-valid.mlir @@ -0,0 +1,79 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +// This file covers all 16 SSA-legal ACIR public types. Channel's 17th +// parser/printer case is covered by ACIRTypesTest.PublicTypeInventoryRoundTrips. +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.protocol"() <{sym_name = "test_protocol"}> ({ + "ac.role"() <{sym_name = "producer", dual = @consumer, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "consumer", dual = @producer, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "idle", initial = true, terminal = true}> : () -> () + "ac.event"() <{sym_name = "send", from = @producer, to = @consumer, payload = i8, action = "offer"}> : () -> () + "ac.transition"() <{source = @idle, target = @idle, event = @send, transfer = true}> ({}) : () -> () + }) : () -> () + "ac.interface"() <{sym_name = "MemoryPort"}> ({ + "ac.role"() <{sym_name = "initiator", dual = @target, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "target", dual = @initiator, cardinality = "exclusive"}> : () -> () + }) : () -> () + "builtin.unrealized_conversion_cast"() : () -> !ac.struct<@types::@Header> + "builtin.unrealized_conversion_cast"() : () -> !ac.packet<@types::@Request> + "builtin.unrealized_conversion_cast"() : () -> !ac.transaction<@types::@Dma> + "builtin.unrealized_conversion_cast"() : () -> !ac.enum<@types::@Opcode> + "builtin.unrealized_conversion_cast"() : () -> !ac.union<@types::@Payload> + "builtin.unrealized_conversion_cast"() : () -> !ac.optional + "builtin.unrealized_conversion_cast"() : () -> !ac.list> + "builtin.unrealized_conversion_cast"() : () -> !ac.vector<4 x i8> + "builtin.unrealized_conversion_cast"() : () -> !ac.vector<9223372036854775807 x i8> + "builtin.unrealized_conversion_cast"() : () -> !ac.flow + "builtin.unrealized_conversion_cast"() : () -> !ac.endpoint<@MemoryPort, @target> + "builtin.unrealized_conversion_cast"() : () -> !ac.resource_ref<@Memory, @reader> + "builtin.unrealized_conversion_cast"() : () -> !ac.duration + "builtin.unrealized_conversion_cast"() : () -> !ac.duration + "builtin.unrealized_conversion_cast"() : () -> !ac.duration + "builtin.unrealized_conversion_cast"() : () -> !ac.duration + "builtin.unrealized_conversion_cast"() : () -> !ac.duration + "builtin.unrealized_conversion_cast"() : () -> !ac.duration + "builtin.unrealized_conversion_cast"() : () -> !ac.duration + "builtin.unrealized_conversion_cast"() : () -> !ac.rate + "builtin.unrealized_conversion_cast"() : () -> !ac.rate + "builtin.unrealized_conversion_cast"() : () -> !ac.rate + "builtin.unrealized_conversion_cast"() : () -> !ac.rate + "builtin.unrealized_conversion_cast"() : () -> !ac.rate + "builtin.unrealized_conversion_cast"() : () -> !ac.rate + "builtin.unrealized_conversion_cast"() : () -> !ac.rate + "builtin.unrealized_conversion_cast"() : () -> !ac.event> + "builtin.unrealized_conversion_cast"() : () -> !ac.address<@global> + "builtin.unrealized_conversion_cast"() : () -> !ac.resource_token<@Memory> +} + +// CHECK: !ac.struct<@types::@Header> +// CHECK: !ac.packet<@types::@Request> +// CHECK: !ac.transaction<@types::@Dma> +// CHECK: !ac.enum<@types::@Opcode> +// CHECK: !ac.union<@types::@Payload> +// CHECK: !ac.optional +// CHECK: !ac.list> +// CHECK: !ac.vector<4 x i8> +// CHECK: !ac.vector<9223372036854775807 x i8> +// CHECK: !ac.flow +// CHECK: !ac.endpoint<@MemoryPort, @target> +// CHECK: !ac.resource_ref<@Memory, @reader> +// CHECK: !ac.duration +// CHECK: !ac.duration +// CHECK: !ac.duration +// CHECK: !ac.duration +// CHECK: !ac.duration +// CHECK: !ac.duration +// CHECK: !ac.duration +// CHECK: !ac.rate +// CHECK: !ac.rate +// CHECK: !ac.rate +// CHECK: !ac.rate +// CHECK: !ac.rate +// CHECK: !ac.rate +// CHECK: !ac.rate +// CHECK: !ac.event> +// CHECK: !ac.address<@global> +// CHECK: !ac.resource_token<@Memory> diff --git a/components/agentic-circuit/test/ACIR/var-expressions-invalid.mlir b/components/agentic-circuit/test/ACIR/var-expressions-invalid.mlir new file mode 100644 index 00000000..61b950ba --- /dev/null +++ b/components/agentic-circuit/test/ACIR/var-expressions-invalid.mlir @@ -0,0 +1,106 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/constant.mlir 2>&1 | %FileCheck %s --check-prefix=CONSTANT +// RUN: %not %acir_opt %t/binary.mlir 2>&1 | %FileCheck %s --check-prefix=BINARY +// RUN: %not %acir_opt %t/get-field.mlir 2>&1 | %FileCheck %s --check-prefix=GET-FIELD +// RUN: %not %acir_opt %t/get-result.mlir 2>&1 | %FileCheck %s --check-prefix=GET-RESULT +// RUN: %not %acir_opt %t/with-result.mlir 2>&1 | %FileCheck %s --check-prefix=WITH-RESULT +// RUN: %not %acir_opt %t/with-value.mlir 2>&1 | %FileCheck %s --check-prefix=WITH-VALUE +// RUN: %not %acir_opt %t/sub-nonnumeric.mlir 2>&1 | %FileCheck %s --check-prefix=SUB-NONNUMERIC +// RUN: %not %acir_opt %t/mul-nonnumeric.mlir 2>&1 | %FileCheck %s --check-prefix=MUL-NONNUMERIC +// RUN: %not %acir_opt %t/cmp-predicate.mlir 2>&1 | %FileCheck %s --check-prefix=CMP-PREDICATE +// RUN: %not %acir_opt %t/popcount-width.mlir 2>&1 | %FileCheck %s --check-prefix=POPCOUNT-WIDTH +// RUN: %not %acir_opt %t/popcount-input.mlir 2>&1 | %FileCheck %s --check-prefix=POPCOUNT-INPUT + +// CONSTANT: error: 'ac.var.constant' op attribute type must match Var element type +// BINARY: error: use of value '%right' expects different type than prior uses +// GET-FIELD: error: 'ac.var.get' op unknown field 'missing' +// GET-RESULT: error: 'ac.var.get' op field 'value' result must be '!ac.var' +// WITH-RESULT: error: 'ac.var.with' op must preserve record Var identity +// WITH-VALUE: error: 'ac.var.with' op field 'value' expects '!ac.var' +// SUB-NONNUMERIC: error: 'ac.var.sub' op arithmetic Var element must be an integer or float +// MUL-NONNUMERIC: error: 'ac.var.mul' op arithmetic Var element must be an integer or float +// CMP-PREDICATE: error: 'ac.var.cmp' op predicate must be eq, ne, slt, sle, sgt, or sge +// POPCOUNT-WIDTH: error: 'ac.var.popcount' op result width must be ceil(log2(input_width + 1)) = 4 +// POPCOUNT-INPUT: error: 'ac.var.popcount' op input width must be in [1, 64] + +//--- constant.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %bad = ac.var.constant 1 : i16 as !ac.var +} + +//--- sub-nonnumeric.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var> + %bad = ac.var.sub %value, %value : !ac.var> +} + +//--- mul-nonnumeric.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var> + %bad = ac.var.mul %value, %value : !ac.var> +} + +//--- popcount-width.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var + %bad = ac.var.popcount %value : !ac.var -> !ac.var +} + +//--- popcount-input.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %value = "builtin.unrealized_conversion_cast"() : () -> !ac.var + %bad = ac.var.popcount %value : !ac.var -> !ac.var +} + +//--- cmp-predicate.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %left = ac.var.constant 1 : i64 as !ac.var + %right = ac.var.constant 2 : i64 as !ac.var + %bad = ac.var.cmp "random" %left, %right : !ac.var -> !ac.var +} + +//--- binary.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + %left = ac.var.constant 1 : i32 as !ac.var + %right = ac.var.constant 1 : i16 as !ac.var + %bad = ac.var.add %left, %right : !ac.var +} + +//--- get-field.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Item", fields = [{name = "value", type = i64}]}> : () -> () + }) : () -> () + %item = "builtin.unrealized_conversion_cast"() : () -> !ac.var> + %bad = ac.var.get %item field "missing" : !ac.var> -> !ac.var +} + +//--- get-result.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Item", fields = [{name = "value", type = i64}]}> : () -> () + }) : () -> () + %item = "builtin.unrealized_conversion_cast"() : () -> !ac.var> + %bad = ac.var.get %item field "value" : !ac.var> -> !ac.var +} + +//--- with-result.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "A", fields = [{name = "value", type = i64}]}> : () -> () + "ac.transaction"() <{sym_name = "B", fields = [{name = "value", type = i64}]}> : () -> () + }) : () -> () + %item = "builtin.unrealized_conversion_cast"() : () -> !ac.var> + %value = ac.var.constant 1 : i64 as !ac.var + %bad = ac.var.with %item, %value field "value" : !ac.var>, !ac.var -> !ac.var> +} + +//--- with-value.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.transaction"() <{sym_name = "Item", fields = [{name = "value", type = i64}]}> : () -> () + }) : () -> () + %item = "builtin.unrealized_conversion_cast"() : () -> !ac.var> + %value = ac.var.constant 1 : i16 as !ac.var + %bad = ac.var.with %item, %value field "value" : !ac.var>, !ac.var -> !ac.var> +} diff --git a/components/agentic-circuit/test/ACIR/var-expressions.mlir b/components/agentic-circuit/test/ACIR/var-expressions.mlir new file mode 100644 index 00000000..9eddd161 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/var-expressions.mlir @@ -0,0 +1,34 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "WorkItem", fields = [{name = "value", type = i64}, {name = "remaining", type = i16}]}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 8 : i64, endianness = "little", preferred_alignment = 8 : i64, size = 16 : i64}>} : () -> () + %input = ac.source depth 4 latency 1 : !ac.queue> + %output = ac.transform %input depths [8] latencies [1] { + ^body(%item: !ac.var>): + %one64 = ac.var.constant 1 : i64 as !ac.var + %one16 = ac.var.constant 1 : i16 as !ac.var + %value = ac.var.get %item field "value" : !ac.var> -> !ac.var + %remaining = ac.var.get %item field "remaining" : !ac.var> -> !ac.var + %sum = ac.var.add %value, %one64 : !ac.var + %difference = ac.var.sub %remaining, %one16 : !ac.var + %product = ac.var.mul %sum, %one64 : !ac.var + %positive = ac.var.cmp "sgt" %product, %one64 : !ac.var -> !ac.var + %updated_value = ac.var.with %item, %product field "value" : !ac.var>, !ac.var -> !ac.var> + %updated = ac.var.with %updated_value, %difference field "remaining" : !ac.var>, !ac.var -> !ac.var> + ac.transform.yield %updated : !ac.var> + } : (!ac.queue>) -> !ac.queue> + ac.sink %output : !ac.queue> +} + +// CHECK: ac.var.constant 1 : i64 as !ac.var +// CHECK: ac.var.get {{.*}} field "value" +// CHECK: ac.var.add +// CHECK: ac.var.sub +// CHECK: ac.var.mul +// CHECK: ac.var.cmp "sgt" +// CHECK: ac.var.with diff --git a/components/agentic-circuit/test/ACIR/view-provenance.mlir b/components/agentic-circuit/test/ACIR/view-provenance.mlir new file mode 100644 index 00000000..69219508 --- /dev/null +++ b/components/agentic-circuit/test/ACIR/view-provenance.mlir @@ -0,0 +1,96 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt %t/zero-chain.mlir | %FileCheck %s --check-prefix=ZERO +// RUN: %not %acir_opt %t/unresolved-source.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED +// RUN: %not %acir_opt %t/repeated-source.mlir 2>&1 | %FileCheck %s --check-prefix=REPEATED +// RUN: %not %acir_opt %t/dotted-view-name.mlir 2>&1 | %FileCheck %s --check-prefix=VIEW-NAME +// RUN: %not %acir_opt %t/empty-view-name.mlir 2>&1 | %FileCheck %s --check-prefix=VIEW-NAME +// RUN: %not %acir_opt %t/slashed-view-name.mlir 2>&1 | %FileCheck %s --check-prefix=VIEW-NAME +// RUN: %not %acir_opt %t/dotted-producer-id.mlir 2>&1 | %FileCheck %s --check-prefix=PRODUCER-ID +// RUN: %not %acir_opt %t/empty-producer-id.mlir 2>&1 | %FileCheck %s --check-prefix=PRODUCER-ID +// RUN: %not %acir_opt %t/slashed-producer-id.mlir 2>&1 | %FileCheck %s --check-prefix=PRODUCER-ID + +//--- zero-chain.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Empty", function_type = () -> (), static_params = {}}> ({ + "ac.return"() : () -> () + }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Empty, sym_name = "unrelated", stable_id = "unrelated", path = "unrelated", static_args = {}}> : () -> () + "ac.instance"() <{definition = @Empty, sym_name = "source", stable_id = "source", path = "source", static_args = {}}> : () -> () + "ac.view"() <{sym_name = "first", kind = "permutation", source_producers = [@source], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.view"() <{sym_name = "second", kind = "permutation", source_producers = [@first], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// ZERO: ac.view @first +// ZERO: ac.view @second + +//--- unresolved-source.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.view"() <{sym_name = "view", kind = "permutation", source_producers = [@missing], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// UNRESOLVED: source producer '@missing' is unresolved + +//--- repeated-source.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Empty", function_type = () -> (), static_params = {}}> ({ "ac.return"() : () -> () }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Empty, sym_name = "source", stable_id = "source", path = "source", static_args = {}}> : () -> () + "ac.view"() <{sym_name = "view", kind = "concat", source_producers = [@source, @source], source_shapes = [array, array], axis = 0 : i64, indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// REPEATED: source producers must not repeat + +//--- dotted-view-name.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.view"() <{sym_name = "bad.name", kind = "permutation", source_producers = [@missing], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} + +//--- empty-view-name.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.view"() <{sym_name = "", kind = "permutation", source_producers = [@missing], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} + +//--- slashed-view-name.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.view"() <{sym_name = "bad/name", kind = "permutation", source_producers = [@missing], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// VIEW-NAME: view name must be a stable local segment + +//--- dotted-producer-id.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.view"() <{sym_name = "view", kind = "permutation", source_producers = [@"bad.name"], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} + +//--- empty-producer-id.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.view"() <{sym_name = "view", kind = "permutation", source_producers = [@""], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} + +//--- slashed-producer-id.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.view"() <{sym_name = "view", kind = "permutation", source_producers = [@"bad/name"], source_shapes = [array], indices = array, shape = array}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// PRODUCER-ID: source producer IDs must be stable local segments diff --git a/components/agentic-circuit/test/ACSim/dialect-smoke.mlir b/components/agentic-circuit/test/ACSim/dialect-smoke.mlir new file mode 100644 index 00000000..eb71fd81 --- /dev/null +++ b/components/agentic-circuit/test/ACSim/dialect-smoke.mlir @@ -0,0 +1,24 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt --show-dialects | %FileCheck %s --check-prefix=DIALECTS +// RUN: %acir_opt %t/canonical.mlir | %FileCheck %s --check-prefix=CANONICAL +// RUN: %not %acir_opt %t/unknown-acsim-op.mlir 2>&1 | %FileCheck %s --check-prefix=UNKNOWN-ACSIM +// RUN: %not %acir_opt %t/unregistered-dialect.mlir 2>&1 | %FileCheck %s --check-prefix=UNREGISTERED + +// DIALECTS: Available Dialects: ac,acsim,arith,builtin,cf,dlti,func,index,scf +// CANONICAL: module attributes {ac.contract_epoch = "0.3"} +// UNKNOWN-ACSIM: error: unregistered operation 'acsim.unknown' +// UNREGISTERED: error: operation being parsed with an unregistered dialect + +//--- canonical.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { +} + +//--- unknown-acsim-op.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.unknown"() : () -> () +} + +//--- unregistered-dialect.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "gpu.unknown"() : () -> () +} diff --git a/components/agentic-circuit/test/ACSim/generated-call-contract.mlir b/components/agentic-circuit/test/ACSim/generated-call-contract.mlir new file mode 100644 index 00000000..76249fc4 --- /dev/null +++ b/components/agentic-circuit/test/ACSim/generated-call-contract.mlir @@ -0,0 +1,114 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt %t/valid.mlir | %FileCheck %s --check-prefix=VALID +// RUN: sed 's/acsim.inline @pure_binding()/acsim.inline @stateful_binding()/' %t/valid.mlir > %t/inline-stateful.mlir +// RUN: %not %acir_opt %t/inline-stateful.mlir 2>&1 | %FileCheck %s --check-prefix=INLINE-STATEFUL +// RUN: sed 's/acsim.invoke @stateful_binding()/acsim.invoke @pure_binding()/' %t/valid.mlir > %t/invoke-pure.mlir +// RUN: %not %acir_opt %t/invoke-pure.mlir 2>&1 | %FileCheck %s --check-prefix=INVOKE-PURE +// RUN: sed 's/acsim.inline @generated_inline()/acsim.inline @cpp_i32()/' %t/valid.mlir > %t/inline-nonimplementation.mlir +// RUN: %not %acir_opt %t/inline-nonimplementation.mlir 2>&1 | %FileCheck %s --check-prefix=NONIMPLEMENTATION +// RUN: sed 's/acsim.invoke @generated_invoke()/acsim.invoke @cpp_i32()/' %t/valid.mlir > %t/invoke-nonimplementation.mlir +// RUN: %not %acir_opt %t/invoke-nonimplementation.mlir 2>&1 | %FileCheck %s --check-prefix=NONIMPLEMENTATION +// RUN: sed 's/acsim.inline @generated_inline()/acsim.inline @missing()/' %t/valid.mlir > %t/unresolved.mlir +// RUN: %not %acir_opt %t/unresolved.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED +// RUN: sed 's/acsim.inline @generated_inline()/acsim.inline @Top()/' %t/valid.mlir > %t/module-callee.mlir +// RUN: %not %acir_opt %t/module-callee.mlir 2>&1 | %FileCheck %s --check-prefix=MODULE-CALLEE +// RUN: sed 's/acsim.invoke @generated_invoke()/acsim.invoke @tick()/' %t/valid.mlir > %t/process-callee.mlir +// RUN: %not %acir_opt %t/process-callee.mlir 2>&1 | %FileCheck %s --check-prefix=PROCESS-CALLEE +// RUN: sed 's/acsim.inline @generated_inline()/acsim.inline @generated_invoke()/' %t/valid.mlir > %t/mixed-effect.mlir +// RUN: %not %acir_opt %t/mixed-effect.mlir 2>&1 | %FileCheck %s --check-prefix=MIXED +// RUN: sed 's/acsim.inline @pure_binding() : () -> !acsim.expr<@cpp_i32>/acsim.inline @pure_binding() : () -> i32/' %t/valid.mlir > %t/module-inline-i32.mlir +// RUN: %not %acir_opt %t/module-inline-i32.mlir 2>&1 | %FileCheck %s --check-prefix=MODULE-RESULT +// RUN: sed 's/acsim.inline @generated_scalar() : () -> i32/acsim.inline @generated_scalar() : () -> !acsim.expr<@cpp_i32>/' %t/valid.mlir > %t/process-inline-expr.mlir +// RUN: %not %acir_opt %t/process-inline-expr.mlir 2>&1 | %FileCheck %s --check-prefix=PROCESS-RESULT +// RUN: sed 's/acsim.inline @generated_scalar() : () -> i32/acsim.inline @generated_scalar() : () -> !acsim.owner<@stateful_binding>/' %t/valid.mlir > %t/process-inline-owner.mlir +// RUN: %not %acir_opt %t/process-inline-owner.mlir 2>&1 | %FileCheck %s --check-prefix=PROCESS-RESULT +// RUN: sed 's/acsim.inline @generated_scalar() : () -> i32/acsim.inline @generated_scalar() : () -> !acsim.ref<@stateful_binding>/' %t/valid.mlir > %t/process-inline-ref.mlir +// RUN: %not %acir_opt %t/process-inline-ref.mlir 2>&1 | %FileCheck %s --check-prefix=PROCESS-RESULT +// RUN: sed 's/acsim.inline @generated_scalar() : () -> i32/acsim.inline @generated_scalar() : () -> !acsim.wake<@wake_kind>/' %t/valid.mlir > %t/process-inline-wake.mlir +// RUN: %not %acir_opt %t/process-inline-wake.mlir 2>&1 | %FileCheck %s --check-prefix=PROCESS-RESULT +// RUN: sed 's/acsim.inline @generated_scalar() : () -> i32/acsim.inline @generated_scalar() : () -> !acsim.pc<@tick>/' %t/valid.mlir > %t/process-inline-other.mlir +// RUN: %not %acir_opt %t/process-inline-other.mlir 2>&1 | %FileCheck %s --check-prefix=PROCESS-RESULT +// RUN: sed 's/acsim.invoke @stateful_binding() : () -> !acsim.value<@cpp_i32>/acsim.invoke @stateful_binding() : () -> i32/' %t/valid.mlir > %t/invoke-i32.mlir +// RUN: %not %acir_opt %t/invoke-i32.mlir 2>&1 | %FileCheck %s --check-prefix=INVOKE-RESULT + +// VALID: acsim.inline @pure_binding +// VALID: acsim.inline @generated_inline +// VALID: acsim.process @tick +// VALID: acsim.inline @generated_scalar +// VALID: acsim.inline @generated_value +// VALID: acsim.invoke @stateful_binding +// VALID: acsim.invoke @generated_invoke +// INLINE-STATEFUL: inline callee '@stateful_binding' requires effect 'pure' +// INVOKE-PURE: invoke callee '@pure_binding' requires effect 'stateful' +// NONIMPLEMENTATION: callee reference '@cpp_i32' resolves to non-implementation acsim.type +// UNRESOLVED: callee reference '@missing' is unresolved +// MODULE-CALLEE: callee reference '@Top' resolves to incompatible operation 'acsim.module' +// PROCESS-CALLEE: callee reference '@tick' resolves to incompatible operation 'acsim.process' +// MIXED: generated implementation callee '@generated_invoke' cannot be used by both acsim.inline and acsim.invoke +// MODULE-RESULT: module inline result must be exactly !acsim.expr +// PROCESS-RESULT: process inline result must be an integer, float, index, or !acsim.value +// INVOKE-RESULT: invoke results must be exact !acsim.value or !acsim.wake types + +//--- valid.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @calls epoch "0.3" root @Top construction ["Top.tick"] destruction ["Top.tick"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @cpp_i32 cpp "int32_t" kind "value" fingerprint "sha256:0100000000000000000000000000000000000000000000000000000000000000" + acsim.type @generated_inline cpp "generated::inline_expr" kind "implementation" fingerprint "sha256:0200000000000000000000000000000000000000000000000000000000000000" + acsim.type @generated_invoke cpp "generated::invoke" kind "implementation" fingerprint "sha256:0500000000000000000000000000000000000000000000000000000000000000" + acsim.type @generated_scalar cpp "generated::scalar" kind "implementation" fingerprint "sha256:0300000000000000000000000000000000000000000000000000000000000000" + acsim.type @generated_value cpp "generated::value" kind "implementation" fingerprint "sha256:0400000000000000000000000000000000000000000000000000000000000000" + acsim.type @provider cpp "gfsim" kind "provider" fingerprint "sha256:0700000000000000000000000000000000000000000000000000000000000000" + acsim.type @pure_impl cpp "gfsim::pure" kind "implementation" fingerprint "sha256:0900000000000000000000000000000000000000000000000000000000000000" + acsim.type @pure_schema cpp "pure.schema" kind "schema" fingerprint "sha256:0800000000000000000000000000000000000000000000000000000000000000" + acsim.type @stateful_impl cpp "gfsim::stateful" kind "implementation" fingerprint "sha256:0b00000000000000000000000000000000000000000000000000000000000000" + acsim.type @stateful_schema cpp "stateful.schema" kind "schema" fingerprint "sha256:0a00000000000000000000000000000000000000000000000000000000000000" + acsim.type @wake_kind cpp "generated::Wake" kind "wake" fingerprint "sha256:0600000000000000000000000000000000000000000000000000000000000000" + acsim.binding @pure_binding record { + activation_sources = [], availability = "available", binding = "pure_binding", binding_schema = "acsim-binding-0.1", + component_schema = @pure_schema, component_schema_fingerprint = "sha256:0800000000000000000000000000000000000000000000000000000000000000", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "gfsim::Pure", entry_points = {pure = "gfsim::pure", reset = "", validate = "", work = "", xfer = ""}, header = "gfsim/pure.hpp", symbol = "gfsim::Pure", target = "gfsim"}, + cpp_type = @cpp_i32, effect = "pure", fingerprint = "sha256:0c00000000000000000000000000000000000000000000000000000000000000", + implementation = @pure_impl, ownership = {kind = "none", placement = "inline"}, parameters = [], ports = [], provider = @provider, + provider_implementation_fingerprint = "sha256:0900000000000000000000000000000000000000000000000000000000000000", resources = [], results = [{cpp_type = @cpp_i32, name = "result"}] + } + acsim.binding @stateful_binding record { + activation_sources = [], availability = "available", binding = "stateful_binding", binding_schema = "acsim-binding-0.1", + component_schema = @stateful_schema, component_schema_fingerprint = "sha256:0a00000000000000000000000000000000000000000000000000000000000000", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "gfsim::Stateful", entry_points = {pure = "", reset = "stateful_reset", validate = "stateful_validate", work = "stateful_work", xfer = "stateful_xfer"}, header = "gfsim/stateful.hpp", symbol = "gfsim::Stateful", target = "gfsim"}, + cpp_type = @cpp_i32, effect = "stateful", fingerprint = "sha256:0d00000000000000000000000000000000000000000000000000000000000000", + implementation = @stateful_impl, ownership = {kind = "unique", placement = "member_or_array"}, parameters = [], ports = [], provider = @provider, + provider_implementation_fingerprint = "sha256:0b00000000000000000000000000000000000000000000000000000000000000", resources = [], results = [] + } + acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:0e00000000000000000000000000000000000000000000000000000000000000" exports [] { + %external_expr = acsim.inline @pure_binding() : () -> !acsim.expr<@cpp_i32> + %generated_expr = acsim.inline @generated_inline() : () -> !acsim.expr<@cpp_i32> + acsim.process @tick captures() names [] entry @entry pcs [@entry] live [] fairness 8 specialization "sha256:0f00000000000000000000000000000000000000000000000000000000000000" { + state @entry { + %scalar = acsim.inline @generated_scalar() : () -> i32 + %value = acsim.inline @generated_value() : () -> !acsim.value<@cpp_i32> + %external = acsim.invoke @stateful_binding() : () -> !acsim.value<@cpp_i32> + %wake = acsim.invoke @generated_invoke() : () -> !acsim.wake<@wake_kind> + acsim.terminate "success" + } + } + acsim.return + } + %object, %activation = acsim.dispatch @Top::@tick path "Top.tick" indices [] object 0 activation 0 + work "acsim_generated::Top::s0e00000000000000000000000000000000000000000000000000000000000000::tick::p0f00000000000000000000000000000000000000000000000000000000000000::work" + xfer "acsim_generated::Top::s0e00000000000000000000000000000000000000000000000000000000000000::tick::p0f00000000000000000000000000000000000000000000000000000000000000::xfer" + reset "acsim_generated::Top::s0e00000000000000000000000000000000000000000000000000000000000000::tick::p0f00000000000000000000000000000000000000000000000000000000000000::reset" + validate "acsim_generated::Top::s0e00000000000000000000000000000000000000000000000000000000000000::tick::p0f00000000000000000000000000000000000000000000000000000000000000::validate" + : !acsim.object_id, !acsim.activation_id + acsim.activate %activation to %object + : !acsim.activation_id to !acsim.object_id + } +} diff --git a/components/agentic-circuit/test/ACSim/generated-wrapper-activation.mlir b/components/agentic-circuit/test/ACSim/generated-wrapper-activation.mlir new file mode 100644 index 00000000..658b2c89 --- /dev/null +++ b/components/agentic-circuit/test/ACSim/generated-wrapper-activation.mlir @@ -0,0 +1,107 @@ +// RUN: %acir_opt_public %s | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @wrapper_activation epoch "0.3" root @Top + construction ["Top.left", "Top.left.child", "Top.port_sink", "Top.resource_sink"] + destruction ["Top.resource_sink", "Top.port_sink", "Top.left.child", "Top.left"] + fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000001", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000002", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000003", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000004", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000005", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000006" + } { + acsim.type @comb_domain cpp "Domain" kind "time_domain" fingerprint "sha256:0100000000000000000000000000000000000000000000000000000000000000" + acsim.type @consumer cpp "Consumer" kind "role" fingerprint "sha256:0200000000000000000000000000000000000000000000000000000000000000" + acsim.type @impl cpp "Component" kind "implementation" fingerprint "sha256:0300000000000000000000000000000000000000000000000000000000000000" + acsim.type @initiator cpp "Initiator" kind "role" fingerprint "sha256:0400000000000000000000000000000000000000000000000000000000000000" + acsim.type @interface cpp "Stream" kind "interface" fingerprint "sha256:0500000000000000000000000000000000000000000000000000000000000000" + acsim.type @payload cpp "Packet" kind "packet" fingerprint "sha256:0600000000000000000000000000000000000000000000000000000000000000" + acsim.type @port_in cpp "input" kind "accessor" fingerprint "sha256:0700000000000000000000000000000000000000000000000000000000000000" + acsim.type @port_out cpp "output" kind "accessor" fingerprint "sha256:0800000000000000000000000000000000000000000000000000000000000000" + acsim.type @protocol cpp "ReadyValid" kind "protocol" fingerprint "sha256:0900000000000000000000000000000000000000000000000000000000000000" + acsim.type @provider cpp "Provider" kind "provider" fingerprint "sha256:0a00000000000000000000000000000000000000000000000000000000000000" + acsim.type @resource_in cpp "target" kind "accessor" fingerprint "sha256:0b00000000000000000000000000000000000000000000000000000000000000" + acsim.type @resource_kind cpp "Memory" kind "resource" fingerprint "sha256:0c00000000000000000000000000000000000000000000000000000000000000" + acsim.type @resource_out cpp "initiator" kind "accessor" fingerprint "sha256:0d00000000000000000000000000000000000000000000000000000000000000" + acsim.type @role cpp "Producer" kind "role" fingerprint "sha256:0e00000000000000000000000000000000000000000000000000000000000000" + acsim.type @schema cpp "schema" kind "schema" fingerprint "sha256:0f00000000000000000000000000000000000000000000000000000000000000" + acsim.type @target cpp "Target" kind "role" fingerprint "sha256:1000000000000000000000000000000000000000000000000000000000000000" + acsim.type @value cpp "bool" kind "value" fingerprint "sha256:1100000000000000000000000000000000000000000000000000000000000000" + + acsim.binding @endpoint_binding record { + activation_sources = [], availability = "available", binding = "endpoint_binding", + binding_schema = "acsim-binding-0.1", component_schema = @schema, + component_schema_fingerprint = "sha256:0f00000000000000000000000000000000000000000000000000000000000000", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "StatefulComponent", entry_points = {pure = "", reset = "endpoint_reset", validate = "endpoint_validate", work = "endpoint_work", xfer = "endpoint_xfer"}, header = "endpoint.hpp", symbol = "Endpoint", target = "model"}, + cpp_type = @value, effect = "stateful", fingerprint = "sha256:1200000000000000000000000000000000000000000000000000000000000000", + implementation = @impl, ownership = {kind = "unique", placement = "member_or_array"}, + parameters = [], ports = [ + {accessor = @port_in, cardinality = "exclusive", delegation = "allowed", direction = "input", interface = @interface, ownership = "borrowed", payload = @payload, protocol = @protocol, role = @consumer, time_domain = @comb_domain}, + {accessor = @port_out, cardinality = "exclusive", delegation = "allowed", direction = "output", interface = @interface, ownership = "borrowed", payload = @payload, protocol = @protocol, role = @role, time_domain = @comb_domain} + ], provider = @provider, + provider_implementation_fingerprint = "sha256:0300000000000000000000000000000000000000000000000000000000000000", + resources = [ + {accessor = @resource_in, delegation = "allowed", mode = "target", ownership = "borrowed", resource = @resource_kind, role = @target, time_domain = @comb_domain}, + {accessor = @resource_out, delegation = "allowed", mode = "initiator", ownership = "borrowed", resource = @resource_kind, role = @initiator, time_domain = @comb_domain} + ], results = [] + } + acsim.module @Leaf interface { + ports = [{accessor = @port_out, cardinality = "exclusive", delegation = "allowed", direction = "output", interface = @interface, name = "out", ownership = "borrowed", payload = @payload, protocol = @protocol, role = @role, time_domain = @comb_domain}], + resources = [{accessor = @resource_out, delegation = "allowed", mode = "initiator", name = "out", ownership = "borrowed", resource = @resource_kind, role = @initiator, time_domain = @comb_domain}], + results = [] + } static [] specialization "sha256:1300000000000000000000000000000000000000000000000000000000000000" exports [@out, @out] { + %child = acsim.instance @child target @endpoint_binding args [] specialization "sha256:1400000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@endpoint_binding> + %port = acsim.port %child accessor @port_out + : !acsim.owner<@endpoint_binding> -> !acsim.port<@interface, @role, @payload, @protocol> + %resource = acsim.resource %child accessor @resource_out + : !acsim.owner<@endpoint_binding> -> !acsim.resource<@resource_kind, @initiator> + %port_out = acsim.export @out %port role @role + : !acsim.port<@interface, @role, @payload, @protocol> -> !acsim.port<@interface, @role, @payload, @protocol> + %resource_out = acsim.export @out %resource role @initiator + : !acsim.resource<@resource_kind, @initiator> -> !acsim.resource<@resource_kind, @initiator> + acsim.return %port_out, %resource_out : !acsim.port<@interface, @role, @payload, @protocol>, !acsim.resource<@resource_kind, @initiator> + } + acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:1500000000000000000000000000000000000000000000000000000000000000" exports [] { + %left = acsim.instance @left target @Leaf args [] specialization "sha256:1300000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@Leaf> + %port_sink = acsim.instance @port_sink target @endpoint_binding args [] specialization "sha256:1400000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@endpoint_binding> + %resource_sink = acsim.instance @resource_sink target @endpoint_binding args [] specialization "sha256:1400000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@endpoint_binding> + %source_port = acsim.port %left accessor @port_out + : !acsim.owner<@Leaf> -> !acsim.port<@interface, @role, @payload, @protocol> + %target_port = acsim.port %port_sink accessor @port_in + : !acsim.owner<@endpoint_binding> -> !acsim.port<@interface, @consumer, @payload, @protocol> + %source_resource = acsim.resource %left accessor @resource_out + : !acsim.owner<@Leaf> -> !acsim.resource<@resource_kind, @initiator> + %target_resource = acsim.resource %resource_sink accessor @resource_in + : !acsim.owner<@endpoint_binding> -> !acsim.resource<@resource_kind, @target> + acsim.bind %source_port to %target_port kind "port" + : !acsim.port<@interface, @role, @payload, @protocol> to !acsim.port<@interface, @consumer, @payload, @protocol> + acsim.bind %source_resource to %target_resource kind "resource" + : !acsim.resource<@resource_kind, @initiator> to !acsim.resource<@resource_kind, @target> + acsim.return + } + + %obj0, %act0 = acsim.dispatch @Leaf::@child path "Top.left.child" indices [] object 0 activation 0 + work "endpoint_work" xfer "endpoint_xfer" reset "endpoint_reset" validate "endpoint_validate" + : !acsim.object_id, !acsim.activation_id + %obj1, %act1 = acsim.dispatch @Top::@port_sink path "Top.port_sink" indices [] object 1 activation 1 + work "endpoint_work" xfer "endpoint_xfer" reset "endpoint_reset" validate "endpoint_validate" + : !acsim.object_id, !acsim.activation_id + %obj2, %act2 = acsim.dispatch @Top::@resource_sink path "Top.resource_sink" indices [] object 2 activation 2 + work "endpoint_work" xfer "endpoint_xfer" reset "endpoint_reset" validate "endpoint_validate" + : !acsim.object_id, !acsim.activation_id + acsim.activate %act0 to %obj0 : !acsim.activation_id to !acsim.object_id + acsim.activate %act0 to %obj1 : !acsim.activation_id to !acsim.object_id + acsim.activate %act0 to %obj2 : !acsim.activation_id to !acsim.object_id + acsim.activate %act1 to %obj1 : !acsim.activation_id to !acsim.object_id + acsim.activate %act2 to %obj2 : !acsim.activation_id to !acsim.object_id + } +} + +// CHECK: acsim.activate %{{.*}} to %{{.*}} diff --git a/components/agentic-circuit/test/ACSim/ops-invalid.mlir b/components/agentic-circuit/test/ACSim/ops-invalid.mlir new file mode 100644 index 00000000..0ff6ff65 --- /dev/null +++ b/components/agentic-circuit/test/ACSim/ops-invalid.mlir @@ -0,0 +1,589 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt_public %t/generic-spelling.mlir 2>&1 | %FileCheck %s --check-prefix=GENERIC +// RUN: %not %acir_opt %t/wrong-epoch.mlir 2>&1 | %FileCheck %s --check-prefix=EPOCH +// RUN: %not %acir_opt %t/two-models.mlir 2>&1 | %FileCheck %s --check-prefix=MODEL-COUNT +// RUN: %not %acir_opt %t/bad-fingerprints.mlir 2>&1 | %FileCheck %s --check-prefix=FINGERPRINTS +// RUN: %not %acir_opt %t/illegal-nested-acir.mlir 2>&1 | %FileCheck %s --check-prefix=CLOSED +// RUN: %not %acir_opt %t/illegal-process-scf.mlir 2>&1 | %FileCheck %s --check-prefix=PROCESS-CLOSED +// RUN: %not %acir_opt %t/unresolved-root.mlir 2>&1 | %FileCheck %s --check-prefix=ROOT +// RUN: %not %acir_opt %t/nonreverse-destruction.mlir 2>&1 | %FileCheck %s --check-prefix=DESTRUCTION +// RUN: %not %acir_opt %t/orphan-acsim-op.mlir 2>&1 | %FileCheck %s --check-prefix=ZERO-MODEL +// RUN: %not %acir_opt %t/nested-orphan-acsim-op.mlir 2>&1 | %FileCheck %s --check-prefix=NESTED-ZERO-MODEL +// RUN: %not %acir_opt %t/legacy-module-binding.mlir 2>&1 | %FileCheck %s --check-prefix=LEGACY-MODULE +// RUN: %not %acir_opt %t/legacy-placement-binding.mlir 2>&1 | %FileCheck %s --check-prefix=LEGACY-PLACEMENT +// RUN: %not %acir_opt %t/legacy-process-binding.mlir 2>&1 | %FileCheck %s --check-prefix=LEGACY-PROCESS +// RUN: %not %acir_opt %t/inline-module-type.mlir 2>&1 | %FileCheck %s --check-prefix=INLINE-MODULE-TYPE +// RUN: %not %acir_opt %t/live-load-type.mlir 2>&1 | %FileCheck %s --check-prefix=LIVE-LOAD-TYPE +// RUN: %not %acir_opt %t/live-store-type.mlir 2>&1 | %FileCheck %s --check-prefix=LIVE-STORE-TYPE +// RUN: %not %acir_opt %t/invoke-type.mlir 2>&1 | %FileCheck %s --check-prefix=INVOKE-TYPE +// RUN: %not %acir_opt %t/continue-missing.mlir 2>&1 | %FileCheck %s --check-prefix=CONTINUE-MISSING +// RUN: %not %acir_opt %t/dispatch-negative.mlir 2>&1 | %FileCheck %s --check-prefix=DISPATCH-NEGATIVE +// RUN: %not %acir_opt %t/activate-types.mlir 2>&1 | %FileCheck %s --check-prefix=ACTIVATE-TYPES +// RUN: %not %acir_opt %t/binding-missing-field.mlir 2>&1 | %FileCheck %s --check-prefix=BINDING-SHAPE +// RUN: %not %acir_opt %t/element-out-of-bounds.mlir 2>&1 | %FileCheck %s --check-prefix=ELEMENT-BOUNDS +// RUN: %not %acir_opt %t/resource-bad-accessor.mlir 2>&1 | %FileCheck %s --check-prefix=RESOURCE-ACCESSOR +// RUN: %not %acir_opt %t/bind-pure-view-mismatch.mlir 2>&1 | %FileCheck %s --check-prefix=BIND-MISMATCH +// RUN: %not %acir_opt %t/suspend-non-wake.mlir 2>&1 | %FileCheck %s --check-prefix=SUSPEND-WAKE +// RUN: %not %acir_opt %t/export-ghost.mlir 2>&1 | %FileCheck %s --check-prefix=EXPORT-COVER +// RUN: %not %acir_opt %t/port-bad-base.mlir 2>&1 | %FileCheck %s --check-prefix=PORT-BASE +// RUN: %not %acir_opt %t/time-domain-partial.mlir 2>&1 | %FileCheck %s --check-prefix=TIME-DOMAIN-PARTIAL +// RUN: %not %acir_opt %t/time-domain-wrong-kind.mlir 2>&1 | %FileCheck %s --check-prefix=TIME-DOMAIN-KIND + +// GENERIC: error: generic ACIR operation spelling is internal-only +// EPOCH: contract epoch must be exactly "0.3" +// MODEL-COUNT: canonical ACSim requires exactly one acsim.model +// FINGERPRINTS: fingerprints must contain exactly frozen_acir, binding_lock, provider, profile, toolchain, and schema_set +// CLOSED: operation 'ac.system' is not legal in canonical ACSim +// PROCESS-CLOSED: operation 'scf.yield' is not legal in an acsim.process body +// ROOT: root reference '@missing' is unresolved +// DESTRUCTION: destruction order must be the exact reverse of construction order +// ZERO-MODEL: canonical ACSim requires exactly one acsim.model +// NESTED-ZERO-MODEL: canonical ACSim requires exactly one acsim.model +// LEGACY-MODULE: custom op 'acsim.module' expected 'interface' +// LEGACY-PLACEMENT: custom op 'acsim.instance' expected 'target' +// LEGACY-PROCESS: custom op 'acsim.process' expected 'captures' +// INLINE-MODULE-TYPE: module inline result must be exactly !acsim.expr +// LIVE-LOAD-TYPE: live load must resolve to an exact typed slot of this process +// LIVE-STORE-TYPE: live store must resolve to an exact typed slot of this process +// INVOKE-TYPE: invoke results must be exact !acsim.value or !acsim.wake types +// CONTINUE-MISSING: expected attribute value +// DISPATCH-NEGATIVE: dispatch object ID has no expanded runtime object +// ACTIVATE-TYPES: invalid kind of type specified +// TIME-DOMAIN-PARTIAL: time_domain requires exact positive period/tick_scale and non-negative phase i64 metadata +// TIME-DOMAIN-KIND: runtime domain metadata is legal only for time_domain + +//--- time-domain-partial.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + "acsim.type"() <{sym_name = "core", cpp_name = "gfsim::TimeDomainRuntime", + kind = "time_domain", fingerprint = "sha256:1000000000000000000000000000000000000000000000000000000000000000", + period = 2 : i64}> : () -> () + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:2000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + } +} + +//--- time-domain-wrong-kind.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + "acsim.type"() <{sym_name = "value", cpp_name = "bool", kind = "value", + fingerprint = "sha256:1000000000000000000000000000000000000000000000000000000000000000", + period = 2 : i64, phase = 0 : i64, tick_scale = 1 : i64}> : () -> () + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:2000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + } +} + +//--- generic-spelling.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.model"() ({}) {sym_name = "m", contract_epoch = "0.3"} : () -> () +} + +// The remaining split cases deliberately use the generic parser through the +// internal test entrypoint so malformed regions reach the real verifiers. + +//--- wrong-epoch.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.model"() <{sym_name = "m", contract_epoch = "0.1", root = @missing, + construction_order = [], destruction_order = [], fingerprints = {}}> + ({}) : () -> () +} + +//--- orphan-acsim-op.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.type"() <{sym_name = "v", cpp_name = "bool", kind = "value", + fingerprint = "sha256:0000000000000000000000000000000000000000000000000000000000000000"}> + : () -> () +} + +//--- nested-orphan-acsim-op.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + builtin.module @nested { + acsim.type @v cpp "bool" kind "value" fingerprint "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } +} + +//--- two-models.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.model"() <{sym_name = "a", contract_epoch = "0.3", root = @missing, + construction_order = [], destruction_order = [], fingerprints = {}}> + ({}) : () -> () + "acsim.model"() <{sym_name = "b", contract_epoch = "0.3", root = @missing, + construction_order = [], destruction_order = [], fingerprints = {}}> + ({}) : () -> () +} + +//--- bad-fingerprints.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.model"() <{sym_name = "m", contract_epoch = "0.3", root = @missing, + construction_order = [], destruction_order = [], fingerprints = {frozen_acir = "x"}}> + ({ + "acsim.type"() <{sym_name = "sentinel", cpp_name = "bool", kind = "value", + fingerprint = "sha256:0000000000000000000000000000000000000000000000000000000000000000"}> + : () -> () + }) : () -> () +} + +//--- illegal-nested-acir.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.model"() <{sym_name = "m", contract_epoch = "0.3", root = @bad, + construction_order = [], destruction_order = [], fingerprints = { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000"}}> + ({ + "ac.system"() <{sym_name = "bad", root = @bad, root_name = "bad", + tick_epoch = 1 : i64, tick_unit = "cycles", seed_policy = {}, + instrumentation = [], result_schema = {}}> : () -> () + }) : () -> () +} + +//--- illegal-process-scf.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @m epoch "0.3" root @Top + construction ["Top.p"] destruction ["Top.p"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.process @p captures() names [] + entry @entry pcs [@entry] live [] fairness 1 specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + "scf.yield"() : () -> () + } + } + acsim.return + } + } +} + +//--- unresolved-root.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.model"() <{sym_name = "m", contract_epoch = "0.3", root = @missing, + construction_order = [], destruction_order = [], fingerprints = { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000"}}> + ({ + "acsim.type"() <{sym_name = "sentinel", cpp_name = "bool", kind = "value", + fingerprint = "sha256:0000000000000000000000000000000000000000000000000000000000000000"}> + : () -> () + }) : () -> () +} + +//--- nonreverse-destruction.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "acsim.model"() <{sym_name = "m", contract_epoch = "0.3", root = @Top, + construction_order = ["Top.a", "Top.b"], + destruction_order = ["Top.a", "Top.b"], fingerprints = { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000"}}> + ({ + "acsim.module"() <{sym_name = "Top", interface = {ports = [], resources = [], results = []}, + static_params = [], specialization_fingerprint = "sha256:0000000000000000000000000000000000000000000000000000000000000000", exports = []}> ({ + %a = "acsim.instance"() <{sym_name = "a", + target = @missing, static_args = [], specialization_fingerprint = "sha256:0000000000000000000000000000000000000000000000000000000000000000"}> + : () -> !acsim.owner<@missing> + %b = "acsim.instance"() <{sym_name = "b", + target = @missing, static_args = [], specialization_fingerprint = "sha256:0000000000000000000000000000000000000000000000000000000000000000"}> + : () -> !acsim.owner<@missing> + "acsim.return"() : () -> () + }) : () -> () + }) : () -> () +} + +//--- legacy-module-binding.mlir +builtin.module { + acsim.module @Top binding @legacy static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } +} + +//--- legacy-placement-binding.mlir +builtin.module { + acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + %legacy = acsim.instance @legacy binding @legacy target @legacy args [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" : !acsim.owner<@legacy> + acsim.return + } +} + +//--- legacy-process-binding.mlir +builtin.module { + acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.process @legacy binding @legacy captures() names [] entry @entry pcs [@entry] live [] fairness 1 specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" { + state @entry { acsim.terminate "success" } + } + acsim.return + } +} + +//--- inline-module-type.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + %bad = acsim.inline @f() : () -> i32 + acsim.return + } + } +} + +//--- live-load-type.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction ["M.p"] destruction ["M.p"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.process @p captures() names [] entry @entry pcs [@entry, @done] live [] fairness 1 specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + %bad = acsim.live.load @p slot "x" : i32 + acsim.continue @done + } + state @done { acsim.terminate "success" } + } + acsim.return + } + } +} + +//--- live-store-type.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction ["M.p"] destruction ["M.p"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.process @p captures() names [] entry @entry pcs [@entry, @done] live [] fairness 1 specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + %val = arith.constant 0 : i32 + acsim.live.store %val in @p slot "x" : i32 + acsim.continue @done + } + state @done { acsim.terminate "success" } + } + acsim.return + } + } +} + +//--- invoke-type.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction ["M.p"] destruction ["M.p"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @f cpp "f" kind "implementation" fingerprint "sha256:0000000000000000000000000000000000000000000000000000000000000000" + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.process @p captures() names [] entry @entry pcs [@entry, @done] live [] fairness 3 specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + %bad = acsim.invoke @f() : () -> i32 + acsim.continue @done + } + state @done { acsim.terminate "success" } + } + acsim.return + } + } +} + +//--- continue-missing.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.process @p captures() names [] entry @entry pcs [@entry, @done] live [] fairness 1 specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + acsim.continue + } + state @done { acsim.terminate "success" } + } + acsim.return + } + } +} + +//--- dispatch-negative.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + %obj, %act = acsim.dispatch @M path "" indices [] object -1 activation 0 work "" xfer "" reset "" validate "" : !acsim.object_id, !acsim.activation_id + } +} + +//--- activate-types.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + %obj, %act = acsim.dispatch @M path "root" indices [] object 1 activation 0 work "w" xfer "x" reset "r" validate "v" : !acsim.object_id, !acsim.activation_id + acsim.activate %obj to %act : !acsim.object_id to !acsim.activation_id + } +} + +//--- binding-missing-field.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @cpp_bool cpp "bool" kind "value" fingerprint "sha256:1000000000000000000000000000000000000000000000000000000000000000" + acsim.type @gfsim cpp "gfsim" kind "provider" fingerprint "sha256:3000000000000000000000000000000000000000000000000000000000000000" + acsim.type @pure_impl cpp "gfsim::is_ready" kind "implementation" fingerprint "sha256:8000000000000000000000000000000000000000000000000000000000000000" + acsim.type @pure_schema cpp "pure.schema" kind "schema" fingerprint "sha256:9000000000000000000000000000000000000000000000000000000000000000" + acsim.binding @pure record { + activation_sources = [], availability = "available", binding = "pure", + binding_schema = "acsim-binding-0.1", component_schema = @pure_schema, + component_schema_fingerprint = "sha256:9000000000000000000000000000000000000000000000000000000000000000", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "gfsim::PureModel", entry_points = {pure = "gfsim::is_ready", reset = "", validate = "", work = "", xfer = ""}, header = "gfsim/pure.hpp", symbol = "gfsim::Pure", target = "gfsim"}, + cpp_type = @cpp_bool, effect = "pure", fingerprint = "sha256:1100000000000000000000000000000000000000000000000000000000000000", + implementation = @pure_impl, ownership = {kind = "none", placement = "inline"}, + parameters = [], ports = [], provider = @gfsim, + provider_implementation_fingerprint = "sha256:8000000000000000000000000000000000000000000000000000000000000000", + resources = [] + } + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + } +} +// BINDING-SHAPE: error: 'acsim.binding' op binding lock must contain exactly the acsim-binding-0.1 fields + +//--- element-out-of-bounds.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction ["M.lanes[0]", "M.lanes[1]"] destruction ["M.lanes[1]", "M.lanes[0]"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @A interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + %lanes = acsim.array @lanes target @A args [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" shape [2] : !acsim.array<[2], !acsim.owner<@A>> + %lane5 = acsim.element %lanes indices [5] : !acsim.array<[2], !acsim.owner<@A>> -> !acsim.ref<@A> + acsim.return + } + } +} +// ELEMENT-BOUNDS: error: 'acsim.element' op element index is out of static bounds + +//--- resource-bad-accessor.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction ["M.i"] destruction ["M.i"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @init cpp "gfsim::Initiator" kind "role" fingerprint "sha256:3f00000000000000000000000000000000000000000000000000000000000000" + acsim.type @rk cpp "gfsim::Memory" kind "resource" fingerprint "sha256:b000000000000000000000000000000000000000000000000000000000000000" + acsim.module @A interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + %inst = acsim.instance @i target @A args [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" : !acsim.owner<@A> + %r = acsim.resource %inst accessor @init : !acsim.owner<@A> -> !acsim.resource<@rk, @init> + acsim.return + } + } +} +// RESOURCE-ACCESSOR: error: 'acsim.resource' op resource accessor reference '@init' has incompatible acsim.type kind 'role' + +//--- bind-pure-view-mismatch.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @cpp_bool cpp "bool" kind "value" fingerprint "sha256:1000000000000000000000000000000000000000000000000000000000000000" + acsim.type @cpp_other cpp "int" kind "value" fingerprint "sha256:1100000000000000000000000000000000000000000000000000000000000000" + acsim.type @pure_impl cpp "gfsim::is_ready" kind "implementation" fingerprint "sha256:8000000000000000000000000000000000000000000000000000000000000000" + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + %a = acsim.inline @pure_impl() : () -> !acsim.expr<@cpp_bool> + %b = acsim.inline @pure_impl() : () -> !acsim.expr<@cpp_other> + acsim.bind %a to %b kind "pure_view" : !acsim.expr<@cpp_bool> to !acsim.expr<@cpp_other> + acsim.return + } + } +} +// BIND-MISMATCH: error: 'acsim.bind' op pure_view target must directly consume the source expression + +//--- suspend-non-wake.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction ["M.p"] destruction ["M.p"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @cpp_bool cpp "bool" kind "value" fingerprint "sha256:1000000000000000000000000000000000000000000000000000000000000000" + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.process @p captures() names [] entry @entry pcs [@entry, @done] live [] fairness 1 specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + %scalar = acsim.inline @cpp_bool() : () -> i32 + acsim.suspend @done on %scalar : i32 + } + state @done { acsim.terminate "success" } + } + acsim.return + } + %obj, %act = acsim.dispatch @M::@p path "M.p" indices [] object 0 activation 0 work "w" xfer "x" reset "r" validate "v" : !acsim.object_id, !acsim.activation_id + acsim.activate %act to %obj : !acsim.activation_id to !acsim.object_id + } +} +// SUSPEND-WAKE: error: 'acsim.suspend' op suspend requires one exact typed wake and a closed next PC + +//--- export-ghost.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @cpp_bool cpp "bool" kind "value" fingerprint "sha256:1000000000000000000000000000000000000000000000000000000000000000" + acsim.type @pure_impl cpp "gfsim::is_ready" kind "implementation" fingerprint "sha256:8000000000000000000000000000000000000000000000000000000000000000" + acsim.type @role cpp "gfsim::Producer" kind "role" fingerprint "sha256:c000000000000000000000000000000000000000000000000000000000000000" + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [@ghost] { + %e = acsim.inline @pure_impl() : () -> !acsim.expr<@cpp_bool> + %x = acsim.export @ghost %e role @role : !acsim.expr<@cpp_bool> -> !acsim.expr<@cpp_bool> + acsim.return %x : !acsim.expr<@cpp_bool> + } + } +} +// EXPORT-COVER: error: 'acsim.module' op module exports must exactly cover its ordered interface records + +//--- port-bad-base.mlir +builtin.module { + acsim.model @m epoch "0.3" root @M construction ["M.i"] destruction ["M.i"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @comb_domain cpp "gfsim::CombinationalDomain" kind "time_domain" fingerprint "sha256:0e00000000000000000000000000000000000000000000000000000000000000" + acsim.type @cpp_bool cpp "bool" kind "value" fingerprint "sha256:1000000000000000000000000000000000000000000000000000000000000000" + acsim.type @event_kind cpp "gfsim::EventWake" kind "wake" fingerprint "sha256:2000000000000000000000000000000000000000000000000000000000000000" + acsim.type @gfsim cpp "gfsim" kind "provider" fingerprint "sha256:3000000000000000000000000000000000000000000000000000000000000000" + acsim.type @interface cpp "gfsim::Stream" kind "interface" fingerprint "sha256:4000000000000000000000000000000000000000000000000000000000000000" + acsim.type @payload cpp "Packet" kind "packet" fingerprint "sha256:5000000000000000000000000000000000000000000000000000000000000000" + acsim.type @port_accessor cpp "output" kind "accessor" fingerprint "sha256:6000000000000000000000000000000000000000000000000000000000000000" + acsim.type @protocol cpp "gfsim::ReadyValid" kind "protocol" fingerprint "sha256:7000000000000000000000000000000000000000000000000000000000000000" + acsim.type @role cpp "gfsim::Producer" kind "role" fingerprint "sha256:c000000000000000000000000000000000000000000000000000000000000000" + acsim.type @stateful_impl cpp "gfsim::Fifo" kind "implementation" fingerprint "sha256:d000000000000000000000000000000000000000000000000000000000000000" + acsim.type @stateful_schema cpp "fifo.schema" kind "schema" fingerprint "sha256:e000000000000000000000000000000000000000000000000000000000000000" + acsim.binding @stateful record { + activation_sources = [{kind = @event_kind, name = "commit"}], availability = "available", binding = "stateful", + binding_schema = "acsim-binding-0.1", component_schema = @stateful_schema, + component_schema_fingerprint = "sha256:e000000000000000000000000000000000000000000000000000000000000000", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "gfsim::StatefulModel", entry_points = {pure = "", reset = "fifo_reset", validate = "fifo_validate", work = "fifo_work", xfer = "fifo_xfer"}, header = "gfsim/fifo.hpp", symbol = "gfsim::Fifo", target = "gfsim"}, + cpp_type = @cpp_bool, effect = "stateful", fingerprint = "sha256:1200000000000000000000000000000000000000000000000000000000000000", + implementation = @stateful_impl, ownership = {kind = "unique", placement = "member_or_array"}, + parameters = [], + ports = [ + {accessor = @port_accessor, cardinality = "exclusive", delegation = "forbidden", direction = "output", interface = @interface, ownership = "borrowed", payload = @payload, protocol = @protocol, role = @role, time_domain = @comb_domain} + ], provider = @gfsim, + provider_implementation_fingerprint = "sha256:d000000000000000000000000000000000000000000000000000000000000000", + resources = [], results = [] + } + acsim.module @M interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + %inst = acsim.instance @i target @stateful args [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" : !acsim.owner<@stateful> + %p = acsim.port %inst accessor @port_accessor + : !acsim.owner<@stateful> -> !acsim.port<@interface, @role, @payload, @protocol> + %bad = acsim.port %p accessor @port_accessor + : !acsim.port<@interface, @role, @payload, @protocol> -> !acsim.port<@interface, @role, @payload, @protocol> + acsim.return + } + } +} +// PORT-BASE: error: 'acsim.port' op port projection requires a typed owner/ref and typed port result diff --git a/components/agentic-circuit/test/ACSim/ops-valid.mlir b/components/agentic-circuit/test/ACSim/ops-valid.mlir new file mode 100644 index 00000000..763324f5 --- /dev/null +++ b/components/agentic-circuit/test/ACSim/ops-valid.mlir @@ -0,0 +1,197 @@ +// RUN: %acir_opt_public %s | %FileCheck %s +// RUN: %acir_opt_public %s | %acir_opt_public > %t.roundtrip +// RUN: %acir_opt_public %s > %t.canonical +// RUN: diff %t.canonical %t.roundtrip +// RUN: %acir_opt_public --emit-bytecode -o %t.bc %s +// RUN: %acir_opt_public %t.bc | %FileCheck %s +// RUN: %acir_opt_public --pass-pipeline='builtin.module(canonicalize,cse)' %s | %FileCheck %s --check-prefix=RETAIN + +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @demo epoch "0.3" root @Top + construction ["Top.fifo", "Top.lanes[0]", "Top.lanes[1]", "Top.tick"] + destruction ["Top.tick", "Top.lanes[1]", "Top.lanes[0]", "Top.fifo"] + fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000001", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000002", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000003", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000004", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000005", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000006" + } { + acsim.type @comb_domain cpp "gfsim::CombinationalDomain" kind "time_domain" fingerprint "sha256:0e00000000000000000000000000000000000000000000000000000000000000" + acsim.type @consumer cpp "gfsim::Consumer" kind "role" fingerprint "sha256:0f00000000000000000000000000000000000000000000000000000000000000" + acsim.type @cpp_bool cpp "bool" kind "value" fingerprint "sha256:1000000000000000000000000000000000000000000000000000000000000000" + acsim.type @event_kind cpp "gfsim::EventWake" kind "wake" fingerprint "sha256:2000000000000000000000000000000000000000000000000000000000000000" + acsim.type @gfsim cpp "gfsim" kind "provider" fingerprint "sha256:3000000000000000000000000000000000000000000000000000000000000000" + acsim.type @initiator cpp "gfsim::Initiator" kind "role" fingerprint "sha256:3f00000000000000000000000000000000000000000000000000000000000000" + acsim.type @interface cpp "gfsim::Stream" kind "interface" fingerprint "sha256:4000000000000000000000000000000000000000000000000000000000000000" + acsim.type @interface_alt cpp "gfsim::AlternateStream" kind "interface" fingerprint "sha256:4100000000000000000000000000000000000000000000000000000000000000" + acsim.type @payload cpp "Packet" kind "packet" fingerprint "sha256:5000000000000000000000000000000000000000000000000000000000000000" + acsim.type @payload_alt cpp "AlternatePacket" kind "packet" fingerprint "sha256:5100000000000000000000000000000000000000000000000000000000000000" + acsim.type @port_accessor cpp "output" kind "accessor" fingerprint "sha256:6000000000000000000000000000000000000000000000000000000000000000" + acsim.type @port_in_accessor cpp "input" kind "accessor" fingerprint "sha256:6f00000000000000000000000000000000000000000000000000000000000000" + acsim.type @protocol cpp "gfsim::ReadyValid" kind "protocol" fingerprint "sha256:7000000000000000000000000000000000000000000000000000000000000000" + acsim.type @protocol_alt cpp "gfsim::AlternateProtocol" kind "protocol" fingerprint "sha256:7100000000000000000000000000000000000000000000000000000000000000" + acsim.type @pure_impl cpp "gfsim::is_ready" kind "implementation" fingerprint "sha256:8000000000000000000000000000000000000000000000000000000000000000" + acsim.type @pure_schema cpp "pure.schema" kind "schema" fingerprint "sha256:9000000000000000000000000000000000000000000000000000000000000000" + acsim.type @resource_accessor cpp "initiator" kind "accessor" fingerprint "sha256:a000000000000000000000000000000000000000000000000000000000000000" + acsim.type @resource_alt cpp "gfsim::AlternateMemory" kind "resource" fingerprint "sha256:a100000000000000000000000000000000000000000000000000000000000000" + acsim.type @resource_in_accessor cpp "target" kind "accessor" fingerprint "sha256:af00000000000000000000000000000000000000000000000000000000000000" + acsim.type @resource_kind cpp "gfsim::Memory" kind "resource" fingerprint "sha256:b000000000000000000000000000000000000000000000000000000000000000" + acsim.type @role cpp "gfsim::Producer" kind "role" fingerprint "sha256:c000000000000000000000000000000000000000000000000000000000000000" + acsim.type @stateful_impl cpp "gfsim::Fifo" kind "implementation" fingerprint "sha256:d000000000000000000000000000000000000000000000000000000000000000" + acsim.type @stateful_schema cpp "fifo.schema" kind "schema" fingerprint "sha256:e000000000000000000000000000000000000000000000000000000000000000" + acsim.type @target cpp "gfsim::Target" kind "role" fingerprint "sha256:ef00000000000000000000000000000000000000000000000000000000000000" + + acsim.binding @pure record { + activation_sources = [], availability = "available", binding = "pure", + binding_schema = "acsim-binding-0.1", component_schema = @pure_schema, + component_schema_fingerprint = "sha256:9000000000000000000000000000000000000000000000000000000000000000", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "gfsim::PureModel", entry_points = {pure = "gfsim::is_ready", reset = "", validate = "", work = "", xfer = ""}, header = "gfsim/pure.hpp", symbol = "gfsim::Pure", target = "gfsim"}, + cpp_type = @cpp_bool, effect = "pure", fingerprint = "sha256:1100000000000000000000000000000000000000000000000000000000000000", + implementation = @pure_impl, ownership = {kind = "none", placement = "inline"}, + parameters = [], ports = [], provider = @gfsim, + provider_implementation_fingerprint = "sha256:8000000000000000000000000000000000000000000000000000000000000000", + resources = [], results = [{cpp_type = @cpp_bool, name = "result"}] + } + acsim.binding @stateful record { + activation_sources = [{kind = @event_kind, name = "commit"}], availability = "available", binding = "stateful", + binding_schema = "acsim-binding-0.1", component_schema = @stateful_schema, + component_schema_fingerprint = "sha256:e000000000000000000000000000000000000000000000000000000000000000", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "gfsim::StatefulModel", entry_points = {pure = "", reset = "fifo_reset", validate = "fifo_validate", work = "fifo_work", xfer = "fifo_xfer"}, header = "gfsim/fifo.hpp", symbol = "gfsim::Fifo", target = "gfsim"}, + cpp_type = @cpp_bool, effect = "stateful", fingerprint = "sha256:1200000000000000000000000000000000000000000000000000000000000000", + implementation = @stateful_impl, ownership = {kind = "unique", placement = "member_or_array"}, + parameters = [], + ports = [ + {accessor = @port_accessor, cardinality = "exclusive", delegation = "forbidden", direction = "output", interface = @interface, ownership = "borrowed", payload = @payload, protocol = @protocol, role = @role, time_domain = @comb_domain}, + {accessor = @port_in_accessor, cardinality = "exclusive", delegation = "forbidden", direction = "input", interface = @interface, ownership = "borrowed", payload = @payload, protocol = @protocol, role = @consumer, time_domain = @comb_domain} + ], provider = @gfsim, + provider_implementation_fingerprint = "sha256:d000000000000000000000000000000000000000000000000000000000000000", + resources = [ + {accessor = @resource_accessor, delegation = "forbidden", mode = "initiator", ownership = "borrowed", resource = @resource_kind, role = @initiator, time_domain = @comb_domain}, + {accessor = @resource_in_accessor, delegation = "forbidden", mode = "target", ownership = "borrowed", resource = @resource_kind, role = @target, time_domain = @comb_domain} + ], results = [] + } + acsim.module @Top interface { + ports = [{accessor = @port_accessor, cardinality = "exclusive", delegation = "forbidden", direction = "output", interface = @interface, name = "out_port", ownership = "borrowed", payload = @payload, protocol = @protocol, role = @role, time_domain = @comb_domain}], + resources = [{accessor = @resource_accessor, delegation = "forbidden", mode = "initiator", name = "memory", ownership = "borrowed", resource = @resource_kind, role = @initiator, time_domain = @comb_domain}], + results = [{cpp_type = @cpp_bool, name = "out"}] + } static [] specialization "sha256:2100000000000000000000000000000000000000000000000000000000000000" exports [@out_port, @memory, @out] { + %fifo = acsim.instance @fifo target @stateful args [] specialization "sha256:2200000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@stateful> + %lanes = acsim.array @lanes target @stateful args [] specialization "sha256:2200000000000000000000000000000000000000000000000000000000000000" shape [2] + : !acsim.array<[2], !acsim.owner<@stateful>> + %lane0 = acsim.element %lanes indices [0] + : !acsim.array<[2], !acsim.owner<@stateful>> -> !acsim.ref<@stateful> + %lane1 = acsim.element %lanes indices [1] + : !acsim.array<[2], !acsim.owner<@stateful>> -> !acsim.ref<@stateful> + %port0 = acsim.port %lane0 accessor @port_accessor + : !acsim.ref<@stateful> -> !acsim.port<@interface, @role, @payload, @protocol> + %port1 = acsim.port %lane1 accessor @port_in_accessor + : !acsim.ref<@stateful> -> !acsim.port<@interface, @consumer, @payload, @protocol> + %resource0 = acsim.resource %lane0 accessor @resource_accessor + : !acsim.ref<@stateful> -> !acsim.resource<@resource_kind, @initiator> + %resource1 = acsim.resource %lane1 accessor @resource_in_accessor + : !acsim.ref<@stateful> -> !acsim.resource<@resource_kind, @target> + acsim.bind %port0 to %port1 kind "port" + : !acsim.port<@interface, @role, @payload, @protocol> + to !acsim.port<@interface, @consumer, @payload, @protocol> + acsim.bind %resource0 to %resource1 kind "resource" + : !acsim.resource<@resource_kind, @initiator> + to !acsim.resource<@resource_kind, @target> + %expr = acsim.inline @pure() + : () -> !acsim.expr<@cpp_bool> + %expr2 = acsim.inline @pure(%expr) + : (!acsim.expr<@cpp_bool>) -> !acsim.expr<@cpp_bool> + %generated_expr = acsim.inline @pure_impl() + : () -> !acsim.expr<@cpp_bool> + acsim.bind %expr to %expr2 kind "pure_view" + : !acsim.expr<@cpp_bool> to !acsim.expr<@cpp_bool> + %out_port = acsim.export @out_port %port0 role @role + : !acsim.port<@interface, @role, @payload, @protocol> -> !acsim.port<@interface, @role, @payload, @protocol> + %memory = acsim.export @memory %resource0 role @initiator + : !acsim.resource<@resource_kind, @initiator> -> !acsim.resource<@resource_kind, @initiator> + %out = acsim.export @out %expr2 role @role + : !acsim.expr<@cpp_bool> -> !acsim.expr<@cpp_bool> + acsim.process @tick captures(%lane0 : !acsim.ref<@stateful>) names ["lane"] entry @entry + pcs [@entry, @wait, @done] + live [{name = "counter", type = !acsim.value<@cpp_bool>}] + fairness 8 specialization "sha256:2300000000000000000000000000000000000000000000000000000000000000" { + state @entry { + ^bb0(%lane_entry : !acsim.ref<@stateful>): + %old = acsim.live.load @tick slot "counter" + : !acsim.value<@cpp_bool> + acsim.live.store %old in @tick slot "counter" : !acsim.value<@cpp_bool> + %next = acsim.invoke @stateful(%old) + : (!acsim.value<@cpp_bool>) -> !acsim.value<@cpp_bool> + acsim.continue @wait + } + state @wait { + ^bb0(%lane_wait : !acsim.ref<@stateful>): + %wake = acsim.invoke @stateful() + : () -> !acsim.wake<@event_kind> + acsim.suspend @done on %wake : !acsim.wake<@event_kind> + } + state @done { + ^bb0(%lane_done : !acsim.ref<@stateful>): + %scalar = acsim.inline @pure_impl() : () -> i32 + %wrapped = acsim.inline @pure_impl() : () -> !acsim.value<@cpp_bool> + %generated_wake = acsim.invoke @stateful_impl() + : () -> !acsim.wake<@event_kind> + acsim.terminate "success" + } + } + acsim.return %out_port, %memory, %out : !acsim.port<@interface, @role, @payload, @protocol>, !acsim.resource<@resource_kind, @initiator>, !acsim.expr<@cpp_bool> + } + + %obj0, %act0 = acsim.dispatch @Top::@fifo path "Top.fifo" indices [] object 0 activation 0 + work "fifo_work" xfer "fifo_xfer" reset "fifo_reset" validate "fifo_validate" + : !acsim.object_id, !acsim.activation_id + %obj1, %act1 = acsim.dispatch @Top::@lanes path "Top.lanes[0]" indices [0] object 1 activation 1 + work "fifo_work" xfer "fifo_xfer" reset "fifo_reset" validate "fifo_validate" + : !acsim.object_id, !acsim.activation_id + %obj2, %act2 = acsim.dispatch @Top::@lanes path "Top.lanes[1]" indices [1] object 2 activation 2 + work "fifo_work" xfer "fifo_xfer" reset "fifo_reset" validate "fifo_validate" + : !acsim.object_id, !acsim.activation_id + %obj3, %act3 = acsim.dispatch @Top::@tick path "Top.tick" indices [] object 3 activation 3 + work "acsim_generated::Top::s2100000000000000000000000000000000000000000000000000000000000000::tick::p2300000000000000000000000000000000000000000000000000000000000000::work" + xfer "acsim_generated::Top::s2100000000000000000000000000000000000000000000000000000000000000::tick::p2300000000000000000000000000000000000000000000000000000000000000::xfer" + reset "acsim_generated::Top::s2100000000000000000000000000000000000000000000000000000000000000::tick::p2300000000000000000000000000000000000000000000000000000000000000::reset" + validate "acsim_generated::Top::s2100000000000000000000000000000000000000000000000000000000000000::tick::p2300000000000000000000000000000000000000000000000000000000000000::validate" + : !acsim.object_id, !acsim.activation_id + acsim.activate %act0 to %obj0 : !acsim.activation_id to !acsim.object_id + acsim.activate %act1 to %obj1 : !acsim.activation_id to !acsim.object_id + acsim.activate %act1 to %obj2 : !acsim.activation_id to !acsim.object_id + acsim.activate %act1 to %obj3 : !acsim.activation_id to !acsim.object_id + acsim.activate %act2 to %obj2 : !acsim.activation_id to !acsim.object_id + acsim.activate %act3 to %obj3 : !acsim.activation_id to !acsim.object_id + } +} + +// CHECK: acsim.model @demo epoch "0.3" +// CHECK: acsim.type @cpp_bool +// CHECK: acsim.binding @pure +// CHECK: acsim.module @Top +// CHECK: acsim.instance @fifo +// CHECK: acsim.array @lanes +// CHECK: acsim.element +// CHECK: acsim.port +// CHECK: acsim.resource +// CHECK: acsim.bind +// CHECK: acsim.inline +// CHECK: acsim.export @out +// CHECK: acsim.process @tick +// CHECK: acsim.live.load +// CHECK: acsim.live.store +// CHECK: acsim.invoke +// CHECK: acsim.continue +// CHECK: acsim.suspend +// CHECK: acsim.terminate +// CHECK: acsim.return +// CHECK: acsim.dispatch @Top::@fifo +// CHECK: acsim.activate % +// RETAIN-COUNT-3: acsim.bind % +// RETAIN-COUNT-4: acsim.dispatch @ +// RETAIN-COUNT-6: acsim.activate % diff --git a/components/agentic-circuit/test/ACSim/reusable-modules.mlir b/components/agentic-circuit/test/ACSim/reusable-modules.mlir new file mode 100644 index 00000000..c03efe93 --- /dev/null +++ b/components/agentic-circuit/test/ACSim/reusable-modules.mlir @@ -0,0 +1,95 @@ +// RUN: %acir_opt_public %s | %FileCheck %s +// RUN: %acir_opt_public --emit-bytecode -o %t.bc %s +// RUN: %acir_opt_public %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @reused epoch "0.3" root @Top + construction ["Top.left", "Top.left.child", "Top.left.pulse", "Top.right[0]", "Top.right[0].child", "Top.right[0].pulse", "Top.right[1]", "Top.right[1].child", "Top.right[1].pulse"] + destruction ["Top.right[1].pulse", "Top.right[1].child", "Top.right[1]", "Top.right[0].pulse", "Top.right[0].child", "Top.right[0]", "Top.left.pulse", "Top.left.child", "Top.left"] + fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000001", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000002", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000003", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000004", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000005", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000006" + } { + acsim.type @impl cpp "Component" kind "implementation" fingerprint "sha256:1000000000000000000000000000000000000000000000000000000000000000" + acsim.type @provider cpp "Provider" kind "provider" fingerprint "sha256:2000000000000000000000000000000000000000000000000000000000000000" + acsim.type @schema cpp "schema" kind "schema" fingerprint "sha256:3000000000000000000000000000000000000000000000000000000000000000" + acsim.type @value cpp "bool" kind "value" fingerprint "sha256:4000000000000000000000000000000000000000000000000000000000000000" + + acsim.binding @child_binding record { + activation_sources = [], availability = "available", binding = "child_binding", + binding_schema = "acsim-binding-0.1", component_schema = @schema, + component_schema_fingerprint = "sha256:3000000000000000000000000000000000000000000000000000000000000000", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "StatefulComponent", entry_points = {pure = "", reset = "child_reset", validate = "child_validate", work = "child_work", xfer = "child_xfer"}, header = "child.hpp", symbol = "Child", target = "model"}, + cpp_type = @value, effect = "stateful", fingerprint = "sha256:5000000000000000000000000000000000000000000000000000000000000000", + implementation = @impl, ownership = {kind = "unique", placement = "member_or_array"}, + parameters = [], ports = [], provider = @provider, + provider_implementation_fingerprint = "sha256:1000000000000000000000000000000000000000000000000000000000000000", + resources = [], results = [] + } + acsim.module @Leaf interface {ports = [], resources = [], results = []} static [2 : i64] specialization "sha256:8000000000000000000000000000000000000000000000000000000000000000" exports [] { + %child = acsim.instance @child target @child_binding args [] specialization "sha256:9000000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@child_binding> + acsim.process @pulse captures() names [] entry @entry pcs [@entry] live [] fairness 1 specialization "sha256:b000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + acsim.terminate "success" + } + } + acsim.return + } + acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:a000000000000000000000000000000000000000000000000000000000000000" exports [] { + %left = acsim.instance @left target @Leaf args [2 : i64] specialization "sha256:8000000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@Leaf> + %right = acsim.array @right target @Leaf args [2 : i64] specialization "sha256:8000000000000000000000000000000000000000000000000000000000000000" shape [2] + : !acsim.array<[2], !acsim.owner<@Leaf>> + acsim.return + } + + %obj0, %act0 = acsim.dispatch @Leaf::@child path "Top.left.child" indices [] object 0 activation 0 + work "child_work" xfer "child_xfer" reset "child_reset" validate "child_validate" + : !acsim.object_id, !acsim.activation_id + %obj1, %act1 = acsim.dispatch @Leaf::@pulse path "Top.left.pulse" indices [] object 1 activation 1 + work "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::work" + xfer "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::xfer" + reset "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::reset" + validate "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::validate" + : !acsim.object_id, !acsim.activation_id + %obj2, %act2 = acsim.dispatch @Leaf::@child path "Top.right[0].child" indices [] object 2 activation 2 + work "child_work" xfer "child_xfer" reset "child_reset" validate "child_validate" + : !acsim.object_id, !acsim.activation_id + %obj3, %act3 = acsim.dispatch @Leaf::@pulse path "Top.right[0].pulse" indices [] object 3 activation 3 + work "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::work" + xfer "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::xfer" + reset "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::reset" + validate "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::validate" + : !acsim.object_id, !acsim.activation_id + %obj4, %act4 = acsim.dispatch @Leaf::@child path "Top.right[1].child" indices [] object 4 activation 4 + work "child_work" xfer "child_xfer" reset "child_reset" validate "child_validate" + : !acsim.object_id, !acsim.activation_id + %obj5, %act5 = acsim.dispatch @Leaf::@pulse path "Top.right[1].pulse" indices [] object 5 activation 5 + work "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::work" + xfer "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::xfer" + reset "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::reset" + validate "acsim_generated::Leaf::s8000000000000000000000000000000000000000000000000000000000000000::pulse::pb000000000000000000000000000000000000000000000000000000000000000::validate" + : !acsim.object_id, !acsim.activation_id + acsim.activate %act0 to %obj0 : !acsim.activation_id to !acsim.object_id + acsim.activate %act1 to %obj1 : !acsim.activation_id to !acsim.object_id + acsim.activate %act2 to %obj2 : !acsim.activation_id to !acsim.object_id + acsim.activate %act3 to %obj3 : !acsim.activation_id to !acsim.object_id + acsim.activate %act4 to %obj4 : !acsim.activation_id to !acsim.object_id + acsim.activate %act5 to %obj5 : !acsim.activation_id to !acsim.object_id + } +} + +// CHECK: acsim.module @Leaf +// CHECK: acsim.module @Top +// CHECK: acsim.dispatch @Leaf::@child path "Top.left.child" +// CHECK: acsim.dispatch @Leaf::@pulse path "Top.left.pulse" +// CHECK: acsim.dispatch @Leaf::@child path "Top.right[0].child" +// CHECK: acsim.dispatch @Leaf::@pulse path "Top.right[0].pulse" +// CHECK: acsim.dispatch @Leaf::@child path "Top.right[1].child" +// CHECK: acsim.dispatch @Leaf::@pulse path "Top.right[1].pulse" diff --git a/components/agentic-circuit/test/ACSim/types-invalid.mlir b/components/agentic-circuit/test/ACSim/types-invalid.mlir new file mode 100644 index 00000000..7ad5eb7d --- /dev/null +++ b/components/agentic-circuit/test/ACSim/types-invalid.mlir @@ -0,0 +1,51 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt %t/negative-array.mlir 2>&1 | %FileCheck %s --check-prefix=NEGATIVE +// RUN: %not %acir_opt %t/rank-zero-array.mlir 2>&1 | %FileCheck %s --check-prefix=RANK-ZERO +// RUN: %not %acir_opt %t/excessive-array.mlir 2>&1 | %FileCheck %s --check-prefix=EXCESSIVE +// RUN: %not %acir_opt %t/malformed-port.mlir 2>&1 | %FileCheck %s --check-prefix=PORT +// RUN: %not %acir_opt %t/malformed-pc.mlir 2>&1 | %FileCheck %s --check-prefix=PC +// RUN: %not %acir_opt %t/malformed-ref.mlir 2>&1 | %FileCheck %s --check-prefix=REF +// RUN: %not %acir_opt %t/malformed-resource.mlir 2>&1 | %FileCheck %s --check-prefix=RESOURCE + +// NEGATIVE: error: array extents must be non-negative +// RANK-ZERO: error: array shape must have at least one extent +// EXCESSIVE: error: array volume exceeds ACSim capability 1048576 +// PORT: error: expected ',' +// PC: error: invalid kind of attribute specified + +//--- negative-array.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !acsim.array<[2, -1], !acsim.owner<@b>> +} + +//--- excessive-array.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !acsim.array<[1048576, 1048576], !acsim.owner<@b>> +} + +//--- rank-zero-array.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !acsim.array<[], !acsim.owner<@b>> +} + +//--- malformed-port.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !acsim.port<@stream, @producer> +} + +//--- malformed-pc.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !acsim.pc +} + +//--- malformed-ref.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !acsim.ref +} +// REF: error: invalid kind of attribute specified + +//--- malformed-resource.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !acsim.resource<@rk> +} +// RESOURCE: error: expected ',' diff --git a/components/agentic-circuit/test/ACSim/types-valid.mlir b/components/agentic-circuit/test/ACSim/types-valid.mlir new file mode 100644 index 00000000..6139a18f --- /dev/null +++ b/components/agentic-circuit/test/ACSim/types-valid.mlir @@ -0,0 +1,30 @@ +// RUN: %acir_opt %s | %FileCheck %s +// RUN: %acir_opt %s | %acir_opt | %FileCheck %s +// RUN: %acir_opt --emit-bytecode -o %t.bc %s +// RUN: %acir_opt %t.bc | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + "builtin.unrealized_conversion_cast"() : () -> !acsim.value<@cpp_i32> + "builtin.unrealized_conversion_cast"() : () -> !acsim.expr<@cpp_i32> + "builtin.unrealized_conversion_cast"() : () -> !acsim.owner<@fifo_binding> + "builtin.unrealized_conversion_cast"() : () -> !acsim.ref<@fifo_binding> + "builtin.unrealized_conversion_cast"() : () -> !acsim.port<@stream, @producer, @packet, @ready_valid> + "builtin.unrealized_conversion_cast"() : () -> !acsim.resource<@memory, @initiator> + "builtin.unrealized_conversion_cast"() : () -> !acsim.array<[2, 3], !acsim.owner<@fifo_binding>> + "builtin.unrealized_conversion_cast"() : () -> !acsim.object_id + "builtin.unrealized_conversion_cast"() : () -> !acsim.activation_id + "builtin.unrealized_conversion_cast"() : () -> !acsim.pc<@tick> + "builtin.unrealized_conversion_cast"() : () -> !acsim.wake<@event> +} + +// CHECK: !acsim.value<@cpp_i32> +// CHECK: !acsim.expr<@cpp_i32> +// CHECK: !acsim.owner<@fifo_binding> +// CHECK: !acsim.ref<@fifo_binding> +// CHECK: !acsim.port<@stream, @producer, @packet, @ready_valid> +// CHECK: !acsim.resource<@memory, @initiator> +// CHECK: !acsim.array<[2, 3], !acsim.owner<@fifo_binding>> +// CHECK: !acsim.object_id +// CHECK: !acsim.activation_id +// CHECK: !acsim.pc<@tick> +// CHECK: !acsim.wake<@event> diff --git a/components/agentic-circuit/test/Analysis/process-state-pass.mlir b/components/agentic-circuit/test/Analysis/process-state-pass.mlir new file mode 100644 index 00000000..59430654 --- /dev/null +++ b/components/agentic-circuit/test/Analysis/process-state-pass.mlir @@ -0,0 +1,27 @@ +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology,ac-lower-process-state)' %s | %FileCheck %s + +// A non-yield-only model. The freeze pass produces a frozen model, +// and the lower-process-state pass routes it through the public planner and +// verifies it non-mutatingly. +// Both passes must succeed; the output must contain the frozen module unchanged. + +// CHECK: module attributes { +// CHECK-SAME: ac.contract_epoch = "0.3" +// CHECK-SAME: ac.freeze_epoch = "0.3" +// CHECK-SAME: ac.topology_frozen = true +// CHECK: ac.process @workload + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %ready = arith.constant true + ac.wait_until %ready + ac.yield_sim + } + ac.return + } +} diff --git a/components/agentic-circuit/test/Analysis/process-state-verifier.mlir b/components/agentic-circuit/test/Analysis/process-state-verifier.mlir new file mode 100644 index 00000000..61efae5e --- /dev/null +++ b/components/agentic-circuit/test/Analysis/process-state-verifier.mlir @@ -0,0 +1,165 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt --verify-each=false %t/static.mlir >/dev/null +// RUN: %acir_opt --verify-each=false %t/dynamic-suspending.mlir >/dev/null +// RUN: %not %acir_opt --verify-each=false %t/dynamic-no-suspend.mlir 2>&1 | %FileCheck %s --check-prefix=DYNAMIC +// RUN: %not %acir_opt --verify-each=false %t/non-positive-step.mlir 2>&1 | %FileCheck %s --check-prefix=STEP +// RUN: %not %acir_opt --verify-each=false %t/trip-cap.mlir 2>&1 | %FileCheck %s --check-prefix=TRIP +// RUN: %not %acir_opt --verify-each=false %t/cyclic-cf.mlir 2>&1 | %FileCheck %s --check-prefix=CF +// RUN: %not %acir_opt --verify-each=false %t/recursive-call.mlir 2>&1 | %FileCheck %s --check-prefix=RECURSION +// RUN: %not %acir_opt --verify-each=false %t/external-call.mlir 2>&1 | %FileCheck %s --check-prefix=EXTERNAL +// RUN: %not %acir_opt --verify-each=false %t/callee-dynamic-no-suspend.mlir 2>&1 | %FileCheck %s --check-prefix=CALLEEDYNAMIC +// RUN: %not %acir_opt --verify-each=false %t/callee-unsupported.mlir 2>&1 | %FileCheck %s --check-prefix=CALLEEBAD + +//--- static.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %lb = arith.constant 0 : index + %ub = arith.constant 4 : index + %step = arith.constant 1 : index + scf.for %i = %lb to %ub step %step { scf.yield } + ac.yield_sim + } + ac.return + } +} + +//--- dynamic-suspending.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top(index, index, index) parameters {} graph { + ^bb0(%lb : index, %ub : index, %step : index): + ac.process @workload kind "workload" captures(%lb, %ub, %step : index, index, index) { + ^bb0(%l : index, %u : index, %s : index): + %true = arith.constant true + scf.for %i = %l to %u step %s { + ac.wait_until %true + scf.yield + } + ac.yield_sim + } + ac.return + } +} + +//--- dynamic-no-suspend.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top(index, index, index) parameters {} graph { + ^bb0(%lb : index, %ub : index, %step : index): + ac.process @workload kind "workload" captures(%lb, %ub, %step : index, index, index) { + ^bb0(%l : index, %u : index, %s : index): + scf.for %i = %l to %u step %s { scf.yield } + ac.yield_sim + } + ac.return + } +} +// DYNAMIC: dynamic scf.for requires every reachable backedge to suspend + +//--- non-positive-step.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %lb = arith.constant 0 : index + %ub = arith.constant 4 : index + %step = arith.constant 0 : index + scf.for %i = %lb to %ub step %step { scf.yield } + ac.yield_sim + } + ac.return + } +} +// STEP: static scf.for step must be positive + +//--- trip-cap.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %lb = arith.constant 0 : index + %ub = arith.constant 1048577 : index + %step = arith.constant 1 : index + scf.for %i = %lb to %ub step %step { scf.yield } + ac.yield_sim + } + ac.return + } +} +// TRIP: static scf.for trip count exceeds ACIR capability limit 1048576 + +//--- cyclic-cf.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.process"() <{kind = "workload", sym_name = "workload"}> ({ + ^bb0: + "ac.yield_sim"() : () -> () + ^bb1: + "cf.br"()[^bb1] : () -> () + }) : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// CF: ac.process contains unsupported operation cf.br + +//--- recursive-call.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @recur() { + func.call @recur() : () -> () + return + } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @recur() : () -> () + ac.yield_sim + } + ac.return + } +} +// RECURSION: recursive func.call purity cycle: @recur -> @recur + +//--- external-call.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func private @external() + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @external() : () -> () + ac.yield_sim + } + ac.return + } +} +// EXTERNAL: process func.call callee '@external' has no body and cannot be proven effect-free + +//--- callee-dynamic-no-suspend.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @loop(%l : index, %u : index, %s : index) { + scf.for %i = %l to %u step %s { scf.yield } + return + } + ac.module @Top(index, index, index) parameters {} graph { + ^bb0(%l : index, %u : index, %s : index): + ac.process @workload kind "workload" captures(%l, %u, %s : index, index, index) { + ^bb0(%pl : index, %pu : index, %ps : index): + func.call @loop(%pl, %pu, %ps) : (index, index, index) -> () + ac.yield_sim + } + ac.return + } +} +// CALLEEDYNAMIC: dynamic scf.for requires every reachable backedge to suspend + +//--- callee-unsupported.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @bad() { + ^entry: + cf.br ^cycle + ^cycle: + cf.br ^cycle + } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @bad() : () -> () + ac.yield_sim + } + ac.return + } +} +// CALLEEBAD: ac.process contains unsupported operation cf.br diff --git a/components/agentic-circuit/test/Analysis/raw-structure-preflight.mlir b/components/agentic-circuit/test/Analysis/raw-structure-preflight.mlir new file mode 100644 index 00000000..8c340c00 --- /dev/null +++ b/components/agentic-circuit/test/Analysis/raw-structure-preflight.mlir @@ -0,0 +1,47 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt --verify-each=false --acir-test-pass-trace --acir-test-raw-depth=512 %t/shallow.mlir -o /dev/null 2>&1 | %FileCheck %s --check-prefix=DEFAULT +// RUN: %acir_opt --verify-each=false --acir-test-pass-trace --acir-test-raw-depth=512 --normalize-ac-file %t/shallow.mlir -o /dev/null 2>&1 | %FileCheck %s --check-prefix=REGISTERED +// RUN: %not %acir_opt --verify-each=false --acir-test-pass-trace --acir-test-raw-depth=513 %t/shallow.mlir -o /dev/null > %t/depth513-default.raw 2>&1 +// RUN: /usr/bin/sed -E 's#^.*shallow.mlir:1:1: error:#LOC: error:#' %t/depth513-default.raw > %t/depth513-default.actual +// RUN: /usr/bin/diff -u %t/expected-depth.txt %t/depth513-default.actual +// RUN: %not %acir_opt --verify-each=false --acir-test-pass-trace --acir-test-raw-depth=513 --normalize-ac-file %t/shallow.mlir -o /dev/null > %t/depth513-registered.raw 2>&1 +// RUN: /usr/bin/sed -E 's#^.*shallow.mlir:1:1: error:#LOC: error:#' %t/depth513-registered.raw > %t/depth513-registered.actual +// RUN: /usr/bin/diff -u %t/expected-depth.txt %t/depth513-registered.actual +// RUN: %not %acir_opt --verify-each=false --acir-test-pass-trace --acir-test-raw-depth=10000 --acir-test-raw-malformed %t/shallow.mlir -o /dev/null > %t/depth10000-default.raw 2>&1 +// RUN: /usr/bin/sed -E 's#^.*shallow.mlir:1:1: error:#LOC: error:#' %t/depth10000-default.raw > %t/depth10000-default.actual +// RUN: /usr/bin/diff -u %t/expected-depth.txt %t/depth10000-default.actual +// RUN: %not %acir_opt --verify-each=false --acir-test-pass-trace --acir-test-raw-depth=10000 --acir-test-raw-malformed --normalize-ac-file %t/shallow.mlir -o /dev/null > %t/depth10000-registered.raw 2>&1 +// RUN: /usr/bin/sed -E 's#^.*shallow.mlir:1:1: error:#LOC: error:#' %t/depth10000-registered.raw > %t/depth10000-registered.actual +// RUN: /usr/bin/diff -u %t/expected-depth.txt %t/depth10000-registered.actual + +// DEFAULT: enter:acir-test-materialize-raw-depth +// DEFAULT-NEXT: complete:acir-test-materialize-raw-depth +// DEFAULT-NEXT: enter:normalize-ac-file +// DEFAULT-NEXT: complete:normalize-ac-file +// DEFAULT-NEXT: enter:verify-ac-file +// DEFAULT-NEXT: complete:verify-ac-file +// DEFAULT-NEXT: enter:ac-verify-model +// DEFAULT-NEXT: complete:ac-verify-model + +// REGISTERED: enter:acir-test-materialize-raw-depth +// REGISTERED-NEXT: complete:acir-test-materialize-raw-depth +// REGISTERED-NEXT: enter:normalize-ac-file +// REGISTERED-NEXT: complete:normalize-ac-file +// REGISTERED-NEXT: enter:verify-ac-file +// REGISTERED-NEXT: complete:verify-ac-file +// REGISTERED-NEXT: enter:normalize-ac-file +// REGISTERED-NEXT: complete:normalize-ac-file +// REGISTERED-NEXT: enter:ac-verify-model +// REGISTERED-NEXT: complete:ac-verify-model + +//--- shallow.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} {} + +//--- expected-depth.txt +enter:acir-test-materialize-raw-depth +complete:acir-test-materialize-raw-depth +enter:normalize-ac-file +LOC: error: whole-model region nesting exceeds ACIR capability limit 512 +builtin.module attributes {ac.contract_epoch = "0.3"} {} +^ +fail:normalize-ac-file diff --git a/components/agentic-circuit/test/Bindings/Inputs/empty.json b/components/agentic-circuit/test/Bindings/Inputs/empty.json new file mode 100644 index 00000000..75473d44 --- /dev/null +++ b/components/agentic-circuit/test/Bindings/Inputs/empty.json @@ -0,0 +1,20 @@ +{ + "candidates": [], + "requests": [{ + "activation_sources": [], + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "contract_epoch": "0.3", + "effect": "pure", + "function_type": "() -> i32", + "parameters": [{"acir_type": "i64", "name": "width", "ordinal": 0, "value": 8}], + "ports": [{"cardinality": "exclusive", "delegation": "forbidden", "direction": "input", "interface": "ac.Stream", "ownership": "borrowed", "payload": "ac.Packet", "protocol": "ac.ReadyValid", "role": "consumer", "time_domain": "ac.cycle"}], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resolution_key": "@Leaf", + "resources": [], + "results": [{"acir_type": "i32", "name": "result"}] + }] +} diff --git a/components/agentic-circuit/test/Bindings/Inputs/leaf-ambiguous.json b/components/agentic-circuit/test/Bindings/Inputs/leaf-ambiguous.json new file mode 100644 index 00000000..08b5aa87 --- /dev/null +++ b/components/agentic-circuit/test/Bindings/Inputs/leaf-ambiguous.json @@ -0,0 +1,37 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "arm64-apple-darwin", + "record": { + "activation_sources": [], "availability": "available", "binding": "Leaf", "binding_schema": "acsim-binding-0.1", "component_schema": "ac.Leaf", "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", "construction": {"arguments": [8], "kind": "constructor"}, "contract_epoch": "0.3", "cpp": {"concept": "gfsim::PureModel", "entry_points": {"pure": "gfsim::leaf", "reset": "", "validate": "", "work": "", "xfer": ""}, "header": "gfsim/leaf.hpp", "symbol": "gfsim::Leaf", "target": "gfsim"}, "cpp_type": "cpp_i32", "effect": "pure", "fingerprint": "sha256:c589295d45f11e7bd368e271ce3dd8928f3207e7ee0287012dcaa15034a313d1", "implementation": "gfsim.Leaf", "ownership": {"kind": "none", "placement": "inline"}, "parameters": [{"acir_type": "i64", "cpp_type": "std::int64_t", "mapping": "constructor_constant", "name": "width", "ordinal": 0, "value": 8}], "ports": [{"accessor": "input", "cardinality": "exclusive", "delegation": "forbidden", "direction": "input", "interface": "ac.Stream", "ownership": "borrowed", "payload": "ac.Packet", "protocol": "ac.ReadyValid", "role": "consumer", "time_domain": "ac.cycle"}], "provider": "gfsim", "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", "resources": [], "results": [{"cpp_type": "cpp_i32", "name": "result"}] + } + }, + { + "available": true, + "profile": "fast", + "target": "arm64-apple-darwin", + "record": { + "activation_sources": [], "availability": "available", "binding": "Leaf", "binding_schema": "acsim-binding-0.1", "component_schema": "ac.Leaf", "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", "construction": {"arguments": [8], "kind": "constructor"}, "contract_epoch": "0.3", "cpp": {"concept": "gfsim::PureModel", "entry_points": {"pure": "gfsim::leaf", "reset": "", "validate": "", "work": "", "xfer": ""}, "header": "gfsim/leaf.hpp", "symbol": "gfsim::Leaf", "target": "gfsim"}, "cpp_type": "cpp_i32", "effect": "pure", "fingerprint": "sha256:c589295d45f11e7bd368e271ce3dd8928f3207e7ee0287012dcaa15034a313d1", "implementation": "gfsim.Leaf", "ownership": {"kind": "none", "placement": "inline"}, "parameters": [{"acir_type": "i64", "cpp_type": "std::int64_t", "mapping": "constructor_constant", "name": "width", "ordinal": 0, "value": 8}], "ports": [{"accessor": "input", "cardinality": "exclusive", "delegation": "forbidden", "direction": "input", "interface": "ac.Stream", "ownership": "borrowed", "payload": "ac.Packet", "protocol": "ac.ReadyValid", "role": "consumer", "time_domain": "ac.cycle"}], "provider": "gfsim", "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", "resources": [], "results": [{"cpp_type": "cpp_i32", "name": "result"}] + } + } + ], + "requests": [{ + "activation_sources": [], + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "contract_epoch": "0.3", + "effect": "pure", + "function_type": "() -> i32", + "parameters": [{"acir_type": "i64", "name": "width", "ordinal": 0, "value": 8}], + "ports": [{"cardinality": "exclusive", "delegation": "forbidden", "direction": "input", "interface": "ac.Stream", "ownership": "borrowed", "payload": "ac.Packet", "protocol": "ac.ReadyValid", "role": "consumer", "time_domain": "ac.cycle"}], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resolution_key": "@Leaf", + "resources": [], + "results": [{"acir_type": "i32", "name": "result"}] + }] +} diff --git a/components/agentic-circuit/test/Bindings/Inputs/leaf-fast.json b/components/agentic-circuit/test/Bindings/Inputs/leaf-fast.json new file mode 100644 index 00000000..cd847571 --- /dev/null +++ b/components/agentic-circuit/test/Bindings/Inputs/leaf-fast.json @@ -0,0 +1,72 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "arm64-apple-darwin", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "construction": {"arguments": [8], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::PureModel", + "entry_points": {"pure": "gfsim::leaf", "reset": "", "validate": "", "work": "", "xfer": ""}, + "header": "gfsim/leaf.hpp", + "symbol": "gfsim::Leaf", + "target": "gfsim" + }, + "cpp_type": "cpp_i32", + "effect": "pure", + "fingerprint": "sha256:c589295d45f11e7bd368e271ce3dd8928f3207e7ee0287012dcaa15034a313d1", + "implementation": "gfsim.Leaf", + "ownership": {"kind": "none", "placement": "inline"}, + "parameters": [{ + "acir_type": "i64", + "cpp_type": "std::int64_t", + "mapping": "constructor_constant", + "name": "width", + "ordinal": 0, + "value": 8 + }], + "ports": [{ + "accessor": "input", + "cardinality": "exclusive", + "delegation": "forbidden", + "direction": "input", + "interface": "ac.Stream", + "ownership": "borrowed", + "payload": "ac.Packet", + "protocol": "ac.ReadyValid", + "role": "consumer", + "time_domain": "ac.cycle" + }], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resources": [], + "results": [{"cpp_type": "cpp_i32", "name": "result"}] + } + } + ], + "requests": [{ + "activation_sources": [], + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "contract_epoch": "0.3", + "effect": "pure", + "function_type": "() -> i32", + "parameters": [{"acir_type": "i64", "name": "width", "ordinal": 0, "value": 8}], + "ports": [{"cardinality": "exclusive", "delegation": "forbidden", "direction": "input", "interface": "ac.Stream", "ownership": "borrowed", "payload": "ac.Packet", "protocol": "ac.ReadyValid", "role": "consumer", "time_domain": "ac.cycle"}], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resolution_key": "@Leaf", + "resources": [], + "results": [{"acir_type": "i32", "name": "result"}] + }] +} diff --git a/components/agentic-circuit/test/Bindings/Inputs/leaf-validated.json b/components/agentic-circuit/test/Bindings/Inputs/leaf-validated.json new file mode 100644 index 00000000..76a60691 --- /dev/null +++ b/components/agentic-circuit/test/Bindings/Inputs/leaf-validated.json @@ -0,0 +1,32 @@ +{ + "candidates": [ + { + "available": true, + "profile": "validated", + "target": "arm64-apple-darwin", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "construction": {"arguments": [8], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": {"concept": "gfsim::PureModel", "entry_points": {"pure": "gfsim::leaf", "reset": "", "validate": "", "work": "", "xfer": ""}, "header": "gfsim/leaf.hpp", "symbol": "gfsim::Leaf", "target": "gfsim"}, + "cpp_type": "cpp_i32", + "effect": "pure", + "fingerprint": "sha256:c589295d45f11e7bd368e271ce3dd8928f3207e7ee0287012dcaa15034a313d1", + "implementation": "gfsim.Leaf", + "ownership": {"kind": "none", "placement": "inline"}, + "parameters": [{"acir_type": "i64", "cpp_type": "std::int64_t", "mapping": "constructor_constant", "name": "width", "ordinal": 0, "value": 8}], + "ports": [{"accessor": "input", "cardinality": "exclusive", "delegation": "forbidden", "direction": "input", "interface": "ac.Stream", "ownership": "borrowed", "payload": "ac.Packet", "protocol": "ac.ReadyValid", "role": "consumer", "time_domain": "ac.cycle"}], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resources": [], + "results": [{"cpp_type": "cpp_i32", "name": "result"}] + } + } + ], + "requests": [] +} diff --git a/components/agentic-circuit/test/Bindings/resolve-ambiguous.mlir b/components/agentic-circuit/test/Bindings/resolve-ambiguous.mlir new file mode 100644 index 00000000..c84de6b8 --- /dev/null +++ b/components/agentic-circuit/test/Bindings/resolve-ambiguous.mlir @@ -0,0 +1,27 @@ +// RUN: rm -f %t.lock +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-registry=%S/Inputs/leaf-ambiguous.json \ +// RUN: --ac-binding-lock-output=%t.lock \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s 2>&1 | %FileCheck %s +// RUN: test ! -e %t.lock + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module.extern @Leaf : () -> i32 parameters {width = 8 : i64} + implementation {registry = "cpp", name = "Leaf"} + ac.module @Top() parameters {} graph { + %leaf = ac.instance @leaf of @Leaf() static {width = 8 : i64} + id "leaf" path "leaf" : () -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +// CHECK: ACLOWER-BINDING-AMBIGUOUS +// CHECK-SAME: key=@Leaf +// CHECK-NOT: acsim. diff --git a/components/agentic-circuit/test/Bindings/resolve-empty.mlir b/components/agentic-circuit/test/Bindings/resolve-empty.mlir new file mode 100644 index 00000000..dcb6f2b5 --- /dev/null +++ b/components/agentic-circuit/test/Bindings/resolve-empty.mlir @@ -0,0 +1,48 @@ +// RUN: rm -f %t.first.lock %t.second.lock +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-lock-output=%t.first.lock \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s -o %t.out +// RUN: %FileCheck %s --check-prefix=UNCHANGED < %t.out +// RUN: %FileCheck %s --check-prefix=EMPTY < %t.first.lock +// RUN: test "$(wc -c < %t.first.lock)" -eq 2 +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-lock-output=%t.second.lock \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s -o /dev/null +// RUN: cmp %t.first.lock %t.second.lock +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s 2>&1 | %FileCheck %s --check-prefix=OUTPUT +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-lock-output=%t.profile.lock \ +// RUN: --ac-binding-target=arm64-apple-darwin %s 2>&1 | %FileCheck %s --check-prefix=PROFILE +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-lock-output=%t.target.lock \ +// RUN: --ac-binding-profile=fast %s 2>&1 | %FileCheck %s --check-prefix=TARGET + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +// UNCHANGED: module attributes { +// UNCHANGED-SAME: ac.topology_frozen = true +// UNCHANGED: ac.module @Top +// UNCHANGED: ac.process @workload kind "workload" +// UNCHANGED-NOT: acsim. +// EMPTY: [] +// OUTPUT: error: ACLOWER-BINDING-OPTIONS: --ac-binding-lock-output is required +// PROFILE: error: ACLOWER-BINDING-OPTIONS: --ac-binding-profile is required +// TARGET: error: ACLOWER-BINDING-OPTIONS: --ac-binding-target is required diff --git a/components/agentic-circuit/test/Bindings/resolve-missing.mlir b/components/agentic-circuit/test/Bindings/resolve-missing.mlir new file mode 100644 index 00000000..7ce438ff --- /dev/null +++ b/components/agentic-circuit/test/Bindings/resolve-missing.mlir @@ -0,0 +1,49 @@ +// RUN: rm -f %t.lock +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-registry=%S/Inputs/empty.json \ +// RUN: --ac-binding-lock-output=%t.lock \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s 2>&1 | %FileCheck %s +// RUN: test ! -e %t.lock +// RUN: rm -f %t.no-registry.lock +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-lock-output=%t.no-registry.lock \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s 2>&1 | %FileCheck %s --check-prefix=NO-REGISTRY +// RUN: test ! -e %t.no-registry.lock +// RUN: rm -f %t.profile.lock +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-registry=%S/Inputs/leaf-fast.json \ +// RUN: --ac-binding-lock-output=%t.profile.lock \ +// RUN: --ac-binding-profile=validated \ +// RUN: --ac-binding-target=arm64-apple-darwin %s 2>&1 | %FileCheck %s --check-prefix=PROFILE +// RUN: test ! -e %t.profile.lock + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module.extern @Leaf : () -> i32 parameters {width = 8 : i64} + implementation {registry = "cpp", name = "Leaf"} + ac.module @Top() parameters {} graph { + %leaf = ac.instance @leaf of @Leaf() static {width = 8 : i64} + id "leaf" path "leaf" : () -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +// CHECK: ACLOWER-BINDING-MISSING +// CHECK-SAME: key=@Leaf +// CHECK-NOT: acsim. +// NO-REGISTRY: ACLOWER-BINDING-MISSING +// NO-REGISTRY-SAME: key=@Leaf +// NO-REGISTRY-NOT: ACLOWER-BINDING-OPTIONS +// PROFILE: ACLOWER-BINDING-MISSING +// PROFILE-SAME: key=@Leaf +// PROFILE-SAME: reason=ACLOWER-PROFILE +// PROFILE-NOT: acsim. diff --git a/components/agentic-circuit/test/Bindings/resolve-valid.mlir b/components/agentic-circuit/test/Bindings/resolve-valid.mlir new file mode 100644 index 00000000..2b4fa94b --- /dev/null +++ b/components/agentic-circuit/test/Bindings/resolve-valid.mlir @@ -0,0 +1,59 @@ +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-registry=%S/Inputs/leaf-fast.json \ +// RUN: --ac-binding-lock-output=%t.lock \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s -o %t.out +// RUN: %FileCheck %s --check-prefix=UNCHANGED < %t.out +// RUN: %FileCheck %s --check-prefix=LOCK < %t.lock +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-registry=%S/Inputs/leaf-validated.json \ +// RUN: --ac-binding-registry=%S/Inputs/leaf-fast.json \ +// RUN: --ac-binding-lock-output=%t.forward.lock \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s -o /dev/null +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' \ +// RUN: --ac-resolve-gfsim-bindings \ +// RUN: --ac-binding-registry=%S/Inputs/leaf-fast.json \ +// RUN: --ac-binding-registry=%S/Inputs/leaf-validated.json \ +// RUN: --ac-binding-lock-output=%t.reverse.lock \ +// RUN: --ac-binding-profile=fast \ +// RUN: --ac-binding-target=arm64-apple-darwin %s -o /dev/null +// RUN: diff %t.forward.lock %t.reverse.lock +// RUN: %acir_opt %s | %FileCheck %s --check-prefix=ORDINARY +// RUN: %acir_opt --help | %FileCheck %s --check-prefix=HELP + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module.extern @Leaf : () -> i32 parameters {width = 8 : i64} + implementation {registry = "cpp", name = "Leaf"} + ac.module @Top() parameters {} graph { + %leaf = ac.instance @leaf of @Leaf() static {width = 8 : i64} + id "leaf" path "leaf" : () -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +// UNCHANGED: module attributes { +// UNCHANGED-SAME: ac.topology_frozen = true +// UNCHANGED: ac.module.extern @Leaf +// UNCHANGED-NOT: acsim. +// ORDINARY: ac.module.extern @Leaf +// LOCK: [{"activation_sources":[] +// LOCK-SAME: "binding":"Leaf" +// LOCK-SAME: "binding_schema":"acsim-binding-0.1" +// LOCK-SAME: "contract_epoch":"0.3" +// LOCK-SAME: "fingerprint":"sha256:{{[0-9a-f]+}}" +// HELP: Exact binding resolution options: +// HELP-EMPTY: +// HELP-NEXT: --ac-binding-lock-output= +// HELP-NEXT: --ac-binding-profile= +// HELP-NEXT: --ac-binding-registry= +// HELP-NEXT: --ac-binding-target= +// HELP-NEXT: --ac-lower-to-acsim +// HELP-NEXT: --ac-resolve-gfsim-bindings diff --git a/components/agentic-circuit/test/CMakeLists.txt b/components/agentic-circuit/test/CMakeLists.txt new file mode 100644 index 00000000..154d2209 --- /dev/null +++ b/components/agentic-circuit/test/CMakeLists.txt @@ -0,0 +1,24 @@ +set(ACIR_TEST_LLVM_LINKER_FLAGS "") +foreach(ACIR_LLVM_LINK_FLAG IN LISTS ACIR_LLVM_LINK_FLAGS) + string(APPEND ACIR_TEST_LLVM_LINKER_FLAGS + " --linker-flag=${ACIR_LLVM_LINK_FLAG}" + ) +endforeach() +string(STRIP "${ACIR_TEST_LLVM_LINKER_FLAGS}" ACIR_TEST_LLVM_LINKER_FLAGS) +get_filename_component(ACIR_LIT_BIN_DIR "${ACIR_LIT_EXECUTABLE}" DIRECTORY) +find_program(ACIR_TEST_PYTHON NAMES python python3 + HINTS "${ACIR_LIT_BIN_DIR}" + REQUIRED +) + +configure_lit_site_cfg( + "${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in" + "${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py" + MAIN_CONFIG "${CMAKE_CURRENT_SOURCE_DIR}/lit.cfg.py" +) + +add_lit_testsuite(check-acir "Running Agentic Circuit lit tests" + "${CMAKE_CURRENT_BINARY_DIR}" + DEPENDS acir-cxxgen acir-opt acir-opt-internal acir-queue-pycgen + agentic_circuit_native +) diff --git a/components/agentic-circuit/test/CodeGen/Inputs/extension/extension.binding.json b/components/agentic-circuit/test/CodeGen/Inputs/extension/extension.binding.json new file mode 100644 index 00000000..6453602f --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/extension/extension.binding.json @@ -0,0 +1 @@ +{"activation_sources":[],"availability":"available","binding":"counter_binding","binding_schema":"acsim-binding-0.1","component_schema":"ac.test.Counter","component_schema_fingerprint":"sha256:14b0d2f17152c2ad41f8cd7eb861d1069230f8e179bf0158986ba9c6d0f33cb8","construction":{"arguments":[],"kind":"constructor"},"contract_epoch":"0.3","cpp":{"concept":"gfsim::Component","entry_points":{"pure":"","reset":"counter_reset","validate":"counter_validate","work":"counter_work","xfer":"counter_xfer"},"header":"extension_provider.h","symbol":"ac_test::Counter","target":"ac_test"},"cpp_type":"uint64_t","effect":"stateful","fingerprint":"sha256:1100000000000000000000000000000000000000000000000000000000000000","implementation":"ac_test::Counter","ownership":{"kind":"unique","placement":"member_or_array"},"parameters":[],"ports":[],"provider":"ac_test","provider_implementation_fingerprint":"sha256:74eaae1048456c1f1426ae8bf65124b2db32ad411b23e26da323a709c36be6ba","resources":[],"results":[]} diff --git a/components/agentic-circuit/test/CodeGen/Inputs/extension/extension.schema.json b/components/agentic-circuit/test/CodeGen/Inputs/extension/extension.schema.json new file mode 100644 index 00000000..f6a72183 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/extension/extension.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"initial":{"minimum":0,"type":"integer"}},"required":["initial"],"title":"ac.test.Counter","type":"object"} diff --git a/components/agentic-circuit/test/CodeGen/Inputs/extension/extension_provider.cpp b/components/agentic-circuit/test/CodeGen/Inputs/extension/extension_provider.cpp new file mode 100644 index 00000000..e0d9eefc --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/extension/extension_provider.cpp @@ -0,0 +1,3 @@ +#include "extension_provider.h" + +static_assert(gfsim::Component); diff --git a/components/agentic-circuit/test/CodeGen/Inputs/extension/extension_provider.h b/components/agentic-circuit/test/CodeGen/Inputs/extension/extension_provider.h new file mode 100644 index 00000000..a0ef2969 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/extension/extension_provider.h @@ -0,0 +1,35 @@ +#ifndef ACIR_TEST_EXTENSION_PROVIDER_H +#define ACIR_TEST_EXTENSION_PROVIDER_H + +#include "gfsim/components.h" + +#include +#include +#include + +namespace ac_test { + +class Counter final : public gfsim::SimObject { +public: + static constexpr std::string_view contractName = "ac.test.Counter"; + static constexpr gfsim::ObjectKind componentKind = gfsim::ObjectKind::Compute; + + Counter(std::string name, gfsim::ObjectId id, gfsim::SimObject *parent) + : SimObject(componentKind, std::move(name), id, parent) {} + + void doWork(gfsim::Epoch) override { proposed_ = committed_ + 1; } + void doXfer(gfsim::Epoch) override { committed_ = proposed_; } + bool hasPendingCommit() const override { return proposed_ != committed_; } + bool validate() const { return id() != gfsim::kInvalidObjectId; } + void reset() override { committed_ = proposed_ = 0; } + +private: + uint64_t committed_ = 0; + uint64_t proposed_ = 0; +}; + +static_assert(gfsim::Component); + +} // namespace ac_test + +#endif // ACIR_TEST_EXTENSION_PROVIDER_H diff --git a/components/agentic-circuit/test/CodeGen/Inputs/extension/frozen.acir b/components/agentic-circuit/test/CodeGen/Inputs/extension/frozen.acir new file mode 100644 index 00000000..b1c5dc23 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/extension/frozen.acir @@ -0,0 +1 @@ +frozen-acir diff --git a/components/agentic-circuit/test/CodeGen/Inputs/extension/prepare_target.py b/components/agentic-circuit/test/CodeGen/Inputs/extension/prepare_target.py new file mode 100644 index 00000000..08bfb53f --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/extension/prepare_target.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Materialize an ACSim fixture for the active compiler target triple.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import subprocess +from pathlib import Path + + +def main() -> int: + parser = argparse.ArgumentParser(allow_abbrev=False) + parser.add_argument("input", type=Path) + parser.add_argument("output", type=Path) + parser.add_argument("--compiler", required=True, type=Path) + arguments = parser.parse_args() + + target = subprocess.run( + (str(arguments.compiler), "-dumpmachine"), + check=True, + capture_output=True, + text=True, + ).stdout.strip() + canonical = json.dumps(target, ensure_ascii=False, separators=(",", ":")) + fingerprint = "sha256:" + hashlib.sha256(canonical.encode()).hexdigest() + + source = arguments.input.read_text(encoding="utf-8") + updated, replacements = re.subn( + r'(toolchain\s*=\s*")sha256:[0-9a-f]{64}("\s*)', + rf"\g<1>{fingerprint}\g<2>", + source, + count=1, + ) + if replacements != 1: + parser.error("input does not contain exactly one toolchain fingerprint") + arguments.output.write_text(updated, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/test/CodeGen/Inputs/full/gfsim/fifo.hpp b/components/agentic-circuit/test/CodeGen/Inputs/full/gfsim/fifo.hpp new file mode 100644 index 00000000..60b1df49 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/full/gfsim/fifo.hpp @@ -0,0 +1,52 @@ +#ifndef ACIR_TEST_FULL_GFSIM_FIFO_HPP +#define ACIR_TEST_FULL_GFSIM_FIFO_HPP + +#include "gfsim/components.h" +#include "gfsim/process.h" + +#include +#include + +namespace gfsim { + +class TestEndpoint { +public: + void bind(TestEndpoint &other) { peer_ = &other; } + +private: + TestEndpoint *peer_ = nullptr; +}; + +class Fifo final : public SimObject { +public: + static constexpr std::string_view contractName = "ac.test.Fifo"; + static constexpr ObjectKind componentKind = ObjectKind::Queue; + + Fifo(std::string name, ObjectId id, SimObject *parent) + : SimObject(componentKind, std::move(name), id, parent) {} + + bool invoke(bool value) { return value; } + ProcessWake invoke() const { return {ProcessWakeKind::Condition, id()}; } + + TestEndpoint &output() { return output_; } + const TestEndpoint &output() const { return output_; } + TestEndpoint &input() { return input_; } + const TestEndpoint &input() const { return input_; } + TestEndpoint &initiator() { return initiator_; } + const TestEndpoint &initiator() const { return initiator_; } + TestEndpoint &target() { return target_; } + const TestEndpoint &target() const { return target_; } + +private: + TestEndpoint output_; + TestEndpoint input_; + TestEndpoint initiator_; + TestEndpoint target_; +}; + +template +concept StatefulModel = Component; + +} // namespace gfsim + +#endif // ACIR_TEST_FULL_GFSIM_FIFO_HPP diff --git a/components/agentic-circuit/test/CodeGen/Inputs/full/gfsim/pure.hpp b/components/agentic-circuit/test/CodeGen/Inputs/full/gfsim/pure.hpp new file mode 100644 index 00000000..fd2cbd1c --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/full/gfsim/pure.hpp @@ -0,0 +1,18 @@ +#ifndef ACIR_TEST_FULL_GFSIM_PURE_HPP +#define ACIR_TEST_FULL_GFSIM_PURE_HPP + +#include + +namespace gfsim { + +struct Pure {}; + +template +concept PureModel = std::same_as; + +inline bool is_ready() { return true; } +inline bool is_ready(bool value) { return value; } + +} // namespace gfsim + +#endif // ACIR_TEST_FULL_GFSIM_PURE_HPP diff --git a/components/agentic-circuit/test/CodeGen/Inputs/nested/child.hpp b/components/agentic-circuit/test/CodeGen/Inputs/nested/child.hpp new file mode 100644 index 00000000..04135b44 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/Inputs/nested/child.hpp @@ -0,0 +1,21 @@ +#ifndef ACIR_TEST_NESTED_CHILD_HPP +#define ACIR_TEST_NESTED_CHILD_HPP + +#include "gfsim/components.h" + +#include +#include + +class Child final : public gfsim::SimObject { +public: + static constexpr std::string_view contractName = "ac.test.Child"; + static constexpr gfsim::ObjectKind componentKind = gfsim::ObjectKind::Compute; + + Child(std::string name, gfsim::ObjectId id, gfsim::SimObject *parent) + : SimObject(componentKind, std::move(name), id, parent) {} +}; + +template +concept StatefulComponent = gfsim::Component; + +#endif // ACIR_TEST_NESTED_CHILD_HPP diff --git a/components/agentic-circuit/test/CodeGen/arbiter-verilog.mlir b/components/agentic-circuit/test/CodeGen/arbiter-verilog.mlir new file mode 100644 index 00000000..b3b399ba --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/arbiter-verilog.mlir @@ -0,0 +1,12 @@ +// RUN: %python %source_root/tools/acir-queue-veriloggen.py %s --pycgen %binary_root/bin/acir-queue-pycgen | %FileCheck %s --check-prefix=VERILOG + +module attributes {ac.contract_epoch = "0.3", ac.system = "arbiter"} { + %left = ac.source depth 1 latency 1 {ac.name = "left"} : !ac.queue + %right = ac.source depth 1 latency 1 {ac.name = "right"} : !ac.queue + %merged = ac.merge %left, %right policy "round_robin" depth 2 latency 1 {ac.name = "merged"} : (!ac.queue, !ac.queue) -> !ac.queue + ac.sink %merged {ac.name = "sink"} : !ac.queue +} + +// VERILOG: module arbiter ( +// VERILOG: assign +// VERILOG: module pyc_fifo #( diff --git a/components/agentic-circuit/test/CodeGen/arbiter.mlir b/components/agentic-circuit/test/CodeGen/arbiter.mlir new file mode 100644 index 00000000..926c8206 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/arbiter.mlir @@ -0,0 +1,13 @@ +// RUN: %binary_root/bin/acir-queue-pycgen %s | %FileCheck %s --check-prefix=PYC + +module attributes {ac.contract_epoch = "0.3", ac.system = "arbiter"} { + %left = ac.source depth 1 latency 1 {ac.name = "left"} : !ac.queue + %right = ac.source depth 1 latency 1 {ac.name = "right"} : !ac.queue + %merged = ac.merge %left, %right policy "round_robin" depth 2 latency 1 {ac.name = "merged"} : (!ac.queue, !ac.queue) -> !ac.queue + ac.sink %merged {ac.name = "sink"} : !ac.queue +} + +// PYC: pyc.alias +// PYC-SAME: num_inputs = 2 +// PYC-SAME: primitive_id = "control.rr_arbiter.v1" +// PYC-SAME: implementation_id = "internal.reference.rr_arbiter.v1" diff --git a/components/agentic-circuit/test/CodeGen/atomic-transform.mlir b/components/agentic-circuit/test/CodeGen/atomic-transform.mlir new file mode 100644 index 00000000..4fc89fe3 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/atomic-transform.mlir @@ -0,0 +1,22 @@ +// RUN: %binary_root/bin/acir-queue-cxxgen %s > %t.cpp +// RUN: %FileCheck %s --check-prefix=GFSIM < %t.cpp +// RUN: %cxx -std=c++20 -I%source_root/include -c %t.cpp -o %t.o +// RUN: %binary_root/bin/acir-queue-pycgen %s | %FileCheck %s --check-prefix=PYC + +module attributes {ac.contract_epoch = "0.3", ac.system = "atomic_sum"} { + %left = ac.source depth 2 latency 1 {ac.name = "left"} : !ac.queue + %right = ac.source depth 2 latency 1 {ac.name = "right"} : !ac.queue + %sum = ac.transform %left, %right depths [2] latencies [1] { + ^transform(%item0: !ac.var, %item1: !ac.var): + %result = ac.var.add %item0, %item1 : !ac.var + ac.transform.yield %result : !ac.var + } {ac.output_names = ["sum"]} : (!ac.queue, !ac.queue) -> !ac.queue + ac.sink %sum {ac.name = "sink_0"} : !ac.queue +} + +// GFSIM: std::tuple operator()(const std::int64_t &item, const std::int64_t &item1) +// GFSIM: gfsim::QueueAtomicTransform, std::tuple> block_0_; + +// PYC: = pyc.wire : i1 +// PYC: = pyc.add +// PYC: pyc.assign diff --git a/components/agentic-circuit/test/CodeGen/determinism.mlir b/components/agentic-circuit/test/CodeGen/determinism.mlir new file mode 100644 index 00000000..fd570af4 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/determinism.mlir @@ -0,0 +1,12 @@ +// REQUIRES: system-darwin +// RUN: rm -rf %t.first %t.second %t.provider.o +// RUN: %cxx -std=c++20 -I%source_root/include -c %S/Inputs/extension/extension_provider.cpp -o %t.provider.o +// RUN: %python %S/Inputs/extension/prepare_target.py %S/extension-provider.mlir %t.acsim --compiler %cxx +// RUN: for out in %t.first %t.second; do %acir_cxxgen %t.acsim --stop-after=publish --output-root=$out --frozen-acir=%S/Inputs/extension/frozen.acir --binding-lock=%S/Inputs/extension/extension.binding.json --project-name=project --project-identity=project.example --system-name=system --system-identity=system.example --profile=fast --compiler=%cxx --standard-library=libc++ --abi-mode=default --object-format=mach-o --contract-flag=-std=c++20 --include-root=%source_root/include --include-root=%S/Inputs/extension --provider-input=ac_test --link-input=%t.provider.o --link-input=%binary_root/lib/gfsim/libgfsim.a --link-input=%binary_root/lib/Bindings/libACIRBindings.a %llvm_linker_flags || exit 1; done +// RUN: diff -r %t.first %t.second +// RUN: %t.first/builds/*/bin/model --build-fingerprint > %t.first.fingerprint +// RUN: %t.second/builds/*/bin/model --build-fingerprint > %t.second.fingerprint +// RUN: cmp %t.first.fingerprint %t.second.fingerprint + +// The complete immutable build, including model plan inputs, generated source, +// compile plan, manifest, executable, and embedded fingerprint, is identical. diff --git a/components/agentic-circuit/test/CodeGen/driver-errors.mlir b/components/agentic-circuit/test/CodeGen/driver-errors.mlir new file mode 100644 index 00000000..6d342976 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/driver-errors.mlir @@ -0,0 +1,24 @@ +// RUN: %not %acir_cxxgen %s --stop-after=unknown 2>&1 | %FileCheck %s --check-prefix=BAD-STAGE +// RUN: %not %acir_cxxgen %s --stop-after=acsim-emit-cxx 2>&1 | %FileCheck %s --check-prefix=MISSING +// RUN: %not %acir_cxxgen %s --stop-after=publish --output-root=%t.publish --project-name=project --project-identity=project.example --system-name=system --system-identity=system.example --profile=fast --compiler=%cxx --standard-library=libc++ --abi-mode=default --object-format=mach-o --contract-flag=-std=c++20 2>&1 | %FileCheck %s --check-prefix=PUBLISH-INPUT +// BAD-STAGE: unknown --stop-after stage 'unknown' +// MISSING: stage=acsim-emit-cxx status=failed +// MISSING-SAME: --output-root is required +// PUBLISH-INPUT: stage=publish status=failed +// PUBLISH-INPUT-SAME: cannot read input file + +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @minimal epoch "0.3" root @Top construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @Top interface {ports = [], resources = [], results = []} + static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + } +} diff --git a/components/agentic-circuit/test/CodeGen/driver-stages.mlir b/components/agentic-circuit/test/CodeGen/driver-stages.mlir new file mode 100644 index 00000000..3275b8e6 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/driver-stages.mlir @@ -0,0 +1,30 @@ +// RUN: rm -rf %t.emit %t.check %t.compile %t.link +// RUN: %acir_cxxgen %s --stop-after=model-plan | %FileCheck %s --check-prefix=PLAN +// RUN: %acir_cxxgen %s --stop-after=acsim-emit-cxx --output-root=%t.emit | %FileCheck %s --check-prefix=EMIT +// RUN: %acir_cxxgen %s --stop-after=acsim-check-cxx-contract --output-root=%t.check | %FileCheck %s --check-prefix=CHECK +// RUN: %acir_cxxgen %s --stop-after=compile --output-root=%t.compile --project-name=project --project-identity=project.example --system-name=system --system-identity=system.example --profile=fast --compiler=%cxx --standard-library=libc++ --abi-mode=default --object-format=mach-o --contract-flag=-std=c++20 --include-root=%source_root/include | %FileCheck %s --check-prefix=COMPILE +// RUN: %acir_cxxgen %s --stop-after=link --output-root=%t.link --project-name=project --project-identity=project.example --system-name=system --system-identity=system.example --profile=fast --compiler=%cxx --standard-library=libc++ --abi-mode=default --object-format=mach-o --contract-flag=-std=c++20 --include-root=%source_root/include --link-input=%binary_root/lib/gfsim/libgfsim.a --link-input=%binary_root/lib/Bindings/libACIRBindings.a %llvm_linker_flags | %FileCheck %s --check-prefix=LINK +// RUN: test ! -e %t.emit/current.json +// RUN: test ! -e %t.compile/current.json +// RUN: test ! -e %t.link/current.json +// PLAN: stage=model-plan status=passed +// EMIT: stage=acsim-emit-cxx status=passed +// CHECK: stage=acsim-check-cxx-contract status=passed +// COMPILE: stage=compile status=passed +// LINK: stage=link status=passed + +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @minimal epoch "0.3" root @Top construction [] destruction [] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.module @Top interface {ports = [], resources = [], results = []} + static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + } +} diff --git a/components/agentic-circuit/test/CodeGen/extension-provider.mlir b/components/agentic-circuit/test/CodeGen/extension-provider.mlir new file mode 100644 index 00000000..f2557a0f --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/extension-provider.mlir @@ -0,0 +1,52 @@ +// REQUIRES: system-darwin +// RUN: rm -rf %t.out %t.provider.o +// RUN: %cxx -std=c++20 -I%source_root/include -c %S/Inputs/extension/extension_provider.cpp -o %t.provider.o +// RUN: %python %S/Inputs/extension/prepare_target.py %s %t.acsim --compiler %cxx +// RUN: %acir_cxxgen %t.acsim --stop-after=publish --output-root=%t.out --frozen-acir=%S/Inputs/extension/frozen.acir --binding-lock=%S/Inputs/extension/extension.binding.json --project-name=project --project-identity=project.example --system-name=system --system-identity=system.example --profile=fast --compiler=%cxx --standard-library=libc++ --abi-mode=default --object-format=mach-o --contract-flag=-std=c++20 --include-root=%source_root/include --include-root=%S/Inputs/extension --provider-input=ac_test --link-input=%t.provider.o --link-input=%binary_root/lib/gfsim/libgfsim.a --link-input=%binary_root/lib/Bindings/libACIRBindings.a %llvm_linker_flags | %FileCheck %s --check-prefix=PUBLISH +// RUN: %t.out/builds/*/bin/model --build-fingerprint | %FileCheck %s --check-prefix=FINGERPRINT +// RUN: grep -F '"namespace":"ac_test"' %t.out/builds/*/build-manifest.json +// RUN: grep -F '"canonical_name":"ac.test.Counter"' %t.out/builds/*/build-manifest.json +// RUN: otool -L %t.out/builds/*/bin/model | %FileCheck %s --check-prefix=DYLIB --implicit-check-not=Python --implicit-check-not=pybind --implicit-check-not=dlopen +// RUN: %not grep -F -- '-frtti' %t.out/builds/*/compile-plan.json +// RUN: %not grep -R "ac.test.Counter" %source_root/lib/CodeGen +// PUBLISH: stage=publish status=passed build=sha256: +// FINGERPRINT: sha256: +// DYLIB: model: + +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @extension epoch "0.3" root @Top construction ["Top.counter"] destruction ["Top.counter"] fingerprints { + frozen_acir = "sha256:80ca3d33c8fd95dd30b8b89a26650dd3f7cba3d7eebfd41269116e72d24a14f1", + binding_lock = "sha256:4cac483aa2f0b6335214ad72a7bde195a7bb909d847d64100dc557ec2e1e8bd0", + provider = "sha256:bc1fecb4eca98d70675797bedd33b046c9d9776e8d7ba036c60b46dfe62b2a43", + profile = "sha256:079c9d12005aad817f722d2f0a34ccc3185b5ec0ce06ee243f945e4e1bb7b4c7", + toolchain = "sha256:9b1db4862fdcda9688af508a4bd6dc716abe7f919a1af4cc09b1e373953a428a", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @counter_impl cpp "ac_test::Counter" kind "implementation" fingerprint "sha256:74eaae1048456c1f1426ae8bf65124b2db32ad411b23e26da323a709c36be6ba" + acsim.type @counter_schema cpp "ac.test.Counter" kind "schema" fingerprint "sha256:14b0d2f17152c2ad41f8cd7eb861d1069230f8e179bf0158986ba9c6d0f33cb8" + acsim.type @counter_value cpp "uint64_t" kind "value" fingerprint "sha256:0100000000000000000000000000000000000000000000000000000000000000" + acsim.type @provider cpp "ac_test" kind "provider" fingerprint "sha256:0400000000000000000000000000000000000000000000000000000000000000" + acsim.binding @counter_binding record { + activation_sources = [], availability = "available", binding = "counter_binding", + binding_schema = "acsim-binding-0.1", component_schema = @counter_schema, + component_schema_fingerprint = "sha256:14b0d2f17152c2ad41f8cd7eb861d1069230f8e179bf0158986ba9c6d0f33cb8", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "gfsim::Component", entry_points = {pure = "", reset = "counter_reset", validate = "counter_validate", work = "counter_work", xfer = "counter_xfer"}, header = "extension_provider.h", symbol = "ac_test::Counter", target = "ac_test"}, + cpp_type = @counter_value, effect = "stateful", fingerprint = "sha256:1100000000000000000000000000000000000000000000000000000000000000", + implementation = @counter_impl, ownership = {kind = "unique", placement = "member_or_array"}, + parameters = [], ports = [], provider = @provider, + provider_implementation_fingerprint = "sha256:74eaae1048456c1f1426ae8bf65124b2db32ad411b23e26da323a709c36be6ba", + resources = [], results = [] + } + acsim.module @Top interface {ports = [], resources = [], results = []} + static [] specialization "sha256:2000000000000000000000000000000000000000000000000000000000000000" exports [] { + %counter = acsim.instance @counter target @counter_binding args [] specialization "sha256:2100000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@counter_binding> + acsim.return + } + %object, %activation = acsim.dispatch @Top::@counter path "Top.counter" indices [] object 0 activation 0 + work "counter_work" xfer "counter_xfer" reset "counter_reset" validate "counter_validate" + : !acsim.object_id, !acsim.activation_id + acsim.activate %activation to %object : !acsim.activation_id to !acsim.object_id + } +} diff --git a/components/agentic-circuit/test/CodeGen/forbidden-generated-dependencies.mlir b/components/agentic-circuit/test/CodeGen/forbidden-generated-dependencies.mlir new file mode 100644 index 00000000..d9383a00 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/forbidden-generated-dependencies.mlir @@ -0,0 +1,6 @@ +// RUN: rm -rf %t.out +// RUN: %acir_cxxgen %S/driver-stages.mlir --stop-after=acsim-emit-cxx --output-root=%t.out +// RUN: %not grep -R -E "Python(\.h)?|libpython|pybind|importlib|libMLIR|mlir/|dl(open|sym)|co_await|std::function|dynamic_cast|runtime_factory|descriptor_interpreter|schema_(walker|catalog)|catalog_(walker|lookup)|topology_(mutation|builder)|plugin_(loader|registry)" %t.out +// RUN: %not grep -R -E "if.*(Counter|Scheduler|Compute|Link|Sink)" %source_root/lib/CodeGen + +// Generated C++ remains statically typed and dependency-closed. diff --git a/components/agentic-circuit/test/CodeGen/full-structured-model.mlir b/components/agentic-circuit/test/CodeGen/full-structured-model.mlir new file mode 100644 index 00000000..cc9aea2a --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/full-structured-model.mlir @@ -0,0 +1,9 @@ +// RUN: rm -rf %t.out +// RUN: %acir_cxxgen %source_root/test/ACSim/ops-valid.mlir --stop-after=link --output-root=%t.out --project-name=project --project-identity=project.example --system-name=system --system-identity=system.example --profile=fast --compiler=%cxx --standard-library=libc++ --abi-mode=default --object-format=mach-o --contract-flag=-std=c++20 --include-root=%source_root/include --include-root=%S/Inputs/full --link-input=%binary_root/lib/gfsim/libgfsim.a --link-input=%binary_root/lib/Bindings/libACIRBindings.a %llvm_linker_flags | %FileCheck %s --check-prefix=LINK +// RUN: grep -F "static_assert(gfsim::StatefulModel)" %t.out/include/generated/modules/Top_s2100000000000000.h +// RUN: grep -F "bindStatic(lanes_[0].output(), lanes_[1].input())" %t.out/src/generated/modules/Top_s2100000000000000.cpp +// RUN: grep -F "switch (static_cast(pc))" %t.out/src/generated/processes/tick_s2300000000000000.cpp +// RUN: %t.out/bin/model --build-fingerprint | %FileCheck %s --check-prefix=FINGERPRINT + +// LINK: stage=link status=passed +// FINGERPRINT: sha256: diff --git a/components/agentic-circuit/test/CodeGen/multidimensional-array.mlir b/components/agentic-circuit/test/CodeGen/multidimensional-array.mlir new file mode 100644 index 00000000..26970376 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/multidimensional-array.mlir @@ -0,0 +1,48 @@ +// RUN: rm -rf %t.out +// RUN: %acir_cxxgen %s --stop-after=link --output-root=%t.out --project-name=project --project-identity=project.example --system-name=system --system-identity=system.example --profile=fast --compiler=%cxx --standard-library=libc++ --abi-mode=default --object-format=mach-o --contract-flag=-std=c++20 --include-root=%source_root/include --include-root=%S/Inputs/extension --link-input=%binary_root/lib/gfsim/libgfsim.a --link-input=%binary_root/lib/Bindings/libACIRBindings.a %llvm_linker_flags | %FileCheck %s --check-prefix=LINK +// RUN: grep -F "for (auto &element1 : element0)" %t.out/src/generated/modules/Top_s2000000000000000.cpp +// RUN: %t.out/bin/model --build-fingerprint | %FileCheck %s --check-prefix=FINGERPRINT +// LINK: stage=link status=passed +// FINGERPRINT: sha256: + +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @array epoch "0.3" root @Top construction ["Top.counters[0][0]", "Top.counters[0][1]"] destruction ["Top.counters[0][1]", "Top.counters[0][0]"] fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { + acsim.type @counter_impl cpp "ac_test::Counter" kind "implementation" fingerprint "sha256:74eaae1048456c1f1426ae8bf65124b2db32ad411b23e26da323a709c36be6ba" + acsim.type @counter_schema cpp "ac.test.Counter" kind "schema" fingerprint "sha256:14b0d2f17152c2ad41f8cd7eb861d1069230f8e179bf0158986ba9c6d0f33cb8" + acsim.type @counter_value cpp "uint64_t" kind "value" fingerprint "sha256:0100000000000000000000000000000000000000000000000000000000000000" + acsim.type @provider cpp "ac_test" kind "provider" fingerprint "sha256:0400000000000000000000000000000000000000000000000000000000000000" + acsim.binding @counter_binding record { + activation_sources = [], availability = "available", binding = "counter_binding", + binding_schema = "acsim-binding-0.1", component_schema = @counter_schema, + component_schema_fingerprint = "sha256:14b0d2f17152c2ad41f8cd7eb861d1069230f8e179bf0158986ba9c6d0f33cb8", + construction = {arguments = [], kind = "constructor"}, contract_epoch = "0.3", + cpp = {concept = "gfsim::Component", entry_points = {pure = "", reset = "counter_reset", validate = "counter_validate", work = "counter_work", xfer = "counter_xfer"}, header = "extension_provider.h", symbol = "ac_test::Counter", target = "ac_test"}, + cpp_type = @counter_value, effect = "stateful", fingerprint = "sha256:1100000000000000000000000000000000000000000000000000000000000000", + implementation = @counter_impl, ownership = {kind = "unique", placement = "member_or_array"}, + parameters = [], ports = [], provider = @provider, + provider_implementation_fingerprint = "sha256:74eaae1048456c1f1426ae8bf65124b2db32ad411b23e26da323a709c36be6ba", + resources = [], results = [] + } + acsim.module @Top interface {ports = [], resources = [], results = []} + static [] specialization "sha256:2000000000000000000000000000000000000000000000000000000000000000" exports [] { + %counters = acsim.array @counters target @counter_binding args [] specialization "sha256:2100000000000000000000000000000000000000000000000000000000000000" shape [1, 2] + : !acsim.array<[1, 2], !acsim.owner<@counter_binding>> + acsim.return + } + %object0, %activation0 = acsim.dispatch @Top::@counters path "Top.counters[0][0]" indices [0, 0] object 0 activation 0 + work "counter_work" xfer "counter_xfer" reset "counter_reset" validate "counter_validate" + : !acsim.object_id, !acsim.activation_id + %object1, %activation1 = acsim.dispatch @Top::@counters path "Top.counters[0][1]" indices [0, 1] object 1 activation 1 + work "counter_work" xfer "counter_xfer" reset "counter_reset" validate "counter_validate" + : !acsim.object_id, !acsim.activation_id + acsim.activate %activation0 to %object0 : !acsim.activation_id to !acsim.object_id + acsim.activate %activation1 to %object1 : !acsim.activation_id to !acsim.object_id + } +} diff --git a/components/agentic-circuit/test/CodeGen/nested-modules.mlir b/components/agentic-circuit/test/CodeGen/nested-modules.mlir new file mode 100644 index 00000000..4d968ebb --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/nested-modules.mlir @@ -0,0 +1,5 @@ +// RUN: rm -rf %t.out +// RUN: %acir_cxxgen %source_root/test/ACSim/reusable-modules.mlir --stop-after=link --output-root=%t.out --project-name=project --project-identity=project.example --system-name=system --system-identity=system.example --profile=fast --compiler=%cxx --standard-library=libc++ --abi-mode=default --object-format=mach-o --contract-flag=-std=c++20 --include-root=%source_root/include --include-root=%S/Inputs/nested --link-input=%binary_root/lib/gfsim/libgfsim.a --link-input=%binary_root/lib/Bindings/libACIRBindings.a %llvm_linker_flags | %FileCheck %s +// RUN: %t.out/bin/model + +// CHECK: stage=link status=passed diff --git a/components/agentic-circuit/test/CodeGen/opcode-catalog.mlir b/components/agentic-circuit/test/CodeGen/opcode-catalog.mlir new file mode 100644 index 00000000..3e2ec27d --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/opcode-catalog.mlir @@ -0,0 +1,25 @@ +// RUN: %binary_root/bin/acir-opcode-catalog > %t +// RUN: diff %source_root/schemas/opcodes.json %t +// RUN: %FileCheck %s < %t +// RUN: %not grep -E '"operation":"ac\.(decode|dispatch|rename|retire)"' %t + +// CHECK: "contract_epoch":"0.3" +// CHECK-SAME: "operation":"ac{{\.}}barrier" +// CHECK-SAME: "operation":"ac{{\.}}broadcast" +// CHECK-SAME: "operation":"ac{{\.}}credit" +// CHECK-SAME: "operation":"ac{{\.}}dependency" +// CHECK-SAME: "operation":"ac{{\.}}expect" +// CHECK-SAME: "operation":"ac{{\.}}feedback" +// CHECK-SAME: "operation":"ac{{\.}}fork" +// CHECK-SAME: "operation":"ac{{\.}}memory{{\.}}instance" +// CHECK-SAME: "operation":"ac{{\.}}memory{{\.}}request" +// CHECK-SAME: "operation":"ac{{\.}}merge" +// CHECK-SAME: "operation":"ac{{\.}}observe" +// CHECK-SAME: "operation":"ac{{\.}}reorder" +// CHECK-SAME: "operation":"ac{{\.}}route" +// CHECK-SAME: "operation":"ac{{\.}}scope" +// CHECK-SAME: "operation":"ac{{\.}}select" +// CHECK-SAME: "operation":"ac{{\.}}sink" +// CHECK-SAME: "operation":"ac{{\.}}source" +// CHECK-SAME: "operation":"ac{{\.}}transform" +// CHECK-SAME: "schema":"agentic-circuit-opcode-catalog" diff --git a/components/agentic-circuit/test/CodeGen/popcount-verilog.mlir b/components/agentic-circuit/test/CodeGen/popcount-verilog.mlir new file mode 100644 index 00000000..bdaa1bb2 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/popcount-verilog.mlir @@ -0,0 +1,21 @@ +// RUN: %python %source_root/tools/acir-queue-veriloggen.py %s --pycgen %binary_root/bin/acir-queue-pycgen | %FileCheck %s --check-prefix=VERILOG + +module attributes {ac.contract_epoch = "0.3", ac.system = "popcount"} { + ac.type_scope @types { + ac.struct @Item fields [{name = "value", type = i8}, {name = "count", type = i4}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 2 : i64}>} + %input = ac.source depth 1 latency 1 {ac.name = "input"} : !ac.queue> + %output = ac.transform %input depths [2] latencies [1] { + ^transform(%item: !ac.var>): + %value = ac.var.get %item field "value" : !ac.var> -> !ac.var + %count = ac.var.popcount %value : !ac.var -> !ac.var + %next = ac.var.with %item, %count field "count" : !ac.var>, !ac.var -> !ac.var> + ac.transform.yield %next : !ac.var> + } {ac.output_names = ["output"]} : (!ac.queue>) -> !ac.queue> + ac.sink %output {ac.name = "sink"} : !ac.queue> +} + +// VERILOG: module popcount ( +// VERILOG: assign +// VERILOG: + +// VERILOG: module pyc_fifo #( diff --git a/components/agentic-circuit/test/CodeGen/popcount.mlir b/components/agentic-circuit/test/CodeGen/popcount.mlir new file mode 100644 index 00000000..bcaef652 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/popcount.mlir @@ -0,0 +1,21 @@ +// RUN: %binary_root/bin/acir-queue-pycgen %s | %FileCheck %s --check-prefix=PYC + +module attributes {ac.contract_epoch = "0.3", ac.system = "popcount"} { + ac.type_scope @types { + ac.struct @Item fields [{name = "value", type = i8}, {name = "count", type = i4}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 2 : i64}>} + %input = ac.source depth 1 latency 1 {ac.name = "input"} : !ac.queue> + %output = ac.transform %input depths [2] latencies [1] { + ^transform(%item: !ac.var>): + %value = ac.var.get %item field "value" : !ac.var> -> !ac.var + %count = ac.var.popcount %value : !ac.var -> !ac.var + %next = ac.var.with %item, %count field "count" : !ac.var>, !ac.var -> !ac.var> + ac.transform.yield %next : !ac.var> + } {ac.output_names = ["output"]} : (!ac.queue>) -> !ac.queue> + ac.sink %output {ac.name = "sink"} : !ac.queue> +} + +// PYC: pyc.extract {{.*}} : i8 -> i1 +// PYC: pyc.zext {{.*}} : i1 -> i4 +// PYC: pyc.alias +// PYC-SAME: primitive_id = "dataflow.popcount.v1" diff --git a/components/agentic-circuit/test/CodeGen/process-control-flow.mlir b/components/agentic-circuit/test/CodeGen/process-control-flow.mlir new file mode 100644 index 00000000..9d4447a4 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/process-control-flow.mlir @@ -0,0 +1,39 @@ +// RUN: %acir_opt %s -o /dev/null + +module attributes {ac.contract_epoch = "0.3"} { + acsim.model @soc epoch "0.3" root @Top construction ["root.workload"] destruction ["root.workload"] fingerprints {binding_lock = "sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", frozen_acir = "sha256:a2576d5d0598c9e71d4f09c4663e4d63c59950c0be81cf669b2024de9e74160e", profile = "sha256:079c9d12005aad817f722d2f0a34ccc3185b5ec0ce06ee243f945e4e1bb7b4c7", provider = "sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", schema_set = "sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", toolchain = "sha256:1db0fba21ec705a314dbbced115002a1c868a1c90beb0313edd03005070db9ac"} { + acsim.type @acir_impl_wake_condition cpp "acir::generated::impl_wake_condition" kind "implementation" fingerprint "sha256:45b3c3013f80d8a2402c692e1664935a088c00032f93510b39647faaecfe7fc5" + acsim.type @acir_impl_wake_next_delta cpp "acir::generated::impl_wake_next_delta" kind "implementation" fingerprint "sha256:28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c" + acsim.type @acir_wake_condition cpp "acir::generated::wake_condition" kind "wake" fingerprint "sha256:698e1e5b1308d66e1487b9860c75ee4596c425662a471714d9d49fad05c1d371" + acsim.type @acir_wake_next_delta cpp "acir::generated::wake_next_delta" kind "wake" fingerprint "sha256:8cf214054e3ad1f49ca7091e040092971fe7dec32ccfd59554fdef160e889c2a" + acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:6e2009a7f73501ebb11af899ad6a1f8e80e16424e8ad0ff5b6ab08c5fb19bc2a" exports [] { + acsim.process @workload captures() names [] entry @entry pcs [@entry, @resume] live [] fairness 8 specialization "sha256:c278be558d87192021d412c469fb2aa37ce1c17c083bc136163ae1dca4ab5588" { + state @entry { + %condition = arith.constant true + %seed = arith.constant 7 : i32 + cf.cond_br %condition, ^then(%seed : i32), ^otherwise(%seed : i32) + ^then(%value : i32): + %doubled = arith.addi %value, %value : i32 + %wake = acsim.invoke @acir_impl_wake_condition() : () -> !acsim.wake<@acir_wake_condition> + acsim.suspend @resume on %wake : !acsim.wake<@acir_wake_condition> + ^otherwise(%other : i32): + %other_doubled = arith.addi %other, %other : i32 + %other_wake = acsim.invoke @acir_impl_wake_next_delta() : () -> !acsim.wake<@acir_wake_next_delta> + acsim.suspend @entry on %other_wake : !acsim.wake<@acir_wake_next_delta> + } + state @resume { + %wake = acsim.invoke @acir_impl_wake_next_delta() : () -> !acsim.wake<@acir_wake_next_delta> + acsim.suspend @entry on %wake : !acsim.wake<@acir_wake_next_delta> + } + } + acsim.return + } + %object, %activation = acsim.dispatch @Top::@workload path "root.workload" indices [] object 0 activation 0 + work "acsim_generated::Top::s6e2009a7f73501ebb11af899ad6a1f8e80e16424e8ad0ff5b6ab08c5fb19bc2a::workload::pc278be558d87192021d412c469fb2aa37ce1c17c083bc136163ae1dca4ab5588::work" + xfer "acsim_generated::Top::s6e2009a7f73501ebb11af899ad6a1f8e80e16424e8ad0ff5b6ab08c5fb19bc2a::workload::pc278be558d87192021d412c469fb2aa37ce1c17c083bc136163ae1dca4ab5588::xfer" + reset "acsim_generated::Top::s6e2009a7f73501ebb11af899ad6a1f8e80e16424e8ad0ff5b6ab08c5fb19bc2a::workload::pc278be558d87192021d412c469fb2aa37ce1c17c083bc136163ae1dca4ab5588::reset" + validate "acsim_generated::Top::s6e2009a7f73501ebb11af899ad6a1f8e80e16424e8ad0ff5b6ab08c5fb19bc2a::workload::pc278be558d87192021d412c469fb2aa37ce1c17c083bc136163ae1dca4ab5588::validate" + : !acsim.object_id, !acsim.activation_id + acsim.activate %activation to %object : !acsim.activation_id to !acsim.object_id + } +} diff --git a/components/agentic-circuit/test/CodeGen/pyc-primitives-smoke.sv b/components/agentic-circuit/test/CodeGen/pyc-primitives-smoke.sv new file mode 100644 index 00000000..65450ac3 --- /dev/null +++ b/components/agentic-circuit/test/CodeGen/pyc-primitives-smoke.sv @@ -0,0 +1,111 @@ +// Small, bounded behavioral smoke test for the in-tree PYC runtime modules. +// It is intentionally separate from the C++/LLVM build so it can be run with +// one Verilator job against a generated self-contained Verilog artifact. +module pyc_primitives_smoke; + reg clk; + reg rst; + reg fifo_in_valid; + reg fifo_out_ready; + reg [7:0] fifo_in_data; + wire fifo_in_ready; + wire fifo_out_valid; + wire [7:0] fifo_out_data; + reg [1:0] req2; + reg cursor2; + wire [1:0] grant2; + reg [2:0] req3; + reg [1:0] cursor3; + wire [2:0] grant3; + reg [3:0] req4; + reg [1:0] cursor4; + wire [3:0] grant4; + reg [7:0] pop_in; + wire [3:0] pop_out; + + pyc_fifo #(.WIDTH(8), .DEPTH(2)) fifo ( + .clk(clk), .rst(rst), .in_valid(fifo_in_valid), + .in_ready(fifo_in_ready), .in_data(fifo_in_data), + .out_valid(fifo_out_valid), .out_ready(fifo_out_ready), + .out_data(fifo_out_data)); + + pyc_rr_arbiter #(.NUM_INPUTS(2), .POINTER_WIDTH(1)) arb2 ( + .req(req2), .cursor(cursor2), .grant(grant2)); + pyc_rr_arbiter #(.NUM_INPUTS(3), .POINTER_WIDTH(2)) arb3 ( + .req(req3), .cursor(cursor3), .grant(grant3)); + pyc_rr_arbiter #(.NUM_INPUTS(4), .POINTER_WIDTH(2)) arb4 ( + .req(req4), .cursor(cursor4), .grant(grant4)); + pyc_popcount #(.IN_WIDTH(8), .OUT_WIDTH(4)) pop ( + .in(pop_in), .out(pop_out)); + + task automatic expect_grant(input [3:0] expected, input [3:0] actual, + input integer case_id); + begin + if (actual !== expected) begin + $display("FAIL arbiter case=%0d expected=%b actual=%b", + case_id, expected, actual); + $finish(1); + end + end + endtask + + initial begin + clk = 1'b0; + rst = 1'b1; + fifo_in_valid = 1'b0; + fifo_out_ready = 1'b0; + fifo_in_data = 8'h00; + + req2 = 2'b11; cursor2 = 1'b0; #1; + expect_grant(4'b0001, {2'b00, grant2}, 20); + cursor2 = 1'b1; #1; + expect_grant(4'b0010, {2'b00, grant2}, 21); + + req3 = 3'b111; cursor3 = 2'd0; #1; + expect_grant(4'b0001, {1'b0, grant3}, 30); + cursor3 = 2'd1; #1; + expect_grant(4'b0010, {1'b0, grant3}, 31); + cursor3 = 2'd2; #1; + expect_grant(4'b0100, {1'b0, grant3}, 32); + // Cursor 3 is outside the legal range for a 3-way arbiter; the primitive + // still normalizes it deterministically instead of indexing out of range. + cursor3 = 2'd3; #1; + expect_grant(4'b0001, {1'b0, grant3}, 33); + + req4 = 4'b1111; cursor4 = 2'd3; #1; + expect_grant(4'b1000, grant4, 40); + req4 = 4'b0101; cursor4 = 2'd1; #1; + expect_grant(4'b0100, grant4, 41); + + pop_in = 8'b10110100; #1; + if (pop_out !== 4'd4) begin + $display("FAIL popcount expected=4 actual=%0d", pop_out); + $finish(1); + end + + // One push, observe the head, then pop it. This also checks the reset and + // ready/valid contract of the migrated cycle-level FIFO. + @(negedge clk); + rst = 1'b0; + fifo_in_data = 8'hA5; + fifo_in_valid = 1'b1; + @(negedge clk); + fifo_in_valid = 1'b0; + #1; + if (!fifo_out_valid || fifo_out_data !== 8'hA5) begin + $display("FAIL fifo head valid=%b data=%h", fifo_out_valid, fifo_out_data); + $finish(1); + end + fifo_out_ready = 1'b1; + @(negedge clk); + fifo_out_ready = 1'b0; + #1; + if (fifo_out_valid) begin + $display("FAIL fifo did not pop head"); + $finish(1); + end + $display("PYC_PRIMITIVES_SMOKE PASS"); + $finish; + end + + always #1 clk = ~clk; +endmodule diff --git a/components/agentic-circuit/test/Conversion/Inputs/bad-metadata-empty-work.json b/components/agentic-circuit/test/Conversion/Inputs/bad-metadata-empty-work.json new file mode 100644 index 00000000..7c7c11fa --- /dev/null +++ b/components/agentic-circuit/test/Conversion/Inputs/bad-metadata-empty-work.json @@ -0,0 +1,96 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "construction": { + "arguments": [ + 8 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::StatefulModel", + "entry_points": { + "pure": "", + "reset": "gfsim::leaf_reset", + "validate": "gfsim::leaf_validate", + "work": "", + "xfer": "gfsim::leaf_xfer" + }, + "header": "gfsim/leaf.hpp", + "symbol": "gfsim::Leaf", + "target": "gfsim" + }, + "cpp_type": "cpp_i32", + "effect": "stateful", + "fingerprint": "sha256:51059aad29f42c7b0c9ad0c6882985ad0d06f3adb0913ef0f5b723c0bae870b7", + "implementation": "gfsim.Leaf", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::int64_t", + "mapping": "constructor_constant", + "name": "width", + "ordinal": 0, + "value": 8 + } + ], + "ports": [], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resources": [], + "results": [ + { + "cpp_type": "cpp_i32", + "name": "result" + } + ] + }, + "target": "arm64-apple-darwin" + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> i32", + "parameters": [ + { + "acir_type": "i64", + "name": "width", + "ordinal": 0, + "value": 8 + } + ], + "ports": [], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resolution_key": "@Leaf", + "resources": [], + "results": [ + { + "acir_type": "i32", + "name": "result" + } + ] + } + ] +} diff --git a/components/agentic-circuit/test/Conversion/Inputs/bad-registry-structure.json b/components/agentic-circuit/test/Conversion/Inputs/bad-registry-structure.json new file mode 100644 index 00000000..c9a4f7d0 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/Inputs/bad-registry-structure.json @@ -0,0 +1 @@ +{"candidates": []} diff --git a/components/agentic-circuit/test/Conversion/Inputs/pure-fast.json b/components/agentic-circuit/test/Conversion/Inputs/pure-fast.json new file mode 100644 index 00000000..cd847571 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/Inputs/pure-fast.json @@ -0,0 +1,72 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "target": "arm64-apple-darwin", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "construction": {"arguments": [8], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::PureModel", + "entry_points": {"pure": "gfsim::leaf", "reset": "", "validate": "", "work": "", "xfer": ""}, + "header": "gfsim/leaf.hpp", + "symbol": "gfsim::Leaf", + "target": "gfsim" + }, + "cpp_type": "cpp_i32", + "effect": "pure", + "fingerprint": "sha256:c589295d45f11e7bd368e271ce3dd8928f3207e7ee0287012dcaa15034a313d1", + "implementation": "gfsim.Leaf", + "ownership": {"kind": "none", "placement": "inline"}, + "parameters": [{ + "acir_type": "i64", + "cpp_type": "std::int64_t", + "mapping": "constructor_constant", + "name": "width", + "ordinal": 0, + "value": 8 + }], + "ports": [{ + "accessor": "input", + "cardinality": "exclusive", + "delegation": "forbidden", + "direction": "input", + "interface": "ac.Stream", + "ownership": "borrowed", + "payload": "ac.Packet", + "protocol": "ac.ReadyValid", + "role": "consumer", + "time_domain": "ac.cycle" + }], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resources": [], + "results": [{"cpp_type": "cpp_i32", "name": "result"}] + } + } + ], + "requests": [{ + "activation_sources": [], + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "contract_epoch": "0.3", + "effect": "pure", + "function_type": "() -> i32", + "parameters": [{"acir_type": "i64", "name": "width", "ordinal": 0, "value": 8}], + "ports": [{"cardinality": "exclusive", "delegation": "forbidden", "direction": "input", "interface": "ac.Stream", "ownership": "borrowed", "payload": "ac.Packet", "protocol": "ac.ReadyValid", "role": "consumer", "time_domain": "ac.cycle"}], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resolution_key": "@Leaf", + "resources": [], + "results": [{"acir_type": "i32", "name": "result"}] + }] +} diff --git a/components/agentic-circuit/test/Conversion/Inputs/stateful-fast.json b/components/agentic-circuit/test/Conversion/Inputs/stateful-fast.json new file mode 100644 index 00000000..3c3c3180 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/Inputs/stateful-fast.json @@ -0,0 +1,96 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "construction": { + "arguments": [ + 8 + ], + "kind": "constructor" + }, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::StatefulModel", + "entry_points": { + "pure": "", + "reset": "gfsim::leaf_reset", + "validate": "gfsim::leaf_validate", + "work": "gfsim::leaf_work", + "xfer": "gfsim::leaf_xfer" + }, + "header": "gfsim/leaf.hpp", + "symbol": "gfsim::Leaf", + "target": "gfsim" + }, + "cpp_type": "cpp_i32", + "effect": "stateful", + "fingerprint": "sha256:42165ea87b2a9a578c95e6c325ee739a34d8b034f45f202187f4ed5bed9b710b", + "implementation": "gfsim.Leaf", + "ownership": { + "kind": "unique", + "placement": "member_or_array" + }, + "parameters": [ + { + "acir_type": "i64", + "cpp_type": "std::int64_t", + "mapping": "constructor_constant", + "name": "width", + "ordinal": 0, + "value": 8 + } + ], + "ports": [], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resources": [], + "results": [ + { + "cpp_type": "cpp_i32", + "name": "result" + } + ] + }, + "target": "arm64-apple-darwin" + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> i32", + "parameters": [ + { + "acir_type": "i64", + "name": "width", + "ordinal": 0, + "value": 8 + } + ], + "ports": [], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resolution_key": "@Leaf", + "resources": [], + "results": [ + { + "acir_type": "i32", + "name": "result" + } + ] + } + ] +} diff --git a/components/agentic-circuit/test/Conversion/Inputs/stateful-graph.json b/components/agentic-circuit/test/Conversion/Inputs/stateful-graph.json new file mode 100644 index 00000000..fc71c372 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/Inputs/stateful-graph.json @@ -0,0 +1,168 @@ +{ + "candidates": [ + { + "available": true, + "profile": "fast", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "Consumer", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Consumer", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "construction": {"arguments": [], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::StatefulModel", + "entry_points": { + "pure": "", + "reset": "gfsim::consumer_reset", + "validate": "gfsim::consumer_validate", + "work": "gfsim::consumer_work", + "xfer": "gfsim::consumer_xfer" + }, + "header": "gfsim/consumer.hpp", + "symbol": "gfsim::Consumer", + "target": "gfsim" + }, + "cpp_type": "cpp_endpoint", + "effect": "stateful", + "fingerprint": "sha256:596eae9fb8166027ec0f237c0e57bdd750700fcdd1075dbb15b0ef8c44c65546", + "implementation": "gfsim.Consumer", + "ownership": {"kind": "unique", "placement": "member_or_array"}, + "parameters": [], + "ports": [ + { + "accessor": "input", + "cardinality": "exclusive", + "delegation": "forbidden", + "direction": "input", + "interface": "Wire", + "ownership": "borrowed", + "payload": "cpp_i8", + "protocol": "wire", + "role": "sink", + "time_domain": "cycle" + } + ], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resources": [], + "results": [] + }, + "target": "arm64-apple-darwin" + }, + { + "available": true, + "profile": "fast", + "record": { + "activation_sources": [], + "availability": "available", + "binding": "Producer", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Producer", + "component_schema_fingerprint": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "construction": {"arguments": [], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::StatefulModel", + "entry_points": { + "pure": "", + "reset": "gfsim::producer_reset", + "validate": "gfsim::producer_validate", + "work": "gfsim::producer_work", + "xfer": "gfsim::producer_xfer" + }, + "header": "gfsim/producer.hpp", + "symbol": "gfsim::Producer", + "target": "gfsim" + }, + "cpp_type": "cpp_endpoint", + "effect": "stateful", + "fingerprint": "sha256:78db155890336c5ebb10b7797e886480eba8f750a653d29ed6d8160d0589385a", + "implementation": "gfsim.Producer", + "ownership": {"kind": "unique", "placement": "member_or_array"}, + "parameters": [], + "ports": [ + { + "accessor": "output", + "cardinality": "exclusive", + "delegation": "forbidden", + "direction": "output", + "interface": "Wire", + "ownership": "borrowed", + "payload": "cpp_i8", + "protocol": "wire", + "role": "source", + "time_domain": "cycle" + } + ], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "resources": [], + "results": [{"cpp_type": "cpp_endpoint", "name": "out"}] + }, + "target": "arm64-apple-darwin" + } + ], + "requests": [ + { + "activation_sources": [], + "binding": "Consumer", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Consumer", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "(!ac.endpoint<@Wire, @source>) -> ()", + "parameters": [], + "ports": [ + { + "cardinality": "exclusive", + "delegation": "forbidden", + "direction": "input", + "interface": "Wire", + "ownership": "borrowed", + "payload": "cpp_i8", + "protocol": "wire", + "role": "sink", + "time_domain": "cycle" + } + ], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resolution_key": "@Consumer", + "resources": [], + "results": [] + }, + { + "activation_sources": [], + "binding": "Producer", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Producer", + "component_schema_fingerprint": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "contract_epoch": "0.3", + "effect": "stateful", + "function_type": "() -> !ac.endpoint<@Wire, @source>", + "parameters": [], + "ports": [ + { + "cardinality": "exclusive", + "delegation": "forbidden", + "direction": "output", + "interface": "Wire", + "ownership": "borrowed", + "payload": "cpp_i8", + "protocol": "wire", + "role": "source", + "time_domain": "cycle" + } + ], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "resolution_key": "@Producer", + "resources": [], + "results": [{"acir_type": "!ac.endpoint<@Wire, @source>", "name": "out"}] + } + ] +} diff --git a/components/agentic-circuit/test/Conversion/atomic-failure.mlir b/components/agentic-circuit/test/Conversion/atomic-failure.mlir new file mode 100644 index 00000000..991a026a --- /dev/null +++ b/components/agentic-circuit/test/Conversion/atomic-failure.mlir @@ -0,0 +1,44 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/unfrozen.mlir -o %t/unfrozen.out 2>&1 | %FileCheck %s --check-prefix=UNFROZEN +// RUN: test ! -s %t/unfrozen.out +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/results.mlir -o %t/results.frozen +// RUN: %not %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/results.frozen -o %t/results.out 2>&1 | %FileCheck %s --check-prefix=RESULTS +// RUN: test ! -s %t/results.out +// RUN: %not %acir_opt_public --ac-lower-to-acsim %t/unfrozen.mlir 2>&1 | %FileCheck %s --check-prefix=OPTIONS +// RUN: %not %acir_opt_public --ac-binding-profile=fast %t/unfrozen.mlir 2>&1 | %FileCheck %s --check-prefix=ORPHAN + +// The lowering is atomic: every rejected input fails with an ACLOWER-* +// diagnostic, a non-zero exit status, and no emitted output. The driver also +// rejects orphaned or incomplete binding option sets before any pass runs. + +//--- unfrozen.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} + +//--- results.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() -> i32 parameters {} graph { + ac.process @workload kind "workload" { + ac.yield_sim + } + %zero = arith.constant 0 : i32 + ac.return %zero : i32 + } +} + +// UNFROZEN: error: ACLOWER-EPOCH-MISMATCH: ac-lower-to-acsim requires a topology-frozen model; run ac-freeze-topology first +// RESULTS: error: ACLOWER-UNSUPPORTED-CONSTRUCT: operation 'arith.constant' has no ACSim realization +// OPTIONS: error: ACLOWER-BINDING-OPTIONS: --ac-lower-to-acsim requires --ac-binding-profile +// ORPHAN: error: ACLOWER-BINDING-OPTIONS: binding options require --ac-resolve-gfsim-bindings or --ac-lower-to-acsim diff --git a/components/agentic-circuit/test/Conversion/bindings-invalid.mlir b/components/agentic-circuit/test/Conversion/bindings-invalid.mlir new file mode 100644 index 00000000..290f3cff --- /dev/null +++ b/components/agentic-circuit/test/Conversion/bindings-invalid.mlir @@ -0,0 +1,83 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/extern.mlir -o %t/extern.frozen +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/pure-fast.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/extern.frozen | %FileCheck %s --check-prefix=PURE +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/sort-order.mlir -o %t/sort-order.frozen +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/sort-order.frozen | %FileCheck %s --check-prefix=SORT +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/heterogeneous-array.mlir -o %t/heterogeneous-array.frozen +// RUN: %not %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/stateful-fast.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/heterogeneous-array.frozen -o %t/array.out 2>&1 | %FileCheck %s --check-prefix=ARRAY +// RUN: test ! -s %t/array.out +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/time-domain.mlir -o %t/time-domain.frozen +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/time-domain.frozen | %FileCheck %s --check-prefix=TD +// RUN: %not %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/bad-registry-structure.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/extern.frozen 2>&1 | %FileCheck %s --check-prefix=REGISTRY +// RUN: %not %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/bad-metadata-empty-work.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/extern.frozen 2>&1 | %FileCheck %s --check-prefix=METADATA + +// Pure external calls lower without ownership. Array-specialization, +// stage-boundary, and registry contract rejections fail atomically with their +// exact ACLOWER-* codes. + +//--- extern.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module.extern @Leaf : () -> i32 parameters {width = 8 : i64} + implementation {registry = "cpp", name = "Leaf"} + ac.module @Top() parameters {} graph { + %leaf = ac.instance @leaf of @Leaf() static {width = 8 : i64} + id "leaf" path "leaf" : () -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +//--- sort-order.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.instance @zed of @Zebra() static {} id "zed" path "zed" : () -> () + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + ac.module @Zebra() parameters {} graph { + ac.return + } +} + +//--- heterogeneous-array.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module.extern @Leaf : () -> i32 parameters {width = 8 : i64} + implementation {registry = "cpp", name = "Leaf"} + ac.module @Top() parameters {} graph { + %cells:2 = ac.array @cells of @Leaf shape [2]() + static [{width = 8 : i64}, {width = 16 : i64}] + id "cells" path "cells" : () -> (i32, i32) + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +//--- time-domain.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.time_domain @global period 1 phase 0 scale 1 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +// PURE: acsim.inline @Leaf() : () -> !acsim.expr<@cpp_i32> +// PURE-NOT: acsim.dispatch @Top::@leaf +// SORT: acsim.module @Zebra +// SORT: acsim.module @Top +// ARRAY: error: ACLOWER-ARRAY: differently specialized array elements are outside the lowering stage; lower them as ordered named members instead +// TD: acsim.type @global cpp "gfsim::TimeDomainRuntime" kind "time_domain" fingerprint "sha256:{{[0-9a-f]+}}" {period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64} +// REGISTRY: error: ACLOWER-BINDING-REGISTRY: registry must contain exactly candidates and requests arrays +// METADATA: error: ACLOWER-BINDING-METADATA: binding effect requires exact executable entry points diff --git a/components/agentic-circuit/test/Conversion/bindings.mlir b/components/agentic-circuit/test/Conversion/bindings.mlir new file mode 100644 index 00000000..edba347f --- /dev/null +++ b/components/agentic-circuit/test/Conversion/bindings.mlir @@ -0,0 +1,60 @@ +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/stateful-fast.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/stateful-fast.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen -o %t.out +// RUN: %acir_opt %t.out -o %t.roundtrip +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/stateful-fast.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen -o %t.again +// RUN: diff %t.out %t.again + +// Binding-backed lowering: the extern module @Leaf resolves against the +// closed registry to a stateful binding. The lowering publishes the exact +// acsim.binding lock record (sorted with the generated wake types), replaces +// the extern declaration, lowers the instance against the binding target +// with the ordered constructor arguments, and emits a dispatch row whose +// thunks are the locked entry points. The internal driver supplies the test +// structural provider for @Leaf; the public driver correctly rejects the +// unknown extern. + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module.extern @Leaf : () -> i32 parameters {width = 8 : i64} + implementation {registry = "cpp", name = "Leaf"} + ac.module @Top() parameters {} graph { + %leaf = ac.instance @leaf of @Leaf() static {width = 8 : i64} + id "leaf" path "leaf" : () -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +// CHECK: acsim.model @soc epoch "0.3" root @Top +// CHECK-SAME: construction ["root.leaf", "root.workload"] +// CHECK-SAME: destruction ["root.workload", "root.leaf"] +// CHECK: acsim.type @ac_Leaf cpp "ac.Leaf" kind "schema" fingerprint "sha256:1111111111111111111111111111111111111111111111111111111111111111" +// CHECK: acsim.type @cpp_i32 cpp "cpp_i32" kind "value" fingerprint "sha256:{{[0-9a-f]+}}" +// CHECK: acsim.type @gfsim cpp "gfsim" kind "provider" fingerprint "sha256:{{[0-9a-f]+}}" +// CHECK: acsim.type @gfsim_Leaf cpp "gfsim.Leaf" kind "implementation" fingerprint "sha256:2222222222222222222222222222222222222222222222222222222222222222" +// CHECK-NEXT: acsim.binding @Leaf record { +// CHECK-SAME: binding = "Leaf" +// CHECK-SAME: binding_schema = "acsim-binding-0.1" +// CHECK-SAME: component_schema = @ac_Leaf +// CHECK-SAME: contract_epoch = "0.3" +// CHECK-SAME: effect = "stateful" +// CHECK-SAME: fingerprint = "sha256:42165ea87b2a9a578c95e6c325ee739a34d8b034f45f202187f4ed5bed9b710b" +// CHECK-SAME: implementation = @gfsim_Leaf +// CHECK-SAME: ownership = {kind = "unique", placement = "member_or_array"} +// CHECK-SAME: parameters = [{acir_type = "i64", cpp_type = "std::int64_t", mapping = "constructor_constant", name = "width", ordinal = 0 : i64, value = 8 : i64}] +// CHECK-SAME: provider = @gfsim +// CHECK: acsim.module @Top +// CHECK-NEXT: %{{.+}} = acsim.instance @leaf target @Leaf args [8] specialization "sha256:{{[0-9a-f]+}}" : !acsim.owner<@Leaf> +// CHECK: acsim.process @workload +// CHECK: acsim.return +// CHECK-NEXT: } +// CHECK-NEXT: %{{.+}}, %{{.+}} = acsim.dispatch @Top::@leaf path "root.leaf" indices [] object 0 activation 0 +// CHECK-SAME: work "gfsim::leaf_work" xfer "gfsim::leaf_xfer" reset "gfsim::leaf_reset" validate "gfsim::leaf_validate" +// CHECK: %{{.+}}, %{{.+}} = acsim.dispatch @Top::@workload path "root.workload" indices [] object 1 activation 1 +// CHECK: acsim.activate +// CHECK-NEXT: acsim.activate +// CHECK-NEXT: } +// CHECK-NOT: ac.module.extern diff --git a/components/agentic-circuit/test/Conversion/collections.mlir b/components/agentic-circuit/test/Conversion/collections.mlir new file mode 100644 index 00000000..cbdbb2eb --- /dev/null +++ b/components/agentic-circuit/test/Conversion/collections.mlir @@ -0,0 +1,44 @@ +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen -o %t.out +// RUN: %acir_opt_public %t.out -o /dev/null + +// Placement collections: a one-dimensional and a two-dimensional array of a +// structural module. Arrays expand lexicographic row-major in the +// construction order and lower to acsim.array with the exact dense shape. +// Array elements of structural modules are ownership-only, so the workload +// process remains the sole dispatch row. + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Cell() parameters {} graph { + ac.return + } + ac.module @Top() parameters {} graph { + ac.array @cells of @Cell shape [2]() static [{}, {}] + id "cells" path "cells" : () -> () + ac.array @grid of @Cell shape [2, 2]() static [{}, {}, {}, {}] + id "grid" path "grid" : () -> () + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} + +// CHECK: acsim.model @soc epoch "0.3" root @Top +// CHECK-SAME: construction ["root.cells[0]", "root.cells[1]", "root.grid[0][0]", "root.grid[0][1]", "root.grid[1][0]", "root.grid[1][1]", "root.workload"] +// CHECK-SAME: destruction ["root.workload", "root.grid[1][1]", "root.grid[1][0]", "root.grid[0][1]", "root.grid[0][0]", "root.cells[1]", "root.cells[0]"] +// CHECK: acsim.module @Cell interface {ports = [], resources = [], results = []} static [] specialization "[[CELL_FP:sha256:[0-9a-f]+]]" exports [] { +// CHECK: acsim.module @Top +// CHECK-NEXT: %{{.+}} = acsim.array @cells target @Cell args [] specialization "[[CELL_FP]]" shape [2] : !acsim.array<[2], !acsim.owner<@Cell>> +// CHECK-NEXT: %{{.+}} = acsim.array @grid target @Cell args [] specialization "[[CELL_FP]]" shape [2, 2] : !acsim.array<[2, 2], !acsim.owner<@Cell>> +// CHECK: acsim.process @workload +// CHECK: acsim.return +// CHECK-NEXT: } +// CHECK-NEXT: %{{.+}}, %{{.+}} = acsim.dispatch @Top::@workload path "root.workload" indices [] object 0 activation 0 +// CHECK-NOT: acsim.dispatch +// CHECK: acsim.activate +// CHECK-NOT: acsim.activate diff --git a/components/agentic-circuit/test/Conversion/hierarchy.mlir b/components/agentic-circuit/test/Conversion/hierarchy.mlir new file mode 100644 index 00000000..992b2d95 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/hierarchy.mlir @@ -0,0 +1,58 @@ +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen -o %t.out +// RUN: %acir_opt_public %t.out -o %t.roundtrip +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen -o %t.again +// RUN: diff %t.out %t.again + +// A three-level hierarchy: Top instantiates Mid and Child, Mid instantiates +// Child twice. ACSim modules are strictly symbol-sorted (Child < Mid < Top), +// construction is DFS preorder from the root, destruction is its exact +// reverse, and every instance records the target module's exact static +// arguments and specialization fingerprint. Instances of structural modules +// are ownership-only: only the workload process produces a dispatch row. + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Child() parameters {} graph { + ac.return + } + ac.module @Mid() parameters {} graph { + ac.instance @left of @Child() static {} id "left" path "left" : () -> () + ac.instance @right of @Child() static {} id "right" path "right" : () -> () + ac.return + } + ac.module @Top() parameters {} graph { + ac.instance @mid of @Mid() static {} id "mid" path "mid" : () -> () + ac.instance @solo of @Child() static {} id "solo" path "solo" : () -> () + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} + +// CHECK: acsim.model @soc epoch "0.3" root @Top +// CHECK-SAME: construction ["root.mid", "root.mid.left", "root.mid.right", "root.solo", "root.workload"] +// CHECK-SAME: destruction ["root.workload", "root.solo", "root.mid.right", "root.mid.left", "root.mid"] +// CHECK: acsim.module @Child interface {ports = [], resources = [], results = []} static [] specialization "[[CHILD_FP:sha256:[0-9a-f]+]]" exports [] { +// CHECK-NEXT: acsim.return +// CHECK-NEXT: } +// CHECK-NEXT: acsim.module @Mid interface {ports = [], resources = [], results = []} static [] specialization "[[MID_FP:sha256:[0-9a-f]+]]" exports [] { +// CHECK-NEXT: %{{.+}} = acsim.instance @left target @Child args [] specialization "[[CHILD_FP]]" : !acsim.owner<@Child> +// CHECK-NEXT: %{{.+}} = acsim.instance @right target @Child args [] specialization "[[CHILD_FP]]" : !acsim.owner<@Child> +// CHECK-NEXT: acsim.return +// CHECK-NEXT: } +// CHECK-NEXT: acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "[[TOP_FP:sha256:[0-9a-f]+]]" exports [] { +// CHECK-NEXT: %{{.+}} = acsim.instance @mid target @Mid args [] specialization "[[MID_FP]]" : !acsim.owner<@Mid> +// CHECK-NEXT: %{{.+}} = acsim.instance @solo target @Child args [] specialization "[[CHILD_FP]]" : !acsim.owner<@Child> +// CHECK: acsim.process @workload +// CHECK: acsim.return +// CHECK-NEXT: } +// CHECK-NEXT: %{{.+}}, %{{.+}} = acsim.dispatch @Top::@workload path "root.workload" indices [] object 0 activation 0 +// CHECK-NOT: acsim.dispatch +// CHECK: acsim.activate +// CHECK-NOT: acsim.activate +// CHECK-NEXT: } diff --git a/components/agentic-circuit/test/Conversion/process-basic.mlir b/components/agentic-circuit/test/Conversion/process-basic.mlir new file mode 100644 index 00000000..2991459e --- /dev/null +++ b/components/agentic-circuit/test/Conversion/process-basic.mlir @@ -0,0 +1,31 @@ +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s + +// Process lowering at the v0.1 stage boundary: a yield-only process body +// lowers to a single-state acsim.process whose entry state suspends on the +// generated next-delta wake. The generated implementation symbol and wake +// type carry the exact plan fingerprints, and the dispatch thunks point at +// the deterministic acsim_generated namespace. + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} + +// CHECK: acsim.type @acir_impl_wake_next_delta_[[IMPL_FP:[0-9a-f]+]] cpp "acir::generated::impl_wake_next_delta_[[IMPL_FP]]" kind "implementation" fingerprint "sha256:[[IMPL_FP]]" +// CHECK-NEXT: acsim.type @acir_wake_next_delta cpp "acir::generated::wake_next_delta" kind "wake" fingerprint "sha256:{{[0-9a-f]+}}" +// CHECK: acsim.process @workload captures() names [] entry @entry pcs [@entry] live [] fairness 2 specialization "sha256:[[PROC_FP:[0-9a-f]+]]" { +// CHECK: %[[WAKE:.+]] = acsim.invoke @acir_impl_wake_next_delta_[[IMPL_FP]]() : () -> !acsim.wake<@acir_wake_next_delta> +// CHECK-NEXT: acsim.suspend @entry on %[[WAKE]] : !acsim.wake<@acir_wake_next_delta> +// CHECK: acsim.dispatch @Top::@workload path "root.workload" indices [] object 0 activation 0 +// CHECK-SAME: work "acsim_generated::Top::s{{[0-9a-f]+}}::workload::p[[PROC_FP]]::work" +// CHECK-SAME: xfer "acsim_generated::Top::s{{[0-9a-f]+}}::workload::p[[PROC_FP]]::xfer" +// CHECK-SAME: reset "acsim_generated::Top::s{{[0-9a-f]+}}::workload::p[[PROC_FP]]::reset" +// CHECK-SAME: validate "acsim_generated::Top::s{{[0-9a-f]+}}::workload::p[[PROC_FP]]::validate" diff --git a/components/agentic-circuit/test/Conversion/process-control-flow.mlir b/components/agentic-circuit/test/Conversion/process-control-flow.mlir new file mode 100644 index 00000000..dc012cbc --- /dev/null +++ b/components/agentic-circuit/test/Conversion/process-control-flow.mlir @@ -0,0 +1,25 @@ +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %ready = arith.constant true + scf.if %ready { + ac.wait_until %ready + } + ac.yield_sim + } + ac.return + } +} + +// CHECK: acsim.process @workload +// CHECK-SAME: entry @entry +// CHECK-SAME: pcs [@entry, @pc00000001] +// CHECK: cf.cond_br +// CHECK: acsim.suspend @pc00000001 +// CHECK: acsim.suspend @entry diff --git a/components/agentic-circuit/test/Conversion/process-invalid.mlir b/components/agentic-circuit/test/Conversion/process-invalid.mlir new file mode 100644 index 00000000..eb447db7 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/process-invalid.mlir @@ -0,0 +1,22 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/compute-body.mlir -o %t/compute-body.frozen +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t/compute-body.frozen | %FileCheck %s --check-prefix=COMPUTE + +// A process containing ordinary scalar work is planned through the public +// ProcessStatePlan API and lowered without a yield-only stage restriction. + +//--- compute-body.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %zero = arith.constant 0 : i32 + ac.yield_sim + } + ac.return + } +} + +// COMPUTE: acsim.process @workload diff --git a/components/agentic-circuit/test/Conversion/process-live-values.mlir b/components/agentic-circuit/test/Conversion/process-live-values.mlir new file mode 100644 index 00000000..43bef0a5 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/process-live-values.mlir @@ -0,0 +1,28 @@ +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %ready = arith.constant true + %value = arith.constant 7 : i32 + ac.wait_until %ready + %used = arith.addi %value, %value : i32 + ac.yield_sim + } + ac.return + } +} + +// CHECK: acsim.process @workload +// CHECK-SAME: pcs [@entry, @pc00000001] +// CHECK-SAME: live [{{.*}}name = "live00000000"{{.*}}] +// CHECK: acsim.inline @acir_impl_scalar_wrap_ +// CHECK: acsim.live.store +// CHECK: acsim.suspend @pc00000001 +// CHECK: acsim.live.load +// CHECK: acsim.inline @acir_impl_scalar_unwrap_ +// CHECK: acsim.suspend @entry diff --git a/components/agentic-circuit/test/Conversion/pure-results.mlir b/components/agentic-circuit/test/Conversion/pure-results.mlir new file mode 100644 index 00000000..84931863 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/pure-results.mlir @@ -0,0 +1,24 @@ +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/pure-fast.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module.extern @Leaf : () -> i32 parameters {width = 8 : i64} + implementation {registry = "cpp", name = "Leaf"} + ac.module @Top() -> i32 parameters {} graph { + %leaf = ac.instance @leaf of @Leaf() static {width = 8 : i64} + id "leaf" path "leaf" : () -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return %leaf : i32 + } +} + +// CHECK: acsim.module @Top interface { +// CHECK-SAME: results = [{cpp_type = @cpp_i32, name = "result_00000000"}] +// CHECK-SAME: exports [@result_00000000] +// CHECK: %[[EXPR:.+]] = acsim.inline @Leaf() : () -> !acsim.expr<@cpp_i32> +// CHECK: %[[EXPORT:.+]] = acsim.export @result_00000000 %[[EXPR]] role @acsim_result_role +// CHECK: acsim.return %[[EXPORT]] : !acsim.expr<@cpp_i32> +// CHECK-NOT: acsim.dispatch @Top::@leaf diff --git a/components/agentic-circuit/test/Conversion/structure.mlir b/components/agentic-circuit/test/Conversion/structure.mlir new file mode 100644 index 00000000..73d04f9b --- /dev/null +++ b/components/agentic-circuit/test/Conversion/structure.mlir @@ -0,0 +1,49 @@ +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen -o %t.out +// RUN: %acir_opt_public %t.out | %FileCheck %s +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen > %t.canonical +// RUN: %acir_opt_public %t.canonical > %t.roundtrip +// RUN: diff %t.canonical %t.roundtrip + +// The smallest lowerable model: one root module with a single yield-only +// workload process. The atomic lowering publishes exactly one acsim.model +// with the wake type pair, one module, one dispatch row, and one +// self-activation edge. + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.time_domain @core period 2 phase 1 scale 2 + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} + +// CHECK: module attributes {ac.contract_epoch = "0.3"} { +// CHECK-NEXT: acsim.model @soc epoch "0.3" root @Top +// CHECK-SAME: construction ["root.workload"] +// CHECK-SAME: destruction ["root.workload"] +// CHECK-SAME: fingerprints {binding_lock = "sha256:{{[0-9a-f]+}}", frozen_acir = "sha256:{{[0-9a-f]+}}", profile = "sha256:{{[0-9a-f]+}}", provider = "sha256:{{[0-9a-f]+}}", schema_set = "sha256:{{[0-9a-f]+}}", toolchain = "sha256:{{[0-9a-f]+}}"} { +// CHECK-NEXT: acsim.type @acir_impl_wake_next_delta_28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c cpp "acir::generated::impl_wake_next_delta_28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c" kind "implementation" fingerprint "sha256:28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c" +// CHECK-NEXT: acsim.type @acir_wake_next_delta cpp "acir::generated::wake_next_delta" kind "wake" fingerprint "sha256:8cf214054e3ad1f49ca7091e040092971fe7dec32ccfd59554fdef160e889c2a" +// CHECK-NEXT: acsim.type @core cpp "gfsim::TimeDomainRuntime" kind "time_domain" fingerprint "sha256:{{[0-9a-f]+}}" {period = 2 : i64, phase = 1 : i64, tick_scale = 2 : i64} +// CHECK-NEXT: acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:{{[0-9a-f]+}}" exports [] { +// CHECK-NEXT: acsim.process @workload captures() names [] entry @entry pcs [@entry] live [] fairness 2 specialization "sha256:{{[0-9a-f]+}}" { +// CHECK: %[[WAKE:.+]] = acsim.invoke @acir_impl_wake_next_delta_28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c() : () -> !acsim.wake<@acir_wake_next_delta> +// CHECK-NEXT: acsim.suspend @entry on %[[WAKE]] : !acsim.wake<@acir_wake_next_delta> +// CHECK: acsim.return +// CHECK-NEXT: } +// CHECK-NEXT: %[[OBJ:.+]], %[[ACT:.+]] = acsim.dispatch @Top::@workload path "root.workload" indices [] object 0 activation 0 +// CHECK-SAME: work "acsim_generated::Top::s{{[0-9a-f]+}}::workload::p{{[0-9a-f]+}}::work" +// CHECK-SAME: xfer "acsim_generated::Top::s{{[0-9a-f]+}}::workload::p{{[0-9a-f]+}}::xfer" +// CHECK-SAME: reset "acsim_generated::Top::s{{[0-9a-f]+}}::workload::p{{[0-9a-f]+}}::reset" +// CHECK-SAME: validate "acsim_generated::Top::s{{[0-9a-f]+}}::workload::p{{[0-9a-f]+}}::validate" +// CHECK-SAME: : !acsim.object_id, !acsim.activation_id +// CHECK-NEXT: acsim.activate %[[ACT]] to %[[OBJ]] : !acsim.activation_id to !acsim.object_id +// CHECK-NEXT: } +// CHECK-NEXT: } diff --git a/components/agentic-circuit/test/Conversion/time-domains.mlir b/components/agentic-circuit/test/Conversion/time-domains.mlir new file mode 100644 index 00000000..698f8e5c --- /dev/null +++ b/components/agentic-circuit/test/Conversion/time-domains.mlir @@ -0,0 +1,24 @@ +// RUN: %acir_opt_public --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt_public --ac-lower-to-acsim --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Bridge() parameters {} graph { + ac.return + } + ac.module @Top() parameters {} graph { + ac.instance @cdc of @Bridge() static {} id "cdc" path "cdc" : () -> () + ac.time_domain @global period 1 phase 0 scale 1 + ac.time_domain @core period 2 phase 1 scale 2 parent @global + bridge {kind = "explicit", owner = @cdc} + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} + +// CHECK: acsim.type @core cpp "gfsim::TimeDomainRuntime" kind "time_domain" fingerprint "sha256:{{[0-9a-f]+}}" {bridge = {kind = "explicit", owner = @cdc}, parent = @global, period = 2 : i64, phase = 1 : i64, tick_scale = 2 : i64} +// CHECK-NEXT: acsim.type @global cpp "gfsim::TimeDomainRuntime" kind "time_domain" fingerprint "sha256:{{[0-9a-f]+}}" {period = 1 : i64, phase = 0 : i64, tick_scale = 1 : i64} diff --git a/components/agentic-circuit/test/Conversion/typed-graph.mlir b/components/agentic-circuit/test/Conversion/typed-graph.mlir new file mode 100644 index 00000000..641376d3 --- /dev/null +++ b/components/agentic-circuit/test/Conversion/typed-graph.mlir @@ -0,0 +1,42 @@ +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/stateful-graph.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen | %FileCheck %s + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @wire { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.event @send from @sender to @receiver payload i8 action "offer" + ac.transition from @idle to @idle on @send transfer true retain false guard {} + } + ac.interface @Wire { + ac.role @source dual @sink cardinality "exclusive" + ac.role @sink dual @source cardinality "exclusive" + ac.port @data : !ac.channel from @source to @sink + protocol_roles @sender to @receiver + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module.extern @Consumer : (!ac.endpoint<@Wire, @source>) -> () parameters {} + implementation {registry = "cpp", name = "Consumer"} + ac.module.extern @Producer : () -> !ac.endpoint<@Wire, @source> parameters {} + implementation {registry = "cpp", name = "Producer"} + ac.module @Top() parameters {} graph { + %out = ac.instance @producer of @Producer() static {} + id "producer" path "producer" : () -> !ac.endpoint<@Wire, @source> + ac.instance @consumer of @Consumer(%out) static {} + id "consumer" path "consumer" : (!ac.endpoint<@Wire, @source>) -> () + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +// CHECK: %[[CONSUMER:.+]] = acsim.instance @consumer target @Consumer +// CHECK: %[[PRODUCER:.+]] = acsim.instance @producer target @Producer +// CHECK: %[[CONSUMER_PORT:.+]] = acsim.port %[[CONSUMER]] accessor @input +// CHECK: %[[PRODUCER_PORT:.+]] = acsim.port %[[PRODUCER]] accessor @output +// CHECK: acsim.bind %[[PRODUCER_PORT]] to %[[CONSUMER_PORT]] kind "port" +// CHECK: acsim.dispatch @Top::@consumer path "root.consumer" +// CHECK: acsim.dispatch @Top::@producer path "root.producer" +// CHECK-COUNT-4: acsim.activate diff --git a/components/agentic-circuit/test/Conversion/whole-model.mlir b/components/agentic-circuit/test/Conversion/whole-model.mlir new file mode 100644 index 00000000..bca610ee --- /dev/null +++ b/components/agentic-circuit/test/Conversion/whole-model.mlir @@ -0,0 +1,76 @@ +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %s -o %t.frozen +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/stateful-graph.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen -o %t.first +// RUN: %acir_opt --ac-lower-to-acsim --ac-binding-registry=%S/Inputs/stateful-graph.json --ac-binding-profile=fast --ac-binding-target=arm64-apple-darwin %t.frozen -o %t.second +// RUN: diff %t.first %t.second +// RUN: %FileCheck %s < %t.first + +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @wire { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.event @send from @sender to @receiver payload i8 action "offer" + ac.transition from @idle to @idle on @send transfer true retain false guard {} + } + ac.interface @Wire { + ac.role @source dual @sink cardinality "exclusive" + ac.role @sink dual @source cardinality "exclusive" + ac.port @data : !ac.channel from @source to @sink + protocol_roles @sender to @receiver + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module.extern @Consumer : (!ac.endpoint<@Wire, @source>) -> () parameters {} + implementation {registry = "cpp", name = "Consumer"} + ac.module.extern @Producer : () -> !ac.endpoint<@Wire, @source> parameters {} + implementation {registry = "cpp", name = "Producer"} + ac.module @Cell() parameters {} graph { + ac.return + } + ac.module @Top() -> !ac.endpoint<@Wire, @source> parameters {} graph { + ac.array @cells of @Cell shape [2]() static [{}, {}] + id "cells" path "cells" : () -> () + %captured = ac.instance @captured of @Producer() static {} + id "captured" path "captured" : () -> !ac.endpoint<@Wire, @source> + %connected = ac.instance @producer of @Producer() static {} + id "producer" path "producer" : () -> !ac.endpoint<@Wire, @source> + ac.instance @consumer of @Consumer(%connected) static {} + id "consumer" path "consumer" : (!ac.endpoint<@Wire, @source>) -> () + %exported = ac.instance @exporter of @Producer() static {} + id "exporter" path "exporter" : () -> !ac.endpoint<@Wire, @source> + ac.process @workload kind "workload" + captures(%captured : !ac.endpoint<@Wire, @source>) { + ^bb0(%capture : !ac.endpoint<@Wire, @source>): + %ready = arith.constant true + %value = arith.constant 7 : i32 + ac.wait_until %ready + %used = arith.addi %value, %value : i32 + ac.yield_sim + } + ac.return %exported : !ac.endpoint<@Wire, @source> + } +} + +// CHECK: acsim.model @soc epoch "0.3" root @Top +// CHECK: acsim.binding @Consumer +// CHECK: acsim.binding @Producer +// CHECK: acsim.module @Cell interface {ports = [], resources = [], results = []} +// CHECK: acsim.module @Top interface { +// CHECK-SAME: ports = [{accessor = @output +// CHECK-SAME: name = "port_00000000" +// CHECK-SAME: role = @source +// CHECK-SAME: results = []} +// CHECK-SAME: exports [@port_00000000] +// CHECK: acsim.array @cells target @Cell +// CHECK: acsim.bind {{.*}} kind "port" +// CHECK: acsim.export @port_00000000 {{.*}} role @source +// CHECK: acsim.process @workload +// CHECK-SAME: captures({{.*}}!acsim.port<@Wire, @source, @cpp_i8, @wire>) +// CHECK-SAME: pcs [@entry, @pc00000001] +// CHECK-SAME: live [{{.*}}name = "live00000000"{{.*}}] +// CHECK: acsim.live.store +// CHECK: acsim.live.load +// CHECK: acsim.return {{.*}} : !acsim.port<@Wire, @source, @cpp_i8, @wire> +// CHECK-COUNT-5: acsim.dispatch +// CHECK-COUNT-7: acsim.activate diff --git a/components/agentic-circuit/test/PYC/minimal-queue.pyc b/components/agentic-circuit/test/PYC/minimal-queue.pyc new file mode 100644 index 00000000..b3f3562c --- /dev/null +++ b/components/agentic-circuit/test/PYC/minimal-queue.pyc @@ -0,0 +1,6 @@ +module attributes {pyc.top = @queue_passthrough, pyc.frontend.contract = "pycircuit"} { + func.func @queue_passthrough(%clk: !pyc.clock, %rst: !pyc.reset, %in_valid: i1, %in_data: i64, %out_ready: i1) -> (i1, i64, i1) attributes {arg_names = ["clk", "rst", "in_valid", "in_data", "out_ready"], result_names = ["out_valid", "out_data", "in_ready"], pyc.value_params = [], pyc.value_param_types = [], pyc.kind = "module", pyc.inline = "false", pyc.params = "{}", pyc.base = "queue_passthrough", pyc.struct.metrics = "{\"ast_node_count\":0,\"collection_count\":0,\"collection_instance_count\":0,\"estimated_inline_cost\":0,\"hardware_call_count\":0,\"instance_count\":0,\"loop_count\":0,\"module_call_count\":0,\"module_family_collection_count\":0,\"repeat_pressure\":0,\"repeated_body_clusters\":[],\"source_loc\":0,\"state_alloc_count\":0,\"state_call_count\":0}", pyc.struct.collections = "[]"} { + %in_ready, %out_valid, %out_data = pyc.fifo %clk, %rst, %in_valid, %in_data, %out_ready {depth = 2} : i64 + func.return %out_valid, %out_data, %in_ready : i1, i64, i1 + } +} diff --git a/components/agentic-circuit/test/Python/cli-compile.mlir b/components/agentic-circuit/test/Python/cli-compile.mlir new file mode 100644 index 00000000..b3cedf39 --- /dev/null +++ b/components/agentic-circuit/test/Python/cli-compile.mlir @@ -0,0 +1,9 @@ +// RUN: rm -rf %t && mkdir -p %t +// RUN: cp %source_root/tests/cli/fixtures/compile/agentic-circuit.toml %t/ +// RUN: cp %source_root/tests/cli/fixtures/compile/architecture.py %t/ +// RUN: cd %t && env PYTHONPATH=%source_root/src:%binary_root/python %python -m agentic_circuit._cli compile architecture.py --emit=frozen-acir,acsim --stop-after=acsim --output-dir=output --json | %FileCheck %s + +// CHECK: "artifacts":["frozen.ac.mlir","model.acsim.mlir"] +// CHECK: "schema":"agentic-circuit-compile-result" +// CHECK: "stage":"acsim" +// CHECK: "status":"passed" diff --git a/components/agentic-circuit/test/Python/frontend-lowering.mlir b/components/agentic-circuit/test/Python/frontend-lowering.mlir new file mode 100644 index 00000000..0f81c91a --- /dev/null +++ b/components/agentic-circuit/test/Python/frontend-lowering.mlir @@ -0,0 +1,31 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt_public %t/hierarchy.mlir -o /dev/null +// RUN: %acir_opt_public %t/process.mlir -o /dev/null + +//--- hierarchy.mlir +module attributes {ac.contract_epoch = "0.3"} { + ac.system @main root @pipeline as "root" tick 0 "cycle" + seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Refine(%input : i32) -> i32 parameters {} graph { + ac.return %input : i32 + } + ac.module @pipeline(%request : i32) -> i32 parameters {} graph { + %refined_0 = ac.instance @refined of @Refine(%request) static {} + id "refined" path "refined" : (i32) -> i32 + ac.return %refined_0 : i32 + } +} + +//--- process.mlir +module attributes {ac.contract_epoch = "0.3"} { + ac.system @main root @top as "root" tick 0 "cycle" + workload @top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @top() parameters {} graph { + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} diff --git a/components/agentic-circuit/test/Transforms/deterministic-canonicalization.mlir b/components/agentic-circuit/test/Transforms/deterministic-canonicalization.mlir new file mode 100644 index 00000000..be890c07 --- /dev/null +++ b/components/agentic-circuit/test/Transforms/deterministic-canonicalization.mlir @@ -0,0 +1,137 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-canonicalize-model,ac-freeze-topology)' --emit-bytecode -o %t/a.mlirbc %t/a.mlir +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-canonicalize-model,ac-freeze-topology)' --emit-bytecode -o %t/b.mlirbc %t/b.mlir +// RUN: cmp %t/a.mlirbc %t/b.mlirbc +// RUN: sha256sum %t/a.mlirbc | cut -d ' ' -f 1 > %t/a.sha256 +// RUN: sha256sum %t/b.mlirbc | cut -d ' ' -f 1 > %t/b.sha256 +// RUN: cmp %t/a.sha256 %t/b.sha256 +// RUN: %acir_opt %t/a.mlirbc | %FileCheck %s --check-prefix=CANONICAL +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-canonicalize-model,ac-freeze-topology)' --emit-bytecode -o %t/nested-a.mlirbc %t/nested-a.mlir +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-canonicalize-model,ac-freeze-topology)' --emit-bytecode -o %t/nested-b.mlirbc %t/nested-b.mlir +// RUN: cmp %t/nested-a.mlirbc %t/nested-b.mlirbc +// RUN: sha256sum %t/nested-a.mlirbc | cut -d ' ' -f 1 > %t/nested-a.sha256 +// RUN: sha256sum %t/nested-b.mlirbc | cut -d ' ' -f 1 > %t/nested-b.sha256 +// RUN: cmp %t/nested-a.sha256 %t/nested-b.sha256 +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' --emit-bytecode %t/nested-a.mlirbc -o %t/nested-refrozen.mlirbc +// RUN: cmp %t/nested-a.mlirbc %t/nested-refrozen.mlirbc + +//--- a.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Z() parameters {} graph { ac.return } + ac.module @Top() parameters {} graph { + ac.instance @z of @Z() static {} id "z" path "z" : () -> () + ac.instance @a of @A() static {} id "a" path "a" : () -> () + ac.stat @requests kind "counter" + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @A() parameters {} graph { ac.return } +} + +//--- b.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @A() parameters {} graph { ac.return } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.stat @requests kind "counter" + ac.instance @a of @A() static {} id "a" path "a" : () -> () + ac.instance @z of @Z() static {} id "z" path "z" : () -> () + ac.return + } + ac.module @Z() parameters {} graph { ac.return } +} + +// CANONICAL: ac.system @soc +// CANONICAL: ac.module @A +// CANONICAL: ac.module @Top +// CANONICAL: ac.instance @a +// CANONICAL-NEXT: ac.instance @z +// CANONICAL: ac.process @workload +// CANONICAL: ac.stat @requests +// CANONICAL: ac.module @Z + +//--- nested-a.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "Z", fields = [{name = "z", type = i8}]}> : () -> () + "ac.struct"() <{sym_name = "A", fields = [{name = "a", type = i8}]}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec< + !ac.struct<@types::@A> = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 1 : i64}, + !ac.struct<@types::@Z> = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 1 : i64} + >} : () -> () + ac.protocol @p { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.event @z from @sender to @receiver payload i8 action "notify" + ac.event @a from @sender to @receiver payload i8 action "notify" + ac.transition from @idle to @idle on @z transfer false retain false guard {} + ac.transition from @idle to @idle on @a transfer false retain false guard {} + } + ac.interface @I { + ac.role @source dual @sink cardinality "exclusive" + ac.role @sink dual @source cardinality "exclusive" + ac.port @z : !ac.channel from @source to @sink protocol_roles @sender to @receiver + ac.port @a : !ac.channel from @source to @sink protocol_roles @sender to @receiver + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + "ac.module"() <{sym_name = "Node", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + ac.process @state kind "control" { ac.yield_sim } + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + ac.module @Top() parameters {} graph { + %left = "ac.instance"(%right) <{definition = @Node, sym_name = "left", stable_id = "left", path = "left", static_args = {}}> : (i32) -> i32 + %right = "ac.instance"(%left) <{definition = @Node, sym_name = "right", stable_id = "right", path = "right", static_args = {}}> : (i32) -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} + +//--- nested-b.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.interface @I { + ac.port @a : !ac.channel from @source to @sink protocol_roles @sender to @receiver + ac.role @sink dual @source cardinality "exclusive" + ac.port @z : !ac.channel from @source to @sink protocol_roles @sender to @receiver + ac.role @source dual @sink cardinality "exclusive" + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.protocol @p { + ac.event @a from @sender to @receiver payload i8 action "notify" + ac.state @idle initial true terminal false + ac.role @receiver dual @sender cardinality "exclusive" + ac.event @z from @sender to @receiver payload i8 action "notify" + ac.role @sender dual @receiver cardinality "exclusive" + ac.transition from @idle to @idle on @z transfer false retain false guard {} + ac.transition from @idle to @idle on @a transfer false retain false guard {} + } + "ac.module"() <{sym_name = "Node", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + ac.process @state kind "control" { ac.yield_sim } + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + ac.module @Top() parameters {} graph { + %right = "ac.instance"(%left) <{definition = @Node, sym_name = "right", stable_id = "right", path = "right", static_args = {}}> : (i32) -> i32 + %left = "ac.instance"(%right) <{definition = @Node, sym_name = "left", stable_id = "left", path = "left", static_args = {}}> : (i32) -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + "ac.type_scope"() <{sym_name = "types"}> ({ + "ac.struct"() <{sym_name = "A", fields = [{name = "a", type = i8}]}> : () -> () + "ac.struct"() <{sym_name = "Z", fields = [{name = "z", type = i8}]}> : () -> () + }) {dlti.dl_spec = #dlti.dl_spec< + !ac.struct<@types::@A> = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 1 : i64}, + !ac.struct<@types::@Z> = {abi_alignment = 1 : i64, endianness = "little", preferred_alignment = 1 : i64, size = 1 : i64} + >} : () -> () +} diff --git a/components/agentic-circuit/test/Transforms/freeze-topology.mlir b/components/agentic-circuit/test/Transforms/freeze-topology.mlir new file mode 100644 index 00000000..b5b0dc97 --- /dev/null +++ b/components/agentic-circuit/test/Transforms/freeze-topology.mlir @@ -0,0 +1,147 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/valid.mlir -o %t/frozen.mlir +// RUN: %FileCheck %s --check-prefix=FROZEN < %t/frozen.mlir +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/frozen.mlir | %FileCheck %s --check-prefix=FROZEN +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/false-contract.mlir 2>&1 | %FileCheck %s --check-prefix=FALSE-CONTRACT +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/unproven-contract.mlir 2>&1 | %FileCheck %s --check-prefix=UNPROVEN-CONTRACT +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/unresolved-call.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED-CALL +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/recursive-call.mlir 2>&1 | %FileCheck %s --check-prefix=RECURSIVE-CALL +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/external-call.mlir 2>&1 | %FileCheck %s --check-prefix=EXTERNAL-CALL +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/mutated-frozen.mlir 2>&1 | %FileCheck %s --check-prefix=MUTATED + +//--- valid.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func private @leaf(%arg0 : i32) -> i32 { + %one = arith.constant 1 : i32 + %sum = arith.addi %arg0, %one : i32 + return %sum : i32 + } + func.func private @helper(%arg0 : i32) -> i32 { + %value = func.call @leaf(%arg0) : (i32) -> i32 + return %value : i32 + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [@Top::@workload::@trace] + results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + %true = arith.constant true + ac.require %true, "topology is concrete" + ac.ensure %true, "topology remains deterministic" + ac.stat @requests kind "counter" + ac.process @workload kind "workload" { + %zero = arith.constant 0 : i32 + %value = func.call @helper(%zero) : (i32) -> i32 + %cursor = ac.trace.open source "pto" + ac.instrumentation @trace { + ac.stat.add @requests %value : i32 + } + ac.yield_sim + } + ac.return + } +} +// FROZEN: module attributes { +// FROZEN-SAME: ac.contract_epoch = "0.3" +// FROZEN-SAME: ac.freeze_epoch = "0.3" +// FROZEN-SAME: ac.frozen_owners = [ +// FROZEN-SAME: ac.topology_digest = "{{[0-9a-f]+}}" +// FROZEN-SAME: ac.topology_frozen = true +// FROZEN: ac.process @workload +// FROZEN: path = "root.workload" +// FROZEN: stable_id = "root/workload" +// FROZEN: trace_sources = ["pto"] +// FROZEN: path = "root.requests" +// FROZEN: stable_id = "root/requests" +// FROZEN: ac.ensure +// FROZEN-SAME: ac.freeze_proven = true +// FROZEN: ac.require +// FROZEN-SAME: ac.freeze_proven = true + +//--- false-contract.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + seed {kind = "fixed", value = 0 : i64} instrumentation [] + results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + %false = arith.constant false + ac.require %false, "must hold" + ac.return + } +} +// FALSE-CONTRACT: topology-freeze contract failed: must hold + +//--- unproven-contract.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + seed {kind = "fixed", value = 0 : i64} instrumentation [] + results {id = "default", format = "json"} selected true + ac.module @Top(i1) parameters {} graph { + ^bb0(%condition : i1): + ac.ensure %condition, "must be statically proven" + ac.return + } +} +// UNPROVEN-CONTRACT: topology-freeze contract is not statically provable: must be statically proven + +//--- unresolved-call.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @missing() : () -> () + ac.yield_sim + } + ac.return + } +} +// UNRESOLVED-CALL: 'func.call' op 'missing' does not reference a valid function + +//--- recursive-call.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func private @loop() { func.call @loop() : () -> () return } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @loop() : () -> () + ac.yield_sim + } + ac.return + } +} +// RECURSIVE-CALL: recursive func.call purity cycle: @loop -> @loop + +//--- external-call.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func private @external() + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @external() : () -> () + ac.yield_sim + } + ac.return + } +} +// EXTERNAL-CALL: process func.call callee '@external' has no body and cannot be proven effect-free + +//--- mutated-frozen.mlir +builtin.module attributes { + ac.contract_epoch = "0.3", + ac.freeze_epoch = "0.3", + ac.frozen_owners = [], + ac.topology_digest = "0000000000000000000000000000000000000000000000000000000000000000", + ac.topology_frozen = true +} { + ac.system @soc root @Top as "root" tick 0 "cycle" + seed {kind = "fixed", value = 0 : i64} instrumentation [] + results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { ac.return } +} +// MUTATED: frozen topology digest mismatch; topology was mutated after ac-freeze-topology diff --git a/components/agentic-circuit/test/Transforms/queue-var-cse.mlir b/components/agentic-circuit/test/Transforms/queue-var-cse.mlir new file mode 100644 index 00000000..07f9abc2 --- /dev/null +++ b/components/agentic-circuit/test/Transforms/queue-var-cse.mlir @@ -0,0 +1,25 @@ +// RUN: %acir_opt --canonicalize --cse %s | %FileCheck %s + +module attributes {ac.contract_epoch = "0.3"} { + ac.type_scope @types { + ac.struct @Item fields [{name = "remaining", type = i64}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 8 : i64, endianness = "little", preferred_alignment = 8 : i64, size = 8 : i64}>} + %input = ac.source depth 1 latency 1 : !ac.queue> + %output = ac.feedback %input depth 1 latency 1 max_iterations 8 { + ^body(%item: !ac.var>): + %remaining0 = ac.var.get %item field "remaining" : !ac.var> -> !ac.var + %remaining1 = ac.var.get %item field "remaining" : !ac.var> -> !ac.var + %one0 = ac.var.constant 1 : i64 as !ac.var + %one1 = ac.var.constant 1 : i64 as !ac.var + %next_remaining = ac.var.sub %remaining0, %one0 : !ac.var + %next = ac.var.with %item, %next_remaining field "remaining" : !ac.var>, !ac.var -> !ac.var> + %continue = ac.var.cmp "sgt" %remaining1, %one1 : !ac.var -> !ac.var + ac.feedback.yield %next continue %continue : !ac.var>, !ac.var + } : !ac.queue> -> !ac.queue> + ac.sink %output : !ac.queue> +} + +// CHECK-COUNT-1: ac.var.get {{.*}} field "remaining" +// CHECK-COUNT-1: ac.var.constant 1 : i64 +// CHECK: ac.var.sub +// CHECK: ac.var.cmp "sgt" diff --git a/components/agentic-circuit/test/Transforms/verify-model.mlir b/components/agentic-circuit/test/Transforms/verify-model.mlir new file mode 100644 index 00000000..5ea0248d --- /dev/null +++ b/components/agentic-circuit/test/Transforms/verify-model.mlir @@ -0,0 +1,141 @@ +// RUN: %split_file %s %t +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/valid.mlir | %FileCheck %s --check-prefix=VALID +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/fanout.mlir 2>&1 | %FileCheck %s --check-prefix=FANOUT +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/duplicate-owner.mlir 2>&1 | %FileCheck %s --check-prefix=DUPLICATE +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/arbitration.mlir 2>&1 | %FileCheck %s --check-prefix=ARBITRATION +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/unresolved.mlir 2>&1 | %FileCheck %s --check-prefix=UNRESOLVED +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/bad-provider.mlir 2>&1 | %FileCheck %s --check-prefix=PROVIDER +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/bad-payload.mlir 2>&1 | %FileCheck %s --check-prefix=PAYLOAD +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/bad-process.mlir 2>&1 | %FileCheck %s --check-prefix=PROCESS +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/bad-probe.mlir 2>&1 | %FileCheck %s --check-prefix=PROBE +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/bad-contract.mlir 2>&1 | %FileCheck %s --check-prefix=CONTRACT + +//--- valid.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func private @identity(%arg0 : i32) -> i32 { + return %arg0 : i32 + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %c0 = arith.constant 0 : i32 + %v = func.call @identity(%c0) : (i32) -> i32 + ac.yield_sim + } + ac.return + } +} +// VALID: func.func private @identity +// VALID: ac.process @workload +// VALID: func.call @identity + +//--- fanout.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @p { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i32 action "offer" + ac.transition from @idle to @done on @push transfer true retain false guard {} + } + ac.module @Leaf(!ac.flow) -> !ac.flow parameters {} graph { + ^bb0(%arg0 : !ac.flow): + ac.return %arg0 : !ac.flow + } + ac.module @Top(!ac.flow) parameters {} graph { + ^bb0(%arg0 : !ac.flow): + %a = ac.instance @a of @Leaf(%arg0) static {} id "a" path "a" : (!ac.flow) -> !ac.flow + %b = ac.instance @b of @Leaf(%arg0) static {} id "b" path "b" : (!ac.flow) -> !ac.flow + ac.return + } +} +// FANOUT: flow value has more than one functional use + +//--- duplicate-owner.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.stat @same kind "counter" + ac.process @same kind "control" { ac.yield_sim } + ac.return + } +} +// DUPLICATE: duplicate local structural name 'same' + +//--- arbitration.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.resource @r capacity 1 issue_width 1 ii 1 + latency {kind = "fixed", ticks = 1 : i64} + lifecycle {reservation = "propose_commit", release = "balanced", cancellation = "explicit"} + ownership "contested" classes [] id "r" path "r" + ac.return + } +} +// ARBITRATION: shared or contested resource requires one arbitration owner + +//--- unresolved.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Missing as "root" tick 0 "cycle" + seed {kind = "fixed", value = 0 : i64} instrumentation [] + results {id = "default", format = "json"} selected true +} +// UNRESOLVED: selected root must resolve to a materialized ac.module + +//--- bad-provider.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module.extern @Missing : () -> () parameters {} + implementation {registry = "cpp", name = "NotRegistered"} +} +// PROVIDER: structural provider 'cpp:NotRegistered' is not registered + +//--- bad-payload.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @p { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i32 action "offer" + ac.transition from @idle to @done on @push transfer true retain false guard {} + } + ac.module @Top() parameters {} graph { + ac.queue @q payload !ac.list entries 1 ordering "fifo" protocol @p + ownership "exclusive" id "q" path "q" + ac.return + } +} +// PAYLOAD: queue payload does not match endpoint protocol schema + +//--- bad-process.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @p kind "control" { ac.wait_for @missing ac.yield_sim } + ac.return + } +} +// PROCESS: unresolved runtime target '@missing' + +//--- bad-probe.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @p kind "monitor" { + %v = ac.probe @missing kind "queue" : i32 + ac.yield_sim + } + ac.return + } +} +// PROBE: unresolved runtime target '@missing' + +//--- bad-contract.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + %true = arith.constant true + ac.assert %true, "not static" + ac.return + } +} +// CONTRACT: operation is not legal in an ac.module structural Graph region diff --git a/components/agentic-circuit/test/Transforms/zero-delay-cycles.mlir b/components/agentic-circuit/test/Transforms/zero-delay-cycles.mlir new file mode 100644 index 00000000..dc18831e --- /dev/null +++ b/components/agentic-circuit/test/Transforms/zero-delay-cycles.mlir @@ -0,0 +1,80 @@ +// RUN: %split_file %s %t +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/self-loop.mlir 2>&1 | %FileCheck %s --check-prefix=SELF +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/multi-node.mlir 2>&1 | %FileCheck %s --check-prefix=MULTI +// RUN: %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-freeze-topology)' %t/stateful-edge.mlir | %FileCheck %s --check-prefix=STATEFUL +// RUN: %not %acir_opt --verify-each=false --pass-pipeline='builtin.module(ac-verify-model)' %t/zero-delay-stateful.mlir 2>&1 | %FileCheck %s --check-prefix=STATEFUL-ZERO + +//--- self-loop.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} instrumentation [] + results {id = "default", format = "json"} selected true + "ac.module"() <{sym_name = "Pure", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + %x = "ac.instance"(%x) <{definition = @Pure, sym_name = "x", stable_id = "x", path = "x", static_args = {}}> : (i32) -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + "ac.return"() : () -> () + }) : () -> () +} +// SELF: forbidden zero-delay cycle: root.x -> root.x + +//--- multi-node.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} instrumentation [] + results {id = "default", format = "json"} selected true + "ac.module"() <{sym_name = "Pure", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + %a = "ac.instance"(%b) <{definition = @Pure, sym_name = "a", stable_id = "a", path = "a", static_args = {}}> : (i32) -> i32 + %b = "ac.instance"(%a) <{definition = @Pure, sym_name = "b", stable_id = "b", path = "b", static_args = {}}> : (i32) -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + "ac.return"() : () -> () + }) : () -> () +} +// MULTI: forbidden zero-delay cycle: root.a -> root.b -> root.a + +//--- stateful-edge.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} instrumentation [] + results {id = "default", format = "json"} selected true + "ac.module"() <{sym_name = "Pure", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + "ac.module"() <{sym_name = "Stateful", function_type = (i32) -> i32, static_params = {}}> ({ + ^bb0(%arg0 : i32): + ac.process @state kind "control" { ac.yield_sim } + "ac.return"(%arg0) : (i32) -> () + }) : () -> () + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + %a = "ac.instance"(%b) <{definition = @Stateful, sym_name = "a", stable_id = "a", path = "a", static_args = {}}> : (i32) -> i32 + %b = "ac.instance"(%a) <{definition = @Pure, sym_name = "b", stable_id = "b", path = "b", static_args = {}}> : (i32) -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + "ac.return"() : () -> () + }) : () -> () +} +// STATEFUL: ac.topology_frozen = true + +//--- zero-delay-stateful.mlir +builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @p { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i32 action "offer" + ac.transition from @idle to @done on @push transfer true retain false guard {} + } + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.queue"() <{sym_name = "q", stable_id = "q", path = "q", payload = i32, entry_capacity = 1 : i64, ordering = "fifo", protocol = @p, ownership = "exclusive", delay_ticks = 0 : i64}> : () -> () + "ac.return"() : () -> () + }) : () -> () +} +// STATEFUL-ZERO: stateful declaration delay_ticks must be exactly one positive tick diff --git a/components/agentic-circuit/test/lit.cfg.py b/components/agentic-circuit/test/lit.cfg.py new file mode 100644 index 00000000..f7669cd9 --- /dev/null +++ b/components/agentic-circuit/test/lit.cfg.py @@ -0,0 +1,46 @@ +import os +import sys + +import lit.formats + + +config.name = "AgenticCircuit" +config.test_format = lit.formats.ShTest(execute_external=True) +config.suffixes = [".mlir"] +config.test_source_root = os.path.dirname(__file__) + +configured_paths = { + "ACIR_TEST_EXEC_ROOT": getattr(config, "acir_test_exec_root", None) + or os.environ.get("ACIR_TEST_EXEC_ROOT"), + "ACIR_TOOLS_DIR": getattr(config, "acir_tools_dir", None) + or os.environ.get("ACIR_TOOLS_DIR"), + "LLVM_TOOLS_DIR": getattr(config, "llvm_tools_dir", None) + or os.environ.get("LLVM_TOOLS_DIR"), +} +missing_paths = [name for name, value in configured_paths.items() if not value] +if missing_paths: + lit_config.fatal( + "standalone lit requires explicit paths: " + ", ".join(missing_paths) + ) + +config.test_exec_root = configured_paths["ACIR_TEST_EXEC_ROOT"] +tools_dir = configured_paths["ACIR_TOOLS_DIR"] +llvm_tools_dir = configured_paths["LLVM_TOOLS_DIR"] + +if sys.platform == "darwin": + config.available_features.add("system-darwin") + +config.substitutions.append(("%binary_root", config.acir_binary_root)) +config.substitutions.append(("%cxx", config.acir_cxx)) +config.substitutions.append( + ("%llvm_linker_flags", config.acir_llvm_linker_flags) +) +config.substitutions.append(("%python", config.acir_python)) +config.substitutions.append(("%source_root", config.acir_source_root)) + +config.substitutions.append(("%acir_opt_public", os.path.join(tools_dir, "acir-opt"))) +config.substitutions.append(("%acir_opt", os.path.join(tools_dir, "acir-opt-internal"))) +config.substitutions.append(("%acir_cxxgen", os.path.join(tools_dir, "acir-cxxgen"))) +config.substitutions.append(("%FileCheck", os.path.join(llvm_tools_dir, "FileCheck"))) +config.substitutions.append(("%split_file", os.path.join(llvm_tools_dir, "split-file"))) +config.substitutions.append(("%not", os.path.join(llvm_tools_dir, "not"))) diff --git a/components/agentic-circuit/test/lit.site.cfg.py.in b/components/agentic-circuit/test/lit.site.cfg.py.in new file mode 100644 index 00000000..c1e5d20b --- /dev/null +++ b/components/agentic-circuit/test/lit.site.cfg.py.in @@ -0,0 +1,12 @@ +@LIT_SITE_CFG_IN_HEADER@ + +config.acir_tools_dir = "@CMAKE_RUNTIME_OUTPUT_DIRECTORY@" +config.acir_test_exec_root = "@CMAKE_CURRENT_BINARY_DIR@" +config.llvm_tools_dir = "@LLVM_TOOLS_BINARY_DIR@" +config.acir_binary_root = "@PROJECT_BINARY_DIR@" +config.acir_cxx = "@CMAKE_CXX_COMPILER@" +config.acir_llvm_linker_flags = "@ACIR_TEST_LLVM_LINKER_FLAGS@" +config.acir_python = "@ACIR_TEST_PYTHON@" +config.acir_source_root = "@PROJECT_SOURCE_DIR@" + +lit_config.load_config(config, "@CMAKE_CURRENT_SOURCE_DIR@/lit.cfg.py") diff --git a/components/agentic-circuit/tests/__init__.py b/components/agentic-circuit/tests/__init__.py new file mode 100644 index 00000000..2dd2fb5b --- /dev/null +++ b/components/agentic-circuit/tests/__init__.py @@ -0,0 +1 @@ +"""Agentic Circuit test suite.""" diff --git a/components/agentic-circuit/tests/cli/__init__.py b/components/agentic-circuit/tests/cli/__init__.py new file mode 100644 index 00000000..6f95f388 --- /dev/null +++ b/components/agentic-circuit/tests/cli/__init__.py @@ -0,0 +1 @@ +"""Command-line integration tests.""" diff --git a/components/agentic-circuit/tests/cli/fixtures/compile/agentic-circuit.toml b/components/agentic-circuit/tests/cli/fixtures/compile/agentic-circuit.toml new file mode 100644 index 00000000..74495277 --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/compile/agentic-circuit.toml @@ -0,0 +1,26 @@ +contract_epoch = "0.3" + +[project] +name = "compile-fixture" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["traces"] +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/tests/cli/fixtures/compile/architecture.py b/components/agentic-circuit/tests/cli/fixtures/compile/architecture.py new file mode 100644 index 00000000..887cd44c --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/compile/architecture.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from agentic_circuit import module, process, system + + +@module +def top() -> None: + return + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/tests/cli/fixtures/frontend/agentic-circuit.toml b/components/agentic-circuit/tests/cli/fixtures/frontend/agentic-circuit.toml new file mode 100644 index 00000000..871181dc --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/frontend/agentic-circuit.toml @@ -0,0 +1,26 @@ +contract_epoch = "0.3" + +[project] +name = "frontend-fixture" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["traces"] +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/tests/cli/fixtures/frontend/architecture.py b/components/agentic-circuit/tests/cli/fixtures/frontend/architecture.py new file mode 100644 index 00000000..73589ab8 --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/frontend/architecture.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from agentic_circuit import module, system + + +@module +def top() -> None: + return + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/tests/cli/fixtures/frontend/noisy.py b/components/agentic-circuit/tests/cli/fixtures/frontend/noisy.py new file mode 100644 index 00000000..7a2ff46a --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/frontend/noisy.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +print("project noise on stdout") + +from agentic_circuit import module, system + + +@module +def top() -> None: + return + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/tests/cli/fixtures/inspect/agentic-circuit.toml b/components/agentic-circuit/tests/cli/fixtures/inspect/agentic-circuit.toml new file mode 100644 index 00000000..8b4bf1f7 --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/inspect/agentic-circuit.toml @@ -0,0 +1,26 @@ +contract_epoch = "0.3" + +[project] +name = "inspect-fixture" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["traces"] +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/tests/cli/fixtures/inspect/architecture.py b/components/agentic-circuit/tests/cli/fixtures/inspect/architecture.py new file mode 100644 index 00000000..00f58bd9 --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/inspect/architecture.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from agentic_circuit import module, process, system + + +@module +def chip() -> None: + return + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="chip") +def main() -> None: + return diff --git a/components/agentic-circuit/tests/cli/fixtures/run/agentic-circuit.toml b/components/agentic-circuit/tests/cli/fixtures/run/agentic-circuit.toml new file mode 100644 index 00000000..e9b0ed2a --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/run/agentic-circuit.toml @@ -0,0 +1,27 @@ +contract_epoch = "0.3" + +[project] +name = "run-fixture" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["."] +default_trace = "trace.json" +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/tests/cli/fixtures/run/architecture.py b/components/agentic-circuit/tests/cli/fixtures/run/architecture.py new file mode 100644 index 00000000..887cd44c --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/run/architecture.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from agentic_circuit import module, process, system + + +@module +def top() -> None: + return + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/tests/cli/fixtures/run/capped_architecture.py b/components/agentic-circuit/tests/cli/fixtures/run/capped_architecture.py new file mode 100644 index 00000000..887cd44c --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/run/capped_architecture.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from agentic_circuit import module, process, system + + +@module +def top() -> None: + return + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/tests/cli/fixtures/run/trace.json b/components/agentic-circuit/tests/cli/fixtures/run/trace.json new file mode 100644 index 00000000..c57fc9ec --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/run/trace.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"record_count":0},"records":[],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/tests/cli/fixtures/workspace/agentic-circuit.toml b/components/agentic-circuit/tests/cli/fixtures/workspace/agentic-circuit.toml new file mode 100644 index 00000000..36ba97de --- /dev/null +++ b/components/agentic-circuit/tests/cli/fixtures/workspace/agentic-circuit.toml @@ -0,0 +1,26 @@ +contract_epoch = "0.3" + +[project] +name = "fixture" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["traces"] +inputs = { max_events = 1000 } + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/tests/cli/test_all_commands.py b/components/agentic-circuit/tests/cli/test_all_commands.py new file mode 100644 index 00000000..644d8275 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_all_commands.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import unittest +from dataclasses import dataclass + +from agentic_circuit._cli import EXACT_COMMANDS + + +@dataclass(frozen=True, slots=True) +class CommandCoverage: + success: tuple[str, ...] + error: tuple[str, ...] + determinism: tuple[str, ...] + machine_readable: tuple[str, ...] + + +def cli_test_ledger() -> dict[str, CommandCoverage]: + parser = "tests.cli.test_cli_parser.CliParserTest" + discovery = "tests.cli.test_discovery_commands.DiscoveryCommandTest" + frontend = "tests.cli.test_frontend_commands.FrontendCommandTest" + compile_command = "tests.cli.test_compile_command.CompileCommandTest" + build = "tests.cli.test_build_command.BuildCommandTest" + run = "tests.cli.test_run_command.RunCommandTest" + inspect = "tests.cli.test_inspect_command.InspectCommandTest" + exits = "tests.cli.test_exit_codes.ExitCodeTest" + return { + "init": CommandCoverage( + (f"{parser}.test_json_stdout_contains_one_value_and_no_prose",), + (f"{parser}.test_init_refuses_conflicts_unless_each_is_forced",), + (f"{parser}.test_json_stdout_contains_one_value_and_no_prose",), + (f"{parser}.test_json_stdout_contains_one_value_and_no_prose",), + ), + "schema": CommandCoverage( + (f"{discovery}.test_component_protocol_and_list_queries_are_exact",), + (f"{discovery}.test_unknown_schema_name_is_a_structured_user_error",), + (f"{discovery}.test_capabilities_match_schema_without_importing_project",), + (f"{discovery}.test_component_protocol_and_list_queries_are_exact",), + ), + "check": CommandCoverage( + (f"{frontend}.test_check_is_fast_machine_readable_and_writes_no_build",), + (f"{parser}.test_unknown_toml_key_is_exit_two",), + (f"{frontend}.test_check_can_stop_after_verified_acpy",), + (f"{frontend}.test_check_is_fast_machine_readable_and_writes_no_build",), + ), + "elaborate": CommandCoverage( + (f"{frontend}.test_elaborate_is_deterministic_and_captures_project_output",), + (f"{compile_command}.test_invalid_options_fail_before_publishing",), + (f"{frontend}.test_elaborate_is_deterministic_and_captures_project_output",), + (f"{frontend}.test_elaborate_acir_is_verified_and_atomically_replaced",), + ), + "compile": CommandCoverage( + (f"{compile_command}.test_all_exact_emits_are_published_in_fixed_order",), + (f"{compile_command}.test_invalid_options_fail_before_publishing",), + (f"{compile_command}.test_dump_after_each_uses_every_selected_logical_stage",), + (f"{compile_command}.test_all_exact_emits_are_published_in_fixed_order",), + ), + "build": CommandCoverage( + (f"{build}.test_manifest_records_frontend_and_exact_profile",), + (f"{exits}.test_missing_cpp_compiler_is_four",), + (f"{build}.test_identical_build_reports_cache_hit",), + (f"{build}.test_identical_build_reports_cache_hit",), + ), + "run": CommandCoverage( + (f"{run}.test_completed_run_publishes_exact_documents",), + (f"{run}.test_invalid_trace_is_preflight_five_and_preserves_output",), + (f"{run}.test_replay_uses_only_the_immutable_bundle",), + (f"{run}.test_tick_cap_is_incomplete_exit_seven",), + ), + "inspect": CommandCoverage( + (f"{inspect}.test_every_exact_view_is_machine_readable_and_read_only",), + (f"{inspect}.test_hierarchy_path_is_canonical_and_unknown_path_is_diagnostic",), + (f"{inspect}.test_graphviz_output_is_deterministic_and_host_independent",), + (f"{inspect}.test_every_exact_view_is_machine_readable_and_read_only",), + ), + "explain": CommandCoverage( + (f"{discovery}.test_explain_and_doctor_are_read_only",), + (f"{discovery}.test_unknown_schema_name_is_a_structured_user_error",), + (f"{discovery}.test_explain_and_doctor_are_read_only",), + (f"{discovery}.test_explain_and_doctor_are_read_only",), + ), + "doctor": CommandCoverage( + (f"{discovery}.test_explain_and_doctor_are_read_only",), + (f"{exits}.test_internal_tool_failure_is_three",), + (f"{discovery}.test_explain_and_doctor_are_read_only",), + (f"{discovery}.test_explain_and_doctor_are_read_only",), + ), + } + + +class AllCommandsTest(unittest.TestCase): + def test_every_command_has_required_behavior_classes(self) -> None: + ledger = cli_test_ledger() + + self.assertEqual(set(EXACT_COMMANDS), set(ledger)) + for command, row in ledger.items(): + for behavior, tests in ( + ("success", row.success), + ("error", row.error), + ("determinism", row.determinism), + ("machine_readable", row.machine_readable), + ): + self.assertTrue(tests, (command, behavior)) + for test_name in tests: + suite = unittest.defaultTestLoader.loadTestsFromName(test_name) + loaded = tuple(suite) + self.assertEqual(1, len(loaded), (command, behavior, test_name)) + self.assertNotEqual("_FailedTest", type(loaded[0]).__name__) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_build_command.py b/components/agentic-circuit/tests/cli/test_build_command.py new file mode 100644 index 00000000..537e899e --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_build_command.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from jsonschema.validators import Draft202012Validator + + +REPOSITORY = Path(__file__).parents[2] +FIXTURE = Path(__file__).parent / "fixtures" / "compile" + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY / "build/dev-llvm22/python")) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +def workspace(temporary: str) -> Path: + root = Path(temporary) / "project" + shutil.copytree(FIXTURE, root) + return root + + +def current_manifest(output: Path) -> dict[str, object] | None: + pointer_path = output / "current.json" + if not pointer_path.is_file(): + return None + pointer = json.loads(pointer_path.read_text()) + manifest = output.joinpath(pointer["path"], "build-manifest.json") + return json.loads(manifest.read_text()) + + +class BuildCommandTest(unittest.TestCase): + def test_manifest_records_frontend_and_exact_profile(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + output = root / "build/model" + result = run_cli( + "build", + "architecture.py", + "--profile=validated", + "-o", + str(output), + "--json", + cwd=root, + ) + manifest = current_manifest(output) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIsNotNone(manifest) + assert manifest is not None + schema = json.loads( + (REPOSITORY / "schemas" / "build-manifest.schema.json").read_text() + ) + Draft202012Validator(schema).validate(manifest) + self.assertEqual("agentic-circuit-build-manifest", manifest["schema"]) + self.assertEqual("validated", manifest["build_profile"]) + self.assertIn( + "architecture.py", + {item["path"] for item in manifest["source_files"]}, + ) + self.assertIn( + "input/model.acpy.json", + {item["path"] for item in manifest["artifacts"]}, + ) + + def test_identical_build_reports_cache_hit(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + first = run_cli( + "build", + "architecture.py", + "-o", + "build/model", + "--json", + cwd=root, + ) + second = run_cli( + "build", + "architecture.py", + "-o", + "build/model", + "--json", + cwd=root, + ) + + self.assertEqual(0, first.returncode, first.stderr) + self.assertEqual(0, second.returncode, second.stderr) + self.assertFalse(json.loads(first.stdout)["cache_hit"]) + self.assertTrue(json.loads(second.stdout)["cache_hit"]) + self.assertEqual( + json.loads(first.stdout)["build_fingerprint"], + json.loads(second.stdout)["build_fingerprint"], + ) + + def test_custom_profile_requires_pipeline_before_output(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + output = root / "build/custom" + result = run_cli( + "build", + "architecture.py", + "--profile=custom", + "-o", + str(output), + cwd=root, + ) + + self.assertEqual(2, result.returncode, result.stderr) + self.assertIn("ACPY-CLI-PIPELINE", result.stderr) + self.assertFalse(output.exists()) + + def test_custom_pipeline_is_recorded_verbatim(self) -> None: + pipeline = "builtin.module(ac-canonicalize-model)" + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + output = root / "build/custom" + result = run_cli( + "build", + "architecture.py", + "--profile=custom", + "--pass-pipeline", + pipeline, + "-o", + str(output), + cwd=root, + ) + manifest = current_manifest(output) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIsNotNone(manifest) + assert manifest is not None + self.assertIn(pipeline, manifest["pass_pipeline"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_cli_parser.py b/components/agentic-circuit/tests/cli/test_cli_parser.py new file mode 100644 index 00000000..d7bbc638 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_cli_parser.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from agentic_circuit._cli import EXACT_COMMANDS, build_parser, command_names +from agentic_circuit._staging import ArtifactStage + + +FIXTURE = Path(__file__).parent / "fixtures" / "workspace" / "agentic-circuit.toml" +REPOSITORY = Path(__file__).parents[2] + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY / "build/dev-llvm22/python")) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +class CliParserTest(unittest.TestCase): + def test_exact_command_inventory(self) -> None: + self.assertEqual(EXACT_COMMANDS, command_names(build_parser())) + + def test_json_stdout_contains_one_value_and_no_prose(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + result = run_cli("init", "--dry-run", "--json", cwd=Path(temporary)) + + self.assertEqual(0, result.returncode) + self.assertIsInstance(json.loads(result.stdout), dict) + self.assertEqual("", result.stderr) + + def test_unknown_toml_key_is_exit_two(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + root.joinpath("agentic-circuit.toml").write_text( + FIXTURE.read_text() + "\nunknown = true\n" + ) + result = run_cli("check", "architecture.py", "--json", cwd=root) + + self.assertEqual(2, result.returncode) + self.assertEqual("ACPY-CONFIG-002", json.loads(result.stdout)["code"]) + self.assertEqual("", result.stderr) + + def test_explicit_project_bypasses_current_directory_discovery(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + base = Path(temporary) + project = base / "project" + unrelated = base / "unrelated" + project.mkdir() + unrelated.mkdir() + manifest = project / "agentic-circuit.toml" + manifest.write_bytes(FIXTURE.read_bytes()) + project.joinpath("architecture.py").write_text( + "from agentic_circuit import module, system\n" + "@module\n" + "def top() -> None:\n" + " return\n" + "@system(root='top')\n" + "def main() -> None:\n" + " return\n" + ) + result = run_cli( + "check", + "architecture.py", + "--project", + str(manifest), + "--json", + cwd=unrelated, + ) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual("fixture", json.loads(result.stdout)["project"]) + + def test_init_refuses_conflicts_unless_each_is_forced(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + root.joinpath("architecture.py").write_text("preserve me\n") + refused = run_cli("init", "--json", cwd=root) + preserved = root.joinpath("architecture.py").read_text() + forced = run_cli( + "init", "--force", "architecture.py", "--json", cwd=root + ) + + self.assertEqual(2, refused.returncode) + self.assertEqual("preserve me\n", preserved) + self.assertEqual(0, forced.returncode) + self.assertTrue(root.joinpath("agentic-circuit.toml").is_file()) + self.assertNotEqual( + "preserve me\n", root.joinpath("architecture.py").read_text() + ) + + def test_stage_failure_preserves_published_files(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + destination = Path(temporary) / "output" + destination.mkdir() + destination.joinpath("old.txt").write_text("old\n") + + with self.assertRaises(ValueError): + with ArtifactStage(destination, expected=("new.txt",)) as stage: + stage.write_text("unexpected.txt", "new\n") + stage.commit() + + self.assertEqual("old\n", destination.joinpath("old.txt").read_text()) + self.assertFalse(destination.joinpath("new.txt").exists()) + + def test_aliases_and_repeated_singleton_options_are_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + root.joinpath("agentic-circuit.toml").write_bytes(FIXTURE.read_bytes()) + alias = run_cli("chk", cwd=root) + repeated = run_cli( + "check", "--system", "first", "--system", "second", cwd=root + ) + + self.assertEqual(2, alias.returncode) + self.assertEqual(2, repeated.returncode) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_compile_command.py b/components/agentic-circuit/tests/cli/test_compile_command.py new file mode 100644 index 00000000..8b7e37b0 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_compile_command.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).parents[2] +FIXTURE = Path(__file__).parent / "fixtures" / "compile" +EXPECTED_COMPILE_TREE = ( + "frozen.ac.mlir", + "include/generated/dispatch.h", + "include/generated/model.h", + "include/generated/modules/top_s289ddf7a6fa5af5e.h", + "include/generated/processes/workload_se1632550834a7396.h", + "input/model.ac.mlir", + "input/model.acpy.json", + "model.acsim.mlir", + "src/generated/main.cpp", + "src/generated/model.cpp", + "src/generated/modules/top_s289ddf7a6fa5af5e.cpp", + "src/generated/processes/workload_se1632550834a7396.cpp", +) + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY / "build/dev-llvm22/python")) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +def workspace(temporary: str) -> Path: + root = Path(temporary) / "project" + shutil.copytree(FIXTURE, root) + return root + + +def relative_tree(root: Path) -> tuple[str, ...]: + if not root.exists(): + return () + return tuple( + item.relative_to(root).as_posix() + for item in sorted(root.rglob("*")) + if item.is_file() + ) + + +class CompileCommandTest(unittest.TestCase): + def test_all_exact_emits_are_published_in_fixed_order(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + result = run_cli( + "compile", + "architecture.py", + "--emit=acpy,acir,frozen-acir,acsim,cpp", + "--output-dir", + "build/main", + "--json", + cwd=root, + ) + tree = relative_tree(root / "build/main") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual(EXPECTED_COMPILE_TREE, tree) + payload = json.loads(result.stdout) + self.assertEqual("agentic-circuit-compile-result", payload["schema"]) + self.assertEqual(list(EXPECTED_COMPILE_TREE), payload["artifacts"]) + + def test_invalid_options_fail_before_publishing(self) -> None: + cases = ( + (("--emit=acpy,acpy",), "ACPY-CLI-EMIT"), + (("--emit=unknown",), "ACPY-CLI-EMIT"), + (("--stop-after=unknown",), "ACPY-CLI-STAGE"), + ( + ("--stop-after=acpy-verify", "--emit=frozen-acir"), + "ACPY-CLI-STAGE", + ), + (("--profile=custom",), "ACPY-CLI-PIPELINE"), + ( + ("--profile=fast", "--pass-pipeline=builtin.module()"), + "ACPY-CLI-PIPELINE", + ), + (("--dump-after=unknown",), "ACPY-CLI-DUMP"), + ( + ("--dump-before=topology-freeze",) * 2, + "ACPY-CLI-DUMP", + ), + ) + for index, (arguments, code) in enumerate(cases): + with self.subTest(arguments=arguments): + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + output = root / f"build/error-{index}" + result = run_cli( + "compile", + "architecture.py", + *arguments, + "--output-dir", + str(output), + cwd=root, + ) + + self.assertEqual(2, result.returncode, result.stderr) + self.assertIn(code, result.stderr) + self.assertFalse(output.exists()) + + def test_logical_dump_names_are_retained_at_a_stopped_stage(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + result = run_cli( + "compile", + "architecture.py", + "--emit=frozen-acir", + "--stop-after=topology-freeze", + "--dump-before=topology-freeze", + "--dump-after=topology-freeze", + "--output-dir=build/dumps", + "--json", + cwd=root, + ) + tree = relative_tree(root / "build/dumps") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual( + ( + "dumps/topology-freeze-after.mlir", + "dumps/topology-freeze-before.mlir", + "frozen.ac.mlir", + ), + tree, + ) + self.assertEqual("topology-freeze", json.loads(result.stdout)["stage"]) + + def test_frontend_stop_does_not_enter_native_lowering(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + result = run_cli( + "compile", + "architecture.py", + "--emit=acpy", + "--stop-after=acpy-verify", + "--output-dir=build/frontend", + "--json", + cwd=root, + ) + tree = relative_tree(root / "build/frontend") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual(("input/model.acpy.json",), tree) + self.assertEqual("acpy-verify", json.loads(result.stdout)["stage"]) + + def test_dump_after_each_uses_every_selected_logical_stage(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + result = run_cli( + "compile", + "architecture.py", + "--emit=acpy", + "--stop-after=acpy-verify", + "--dump-after-each", + "--output-dir=build/after-each", + cwd=root, + ) + tree = relative_tree(root / "build/after-each") + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual( + ( + "dumps/acpy-construction-after.mlir", + "dumps/acpy-verify-after.mlir", + "dumps/frontend-capture-after.mlir", + "input/model.acpy.json", + ), + tree, + ) + + def test_output_symlink_cannot_redirect_generated_artifacts(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + output = root / "build/redirected" + outside = root / "outside" + output.mkdir(parents=True) + outside.mkdir() + output.joinpath("include").symlink_to(outside, target_is_directory=True) + + result = run_cli( + "compile", + "architecture.py", + "--emit=cpp", + "--output-dir", + str(output), + cwd=root, + ) + outside_tree = relative_tree(outside) + + self.assertEqual(2, result.returncode, result.stderr) + self.assertIn("ACPY-CLI-OUTPUT", result.stderr) + self.assertEqual((), outside_tree) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_determinism.py b/components/agentic-circuit/tests/cli/test_determinism.py new file mode 100644 index 00000000..ddff2711 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_determinism.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).parents[2] +COMPILE_FIXTURE = Path(__file__).parent / "fixtures" / "compile" +RUN_FIXTURE = Path(__file__).parent / "fixtures" / "run" + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONHASHSEED"] = "0" + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY / "build/dev-llvm22/python")) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +def copy_fixture(source: Path, root: Path) -> None: + shutil.copytree(source, root) + + +def file_bytes(root: Path) -> tuple[tuple[str, bytes], ...]: + return tuple( + (path.relative_to(root).as_posix(), path.read_bytes()) + for path in sorted(root.rglob("*")) + if path.is_file() + ) + + +class CliDeterminismTest(unittest.TestCase): + def test_compile_outputs_are_identical_across_workspace_roots(self) -> None: + with ( + tempfile.TemporaryDirectory() as first, + tempfile.TemporaryDirectory() as second, + ): + first_root = Path(first) / "project" + second_root = Path(second) / "project" + copy_fixture(COMPILE_FIXTURE, first_root) + copy_fixture(COMPILE_FIXTURE, second_root) + first_result = run_cli( + "compile", + "architecture.py", + "--emit", + "acpy,acir,acsim", + "--output-dir", + "out", + cwd=first_root, + ) + second_result = run_cli( + "compile", + "architecture.py", + "--emit", + "acpy,acir,acsim", + "--output-dir", + "out", + cwd=second_root, + ) + first_files = file_bytes(first_root / "out") + second_files = file_bytes(second_root / "out") + + self.assertEqual(0, first_result.returncode, first_result.stderr) + self.assertEqual(0, second_result.returncode, second_result.stderr) + self.assertEqual(first_files, second_files) + + def test_build_and_run_manifests_are_root_independent(self) -> None: + with ( + tempfile.TemporaryDirectory() as first, + tempfile.TemporaryDirectory() as second, + ): + first_root = Path(first) / "project" + second_root = Path(second) / "project" + copy_fixture(RUN_FIXTURE, first_root) + copy_fixture(RUN_FIXTURE, second_root) + arguments = ( + "run", + "capped_architecture.py", + "--trace", + "trace.json", + "--max-ticks", + "1", + "--output-dir", + "runs/one", + ) + first_result = run_cli(*arguments, cwd=first_root) + second_result = run_cli(*arguments, cwd=second_root) + first_manifest = first_root.joinpath("runs/one/run-manifest.json").read_bytes() + second_manifest = second_root.joinpath("runs/one/run-manifest.json").read_bytes() + first_run_result = first_root.joinpath("runs/one/run-result.json").read_bytes() + second_run_result = second_root.joinpath("runs/one/run-result.json").read_bytes() + + self.assertEqual(7, first_result.returncode, first_result.stderr) + self.assertEqual(7, second_result.returncode, second_result.stderr) + self.assertEqual(first_manifest, second_manifest) + self.assertEqual(first_run_result, second_run_result) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_discovery_commands.py b/components/agentic-circuit/tests/cli/test_discovery_commands.py new file mode 100644 index 00000000..ab00e3ec --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_discovery_commands.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import hashlib +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from jsonschema.validators import Draft202012Validator + + +REPOSITORY = Path(__file__).parents[2] +BUILD_PYTHON = REPOSITORY / "build" / "dev-llvm22" / "python" + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(BUILD_PYTHON)) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +def snapshot_tree(root: Path) -> tuple[tuple[str, str], ...]: + return tuple( + ( + path.relative_to(root).as_posix(), + hashlib.sha256(path.read_bytes()).hexdigest(), + ) + for path in sorted(root.rglob("*")) + if path.is_file() + ) + + +class DiscoveryCommandTest(unittest.TestCase): + def test_capabilities_match_schema_without_importing_project(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + hostile = Path(temporary) + hostile.joinpath("architecture.py").write_text( + "from pathlib import Path\nPath('imported.marker').write_text('bad')\n" + ) + + result = run_cli("schema", "capabilities", "--json", cwd=hostile) + document = json.loads(result.stdout) + + schema = json.loads( + (REPOSITORY / "schemas" / "capabilities.schema.json").read_text() + ) + Draft202012Validator(schema).validate(document) + self.assertEqual(0, result.returncode, result.stderr) + self.assertFalse(hostile.joinpath("imported.marker").exists()) + self.assertEqual( + sorted((item["kind"], item["name"]) for item in document["items"]), + [(item["kind"], item["name"]) for item in document["items"]], + ) + self.assertIn( + "declared_unavailable", + {item["availability"] for item in document["items"]}, + ) + for item in document["items"]: + if item["availability"] == "available": + self.assertIsNotNone(item["implementation_fingerprint"]) + else: + self.assertIsNone(item["implementation_fingerprint"]) + + def test_component_protocol_and_list_queries_are_exact(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + component = run_cli("schema", "component", "ac.Queue", "--json", cwd=root) + protocol = run_cli( + "schema", "protocol", "ac.ready_valid", "--json", cwd=root + ) + listing = run_cli("schema", "component", "--json", cwd=root) + opcode = run_cli("schema", "opcode", "ac.reorder", "--json", cwd=root) + opcode_listing = run_cli("schema", "opcode", "--json", cwd=root) + block = run_cli("schema", "block", "ac.schedule", "--json", cwd=root) + block_listing = run_cli("schema", "block", "--json", cwd=root) + + self.assertEqual("ac.Queue", json.loads(component.stdout)["canonical_name"]) + self.assertEqual( + "ac.ready_valid", json.loads(protocol.stdout)["canonical_name"] + ) + names = json.loads(listing.stdout)["items"] + self.assertEqual(sorted(names), names) + reorder = json.loads(opcode.stdout) + self.assertEqual("ac.reorder", reorder["operation"]) + self.assertEqual("design", reorder["role"]) + self.assertTrue(reorder["gfsim"]["available"]) + self.assertTrue(reorder["pyc"]["available"]) + opcode_names = json.loads(opcode_listing.stdout)["items"] + self.assertEqual(sorted(opcode_names), opcode_names) + self.assertIn("ac.dependency", opcode_names) + self.assertIn("ac.reorder", opcode_names) + self.assertIn("ac.transform", opcode_names) + schedule = json.loads(block.stdout) + self.assertEqual("ac.schedule", schedule["operation"]) + self.assertEqual("ac.dependency", schedule["lowers_to"]) + block_names = json.loads(block_listing.stdout)["items"] + self.assertEqual(sorted(block_names), block_names) + self.assertIn("ac.compute", block_names) + self.assertIn("ac.engine", block_names) + + def test_unknown_schema_name_is_a_structured_user_error(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + result = run_cli( + "schema", "component", "Missing", "--json", cwd=Path(temporary) + ) + + self.assertEqual(2, result.returncode) + self.assertEqual("ACPY-SCHEMA-001", json.loads(result.stdout)["code"]) + + def test_explain_and_doctor_are_read_only(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + workspace.joinpath("sentinel.txt").write_text("unchanged\n") + before = snapshot_tree(workspace) + + explained = run_cli("explain", "ACIR-PROTOCOL-004", "--json", cwd=workspace) + doctor = run_cli("doctor", "--json", cwd=workspace) + + self.assertEqual(before, snapshot_tree(workspace)) + + self.assertEqual(0, explained.returncode, explained.stderr) + self.assertEqual("ACIR-PROTOCOL-004", json.loads(explained.stdout)["code"]) + self.assertEqual(0, doctor.returncode, doctor.stderr) + checks = json.loads(doctor.stdout)["checks"] + self.assertTrue(all(check["status"] == "passed" for check in checks)) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_exit_codes.py b/components/agentic-circuit/tests/cli/test_exit_codes.py new file mode 100644 index 00000000..892c0440 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_exit_codes.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import json +import os +import shutil +import signal +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from agentic_circuit._canonical_json import sha256_bytes +from agentic_circuit._commands.build import BuildPublication +from agentic_circuit._run import RunOptions, create_run_manifest + + +REPOSITORY = Path(__file__).parents[2] +FIXTURE = Path(__file__).parent / "fixtures" / "compile" +TRACE = Path(__file__).parent / "fixtures" / "run" / "trace.json" + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY / "build/dev-llvm22/python")) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +class ExitCodeTest(unittest.TestCase): + def test_internal_tool_failure_is_three(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + environment = os.environ.copy() + environment["PYTHONPATH"] = str(REPOSITORY / "src") + result = subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", "doctor", "--json"], + cwd=temporary, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + self.assertEqual(3, result.returncode, result.stderr) + diagnostic = json.loads(result.stdout) + self.assertEqual("agentic-circuit-diagnostic", diagnostic["schema"]) + self.assertEqual("ACPY-INTERNAL-001", diagnostic["code"]) + + def test_missing_cpp_compiler_is_four(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / "project" + shutil.copytree(FIXTURE, root) + manifest = root / "agentic-circuit.toml" + manifest.write_text( + manifest.read_text().replace( + 'compiler = "c++"', 'compiler = "missing-agentic-cxx"' + ) + ) + result = run_cli( + "build", + "architecture.py", + "--output-dir", + "build/model", + "--json", + cwd=root, + ) + + self.assertEqual(4, result.returncode, result.stderr) + self.assertEqual("ACBUILD-COMPILER-001", json.loads(result.stdout)["code"]) + + def test_runtime_interrupt_is_130_and_publishes_nothing(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + build = root / "build" + executable = build / "bin/model" + executable.parent.mkdir(parents=True) + executable.write_text( + "#!/usr/bin/env python3\n" + "import os,signal\n" + "os.kill(os.getpid(), signal.SIGINT)\n" + ) + executable.chmod(0o755) + build_manifest = build / "build-manifest.json" + build_manifest.write_text( + json.dumps( + { + "artifacts": [ + { + "path": "bin/model", + "kind": "executable", + "sha256": sha256_bytes(executable.read_bytes()), + } + ] + }, + sort_keys=True, + separators=(",", ":"), + ) + + "\n" + ) + trace = root / "trace.json" + trace.write_bytes(TRACE.read_bytes()) + publication = BuildPublication( + build, executable, build_manifest, "sha256:" + "0" * 64, False + ) + options = RunOptions( + trace, root / "unused", 0, None, None, (), "json", "disabled", "any" + ) + bundle = root / "bundle" + bundle.joinpath("bin").mkdir(parents=True) + shutil.copy2(executable, bundle / "bin/model") + shutil.copy2(build_manifest, bundle / "build-manifest.json") + shutil.copy2(trace, bundle / "trace.json") + manifest = bundle / "run-manifest.json" + manifest.write_bytes(create_run_manifest(publication, options)) + output = root / "replay" + result = run_cli( + "run", + "--replay-manifest", + str(manifest), + "--output-dir", + str(output), + "--json", + cwd=root, + ) + published = output.exists() + + self.assertEqual(128 + signal.SIGINT, result.returncode, result.stderr) + self.assertEqual("ACRUN-INTERRUPTED-001", json.loads(result.stdout)["code"]) + self.assertFalse(published) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_frontend_commands.py b/components/agentic-circuit/tests/cli/test_frontend_commands.py new file mode 100644 index 00000000..e4597891 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_frontend_commands.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).parents[2] +FIXTURE = Path(__file__).parent / "fixtures" / "frontend" + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY / "build/dev-llvm22/python")) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +def workspace(temporary: str) -> Path: + root = Path(temporary) / "project" + shutil.copytree(FIXTURE, root) + return root + + +class FrontendCommandTest(unittest.TestCase): + def test_check_is_fast_machine_readable_and_writes_no_build(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + result = run_cli( + "check", + "architecture.py", + "--system", + "main", + "--json", + cwd=root, + ) + + self.assertFalse(root.joinpath("build").exists()) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual("passed", json.loads(result.stdout)["status"]) + + def test_check_can_stop_after_verified_acpy(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + result = run_cli( + "check", + "--stop-after", + "acpy-verify", + "--json", + cwd=root, + ) + + self.assertEqual(0, result.returncode, result.stderr) + self.assertEqual("acpy-verify", json.loads(result.stdout)["stage"]) + + def test_elaborate_is_deterministic_and_captures_project_output(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + first = run_cli( + "elaborate", "noisy.py", "--emit", "acpy", "-o", "a.json", cwd=root + ) + second = run_cli( + "elaborate", "noisy.py", "--emit", "acpy", "-o", "b.json", cwd=root + ) + first_bytes = root.joinpath("a.json").read_bytes() + second_bytes = root.joinpath("b.json").read_bytes() + + self.assertEqual(0, first.returncode, first.stderr) + self.assertEqual(first_bytes, second_bytes) + self.assertNotIn("project noise", first.stdout) + self.assertNotIn("project noise", first.stderr) + + def test_elaborate_acir_is_verified_and_atomically_replaced(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + output = root / "model.ac.mlir" + output.write_text("old\n") + result = run_cli( + "elaborate", "architecture.py", "-o", output.name, "--json", cwd=root + ) + contents = output.read_text() + + self.assertEqual(0, result.returncode, result.stderr) + self.assertIn("ac.system @main", contents) + self.assertEqual("sha256:", json.loads(result.stdout)["sha256"][:7]) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_inspect_command.py b/components/agentic-circuit/tests/cli/test_inspect_command.py new file mode 100644 index 00000000..93550018 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_inspect_command.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).parents[2] +FIXTURE = Path(__file__).parent / "fixtures" / "inspect" +KINDS = ( + "graph", + "hierarchy", + "ports", + "resources", + "address-map", + "protocols", + "specialization", + "artifacts", +) + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY / "build/dev-llvm22/python")) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +def workspace(temporary: str) -> Path: + root = Path(temporary) / "project" + shutil.copytree(FIXTURE, root) + return root + + +class InspectCommandTest(unittest.TestCase): + def test_every_exact_view_is_machine_readable_and_read_only(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + documents: dict[str, dict[str, object]] = {} + for kind in KINDS: + result = run_cli("inspect", kind, "--json", cwd=root) + self.assertEqual(0, result.returncode, (kind, result.stderr)) + documents[kind] = json.loads(result.stdout) + formatted = run_cli( + "inspect", "graph", "--format", "json", cwd=root + ) + build_exists = root.joinpath("build").exists() + + self.assertFalse(build_exists) + self.assertEqual("graph", json.loads(formatted.stdout)["kind"]) + for kind, document in documents.items(): + self.assertEqual("agentic-circuit-inspection", document["schema"]) + self.assertEqual(kind, document["kind"]) + self.assertEqual("main", document["system"]) + self.assertIsInstance(document["records"], list) + + def test_hierarchy_path_is_canonical_and_unknown_path_is_diagnostic(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + found = run_cli( + "inspect", "ports", "--path", "chip", "--json", cwd=root + ) + missing = run_cli( + "inspect", "ports", "--path", "missing", "--json", cwd=root + ) + + self.assertEqual(0, found.returncode, found.stderr) + self.assertEqual("chip", json.loads(found.stdout)["path"]) + self.assertEqual(2, missing.returncode) + self.assertEqual("ACPY-INSPECT-001", json.loads(missing.stdout)["code"]) + + def test_graphviz_output_is_deterministic_and_host_independent(self) -> None: + with ( + tempfile.TemporaryDirectory() as first, + tempfile.TemporaryDirectory() as second, + ): + first_root = workspace(first) + second_root = workspace(second) + first_dot = run_cli("inspect", "graph", "--format", "dot", cwd=first_root) + second_dot = run_cli( + "inspect", "graph", "--format", "dot", cwd=second_root + ) + + self.assertEqual(0, first_dot.returncode, first_dot.stderr) + self.assertEqual(first_dot.stdout, second_dot.stdout) + self.assertTrue(first_dot.stdout.startswith("digraph agentic_circuit {\n")) + self.assertNotIn(str(first_root), first_dot.stdout) + self.assertNotIn(str(second_root), first_dot.stdout) + + def test_artifacts_use_the_verified_current_build_when_present(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + built = run_cli( + "build", "architecture.py", "-o", "build/main", cwd=root + ) + root.joinpath("architecture.py").unlink() + inspected = run_cli("inspect", "artifacts", "--json", cwd=root) + records = json.loads(inspected.stdout)["records"] + pointer = json.loads(root.joinpath("build/main/current.json").read_text()) + build_directory = root / "build/main" / pointer["path"] + manifest = json.loads( + build_directory.joinpath("build-manifest.json").read_text() + ) + executable_path = next( + artifact["path"] + for artifact in manifest["artifacts"] + if artifact["kind"] == "executable" + ) + executable = build_directory / executable_path + executable.write_bytes(executable.read_bytes() + b"tampered") + corrupted = run_cli("inspect", "artifacts", "--json", cwd=root) + + self.assertEqual(0, built.returncode, built.stderr) + self.assertEqual(0, inspected.returncode, inspected.stderr) + self.assertIn("build_manifest", {record["kind"] for record in records}) + self.assertIn("executable", {record["kind"] for record in records}) + self.assertEqual(2, corrupted.returncode) + self.assertEqual("ACPY-INSPECT-001", json.loads(corrupted.stdout)["code"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_installation.py b/components/agentic-circuit/tests/cli/test_installation.py new file mode 100644 index 00000000..8296044f --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_installation.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).parents[2] +BUILD = REPOSITORY / "build" / "dev-llvm22" +FIXTURE = Path(__file__).parent / "fixtures" / "inspect" + + +def install_to(prefix: Path) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["cmake", "--install", str(BUILD), "--prefix", str(prefix)], + cwd=REPOSITORY, + text=True, + capture_output=True, + check=False, + ) + + +def run_installed( + prefix: Path, *arguments: str, cwd: Path +) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment.pop("PYTHONPATH", None) + return subprocess.run( + [str(prefix / "bin/agentic-circuit"), *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +class InstallationTest(unittest.TestCase): + def test_installed_prefix_runs_without_source_checkout(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + prefix = root / "prefix" + project = root / "project" + unrelated = root / "outside" + shutil.copytree(FIXTURE, project) + unrelated.mkdir() + installed = install_to(prefix) + self.assertEqual(0, installed.returncode, installed.stderr) + installed_bins = {path.name for path in (prefix / "bin").iterdir()} + + doctor = run_installed(prefix, "doctor", "--json", cwd=unrelated) + capabilities = run_installed( + prefix, "schema", "capabilities", "--json", cwd=unrelated + ) + opcode = run_installed( + prefix, + "schema", + "opcode", + "ac.reorder", + "--json", + cwd=unrelated, + ) + checked = run_installed( + prefix, + "check", + "architecture.py", + "--project", + str(project / "agentic-circuit.toml"), + "--json", + cwd=unrelated, + ) + initialized = run_installed( + prefix, "init", str(root / "initialized"), "--json", cwd=unrelated + ) + built = run_installed( + prefix, + "build", + "architecture.py", + "--project", + str(project / "agentic-circuit.toml"), + "--output-dir", + str(project / "build/main"), + "--json", + cwd=unrelated, + ) + + self.assertEqual(0, doctor.returncode, doctor.stderr) + self.assertEqual(0, capabilities.returncode, capabilities.stderr) + self.assertEqual( + "agentic-circuit-capabilities", + json.loads(capabilities.stdout)["schema"], + ) + self.assertEqual(0, opcode.returncode, opcode.stderr) + self.assertEqual("ac.reorder", json.loads(opcode.stdout)["operation"]) + self.assertEqual(0, checked.returncode, checked.stderr) + self.assertEqual("passed", json.loads(checked.stdout)["status"]) + self.assertEqual(0, initialized.returncode, initialized.stderr) + self.assertEqual(0, built.returncode, built.stderr) + self.assertNotIn("import-davincioo-pto-trace.py", installed_bins) + self.assertNotIn("pack-perfetto-trace.py", installed_bins) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_native_api.py b/components/agentic-circuit/tests/cli/test_native_api.py new file mode 100644 index 00000000..1ff823f7 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_native_api.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import unittest +from dataclasses import FrozenInstanceError + +import agentic_circuit +from agentic_circuit._native_api import NativeRequest, capabilities, run_native_compiler + + +VALID_ACIR = b""" +module attributes {ac.contract_epoch = "0.3"} { + ac.system @main root @top as "root" tick 0 "cycle" + workload @top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @top() parameters {} graph { + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} +""" + + +class NativeApiTest(unittest.TestCase): + def test_private_extension_compiles_verified_acir(self) -> None: + result = run_native_compiler( + NativeRequest( + acir=VALID_ACIR, + stop_after="acsim-verify", + emits=("frozen-acir", "acsim"), + ) + ) + + self.assertEqual((), result.diagnostics) + self.assertEqual( + ("frozen.ac.mlir", "model.acsim.mlir"), + tuple(item.path for item in result.artifacts), + ) + self.assertTrue(all(item.sha256.startswith("sha256:") for item in result.artifacts)) + + def test_compiler_failure_is_a_structured_result(self) -> None: + result = run_native_compiler( + NativeRequest( + acir=b"not mlir", + stop_after="acir-parse", + emits=(), + ) + ) + + self.assertEqual((), result.artifacts) + self.assertEqual("ACIR-PARSE-001", result.diagnostics[0].code) + + def test_private_extension_is_not_exported_publicly(self) -> None: + self.assertNotIn("_native", agentic_circuit.__all__) + self.assertNotIn("_native_api", agentic_circuit.__all__) + + def test_wrapper_records_are_immutable(self) -> None: + result = run_native_compiler( + NativeRequest(acir=VALID_ACIR, stop_after="acir-parse", emits=()) + ) + with self.assertRaises(FrozenInstanceError): + result.executable = "changed" # type: ignore[misc] + + def test_capabilities_have_stable_build_identity(self) -> None: + found = capabilities() + self.assertTrue(found.compiler_build_id) + self.assertTrue(found.runtime_build_id) + self.assertIsInstance(found.items, tuple) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_run_command.py b/components/agentic-circuit/tests/cli/test_run_command.py new file mode 100644 index 00000000..87ce259a --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_run_command.py @@ -0,0 +1,247 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from jsonschema.validators import Draft202012Validator + +from agentic_circuit._canonical_json import sha256_bytes +from agentic_circuit._commands.build import BuildPublication +from agentic_circuit._run import RunOptions, execute_run + + +REPOSITORY = Path(__file__).parents[2] +FIXTURE = Path(__file__).parent / "fixtures" / "run" + + +def run_cli(*arguments: str, cwd: Path) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY / "build/dev-llvm22/python")) + ) + return subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=environment, + text=True, + capture_output=True, + check=False, + ) + + +def workspace(temporary: str) -> Path: + root = Path(temporary) / "project" + shutil.copytree(FIXTURE, root) + return root + + +def load_json(path: Path) -> dict[str, object]: + return json.loads(path.read_text()) + + +def validate_schema(name: str, document: dict[str, object]) -> None: + schema = load_json(REPOSITORY / "schemas" / name) + Draft202012Validator(schema).validate(document) + + +class RunCommandTest(unittest.TestCase): + def test_completed_run_publishes_exact_documents(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + build = root / "build" + executable = build / "bin/model" + executable.parent.mkdir(parents=True) + executable.write_text( + "#!/usr/bin/env python3\n" + "import hashlib,json,pathlib,sys\n" + "manifest=pathlib.Path(sys.argv[2]).read_bytes()\n" + "stage=pathlib.Path(sys.argv[4]);stage.mkdir()\n" + "stats=b'[]\\n';validation=b'{\"status\":\"passed\"}\\n'\n" + "(stage/'stats.json').write_bytes(stats)\n" + "(stage/'validation-report.json').write_bytes(validation)\n" + "digest=lambda data:'sha256:'+hashlib.sha256(data).hexdigest()\n" + "result={'schema':'agentic-circuit-run-result','version':'0.1'," + "'contract_epoch':'0.3','run_manifest':{'path':'run-manifest.json'," + "'sha256':digest(manifest)},'status':'completed'," + "'termination_reason':'trace_drained','simulated_ticks':0," + "'domain_cycles':{},'event_count':0,'trace_position':{" + "'next_record_index':0,'last_committed_sequence_id':None}," + "'outputs':[{'path':'stats.json','sha256':digest(stats)}," + "{'path':'validation-report.json','sha256':digest(validation)}]," + "'validation':{'status':'passed','report_sha256':digest(validation)}}\n" + "(stage/'run-result.json').write_text(json.dumps(result," + "sort_keys=True,separators=(',',':'))+'\\n')\n" + ) + executable.chmod(0o755) + executable_bytes = executable.read_bytes() + manifest = build / "build-manifest.json" + manifest.write_text( + json.dumps( + { + "artifacts": [ + { + "path": "bin/model", + "kind": "executable", + "sha256": sha256_bytes(executable_bytes), + } + ] + }, + sort_keys=True, + separators=(",", ":"), + ) + + "\n" + ) + trace = root / "trace.json" + trace.write_bytes((FIXTURE / "trace.json").read_bytes()) + output = root / "runs/one" + publication = execute_run( + BuildPublication(build, executable, manifest, "sha256:" + "0" * 64, False), + RunOptions(trace, output, 1, None, None, (), "json", "disabled", "complete"), + ) + run_manifest = load_json(output / "run-manifest.json") + run_result = load_json(output / "run-result.json") + files = { + path.relative_to(output).as_posix() + for path in output.rglob("*") + if path.is_file() + } + + self.assertEqual(0, publication.exit_code) + validate_schema("run-manifest.schema.json", run_manifest) + validate_schema("run-result.schema.json", run_result) + self.assertEqual("completed", run_result["status"]) + self.assertEqual( + { + "bin/model", + "build-manifest.json", + "run-manifest.json", + "run-result.json", + "stats.json", + "trace.json", + "validation-report.json", + }, + files, + ) + + def test_tick_cap_is_incomplete_exit_seven(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + output = root / "runs/capped" + result = run_cli( + "run", + "capped_architecture.py", + "--trace", + "trace.json", + "--max-ticks", + "1", + "--event-log", + "jsonl", + "--output-dir", + "runs/capped", + cwd=root, + ) + run_result = load_json(output / "run-result.json") + event_log_exists = output.joinpath("events.jsonl").is_file() + + self.assertEqual(7, result.returncode, result.stderr) + self.assertEqual("incomplete", run_result["status"]) + self.assertEqual("max_ticks", run_result["termination_reason"]) + self.assertTrue(event_log_exists) + + def test_replay_uses_only_the_immutable_bundle(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + first = run_cli( + "run", + "architecture.py", + "--trace", + "trace.json", + "--output-dir", + "runs/one", + cwd=root, + ) + root.joinpath("architecture.py").unlink() + root.joinpath("trace.json").unlink() + replay = run_cli( + "run", + "--replay-manifest", + "runs/one/run-manifest.json", + "--output-dir", + "runs/two", + cwd=root, + ) + first_manifest = (root / "runs/one/run-manifest.json").read_bytes() + replay_manifest = (root / "runs/two/run-manifest.json").read_bytes() + + self.assertEqual(6, first.returncode, first.stderr) + self.assertEqual(6, replay.returncode, replay.stderr) + self.assertEqual(first_manifest, replay_manifest) + + def test_replay_rejects_ambient_override(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + result = run_cli( + "run", + "--replay-manifest", + "runs/one/run-manifest.json", + "--seed", + "2", + cwd=root, + ) + + self.assertEqual(2, result.returncode) + + def test_invalid_trace_is_preflight_five_and_preserves_output(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + output = root / "runs/one" + output.mkdir(parents=True) + output.joinpath("sentinel.txt").write_text("preserve\n") + root.joinpath("trace.json").write_text("{}\n") + result = run_cli( + "run", + "architecture.py", + "--trace", + "trace.json", + "--output-dir", + "runs/one", + cwd=root, + ) + + self.assertEqual("preserve\n", output.joinpath("sentinel.txt").read_text()) + self.assertEqual({"sentinel.txt"}, {path.name for path in output.iterdir()}) + + self.assertEqual(5, result.returncode, result.stderr) + + def test_raw_davincioo_jsonl_is_rejected_before_publication(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = workspace(temporary) + raw = root / "davincioo.jsonl" + raw.write_text( + '{"block_idx":0,"sequence_id":0,"opcode":"TASSIGN",' + '"input_tiles":[],"scalar_inputs":[],"output_tiles":[]}\n' + ) + result = run_cli( + "run", + "architecture.py", + "--trace", + raw.name, + "--output-dir", + "runs/raw", + cwd=root, + ) + + self.assertFalse((root / "runs/raw").exists()) + + self.assertEqual(5, result.returncode, result.stderr) + self.assertIn("ACTRACE-SCHEMA-001", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/cli/test_workspace.py b/components/agentic-circuit/tests/cli/test_workspace.py new file mode 100644 index 00000000..0fe72233 --- /dev/null +++ b/components/agentic-circuit/tests/cli/test_workspace.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from agentic_circuit._workspace import ( + UserInputError, + discover_workspace, + load_workspace, +) + + +FIXTURE = Path(__file__).parent / "fixtures" / "workspace" / "agentic-circuit.toml" + + +class WorkspaceTest(unittest.TestCase): + def test_discovers_upward_and_normalizes_paths(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "agentic-circuit.toml").write_bytes(FIXTURE.read_bytes()) + nested = root / "sources" / "nested" + nested.mkdir(parents=True) + + config = discover_workspace(nested) + + self.assertEqual(root.resolve(), config.root) + self.assertEqual(root.resolve() / "architecture.py", config.architecture) + self.assertEqual((root.resolve() / "components",), config.component_roots) + self.assertEqual((("max_events", 1000),), config.default_run_inputs) + + def test_explicit_manifest_is_loaded_without_discovery(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + manifest = root / "project" / "agentic-circuit.toml" + manifest.parent.mkdir() + manifest.write_bytes(FIXTURE.read_bytes()) + + config = load_workspace(manifest) + + self.assertEqual(manifest.parent.resolve(), config.root) + + def test_unknown_key_has_stable_diagnostic(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + manifest = Path(temporary) / "agentic-circuit.toml" + manifest.write_text(FIXTURE.read_text() + "\nunknown = true\n") + + with self.assertRaises(UserInputError) as caught: + load_workspace(manifest) + + self.assertEqual("ACPY-CONFIG-002", caught.exception.diagnostic.code) + + def test_paths_cannot_escape_the_workspace(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + manifest = Path(temporary) / "agentic-circuit.toml" + contents = FIXTURE.read_text().replace( + 'architecture = "architecture.py"', 'architecture = "../outside.py"' + ) + manifest.write_text(contents) + + with self.assertRaises(UserInputError) as caught: + load_workspace(manifest) + + self.assertEqual("ACPY-CONFIG-003", caught.exception.diagnostic.code) + + def test_epoch_and_duplicate_ownership_are_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + manifest = Path(temporary) / "agentic-circuit.toml" + contents = FIXTURE.read_text().replace( + 'contract_epoch = "0.3"', 'contract_epoch = "0.1"' + ) + manifest.write_text(contents) + with self.assertRaises(UserInputError): + load_workspace(manifest) + + contents = FIXTURE.read_text().replace( + 'protocol_roots = ["protocols"]', 'protocol_roots = ["components"]' + ) + manifest.write_text(contents) + with self.assertRaises(UserInputError) as caught: + load_workspace(manifest) + self.assertEqual("ACPY-CONFIG-004", caught.exception.diagnostic.code) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/contracts/__init__.py b/components/agentic-circuit/tests/contracts/__init__.py new file mode 100644 index 00000000..befedf1a --- /dev/null +++ b/components/agentic-circuit/tests/contracts/__init__.py @@ -0,0 +1 @@ +"""Repository contract tests.""" diff --git a/components/agentic-circuit/tests/contracts/test_contracts.py b/components/agentic-circuit/tests/contracts/test_contracts.py new file mode 100644 index 00000000..689c3a15 --- /dev/null +++ b/components/agentic-circuit/tests/contracts/test_contracts.py @@ -0,0 +1,967 @@ +import hashlib +import importlib.util +import json +import re +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +CONTRACT_EPOCH = "0.3" +LLVM_LOCK = { + "release": "22.1.8", + "upstream_commit": "ca7933e47d3a3451d81e72ac174dcb5aa28b59d1", + "source_url": ( + "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/" + "llvm-project-22.1.8.src.tar.xz" + ), + "source_sha256": "922f1817a0df7b1489272d18134ee0087a8b068828f87ac63b9861b1a9965888", + "local_prefix": "/opt/homebrew/opt/llvm", + "supported_host_triples": ["arm64-apple-darwin", "x86_64-linux-gnu"], + "package_version_policy": "exact", +} +GOVERNANCE_FILES = { + "LICENSE", + "CONTRIBUTING.md", + "CODE_OF_CONDUCT.md", + "SECURITY.md", + "SUPPORT.md", + "CHANGELOG.md", +} + + +def load_contract_checker(): + path = ROOT / "scripts/check-contracts.py" + spec = importlib.util.spec_from_file_location("contract_checker", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def initialize_markdown_fixture(files): + temporary_directory = tempfile.TemporaryDirectory() + root = Path(temporary_directory.name) + subprocess.run(["git", "init", "-q"], cwd=root, check=True) + for relative_path, content in files.items(): + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + subprocess.run(["git", "add", "."], cwd=root, check=True) + return temporary_directory, root + + +def assert_exact_ci_cache_commands(test_case, workflow): + fingerprint_step = re.search( + r" - name: Compute LLVM build fingerprint.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + verification_step = re.search( + r" - name: Verify exact LLVM build outputs.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + + for command in ( + "pwd -P", + "command -v c++", + "c++ --version", + "cmake --version", + "ninja --version", + "uname -m", + "ldd --version 2>&1 | head -n 1", + ): + test_case.assertRegex( + fingerprint_step, + rf"(?m)^\s+{re.escape(command)}$", + ) + + exact_verification_commands = ( + '.cache/llvm-build/bin/mlir-opt --version | grep -F "LLVM version ${LLVM_RELEASE}"', + 'grep -F "set(PACKAGE_VERSION \\"${LLVM_RELEASE}\\")" .cache/llvm-build/lib/cmake/llvm/LLVMConfigVersion.cmake', + 'grep -F "set(PACKAGE_VERSION \\"${LLVM_RELEASE}\\")" .cache/llvm-build/lib/cmake/mlir/MLIRConfigVersion.cmake', + 'grep -F "CMAKE_HOME_DIRECTORY:INTERNAL=${GITHUB_WORKSPACE}/.cache/llvm-project-${LLVM_RELEASE}.src/llvm" .cache/llvm-build/CMakeCache.txt', + 'grep -F "CMAKE_CACHEFILE_DIR:INTERNAL=${GITHUB_WORKSPACE}/.cache/llvm-build" .cache/llvm-build/CMakeCache.txt', + ) + for command in exact_verification_commands: + test_case.assertRegex( + verification_step, + rf"(?m)^\s+{re.escape(command)}$", + ) + + +class RepositoryContractsTest(unittest.TestCase): + def test_release_layout_and_ndf_profile_are_closed(self): + for script in ("check-release-layout.py", "check-ndf.py"): + with self.subTest(script=script): + completed = subprocess.run( + [sys.executable, ROOT / "scripts" / script], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + + def test_high_level_block_spec_is_closed_and_provider_complete(self): + from jsonschema import Draft202012Validator + + schema = json.loads((ROOT / "schemas/block-spec.schema.json").read_text()) + catalog = json.loads((ROOT / "schemas/blocks.json").read_text()) + Draft202012Validator(schema).validate(catalog) + blocks = catalog["blocks"] + self.assertEqual( + sorted(block["name"] for block in blocks), + [block["name"] for block in blocks], + ) + self.assertEqual( + [ + "barrier", + "compute", + "engine", + "fork", + "merge", + "pipeline", + "reorder", + "route", + "schedule", + "table", + ], + [block["name"] for block in blocks], + ) + for block in blocks: + self.assertEqual(f'ac.{block["name"]}', block["operation"]) + self.assertTrue(block["providers"]["cpp"]["optimized"]) + self.assertTrue(block["providers"]["verilog"]["optimized"]) + self.assertTrue(block["refinement_observations"]) + self.assertTrue( + all(port["ownership"] == "borrowed_simqueue" for port in block["ports"]) + ) + lambda_blocks = [block["name"] for block in blocks if block["lambda_regions"]] + self.assertEqual(["compute"], lambda_blocks) + + def test_official_opcode_catalog_is_closed_backend_complete_and_standard(self): + from jsonschema import Draft202012Validator + + schema = json.loads( + (ROOT / "schemas/opcode-catalog.schema.json").read_text() + ) + catalog = json.loads((ROOT / "schemas/opcodes.json").read_text()) + Draft202012Validator(schema).validate(catalog) + expected = { + "ac.barrier", + "ac.broadcast", + "ac.credit", + "ac.dependency", + "ac.expect", + "ac.feedback", + "ac.fork", + "ac.merge", + "ac.memory.instance", + "ac.memory.request", + "ac.observe", + "ac.reorder", + "ac.route", + "ac.scope", + "ac.select", + "ac.sink", + "ac.source", + "ac.transform", + } + operations = [entry["operation"] for entry in catalog["entries"]] + self.assertEqual(sorted(expected), operations) + self.assertEqual(len(operations), len(set(operations))) + forbidden = {"decode", "dispatch", "rename", "retire"} + for entry in catalog["entries"]: + operation_kind = entry["operation"].removeprefix("ac.").replace(".", "_") + self.assertEqual(entry["kind"], operation_kind) + self.assertTrue(entry["gfsim"]["available"]) + if entry["role"] == "design": + self.assertTrue(entry["pyc"]["available"]) + self.assertTrue(entry["gfsim"]["realization"]) + self.assertTrue(entry["pyc"]["realization"]) + self.assertTrue(entry["refinement_observations"]) + self.assertNotIn(entry["kind"], forbidden) + maximum_inputs = entry["inputs"]["max"] + maximum_outputs = entry["outputs"]["max"] + if maximum_inputs is not None: + self.assertGreaterEqual(maximum_inputs, entry["inputs"]["min"]) + if maximum_outputs is not None: + self.assertGreaterEqual(maximum_outputs, entry["outputs"]["min"]) + roles = {entry["operation"]: entry["role"] for entry in catalog["entries"]} + self.assertEqual("observation", roles["ac.observe"]) + self.assertEqual("verification", roles["ac.expect"]) + self.assertTrue( + all( + role == "design" + for operation, role in roles.items() + if operation not in {"ac.observe", "ac.expect"} + ) + ) + + def test_diagnostic_explanation_catalog_is_closed_and_versioned(self): + path = ROOT / "resources/diagnostics.json" + self.assertTrue(path.is_file()) + document = json.loads(path.read_text()) + self.assertEqual( + {"schema", "version", "contract_epoch", "entries"}, set(document) + ) + self.assertEqual("agentic-circuit-diagnostic-catalog", document["schema"]) + self.assertEqual("0.1", document["version"]) + self.assertEqual(CONTRACT_EPOCH, document["contract_epoch"]) + codes = [] + for entry in document["entries"]: + self.assertEqual( + {"code", "title", "rule", "causes", "examples", "repairs"}, + set(entry), + ) + self.assertRegex( + entry["code"], + r"^AC(PY|ELAB|IR-[A-Z]+|LOWER|BUILD|TRACE|RUN)-[A-Z0-9-]+$", + ) + for key in ("causes", "examples", "repairs"): + self.assertTrue(entry[key]) + self.assertTrue( + all(isinstance(item, str) and item for item in entry[key]) + ) + codes.append(entry["code"]) + self.assertEqual(sorted(codes), codes) + self.assertEqual(len(codes), len(set(codes))) + + def test_minimal_acpy_fixture_matches_the_public_schema(self): + self.assertIsNotNone(importlib.util.find_spec("jsonschema")) + from jsonschema.validators import Draft202012Validator + + schema = json.loads((ROOT / "schemas/acpy.schema.json").read_text()) + fixture = json.loads( + ( + ROOT + / "tests/python_frontend/fixtures/acpy/minimal.acpy.json" + ).read_text() + ) + + Draft202012Validator(schema).validate(fixture) + + def test_ci_runs_address_and_undefined_sanitizer_presets(self): + workflow = (ROOT / ".github/workflows/ci.yml").read_text() + sanitizer_job = re.search(r"(?ms)^ sanitizers:\n.*\Z", workflow) + self.assertIsNotNone(sanitizer_job, "CI lacks sanitizer job legs") + job = sanitizer_job.group() + self.assertIn("needs: contracts-and-configure", job) + self.assertIn('preset: ["asan-llvm22", "ubsan-llvm22"]', job) + self.assertIn("fail-on-cache-miss: true", job) + self.assertIn('cmake --preset "${{ matrix.preset }}"', job) + self.assertIn('cmake --build --preset "${{ matrix.preset }}"', job) + self.assertIn("--target GfsimTests CodeGenTests", job) + self.assertIn('ctest --test-dir "build/${{ matrix.preset }}"', job) + self.assertIn("--output-on-failure -R '^(GfsimTests|CodeGenTests)$'", job) + self.assertIn("ASAN_OPTIONS", job) + self.assertIn("UBSAN_OPTIONS", job) + self.assertIn("halt_on_error=1", job) + + def test_ci_runs_frontend_on_every_supported_python_minor(self): + workflow = (ROOT / ".github/workflows/ci.yml").read_text() + frontend_job = re.search( + r"(?ms)^ python-frontend:\n.*?(?=^ [a-z][a-z0-9-]*:\n|\Z)", + workflow, + ) + self.assertIsNotNone(frontend_job, "CI lacks the Python frontend matrix") + job = frontend_job.group() + self.assertIn('python-version: ["3.11", "3.12", "3.13"]', job) + self.assertIn("python-version: ${{ matrix.python-version }}", job) + self.assertIn("python -m unittest discover -s tests/python_frontend -v", job) + self.assertIn( + "PYTHONHASHSEED=1 python -m unittest " + "tests.python_frontend.test_determinism -v", + job, + ) + self.assertIn( + "PYTHONHASHSEED=99 python -m unittest " + "tests.python_frontend.test_determinism -v", + job, + ) + + def test_ci_caches_verified_llvm_sources_and_exact_build_outputs(self): + workflow = (ROOT / ".github/workflows/ci.yml").read_text() + assert_exact_ci_cache_commands(self, workflow) + source_cache_step = re.search( + r" - name: Cache content-addressed LLVM source archive.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + build_cache_restore_step = re.search( + r" - name: Restore exact LLVM build outputs.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + build_cache_save_step = re.search( + r" - name: Save exact LLVM build outputs.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + fingerprint_step = re.search( + r" - name: Compute LLVM build fingerprint.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + verification_step = re.search( + r" - name: Fetch and verify locked LLVM source.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + build_step = re.search( + r" - name: Build locked MLIR package.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + + self.assertRegex( + source_cache_step, + r"(?m)^\s+path: \.cache/llvm-project-22\.1\.8\.src\.tar\.xz$", + ) + self.assertEqual(1, source_cache_step.count(".cache/"), source_cache_step) + self.assertNotRegex(verification_step, r"(?m)^\s+if:") + self.assertIn("sha256sum --check --strict", verification_step) + expected_build_cache_key = ( + "key: llvm-build-${{ runner.os }}-${{ runner.arch }}-" + "${{ env.LLVM_SOURCE_SHA256 }}-" + "${{ hashFiles('toolchains/llvm.lock.json', " + "'scripts/ci-build-llvm.sh') }}-" + "${{ steps.llvm-build-fingerprint.outputs.value }}" + ) + self.assertIn( + "uses: actions/cache/restore@" + "5a3ec84eff668545956fd18022155c47e93e2684", + build_cache_restore_step, + ) + self.assertIn("path: .cache/llvm-build", build_cache_restore_step) + self.assertIn(expected_build_cache_key, build_cache_restore_step) + self.assertIn( + "uses: actions/cache/save@" + "5a3ec84eff668545956fd18022155c47e93e2684", + build_cache_save_step, + ) + self.assertIn("path: .cache/llvm-build", build_cache_save_step) + self.assertIn(expected_build_cache_key, build_cache_save_step) + # The hour-long LLVM build must be cached even when a later test step + # fails, but a partially built tree must never be cached. + self.assertIn("if: always()", build_cache_save_step) + self.assertIn( + "steps.llvm-build-cache.outputs.cache-hit != 'true'", + build_cache_save_step, + ) + self.assertIn( + "steps.llvm-build.conclusion == 'success'", + build_cache_save_step, + ) + for fingerprint_input in ( + "pwd -P", + "command -v c++", + "c++ --version", + "cmake --version", + "ninja --version", + "ldd --version", + ): + self.assertIn(fingerprint_input, fingerprint_step) + self.assertIn("steps.llvm-build-cache.outputs.cache-hit != 'true'", build_step) + + native_dependencies = re.search( + r" - name: Install native test dependencies.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + self.assertRegex( + native_dependencies, + r"sudo apt-get install --yes --no-install-recommends libgtest-dev", + ) + + build_verification = re.search( + r" - name: Verify exact LLVM build outputs.*?(?=\n - name:)", + workflow, + re.DOTALL, + ).group() + self.assertNotRegex(build_verification, r"(?m)^\s+if:") + self.assertIn("test -x .cache/llvm-build/bin/mlir-opt", build_verification) + self.assertIn( + "test -f .cache/llvm-build/lib/cmake/llvm/LLVMConfigVersion.cmake", + build_verification, + ) + self.assertIn( + "test -f .cache/llvm-build/lib/cmake/mlir/MLIRConfigVersion.cmake", + build_verification, + ) + self.assertIn( + 'mlir-opt --version | grep -F "LLVM version ${LLVM_RELEASE}"', + build_verification, + ) + self.assertEqual( + 2, + build_verification.count( + 'grep -F "set(PACKAGE_VERSION \\"${LLVM_RELEASE}\\")"' + ), + build_verification, + ) + self.assertIn("CMAKE_HOME_DIRECTORY:INTERNAL=${GITHUB_WORKSPACE}", build_verification) + self.assertIn("CMAKE_CACHEFILE_DIR:INTERNAL=${GITHUB_WORKSPACE}", build_verification) + + def test_ci_cache_contract_rejects_inert_or_misdirected_checks(self): + workflow = (ROOT / ".github/workflows/ci.yml").read_text() + mutations = ( + (" c++ --version", " echo 'c++ --version'"), + ( + 'grep -F "set(PACKAGE_VERSION \\"${LLVM_RELEASE}\\")" .cache/llvm-build/lib/cmake/mlir/MLIRConfigVersion.cmake', + 'grep -F "set(PACKAGE_VERSION \\"${LLVM_RELEASE}\\")" .cache/llvm-build/lib/cmake/llvm/LLVMConfigVersion.cmake', + ), + ( + "CMAKE_HOME_DIRECTORY:INTERNAL=${GITHUB_WORKSPACE}/.cache/llvm-project-${LLVM_RELEASE}.src/llvm", + "CMAKE_HOME_DIRECTORY:INTERNAL=${GITHUB_WORKSPACE}/wrong-source", + ), + ( + "CMAKE_CACHEFILE_DIR:INTERNAL=${GITHUB_WORKSPACE}/.cache/llvm-build", + "CMAKE_CACHEFILE_DIR:INTERNAL=${GITHUB_WORKSPACE}/wrong-build", + ), + ) + for original, replacement in mutations: + with self.subTest(original=original): + self.assertIn(original, workflow) + mutated = workflow.replace(original, replacement, 1) + with self.assertRaises(AssertionError): + assert_exact_ci_cache_commands(self, mutated) + + def test_cmake_package_versions_require_exact_match(self): + cmake = (ROOT / "CMakeLists.txt").read_text() + package_version = re.search( + r"write_basic_package_version_file\(.*?\n\)", cmake, re.DOTALL + ).group() + self.assertIn("COMPATIBILITY ExactVersion", package_version) + + def test_governance_files_are_present_and_nonempty(self): + missing = sorted( + path for path in GOVERNANCE_FILES if not (ROOT / path).is_file() + ) + empty = sorted( + path + for path in GOVERNANCE_FILES + if (ROOT / path).is_file() and not (ROOT / path).read_text().strip() + ) + self.assertEqual([], missing, f"missing governance files: {missing}") + self.assertEqual([], empty, f"empty governance files: {empty}") + + def test_all_machine_contracts_use_the_exact_epoch(self): + schema_epochs = {} + for path in sorted((ROOT / "schemas").glob("*.schema.json")): + document = json.loads(path.read_text()) + properties = document.get("properties", {}) + epoch = properties.get("contract_epoch", {}).get("const") + schema_epochs[path.name] = epoch + + pyproject = (ROOT / "pyproject.toml").read_text() + declared_epoch = re.search( + r'^contract-epoch\s*=\s*"([^"]+)"\s*$', pyproject, re.MULTILINE + ) + + self.assertEqual(12, len(schema_epochs)) + self.assertEqual( + {CONTRACT_EPOCH}, set(schema_epochs.values()), schema_epochs + ) + self.assertIsNotNone(declared_epoch, "pyproject.toml lacks contract-epoch") + self.assertEqual(CONTRACT_EPOCH, declared_epoch.group(1)) + + def test_all_json_schemas_compile_as_draft_2020_12(self): + self.assertIsNotNone( + importlib.util.find_spec("jsonschema"), + "install the locked development requirements before running contracts", + ) + from jsonschema.validators import Draft202012Validator + + checked = [] + for path in sorted((ROOT / "schemas").glob("*.schema.json")): + document = json.loads(path.read_text()) + self.assertEqual( + "https://json-schema.org/draft/2020-12/schema", + document.get("$schema"), + path.name, + ) + Draft202012Validator.check_schema(document) + checked.append(path.name) + self.assertEqual(12, len(checked), checked) + + def test_trace_source_decoder_uses_the_runtime_decoder_concept(self): + record = json.loads((ROOT / "schemas/stdlib/TraceSource.json").read_text()) + parameters = { + parameter["name"]: parameter for parameter in record["static_parameters"] + } + + self.assertEqual( + "gfsim::TraceDecoder", + parameters["Decoder"]["constraint"], + ) + + def test_process_state_plan_schema_is_closed_and_accepts_exact_baseline(self): + self.assertIsNotNone(importlib.util.find_spec("jsonschema")) + from jsonschema import ValidationError + from jsonschema.validators import Draft202012Validator + + schema = json.loads( + (ROOT / "schemas/acir-process-state-plan.schema.json").read_text() + ) + validator = Draft202012Validator(schema) + occurrence = { + "call_sites": [], + "iteration_vector": [], + "kind": "original", + "operation_path": "@Top::@workload/r0/b0/o0", + } + descriptor = { + "cpp": "acir::generated::impl_wake_next_delta_28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c", + "effect": "stateful", + "fingerprint": "sha256:28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c", + "inputs": [], + "kind": "implementation", + "ordinal": 0, + "payload": {"wake_kind": "next_delta", "wake_type": "@acir_wake_next_delta"}, + "results": ["@acir_wake_next_delta"], + "role": "wake_next_delta", + "source_paths": [], + "symbol": "@acir_impl_wake_next_delta_28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c", + } + fixture = { + "callees": [descriptor], + "contract_epoch": "0.3", + "processes": [{ + "blocks": [{"actions": [], "cost": 2, "edge": {"kind": "suspend", "transition": 0}, "frames": [], "loads": [], "ordinal": 0, "path": "@Top::@workload/plan/pc/entry/b00000000", "pc": 0}], + "captures": [], "definition_key": "@Top::@workload", "entry_pc": 0, + "fairness_work": 2, "live_slots": [], "pc_bit_width": 1, + "pcs": [{"blocks": [0], "entry_path": "@Top::@workload/plan/pc/entry/b00000000", "name": "entry", "ordinal": 0}], + "transitions": [{"iteration_vector": [], "loads": [], "ordinal": 0, "source_pc": 0, "stores": [], "target_pc": 0, "wake": 0}], + "wakes": [{"callee": 0, "iteration_vector": [], "kind": "next_delta", "occurrence": occurrence, "operation_path": "@Top::@workload/r0/b0/o0", "ordinal": 0, "sources": [], "target": "", "type_key": "@acir_wake_next_delta"}], + }], + "schema": "acir-process-state-plan-0.1", + "value_types": [], + } + validator.validate(fixture) + + def validate_definition(name, value): + Draft202012Validator({ + "$schema": schema["$schema"], + "$ref": f"#/$defs/{name}", + "$defs": schema["$defs"], + }).validate(value) + + loop_occurrence = {"anchor": occurrence, "kind": "synthetic", "phase": "condition"} + wrapper_occurrence = {"anchor": occurrence, "direction": "wrap", "kind": "synthetic", "slot": 0, "transition": 0} + constant_occurrence = {"anchor": occurrence, "constant": 7, "kind": "synthetic"} + for arm in (occurrence, loop_occurrence, wrapper_occurrence, constant_occurrence): + validate_definition("occurrence", arm) + + coordinate = {"index": 0, "kind": "result", "owner_path": "@Top::@workload/r0/b0/o0"} + planned_arms = ( + {"coordinate": coordinate, "kind": "original", "occurrence": occurrence, "path": "@Top::@workload/r0/b0/o0/r0"}, + {"capture": 0, "kind": "capture"}, + {"kind": "live_slot", "slot": 0}, + {"coordinate": coordinate, "kind": "synthetic", "occurrence": loop_occurrence}, + {"kind": "constant", "value": "0"}, + ) + for arm in planned_arms: + validate_definition("planned_value", {**arm, "type": "i32"}) + + binding = {"from": {**planned_arms[-1], "type": "i32"}, "to": {**planned_arms[-1], "type": "i32"}} + edge_arms = ( + {"condition": {**planned_arms[-1], "type": "i1"}, "false_bindings": [], "false_block": 1, "kind": "branch", "true_bindings": [binding], "true_block": 0}, + {"bindings": [binding], "kind": "local_continue", "target_block": 0}, + {"kind": "suspend", "transition": 0}, + {"kind": "terminate", "status": "success"}, + ) + for arm in edge_arms: + validate_definition("edge", arm) + for kind, phases in (("entry", ("entry",)), ("scf.if", ("then", "else", "merge")), ("scf.for", ("header", "body", "exit")), ("scf.while", ("before", "after", "exit"))): + for phase in phases: + validate_definition("frame", {"bindings": [], "kind": kind, "operation_path": "@Top::@workload/r0/b0/o0", "phase": phase}) + + action_base = {"cost": 1, "iteration_vector": [], "kind": "original", "occurrence": occurrence, "operands": [], "ordinal": 0, "result_types": [], "results": []} + for emission in ("inline", "invoke", "wrap", "unwrap"): + validate_definition("action", {**action_base, "callee": 0, "emission": emission}) + validate_definition("action", {**action_base, "emission": "copy_scalar", "scalar_op": {"attributes": [], "name": "arith.constant", "properties": "{}"}}) + validate_definition("action", {**action_base, "cost": 0, "emission": "forward_only"}) + validate_definition("live_slot", {"member_values": [], "name": "live00000000", "ordinal": 0, "storage_type": 0, "type": "i32"}) + validate_definition("live_slot", {"member_values": [], "name": "live00000000", "ordinal": 0, "storage_type": 0, "type": "i32", "unwrap_callee": 1, "wrap_callee": 0}) + + payloads = { + "record_create": {"fields": [{"name": "x", "type_key": "mlir:i32"}], "record_type": "mlir:!ac.record"}, + "record_get": {"field": "x", "record": "mlir:!ac.record", "result": "mlir:i32"}, + "record_with": {"field": "x", "record": "mlir:!ac.record", "value": "mlir:i32"}, + "packet_serialize": {"bytes": 4, "packet": "@packet", "packet_type": "mlir:!ac.packet"}, + "packet_deserialize": {"bytes": 4, "packet": "@packet", "packet_type": "mlir:!ac.packet"}, + "trace_decode": {"entry": "mlir:i32", "result": "mlir:i64", "source": "trace"}, + "queue_try_send": {"element": "mlir:i32", "queue": "@queue"}, + "queue_try_recv": {"element": "mlir:i32", "queue": "@queue"}, + "event_schedule": {"delay": "mlir:i64", "target": "@event", "value": "mlir:i32"}, + "trace_open": {"source": "trace"}, "trace_next": {"entry": "mlir:i32", "source": "trace"}, + "trace_eof": {"source": "trace"}, "trace_position": {"source": "trace"}, + "contract_require": {"message": "required"}, "contract_ensure": {"message": "ensured"}, "contract_assert": {"message": "asserted"}, + "probe": {"kind": "counter", "result": "mlir:i64", "target": "@probe"}, + "stat_add": {"stat": "@stat", "value_type": "mlir:i64"}, + "wake_condition": {"wake_kind": "condition", "wake_type": "@acir_wake_condition"}, + "wake_resource": {"wake_kind": "resource", "wake_type": "@acir_wake_resource"}, + "wake_event_queue": {"wake_kind": "event_queue", "wake_type": "@acir_wake_event_queue"}, + "wake_next_delta": {"wake_kind": "next_delta", "wake_type": "@acir_wake_next_delta"}, + "scalar_wrap": {"direction": "wrap", "scalar": "mlir:i32", "value_type": "storage:value:" + "0" * 64}, + "scalar_unwrap": {"direction": "unwrap", "scalar": "mlir:i32", "value_type": "storage:value:" + "0" * 64}, + } + pure_roles = {"record_create", "record_get", "record_with", "packet_serialize", "packet_deserialize", "trace_decode", "scalar_wrap", "scalar_unwrap"} + for ordinal, (role, payload) in enumerate(payloads.items()): + candidate = {**descriptor, "ordinal": ordinal, "role": role, "payload": payload, "effect": "pure" if role in pure_roles else "stateful"} + if role == "wake_next_delta": + candidate.update(inputs=[], results=["@acir_wake_next_delta"]) + validate_definition("callee", candidate) + + value_identity = "0" * 64 + value_base = {"acir_type": "i32", "cpp": "acir::generated::value_" + value_identity, "fingerprint": "sha256:" + value_identity, "ordinal": 0, "symbol": "@acir_value_" + value_identity} + validate_definition("value_type", {**value_base, "kind": "value", "payload": {"encoding": "i32", "members": [], "width_bits": 32}}) + validate_definition("value_type", {**value_base, "kind": "packet", "payload": {"bytes": 4, "encoding": "array<4xi8>", "members": [], "width_bits": 32}}) + + mismatched_role = json.loads(json.dumps(fixture)) + mismatched_role["callees"][0]["role"] = "record_create" + with self.assertRaises(ValidationError): + validator.validate(mismatched_role) + + mismatched_effect = json.loads(json.dumps(fixture)) + mismatched_effect["callees"][0].update({ + "role": "record_create", + "payload": {"fields": [], "record_type": "mlir:i32"}, + "results": ["mlir:i32"], + }) + with self.assertRaises(ValidationError): + validator.validate(mismatched_effect) + + unpaired_wrapper = json.loads(json.dumps(fixture)) + unpaired_wrapper["processes"][0]["live_slots"] = [{ + "member_values": [], + "name": "live00000000", + "ordinal": 0, + "storage_type": 0, + "type": "i32", + "wrap_callee": 0, + }] + with self.assertRaises(ValidationError): + validator.validate(unpaired_wrapper) + + missing_scalar_op = json.loads(json.dumps(fixture)) + missing_scalar_op["processes"][0]["blocks"][0]["actions"] = [{ + "cost": 1, + "emission": "copy_scalar", + "iteration_vector": [], + "kind": "original", + "occurrence": occurrence, + "operands": [], + "ordinal": 0, + "result_types": [], + "results": [], + }] + with self.assertRaises(ValidationError): + validator.validate(missing_scalar_op) + + branch = schema["$defs"]["edge_branch"] + self.assertEqual( + {"condition", "false_bindings", "false_block", "kind", "true_bindings", "true_block"}, + set(branch["properties"]), + ) + self.assertEqual(set(branch["properties"]), set(branch["required"])) + + object_definitions = [schema] + object_definitions.extend( + value for value in schema["$defs"].values() + if isinstance(value, dict) and value.get("type") == "object" + ) + for object_schema in object_definitions: + self.assertIs(False, object_schema.get("additionalProperties")) + + def reject_unknown(path): + mutated = json.loads(json.dumps(fixture)) + node = mutated + for key in path: + node = node[key] + node["unknown"] = True + with self.assertRaises(ValidationError): + validator.validate(mutated) + + for path in ( + (), ("callees", 0), ("callees", 0, "payload"), + ("processes", 0), ("processes", 0, "blocks", 0), + ("processes", 0, "blocks", 0, "edge"), + ("processes", 0, "pcs", 0), + ("processes", 0, "transitions", 0), + ("processes", 0, "wakes", 0), + ("processes", 0, "wakes", 0, "occurrence"), + ): + reject_unknown(path) + + def test_acsim_binding_schema_is_closed_and_accepts_only_lock_records(self): + self.assertIsNotNone( + importlib.util.find_spec("jsonschema"), + "install the locked development requirements before running contracts", + ) + from jsonschema import ValidationError + from jsonschema.validators import Draft202012Validator + + schema = json.loads( + (ROOT / "schemas/acsim-binding.schema.json").read_text() + ) + registry = json.loads( + (ROOT / "test/Bindings/Inputs/leaf-fast.json").read_text() + ) + candidate = registry["candidates"][0] + record = candidate["record"] + validator = Draft202012Validator(schema) + validator.validate(record) + unit_record = json.loads(json.dumps(record)) + unit_record["parameters"][0]["value"] = {"unit": "cycles", "value": 4} + unit_record["construction"]["arguments"][0] = { + "unit": "cycles", + "value": 4, + } + validator.validate(unit_record) + + stateful_duplicate_name = json.loads(json.dumps(record)) + stateful_duplicate_name["effect"] = "stateful" + stateful_duplicate_name["cpp"]["entry_points"].update( + {"pure": "", "work": "gfsim::work", "xfer": "gfsim::xfer"} + ) + stateful_duplicate_name["ownership"] = { + "kind": "unique", + "placement": "member_or_array", + } + stateful_duplicate_name["activation_sources"] = [ + {"kind": "ac.Clock", "name": "wake"}, + {"kind": "ac.Reset", "name": "wake"}, + ] + # Draft 2020-12 cannot state unique-by-name. The schema accepts this + # structurally; BindingRecord semantic validation rejects it. + validator.validate(stateful_duplicate_name) + identical_activation = json.loads(json.dumps(stateful_duplicate_name)) + identical_activation["activation_sources"][1] = dict( + identical_activation["activation_sources"][0] + ) + with self.assertRaises(ValidationError): + validator.validate(identical_activation) + + def assert_closed_objects(fragment, path="#"): + if isinstance(fragment, list): + for index, value in enumerate(fragment): + assert_closed_objects(value, f"{path}/{index}") + return + if not isinstance(fragment, dict): + return + if fragment.get("type") == "object": + self.assertIs( + False, + fragment.get("additionalProperties"), + f"open object schema at {path}", + ) + for key, value in fragment.items(): + assert_closed_objects(value, f"{path}/{key}") + + assert_closed_objects(schema) + + mutations = [] + unknown = dict(record) + unknown["emitter_callback"] = "emitLeaf" + mutations.append(unknown) + unavailable = dict(record) + unavailable["availability"] = "unavailable" + mutations.append(unavailable) + wrong_epoch = dict(record) + wrong_epoch["contract_epoch"] = "0.1" + mutations.append(wrong_epoch) + wrong_schema = dict(record) + wrong_schema["binding_schema"] = "acsim-binding-0.2" + mutations.append(wrong_schema) + wrong_cardinality = dict(record) + wrong_cardinality["ports"] = [dict(record["ports"][0])] + wrong_cardinality["ports"][0]["cardinality"] = "many" + mutations.append(wrong_cardinality) + raw_parameter_type = json.loads(json.dumps(record)) + raw_parameter_type["parameters"][0]["cpp_type"] = "int; emit()" + mutations.append(raw_parameter_type) + invalid_static_key = json.loads(json.dumps(unit_record)) + invalid_static_key["parameters"][0]["value"] = {"not-valid": 4} + mutations.append(invalid_static_key) + for mutation in mutations: + with self.assertRaises(ValidationError): + validator.validate(mutation) + + def test_markdown_local_links_resolve(self): + broken = [] + link_pattern = re.compile(r"(? {target}") + self.assertEqual([], broken, f"broken Markdown links: {broken}") + + def test_checker_scans_tracked_markdown_and_image_targets(self): + temporary_directory, root = initialize_markdown_fixture( + { + "README.md": "# Fixture\n", + "nested/guide.md": "# Guide\n\n![missing](missing.png)\n", + } + ) + self.addCleanup(temporary_directory.cleanup) + checker = load_contract_checker() + checker.ROOT = root + errors = [] + + checker.check_links(errors) + + self.assertTrue(errors, "tracked Markdown image target was not checked") + self.assertIn("nested/guide.md -> missing.png", errors[0]) + + def test_checker_rejects_missing_markdown_fragments(self): + temporary_directory, root = initialize_markdown_fixture( + { + "README.md": "# Fixture\n\n[section](guide.md#missing-heading)\n", + "guide.md": "# Existing Heading\n", + } + ) + self.addCleanup(temporary_directory.cleanup) + checker = load_contract_checker() + checker.ROOT = root + errors = [] + + checker.check_links(errors) + + self.assertTrue(errors, "missing Markdown fragment was not checked") + self.assertIn("guide.md#missing-heading", errors[0]) + + def test_checker_ignores_links_in_fenced_and_indented_code(self): + temporary_directory, root = initialize_markdown_fixture( + { + "README.md": ( + "# Fixture\n\n" + "```markdown\n" + "[fenced](missing-fenced.txt)\n" + "![fenced image](missing-fenced.png)\n" + "```\n\n" + " [indented](missing-indented.txt)\n" + ), + } + ) + self.addCleanup(temporary_directory.cleanup) + checker = load_contract_checker() + checker.ROOT = root + errors = [] + + checker.check_links(errors) + + self.assertEqual([], errors, f"code example links were checked: {errors}") + + def test_code_example_headings_do_not_satisfy_fragments(self): + temporary_directory, root = initialize_markdown_fixture( + { + "README.md": "# Fixture\n\n[section](guide.md#example-heading)\n", + "guide.md": ( + "# Guide\n\n" + "```markdown\n" + "# Example Heading\n" + "```\n" + ), + } + ) + self.addCleanup(temporary_directory.cleanup) + checker = load_contract_checker() + checker.ROOT = root + errors = [] + + checker.check_links(errors) + + self.assertTrue(errors, "code example heading satisfied a real fragment") + self.assertIn("guide.md#example-heading", errors[0]) + + def test_repository_has_no_placeholder_markers(self): + offenders = [] + marker = re.compile(r"\b(?:TODO|TBD|FIXME|XXX)\b") + checked = [ROOT / "README.md"] + checked.extend(ROOT / path for path in GOVERNANCE_FILES) + for path in checked: + if path.is_file() and marker.search(path.read_text()): + offenders.append(str(path.relative_to(ROOT))) + readme = (ROOT / "README.md").read_text() + if "proposed Python" in readme or "No implementation contract is approved yet" in readme: + offenders.append("README.md (stale specification-phase placeholder)") + self.assertEqual([], offenders, f"placeholder content: {offenders}") + + def test_davincioo_reference_snapshot_is_provenance_locked(self): + reference = ROOT / "references/davincioo-gfsim" + source = json.loads((reference / "SOURCE.json").read_text()) + self.assertEqual( + "agentic-circuit-reference-source@0.1", source.get("schema") + ) + self.assertEqual( + "https://github.com/hengliao1972/DavinciOO.git", + source.get("repository"), + ) + self.assertEqual( + "a542b9cf705096288c615575be222b974b570a18", + source.get("commit"), + ) + self.assertEqual("model", source.get("subtree")) + self.assertEqual("unresolved", source.get("license_status")) + + manifest = {} + for line in (reference / "UPSTREAM_FILES.sha256").read_text().splitlines(): + digest, separator, path = line.partition(" ") + self.assertEqual(" ", separator) + self.assertRegex(digest, r"^[0-9a-f]{64}$") + self.assertNotIn(path, manifest) + manifest[path] = digest + + upstream = reference / "upstream" + actual = { + path.relative_to(reference).as_posix() + for path in upstream.rglob("*") + if path.is_file() + } + self.assertEqual(actual, set(manifest)) + for relative, expected in manifest.items(): + digest = hashlib.sha256((reference / relative).read_bytes()).hexdigest() + self.assertEqual(expected, digest, relative) + + def test_llvm_lock_is_exact_and_complete(self): + lock = json.loads((ROOT / "toolchains/llvm.lock.json").read_text()) + self.assertEqual(1, lock.get("lock_version")) + self.assertEqual(LLVM_LOCK, lock.get("llvm")) + + def test_read_only_checker_accepts_the_repository(self): + before = subprocess.run( + ["git", "status", "--porcelain=v1"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout + result = subprocess.run( + [sys.executable, "scripts/check-contracts.py"], + cwd=ROOT, + capture_output=True, + text=True, + ) + after = subprocess.run( + ["git", "status", "--porcelain=v1"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertEqual(before, after, "contract checker modified the repository") + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/contracts/test_ir_coverage.py b/components/agentic-circuit/tests/contracts/test_ir_coverage.py new file mode 100644 index 00000000..8a982b8c --- /dev/null +++ b/components/agentic-circuit/tests/contracts/test_ir_coverage.py @@ -0,0 +1,264 @@ +import importlib.util +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def load_coverage_checker(): + path = ROOT / "scripts/check-ir-coverage.py" + spec = importlib.util.spec_from_file_location("coverage_checker", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +ACIR_MANIFEST = """schema: acir-ir-inventory +contract_epoch: "0.3" +dialect: acir +operations: + - ac.system +types: + - struct +""" + +ACSIM_MANIFEST = """schema: acir-ir-inventory +contract_epoch: "0.3" +dialect: acsim +operations: + - acsim.model +types: + - value +""" + +FIXTURE_FILES = { + "contracts/acir.yaml": ACIR_MANIFEST, + "contracts/acsim.yaml": ACSIM_MANIFEST, + "include/acir/Dialect/ACIR/ACIROps.td": ( + 'def ACIR_SystemOp : ACIR_Op<"system", [Symbol]> {\n}\n' + ), + "include/acir/Dialect/ACIR/ACIRTypes.td": ( + 'def ACIR_StructType : ACIR_LayoutNamedType<"Struct", "struct">;\n' + ), + "include/acir/Dialect/ACSim/ACSimOps.td": ( + 'def ACSim_ModelOp : ACSim_Op<"model", [Symbol]> {\n}\n' + ), + "include/acir/Dialect/ACSim/ACSimTypes.td": ( + 'def ACSim_ValueType : ACSim_SymbolType<"Value", "value">;\n' + ), + "lib/Dialect/ACIR/ACIRTypes.cpp": ( + "void initialize() {\n" + " addTypes<\n" + '#include "acir/Dialect/ACIR/ACIRTypes.cpp.inc"\n' + " >();\n" + " addOperations<\n" + '#include "acir/Dialect/ACIR/ACIROps.cpp.inc"\n' + " >();\n" + "}\n" + ), + "lib/Dialect/ACSim/ACSimTypes.cpp": ( + "void initialize() {\n" + " addTypes<\n" + '#include "acir/Dialect/ACSim/ACSimTypes.cpp.inc"\n' + " >();\n" + " addOperations<\n" + '#include "acir/Dialect/ACSim/ACSimOps.cpp.inc"\n' + " >();\n" + "}\n" + ), + "test/ACIR/system-valid.mlir": "ac.system @soc root @Top {}\n!ac.struct<@s>\n", + "test/ACIR/system-invalid.mlir": "ac.system\n!ac.struct<@s>\n", + "test/ACSim/model-valid.mlir": "acsim.model @m {}\n!acsim.value<@v>\n", + "test/ACSim/model-invalid.mlir": "acsim.model\n!acsim.value<@v>\n", +} + + +def initialize_coverage_fixture(overrides=None, removals=()): + temporary_directory = tempfile.TemporaryDirectory() + root = Path(temporary_directory.name) + files = dict(FIXTURE_FILES) + if overrides: + files.update(overrides) + for relative_path, content in files.items(): + path = root / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + for relative_path in removals: + (root / relative_path).unlink() + checker = load_coverage_checker() + errors = [] + surface = checker.check_ods_surface(root, errors) + if not errors: + ledger = root / checker.LEDGER_PATH + ledger.parent.mkdir(parents=True, exist_ok=True) + ledger.write_text(checker.render_ledger(root, surface)) + return temporary_directory, root + + +class IRCoverageTest(unittest.TestCase): + def check_fixture(self, root): + checker = load_coverage_checker() + return checker.run_checks(root) + + def test_read_only_checker_accepts_the_repository(self): + before = subprocess.run( + ["git", "status", "--porcelain=v1"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout + result = subprocess.run( + [sys.executable, "scripts/check-ir-coverage.py"], + cwd=ROOT, + capture_output=True, + text=True, + ) + after = subprocess.run( + ["git", "status", "--porcelain=v1"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ).stdout + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertEqual(before, after, "coverage checker modified the repository") + + def test_minimal_fixture_passes(self): + temporary_directory, root = initialize_coverage_fixture() + self.addCleanup(temporary_directory.cleanup) + self.assertEqual([], self.check_fixture(root)) + + def test_manifest_missing_operation_is_reported(self): + temporary_directory, root = initialize_coverage_fixture( + overrides={ + "contracts/acir.yaml": ACIR_MANIFEST.replace( + "operations:\n - ac.system\n", "operations: []\n" + ) + } + ) + self.addCleanup(temporary_directory.cleanup) + errors = self.check_fixture(root) + self.assertTrue( + any("implementation-only public alias" in error for error in errors), + errors, + ) + + def test_manifest_extra_operation_needs_source_symbol(self): + temporary_directory, root = initialize_coverage_fixture( + overrides={ + "contracts/acir.yaml": ACIR_MANIFEST.replace( + " - ac.system\n", " - ac.system\n - ac.ghost\n" + ) + } + ) + self.addCleanup(temporary_directory.cleanup) + errors = self.check_fixture(root) + self.assertTrue( + any("ac.ghost" in error and "no source symbol" in error for error in errors), + errors, + ) + + def test_stale_epoch_is_rejected(self): + temporary_directory, root = initialize_coverage_fixture( + overrides={ + "contracts/acsim.yaml": ACSIM_MANIFEST.replace( + '"0.3"', '"0.0"' + ) + } + ) + self.addCleanup(temporary_directory.cleanup) + errors = self.check_fixture(root) + self.assertTrue( + any("stale contract_epoch" in error for error in errors), errors + ) + + def test_missing_negative_coverage_is_reported(self): + temporary_directory, root = initialize_coverage_fixture( + removals=("test/ACIR/system-invalid.mlir",) + ) + self.addCleanup(temporary_directory.cleanup) + errors = self.check_fixture(root) + self.assertTrue( + any( + "ac.system" in error and "no negative lit test coverage" in error + for error in errors + ), + errors, + ) + self.assertTrue( + any( + "!ac.struct" in error and "no negative lit test coverage" in error + for error in errors + ), + errors, + ) + + def test_missing_positive_coverage_is_reported(self): + temporary_directory, root = initialize_coverage_fixture( + removals=("test/ACSim/model-valid.mlir",) + ) + self.addCleanup(temporary_directory.cleanup) + errors = self.check_fixture(root) + self.assertTrue( + any( + "acsim.model" in error and "no positive lit test coverage" in error + for error in errors + ), + errors, + ) + + def test_substring_coverage_does_not_count(self): + # ac.systemx must not satisfy ac.system coverage. + temporary_directory, root = initialize_coverage_fixture( + overrides={ + "test/ACIR/system-valid.mlir": "ac.systemx @soc {}\n!ac.struct<@s>\n" + } + ) + self.addCleanup(temporary_directory.cleanup) + errors = self.check_fixture(root) + self.assertTrue( + any( + "ac.system" in error and "no positive lit test coverage" in error + for error in errors + ), + errors, + ) + + def test_missing_registration_table_is_reported(self): + temporary_directory, root = initialize_coverage_fixture( + overrides={ + "lib/Dialect/ACSim/ACSimTypes.cpp": "void initialize() {}\n" + } + ) + self.addCleanup(temporary_directory.cleanup) + errors = self.check_fixture(root) + self.assertTrue( + any("does not register the generated" in error for error in errors), + errors, + ) + + def test_stale_ledger_is_rejected(self): + temporary_directory, root = initialize_coverage_fixture() + self.addCleanup(temporary_directory.cleanup) + ledger = root / "docs/spec/50-verification/ir-coverage.md" + ledger.write_text(ledger.read_text() + "\nstale edit\n") + errors = self.check_fixture(root) + self.assertTrue(any("is stale" in error for error in errors), errors) + + def test_missing_ledger_is_reported(self): + temporary_directory, root = initialize_coverage_fixture() + self.addCleanup(temporary_directory.cleanup) + (root / "docs/spec/50-verification/ir-coverage.md").unlink() + errors = self.check_fixture(root) + self.assertTrue( + any("missing coverage ledger" in error for error in errors), errors + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/__init__.py b/components/agentic-circuit/tests/e2e/__init__.py new file mode 100644 index 00000000..c201e206 --- /dev/null +++ b/components/agentic-circuit/tests/e2e/__init__.py @@ -0,0 +1 @@ +"""End-to-end public-pipeline tests.""" diff --git a/components/agentic-circuit/tests/e2e/test_barrier_backend.py b/components/agentic-circuit/tests/e2e/test_barrier_backend.py new file mode 100644 index 00000000..ff0bbee0 --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_barrier_backend.py @@ -0,0 +1,231 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +from agentic_circuit._queue_frontend import lower_queue_source + + +ROOT = Path(__file__).resolve().parents[2] +EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_barrier_pipeline.py" +PYC_REPOSITORY = ROOT.parents[1] +DEFAULT_TOOLCHAIN = PYC_REPOSITORY / ".pycircuit_out/toolchain/install" + + +class BarrierBackendTest(unittest.TestCase): + def test_barrier_is_atomic_and_cycle_equivalent_in_cpp_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if not pycc.is_file() or not metadata.is_file() or cxx is None or verilator is None: + self.skipTest("pinned pyCircuit toolchain, C++, or Verilator is unavailable") + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "barrier.raw.ac.mlir" + frozen = root / "barrier.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source( + EXAMPLE.read_text(encoding="utf-8"), "pyc_barrier_pipeline" + ), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual( + ["ac.barrier", "ac.sink", "ac.source"], + manifest["opcode_lowering_inventory"], + ) + + cpp_harness = root / "cpp_harness.cpp" + cpp_executable = root / "cpp_model" + cpp_harness.write_text( + '''#include "pyc_barrier_pipeline.hpp" +#include +#include + +int main() { + pyc::gen::pyc_barrier_pipeline dut; + bool left_sent = false; + bool right_sent = false; + for (std::uint64_t cycle = 0; cycle < 16; ++cycle) { + const bool left_offering = cycle >= 1 && !left_sent; + const bool right_offering = cycle >= 3 && !right_sent; + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in0_valid = pyc::cpp::Wire<1>(left_offering ? 1 : 0); + dut.in0_data = pyc::cpp::Wire<16>(left_offering ? 11 : 0); + dut.in1_valid = pyc::cpp::Wire<1>(right_offering ? 1 : 0); + dut.in1_data = pyc::cpp::Wire<32>(right_offering ? 22 : 0); + dut.out0_ready = pyc::cpp::Wire<1>(1); + dut.out1_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out0_valid.value() << " " + << dut.out0_data.value() << " " << dut.out1_valid.value() << " " + << dut.out1_data.value() << " " << dut.in0_ready.value() << " " + << dut.in1_ready.value() << "\\n"; + if (left_offering && dut.in0_ready.value() != 0) + left_sent = true; + if (right_offering && dut.in1_ready.value() != 0) + right_sent = true; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + } +} +''', + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(output / "cpp"), + "-I", + str(toolchain / "include"), + str(output / "cpp/pyc_barrier_pipeline.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), text=True, capture_output=True, check=False + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stderr) + + verilator_harness = root / "verilator_harness.cpp" + verilator_harness.write_text( + '''#include "Vpyc_barrier_pipeline.h" +#include +#include + +int main() { + Vpyc_barrier_pipeline dut; + bool left_sent = false; + bool right_sent = false; + for (std::uint64_t cycle = 0; cycle < 16; ++cycle) { + const bool left_offering = cycle >= 1 && !left_sent; + const bool right_offering = cycle >= 3 && !right_sent; + dut.rst = cycle == 0 ? 1 : 0; + dut.in0_valid = left_offering ? 1 : 0; + dut.in0_data = left_offering ? 11 : 0; + dut.in1_valid = right_offering ? 1 : 0; + dut.in1_data = right_offering ? 22 : 0; + dut.out0_ready = 1; + dut.out1_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + std::cout << cycle << " " << unsigned(dut.out0_valid) << " " + << dut.out0_data << " " << unsigned(dut.out1_valid) << " " + << dut.out1_data << " " << unsigned(dut.in0_ready) << " " + << unsigned(dut.in1_ready) << "\\n"; + if (left_offering && dut.in0_ready != 0) + left_sent = true; + if (right_offering && dut.in1_ready != 0) + right_sent = true; + dut.clk = 0; + dut.eval(); + } +} +''', + encoding="utf-8", + ) + object_dir = root / "verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "pyc_barrier_pipeline", + "--Mdir", + str(object_dir), + str(output / "verilog/pyc_primitives.v"), + str(output / "verilog/pyc_barrier_pipeline.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vpyc_barrier_pipeline"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + + rows = [line.split() for line in cpp_run.stdout.splitlines()] + left = [int(row[2]) for row in rows if row[1] == "1"] + right = [int(row[4]) for row in rows if row[3] == "1"] + self.assertEqual([11], left) + self.assertEqual([22], right) + left_cycle = next(int(row[0]) for row in rows if row[1] == "1") + right_cycle = next(int(row[0]) for row in rows if row[3] == "1") + self.assertEqual(left_cycle, right_cycle) + self.assertGreaterEqual(left_cycle, 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_credit_backend.py b/components/agentic-circuit/tests/e2e/test_credit_backend.py new file mode 100644 index 00000000..d2fd2248 --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_credit_backend.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +from agentic_circuit._queue_frontend import lower_queue_source + + +ROOT = Path(__file__).resolve().parents[2] +EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_credit_pipeline.py" +PYC_REPOSITORY = ROOT.parents[1] +DEFAULT_TOOLCHAIN = PYC_REPOSITORY / ".pycircuit_out/toolchain/install" + + +class CreditBackendTest(unittest.TestCase): + def test_credit_completion_is_cycle_equivalent_in_cpp_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if not pycc.is_file() or not metadata.is_file() or cxx is None or verilator is None: + self.skipTest("pinned pyCircuit toolchain, C++, or Verilator is unavailable") + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "credit.raw.ac.mlir" + frozen = root / "credit.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source( + EXAMPLE.read_text(encoding="utf-8"), "pyc_credit_pipeline" + ), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertEqual(2, pyc.count("pyc.reg")) + self.assertIn("credit_nonpositive_cost", pyc) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual( + ["ac.credit", "ac.sink", "ac.source"], + manifest["opcode_lowering_inventory"], + ) + + cpp_harness = root / "cpp_harness.cpp" + cpp_executable = root / "cpp_model" + cpp_harness.write_text( + '''#include "pyc_credit_pipeline.hpp" +#include +#include +#include + +int main() { + pyc::gen::pyc_credit_pipeline dut; + const std::array input{ + (0ULL << 20) | (4ULL << 16) | 10, + (1ULL << 20) | (1ULL << 16) | 20, + (2ULL << 20) | (1ULL << 16) | 30}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 24; ++cycle) { + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in_valid = pyc::cpp::Wire<1>(offering ? 1 : 0); + dut.in_data = pyc::cpp::Wire<24>(offering ? input[cursor] : 0); + dut.out_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out_valid.value() << " " + << dut.out_data.value() << " " << dut.in_ready.value() << "\\n"; + if (offering && dut.in_ready.value() != 0) + ++cursor; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + } +} +''', + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(output / "cpp"), + "-I", + str(toolchain / "include"), + str(output / "cpp/pyc_credit_pipeline.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stderr) + + verilator_harness = root / "verilator_harness.cpp" + verilator_harness.write_text( + '''#include "Vpyc_credit_pipeline.h" +#include +#include +#include + +int main() { + Vpyc_credit_pipeline dut; + const std::array input{ + (0ULL << 20) | (4ULL << 16) | 10, + (1ULL << 20) | (1ULL << 16) | 20, + (2ULL << 20) | (1ULL << 16) | 30}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 24; ++cycle) { + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = cycle == 0 ? 1 : 0; + dut.in_valid = offering ? 1 : 0; + dut.in_data = offering ? input[cursor] : 0; + dut.out_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + std::cout << cycle << " " << unsigned(dut.out_valid) << " " + << dut.out_data << " " << unsigned(dut.in_ready) << "\\n"; + if (offering && dut.in_ready != 0) + ++cursor; + dut.clk = 0; + dut.eval(); + } +} +''', + encoding="utf-8", + ) + object_dir = root / "verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "pyc_credit_pipeline", + "--Mdir", + str(object_dir), + str(output / "verilog/pyc_primitives.v"), + str(output / "verilog/pyc_credit_pipeline.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vpyc_credit_pipeline"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + + transactions = [ + int(fields[2]) + for line in cpp_run.stdout.splitlines() + if len(fields := line.split()) == 4 and fields[1] == "1" + ] + self.assertEqual( + [ + (1 << 20) | (1 << 16) | 20, + (0 << 20) | (4 << 16) | 10, + (2 << 20) | (1 << 16) | 30, + ], + transactions, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_davincioo_trace.py b/components/agentic-circuit/tests/e2e/test_davincioo_trace.py new file mode 100644 index 00000000..a204025c --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_davincioo_trace.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import json +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] + + +class DavinciOOTraceTest(unittest.TestCase): + def test_generated_gfsim_matches_reference_projection_and_artifacts(self) -> None: + if shutil.which("c++") is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "run" + completed = subprocess.run( + ( + sys.executable, + str(ROOT / "tools/run-davincioo.py"), + "--output-dir", + str(output), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + report = json.loads((output / "run.json").read_text(encoding="utf-8")) + self.assertEqual(15, report["record_count"]) + self.assertEqual(453, report["cycles"]) + self.assertEqual(report["reference_cycles"], report["cycles"]) + self.assertEqual(list(range(15)), report["retirement_order"]) + self.assertEqual( + [0, 2, 3, 5, 7, 9, 10, 12, 1, 4, 6, 8, 11, 13, 14], + report["completion_order"], + ) + artifacts = ROOT / "tests/goldens/davincioo" + self.assertEqual( + (artifacts / "davincioo-softmax-run.json").read_bytes(), + (output / "run.json").read_bytes(), + ) + self.assertEqual( + (artifacts / "davincioo-softmax-swimlane.svg").read_bytes(), + (output / "swimlane.svg").read_bytes(), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_deadlock.py b/components/agentic-circuit/tests/e2e/test_deadlock.py new file mode 100644 index 00000000..1b17bbe9 --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_deadlock.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_barrier_pipeline.py" + + +class GeneratedDeadlockTest(unittest.TestCase): + def test_generated_barrier_reports_missing_peer_as_no_progress(self) -> None: + cxx = shutil.which("c++") + runtime = ROOT / "build/dev-llvm22/lib/gfsim/libgfsim.a" + if cxx is None or not runtime.is_file(): + self.skipTest("C++ compiler or gfsim runtime is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + model = root / "barrier.cpp" + acir = root / "barrier.ac.mlir" + plan = root / "barrier.queue-plan.json" + generated = subprocess.run( + ( + str(ROOT / "tools/ac-queue-cxxgen.py"), + str(EXAMPLE), + "--system", + "pyc_barrier_pipeline", + "--acir-output", + str(acir), + "--plan-output", + str(plan), + "--acir-opt", + str(ROOT / "build/dev-llvm22/bin/acir-opt"), + "--queue-plan-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-plan"), + "--queue-cxxgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-cxxgen"), + "--output", + str(model), + ), + cwd=ROOT, + env={**os.environ, "PYTHONPATH": str(ROOT / "src")}, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, generated.returncode, generated.stderr) + content = model.read_text(encoding="utf-8") + self.assertIn('block_0_("barrier_left_ready"', content) + + harness = root / "harness.cpp" + executable = root / "deadlock" + harness.write_text( + f'''#include "{model.name}" +#include "gfsim/object.h" +#include + +int main() {{ + ac_generated::PycBarrierPipeline model; + if (!model.left().proposePush(ac_generated::LeftToken{{11}})) + return 1; + model.left().doXfer({{0, 0}}); + auto rows = model.dispatch_rows(); + gfsim::SimSystem system("deadlock"); + if (!system.setDispatchTable(rows)) + return 2; + for (const auto &row : rows) + if (!system.scheduleWork(row.id, {{0, 0}})) + return 3; + auto result = system.run(); + if (result.classification != gfsim::TerminationClass::Failed || + result.diagnosticCode != "no_progress") {{ + std::cerr << result.diagnosticCode << "\\n"; + return 4; + }} + return 0; +}} +''', + encoding="utf-8", + ) + compiled = subprocess.run( + ( + cxx, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + str(runtime), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, compiled.returncode, compiled.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_inventory_determinism.py b/components/agentic-circuit/tests/e2e/test_inventory_determinism.py new file mode 100644 index 00000000..a9b4bc49 --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_inventory_determinism.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +from agentic_circuit._queue_frontend import lower_queue_source + + +ROOT = Path(__file__).resolve().parents[2] +PYC_REPOSITORY = ROOT.parents[1] +DEFAULT_TOOLCHAIN = PYC_REPOSITORY / ".pycircuit_out/toolchain/install" +CASES = ( + "pyc_barrier_pipeline", + "pyc_credit_pipeline", + "pyc_firing_pipeline", + "pyc_loop_control_pipeline", + "pyc_memory_pipeline", + "pyc_recursive_pipeline", + "pyc_select_pipeline", +) + + +class InventoryDeterminismTest(unittest.TestCase): + def test_expanded_inventory_is_byte_identical_across_output_roots(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if not pycc.is_file() or not metadata.is_file() or cxx is None or verilator is None: + self.skipTest("pinned pyCircuit toolchain, C++, or Verilator is unavailable") + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for system in CASES: + with self.subTest(system=system): + source = ROOT / "examples" / "pipelines" / f"{system}.py" + raw = root / f"{system}.raw.ac.mlir" + frozen = root / f"{system}.frozen.ac.mlir" + raw.write_text( + lower_queue_source(source.read_text(encoding="utf-8"), system), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + + artifacts: list[dict[str, bytes]] = [] + for run in range(2): + output = root / f"{system}-run-{run}" + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + artifacts.append( + { + path.relative_to(output).as_posix(): path.read_bytes() + for path in sorted(output.rglob("*")) + if path.is_file() + } + ) + self.assertEqual(artifacts[0], artifacts[1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_pyc_backend.py b/components/agentic-circuit/tests/e2e/test_pyc_backend.py new file mode 100644 index 00000000..5224284f --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_pyc_backend.py @@ -0,0 +1,1866 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +from agentic_circuit._queue_frontend import lower_queue_source + + +ROOT = Path(__file__).resolve().parents[2] +EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_queue_pipeline.py" +DAVINCIOO_EXAMPLE = ROOT / "examples" / "pipelines" / "davincioo_queue_model.py" +STRUCT_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_struct_pipeline.py" +ROUTE_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_route_merge_pipeline.py" +ATOMIC_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_atomic_pipeline.py" +REORDER_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_reorder_pipeline.py" +DEPENDENCY_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_dependency_pipeline.py" +MEMORY_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_memory_pipeline.py" +MEMORY_BANKS_EXAMPLE = ROOT / "examples" / "memory" / "memory_banks.py" +FORK_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_fork_pipeline.py" +CONDITIONAL_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_conditional_pipeline.py" +FEEDBACK_EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_feedback_pipeline.py" +PYC_REPOSITORY = ROOT.parents[1] +DEFAULT_TOOLCHAIN = PYC_REPOSITORY / ".pycircuit_out/toolchain/install" +DAVINCIOO_TRACE = ( + ROOT + / "references/davincioo-gfsim/upstream/tests/fixtures/traces" + / "examples_intermediate_softmax.pto.trace" +) +DAVINCIOO_PROJECTION = ROOT / "tests/goldens/davincioo/softmax-projection.json" + + +class PycBackendTest(unittest.TestCase): + def test_memory_array_frontend_expands_to_one_sync_mem_per_bank(self) -> None: + acir_opt = ROOT / "build/dev-llvm22/bin/acir-opt" + pycgen = ROOT / "build/dev-llvm22/bin/acir-queue-pycgen" + if not acir_opt.is_file() or not pycgen.is_file(): + self.skipTest("ACIR optimizer or Queue PYC generator is unavailable") + source = MEMORY_BANKS_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "memory_banks.raw.ac.mlir" + frozen = root / "memory_banks.frozen.ac.mlir" + raw.write_text( + lower_queue_source(source, "memory_banks"), encoding="utf-8" + ) + optimized = subprocess.run( + (str(acir_opt), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + generated = subprocess.run( + (str(pycgen), str(frozen)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, generated.returncode, generated.stderr) + self.assertEqual(4, generated.stdout.count("pyc.sync_mem")) + for bank in range(4): + self.assertIn(f'name = "banks__{bank}"', generated.stdout) + + def test_bounded_feedback_is_cycle_equivalent_in_pyc_cpp_and_verilog( + self, + ) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = FEEDBACK_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "feedback.raw.ac.mlir" + frozen = root / "feedback.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_feedback_pipeline"), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertEqual(3, pyc.count("pyc.reg")) + self.assertIn("pyc.ult", pyc) + self.assertIn("feedback_iteration_limit", pyc) + + cpp_harness = root / "cpp_harness.cpp" + cpp_executable = root / "cpp_model" + cpp_harness.write_text( + """#include "pyc_feedback_pipeline.hpp" +#include +#include + +int main() { + pyc::gen::pyc_feedback_pipeline dut; + for (std::uint64_t cycle = 0; cycle < 14; ++cycle) { + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in_valid = pyc::cpp::Wire<1>(cycle == 1 ? 1 : 0); + dut.in_data = pyc::cpp::Wire<36>(cycle == 1 ? ((10ULL << 4) | 3) : 0); + dut.out_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out_valid.value() << " " + << dut.out_data.value() << " " << dut.in_ready.value() << "\\n"; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + } +} +""", + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(output / "cpp"), + "-I", + str(toolchain / "include"), + str(output / "cpp/pyc_feedback_pipeline.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stderr) + + verilator_harness = root / "verilator_harness.cpp" + verilator_harness.write_text( + """#include "Vpyc_feedback_pipeline.h" +#include +#include + +int main() { + Vpyc_feedback_pipeline dut; + for (std::uint64_t cycle = 0; cycle < 14; ++cycle) { + dut.rst = cycle == 0 ? 1 : 0; + dut.in_valid = cycle == 1 ? 1 : 0; + dut.in_data = cycle == 1 ? ((10ULL << 4) | 3) : 0; + dut.out_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + std::cout << cycle << " " << unsigned(dut.out_valid) << " " + << dut.out_data << " " << unsigned(dut.in_ready) << "\\n"; + dut.clk = 0; + dut.eval(); + } +} +""", + encoding="utf-8", + ) + object_dir = root / "verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "pyc_feedback_pipeline", + "--Mdir", + str(object_dir), + str(output / "verilog/pyc_primitives.v"), + str(output / "verilog/pyc_feedback_pipeline.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vpyc_feedback_pipeline"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + transactions = [ + int(fields[2]) + for line in cpp_run.stdout.splitlines() + if len(fields := line.split()) == 4 and fields[1] == "1" + ] + self.assertEqual([13 << 4], transactions) + + gfsim_model = root / "gfsim_model.cpp" + gfsim_harness = root / "gfsim_harness.cpp" + gfsim_executable = root / "gfsim_model" + gfsim_generated = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-queue-cxxgen"), str(frozen)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, gfsim_generated.returncode, gfsim_generated.stderr) + gfsim_model.write_text(gfsim_generated.stdout, encoding="utf-8") + gfsim_harness.write_text( + f'''#include "{gfsim_model.name}" +#include +#include + +int main() {{ + ac_generated::PycFeedbackPipeline model; + if (!model.current().proposePush(ac_generated::Item{{10, 3}})) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 14; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + for (const auto &item : model.sink_0_values()) + std::cout << ((static_cast(item.value) << 4) | + static_cast(item.remaining)) << "\\n"; +}} +''', + encoding="utf-8", + ) + gfsim_build = subprocess.run( + ( + cxx, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(gfsim_harness), + "-o", + str(gfsim_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, gfsim_build.returncode, gfsim_build.stderr) + gfsim_run = subprocess.run( + (str(gfsim_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, gfsim_run.returncode, gfsim_run.stderr) + self.assertEqual( + transactions, [int(value) for value in gfsim_run.stdout.split()] + ) + + def test_serial_runtime_if_builds_pyc_cpp_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = CONDITIONAL_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "conditional.raw.ac.mlir" + frozen = root / "conditional.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_conditional_pipeline"), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertIn("pyc.eq", pyc) + self.assertIn("pyc.fifo", pyc) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual("0.3", manifest["contract_epoch"]) + verilog = "\n".join( + path.read_text(encoding="utf-8") + for path in sorted((output / "verilog").glob("*.v")) + ) + self.assertIn("==", verilog) + + def test_decoupled_fork_builds_delivered_state_in_pyc(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = FORK_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "fork.raw.ac.mlir" + frozen = root / "fork.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_fork_pipeline"), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual("0.3", manifest["contract_epoch"]) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertEqual(3, pyc.count("pyc.reg")) + self.assertIn("%out0_ready", pyc) + self.assertIn("%out1_ready", pyc) + + def test_atomic_multi_queue_firing_builds_pyc_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = ATOMIC_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "atomic.raw.ac.mlir" + frozen = root / "atomic.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_atomic_pipeline"), + encoding="utf-8", + ) + optimized = subprocess.run( + ( + str(ROOT / "build/dev-llvm22/bin/acir-opt"), + "--canonicalize", + "--cse", + str(raw), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertIn("%in0_valid", pyc) + self.assertIn("%in1_valid", pyc) + self.assertIn("%out0_ready", pyc) + self.assertIn("%out1_ready", pyc) + self.assertIn('result_names = ["out0_valid"', pyc) + + def test_dependency_is_cycle_equivalent_in_pyc_cpp_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = DEPENDENCY_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "dependency.raw.ac.mlir" + frozen = root / "dependency.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_dependency_pipeline"), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertEqual(4, pyc.count("pyc.reg")) + self.assertIn("pyc.sub", pyc) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual( + ["ac.dependency", "ac.sink", "ac.source"], + manifest["opcode_lowering_inventory"], + ) + + cpp_harness = root / "cpp_harness.cpp" + cpp_executable = root / "cpp_model" + cpp_harness.write_text( + """#include "pyc_dependency_pipeline.hpp" +#include +#include +#include + +int main() { + pyc::gen::pyc_dependency_pipeline dut; + const std::array input{ + (0ULL << 25) | (15ULL << 21) | (0ULL << 20) | (4ULL << 16) | 10, + (1ULL << 25) | (15ULL << 21) | (0ULL << 20) | (1ULL << 16) | 20, + (2ULL << 25) | (15ULL << 21) | (1ULL << 20) | (1ULL << 16) | 30}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 24; ++cycle) { + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in_valid = pyc::cpp::Wire<1>(offering ? 1 : 0); + dut.in_data = pyc::cpp::Wire<29>(offering ? input[cursor] : 0); + dut.out_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out_valid.value() << " " + << dut.out_data.value() << " " << dut.in_ready.value() << "\\n"; + if (offering && dut.in_ready.value() != 0) + ++cursor; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + } +} +""", + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(output / "cpp"), + "-I", + str(toolchain / "include"), + str(output / "cpp/pyc_dependency_pipeline.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stderr) + + verilator_harness = root / "verilator_harness.cpp" + verilator_harness.write_text( + """#include "Vpyc_dependency_pipeline.h" +#include +#include +#include + +int main() { + Vpyc_dependency_pipeline dut; + const std::array input{ + (0ULL << 25) | (15ULL << 21) | (0ULL << 20) | (4ULL << 16) | 10, + (1ULL << 25) | (15ULL << 21) | (0ULL << 20) | (1ULL << 16) | 20, + (2ULL << 25) | (15ULL << 21) | (1ULL << 20) | (1ULL << 16) | 30}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 24; ++cycle) { + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = cycle == 0 ? 1 : 0; + dut.in_valid = offering ? 1 : 0; + dut.in_data = offering ? input[cursor] : 0; + dut.out_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + std::cout << cycle << " " << unsigned(dut.out_valid) << " " + << dut.out_data << " " << unsigned(dut.in_ready) << "\\n"; + if (offering && dut.in_ready != 0) + ++cursor; + dut.clk = 0; + dut.eval(); + } +} +""", + encoding="utf-8", + ) + object_dir = root / "verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "pyc_dependency_pipeline", + "--Mdir", + str(object_dir), + str(output / "verilog/pyc_primitives.v"), + str(output / "verilog/pyc_dependency_pipeline.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vpyc_dependency_pipeline"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + transactions = [ + int(fields[2]) + for line in cpp_run.stdout.splitlines() + if len(fields := line.split()) == 4 and fields[1] == "1" + ] + self.assertEqual( + [ + (2 << 25) | (15 << 21) | (1 << 20) | (1 << 16) | 30, + (0 << 25) | (15 << 21) | (0 << 20) | (4 << 16) | 10, + (1 << 25) | (15 << 21) | (0 << 20) | (1 << 16) | 20, + ], + transactions, + ) + + def test_memory_is_old_data_and_cycle_equivalent_in_pyc_cpp_and_verilog( + self, + ) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = MEMORY_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "memory.raw.ac.mlir" + frozen = root / "memory.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_memory_pipeline"), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertEqual(4, pyc.count("pyc.reg")) + self.assertEqual(1, pyc.count("pyc.sync_mem")) + self.assertIn("pyc.sub", pyc) + self.assertIn('{depth = 16, name = "sram"}', pyc) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual( + [ + "ac.memory.instance", + "ac.memory.request", + "ac.sink", + "ac.source", + ], + manifest["opcode_lowering_inventory"], + ) + + cpp_harness = root / "cpp_harness.cpp" + cpp_executable = root / "cpp_model" + cpp_harness.write_text( + """#include "pyc_memory_pipeline.hpp" +#include +#include +#include + +int main() { + pyc::gen::pyc_memory_pipeline dut; + const std::array input{ + (3ULL << 25) | (1ULL << 24) | (42ULL << 8) | 1, + (3ULL << 25) | (0ULL << 24) | (0ULL << 8) | 2, + (3ULL << 25) | (1ULL << 24) | (99ULL << 8) | 3, + (3ULL << 25) | (0ULL << 24) | (0ULL << 8) | 4}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 24; ++cycle) { + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in_valid = pyc::cpp::Wire<1>(offering ? 1 : 0); + dut.in_data = pyc::cpp::Wire<29>(offering ? input[cursor] : 0); + dut.out_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out_valid.value() << " " + << dut.out_data.value() << " " << dut.in_ready.value() << "\\n"; + if (offering && dut.in_ready.value() != 0) + ++cursor; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + } +} +""", + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(output / "cpp"), + "-I", + str(toolchain / "include"), + str(output / "cpp/pyc_memory_pipeline.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stderr) + + verilator_harness = root / "verilator_harness.cpp" + verilator_harness.write_text( + """#include "Vpyc_memory_pipeline.h" +#include +#include +#include + +int main() { + Vpyc_memory_pipeline dut; + const std::array input{ + (3ULL << 25) | (1ULL << 24) | (42ULL << 8) | 1, + (3ULL << 25) | (0ULL << 24) | (0ULL << 8) | 2, + (3ULL << 25) | (1ULL << 24) | (99ULL << 8) | 3, + (3ULL << 25) | (0ULL << 24) | (0ULL << 8) | 4}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 24; ++cycle) { + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = cycle == 0 ? 1 : 0; + dut.in_valid = offering ? 1 : 0; + dut.in_data = offering ? input[cursor] : 0; + dut.out_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + std::cout << cycle << " " << unsigned(dut.out_valid) << " " + << dut.out_data << " " << unsigned(dut.in_ready) << "\\n"; + if (offering && dut.in_ready != 0) + ++cursor; + dut.clk = 0; + dut.eval(); + } +} +""", + encoding="utf-8", + ) + object_dir = root / "verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "pyc_memory_pipeline", + "--Mdir", + str(object_dir), + str(output / "verilog/pyc_primitives.v"), + str(output / "verilog/pyc_memory_pipeline.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vpyc_memory_pipeline"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + transactions = [ + int(fields[2]) + for line in cpp_run.stdout.splitlines() + if len(fields := line.split()) == 4 and fields[1] == "1" + ] + self.assertEqual( + [ + (3 << 25) | (1 << 24) | (0 << 8) | 1, + (3 << 25) | (0 << 24) | (42 << 8) | 2, + (3 << 25) | (1 << 24) | (42 << 8) | 3, + (3 << 25) | (0 << 24) | (99 << 8) | 4, + ], + transactions, + ) + + def test_reorder_is_cycle_equivalent_in_pyc_cpp_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = REORDER_EXAMPLE.read_text(encoding="utf-8").replace( + "capacity=16", "capacity=4" + ) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "reorder.raw.ac.mlir" + frozen = root / "reorder.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_reorder_pipeline"), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertEqual(5, pyc.count("pyc.reg")) + self.assertIn("pyc.ult", pyc) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual( + ["ac.reorder", "ac.sink", "ac.source"], + manifest["opcode_lowering_inventory"], + ) + self.assertEqual("strict", manifest["hierarchy_policy"]) + self.assertEqual(["cpp", "verilog"], manifest["targets"]) + + cpp_harness = root / "cpp_harness.cpp" + cpp_executable = root / "cpp_model" + cpp_harness.write_text( + """#include "pyc_reorder_pipeline.hpp" +#include +#include +#include + +int main() { + pyc::gen::pyc_reorder_pipeline dut; + const std::array input{ + (2ULL << 32) | 20, (0ULL << 32) | 0, (1ULL << 32) | 10}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 20; ++cycle) { + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in_valid = pyc::cpp::Wire<1>(offering ? 1 : 0); + dut.in_data = pyc::cpp::Wire<64>(offering ? input[cursor] : 0); + dut.out_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out_valid.value() << " " + << dut.out_data.value() << " " << dut.in_ready.value() << "\\n"; + if (offering && dut.in_ready.value() != 0) + ++cursor; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + } +} +""", + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(output / "cpp"), + "-I", + str(toolchain / "include"), + str(output / "cpp/pyc_reorder_pipeline.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stdout + cpp_run.stderr) + + verilator_harness = root / "verilator_harness.cpp" + verilator_harness.write_text( + """#include "Vpyc_reorder_pipeline.h" +#include +#include +#include + +int main() { + Vpyc_reorder_pipeline dut; + const std::array input{ + (2ULL << 32) | 20, (0ULL << 32) | 0, (1ULL << 32) | 10}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 20; ++cycle) { + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = cycle == 0 ? 1 : 0; + dut.in_valid = offering ? 1 : 0; + dut.in_data = offering ? input[cursor] : 0; + dut.out_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + std::cout << cycle << " " << unsigned(dut.out_valid) << " " + << dut.out_data << " " << unsigned(dut.in_ready) << "\\n"; + if (offering && dut.in_ready != 0) + ++cursor; + dut.clk = 0; + dut.eval(); + } +} +""", + encoding="utf-8", + ) + object_dir = root / "verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "pyc_reorder_pipeline", + "--Mdir", + str(object_dir), + str(output / "verilog/pyc_primitives.v"), + str(output / "verilog/pyc_reorder_pipeline.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vpyc_reorder_pipeline"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + transactions = [ + int(fields[2]) + for line in cpp_run.stdout.splitlines() + if len(fields := line.split()) == 4 and fields[1] == "1" + ] + self.assertEqual([0, (1 << 32) | 10, (2 << 32) | 20], transactions) + + def test_davincioo_like_graph_builds_full_pyc_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = DAVINCIOO_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "davincioo.raw.ac.mlir" + frozen = root / "davincioo.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "davincioo_queue_model"), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertIn("%in_data: i106", pyc) + self.assertIn("pyc.reg", pyc) + self.assertIn(": i2", pyc) + self.assertGreaterEqual(pyc.count("pyc.fifo"), 14) + self.assertGreaterEqual(pyc.count("pyc.reg"), 50) + self.assertTrue((output / "verilog/davincioo_queue_model.v").is_file()) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual( + [ + "ac.dependency", + "ac.merge", + "ac.observe", + "ac.reorder", + "ac.route", + "ac.scope", + "ac.sink", + "ac.source", + "ac.transform", + ], + manifest["opcode_lowering_inventory"], + ) + + projection = json.loads(DAVINCIOO_PROJECTION.read_text(encoding="utf-8")) + records = [ + json.loads(line) + for line in DAVINCIOO_TRACE.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + packed_inputs: list[tuple[int, int]] = [] + packed_outputs: list[tuple[int, int]] = [] + for row, value in zip( + records, projection["architectural_values"], strict=True + ): + sequence = row["sequence_id"] + opcode = projection["opcode_ids"][row["opcode"]] + route = projection["routes"][row["opcode"]] + waits_for = projection["waits_for"][sequence] + cycles = projection["model_cost"][row["opcode"]] + high = ( + cycles + | (waits_for << 16) + | (route << 24) + | (opcode << 26) + | (sequence << 34) + ) + packed_inputs.append((sequence * 10, high)) + output_high = high + packed_outputs.append((value, output_high)) + input_rows = ",\n ".join( + f"Packed{{{low}ULL, {high}ULL}}" for low, high in packed_inputs + ) + + cpp_harness = root / "davinci_cpp_harness.cpp" + cpp_executable = root / "davinci_cpp_model" + cpp_harness.write_text( + f"""#include "davincioo_queue_model.hpp" +#include +#include +#include + +struct Packed {{ std::uint64_t low; std::uint64_t high; }}; + +int main() {{ + pyc::gen::davincioo_queue_model dut; + const std::array input{{ + {input_rows}, + }}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 700; ++cycle) {{ + const bool offering = cycle != 0 && cursor < input.size(); + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in_valid = pyc::cpp::Wire<1>(offering ? 1 : 0); + dut.in_data = offering + ? pyc::cpp::Wire<106>{{input[cursor].low, input[cursor].high}} + : pyc::cpp::Wire<106>{{}}; + dut.out_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out_valid.value() << " " + << dut.out_data.word(0) << " " << dut.out_data.word(1) << " " + << dut.in_ready.value() << "\\n"; + if (offering && dut.in_ready.value() != 0) + ++cursor; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + }} +}} +""", + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(output / "cpp"), + "-I", + str(toolchain / "include"), + str(output / "cpp/davincioo_queue_model.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stderr) + + verilator_harness = root / "davinci_verilator_harness.cpp" + verilator_harness.write_text( + f"""#include "Vdavincioo_queue_model.h" +#include +#include +#include + +struct Packed {{ std::uint64_t low; std::uint64_t high; }}; + +int main() {{ + Vdavincioo_queue_model dut; + const std::array input{{ + {input_rows}, + }}; + std::size_t cursor = 0; + for (std::uint64_t cycle = 0; cycle < 700; ++cycle) {{ + const bool offering = cycle != 0 && cursor < input.size(); + const Packed value = offering ? input[cursor] : Packed{{0, 0}}; + dut.rst = cycle == 0 ? 1 : 0; + dut.in_valid = offering ? 1 : 0; + dut.in_data[0] = static_cast(value.low); + dut.in_data[1] = static_cast(value.low >> 32); + dut.in_data[2] = static_cast(value.high); + dut.in_data[3] = static_cast(value.high >> 32); + dut.out_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + const std::uint64_t outLow = + static_cast(dut.out_data[0]) | + (static_cast(dut.out_data[1]) << 32); + const std::uint64_t outHigh = + static_cast(dut.out_data[2]) | + (static_cast(dut.out_data[3]) << 32); + std::cout << cycle << " " << unsigned(dut.out_valid) << " " << outLow + << " " << outHigh << " " << unsigned(dut.in_ready) << "\\n"; + if (offering && dut.in_ready != 0) + ++cursor; + dut.clk = 0; + dut.eval(); + }} +}} +""", + encoding="utf-8", + ) + object_dir = root / "davinci_verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "davincioo_queue_model", + "--Mdir", + str(object_dir), + str(output / "verilog/pyc_primitives.v"), + str(output / "verilog/davincioo_queue_model.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vdavincioo_queue_model"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + transactions = [ + (int(fields[2]), int(fields[3])) + for line in cpp_run.stdout.splitlines() + if len(fields := line.split()) == 5 and fields[1] == "1" + ] + self.assertEqual(packed_outputs, transactions) + + def test_route_and_priority_merge_lower_to_static_pyc_topology(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = ROUTE_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "route.raw.ac.mlir" + frozen = root / "route.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_route_merge_pipeline"), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + self.assertIn("pyc.eq", pyc) + self.assertIn("pyc.mux", pyc) + self.assertIn("pyc.not", pyc) + self.assertIn("pyc.or", pyc) + self.assertIn("pyc.and", pyc) + self.assertTrue((output / "verilog/pyc_route_merge_pipeline.v").is_file()) + + def test_struct_payload_has_stable_packed_pyc_and_verilog_layout(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + source = STRUCT_EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "struct.raw.ac.mlir" + frozen = root / "struct.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source(source, "pyc_struct_pipeline"), encoding="utf-8" + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + pyc = (output / "model.pyc").read_text(encoding="utf-8") + verilog = (output / "verilog/pyc_struct_pipeline.v").read_text( + encoding="utf-8" + ) + self.assertIn("%in_data: i128", pyc) + self.assertIn("pyc.extract", pyc) + self.assertIn("pyc.concat", pyc) + self.assertIn("input [127:0] in_data", verilog) + self.assertIn("pyc_fifo #(.WIDTH(128), .DEPTH(2))", verilog) + + def test_frozen_acir_builds_deterministic_pyc_cpp_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if ( + not pycc.is_file() + or not metadata.is_file() + or cxx is None + or verilator is None + ): + self.skipTest( + "pinned pyCircuit toolchain, C++, or Verilator is unavailable" + ) + + source = EXAMPLE.read_text(encoding="utf-8") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "model.raw.ac.mlir" + frozen = root / "model.frozen.ac.mlir" + raw.write_text( + lower_queue_source(source, "pyc_queue_pipeline"), encoding="utf-8" + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + + manifests: list[bytes] = [] + pyc_files: list[bytes] = [] + artifact_sets: list[dict[str, bytes]] = [] + for index in range(2): + output = root / f"run_{index}" + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + manifests.append((output / "manifest.json").read_bytes()) + pyc_files.append((output / "model.pyc").read_bytes()) + artifact_sets.append( + { + path.relative_to(output).as_posix(): path.read_bytes() + for path in sorted(output.rglob("*")) + if path.is_file() and path.name != "manifest.json" + } + ) + self.assertEqual(manifests[0], manifests[1]) + self.assertEqual(pyc_files[0], pyc_files[1]) + self.assertEqual(artifact_sets[0], artifact_sets[1]) + self.assertIn(b"pyc.fifo", pyc_files[0]) + self.assertIn(b"pyc.add", pyc_files[0]) + + first = root / "run_0" + cpp_harness = first / "cpp_harness.cpp" + cpp_executable = first / "cpp_model" + cpp_harness.write_text( + """#include "pyc_queue_pipeline.hpp" +#include +#include + +int main() { + pyc::gen::pyc_queue_pipeline dut; + for (std::uint64_t cycle = 0; cycle < 7; ++cycle) { + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in_valid = pyc::cpp::Wire<1>(cycle == 1 ? 1 : 0); + dut.in_data = pyc::cpp::Wire<64>(cycle == 1 ? 10 : 0); + dut.out_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out_valid.value() << " " + << dut.out_data.value() << " " << dut.in_ready.value() << "\\n"; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + } +} +""", + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(first / "cpp"), + "-I", + str(toolchain / "include"), + str(first / "cpp/pyc_queue_pipeline.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stderr) + + verilator_harness = first / "verilator_harness.cpp" + verilator_harness.write_text( + """#include "Vpyc_queue_pipeline.h" +#include +#include + +int main() { + Vpyc_queue_pipeline dut; + for (std::uint64_t cycle = 0; cycle < 7; ++cycle) { + dut.rst = cycle == 0 ? 1 : 0; + dut.in_valid = cycle == 1 ? 1 : 0; + dut.in_data = cycle == 1 ? 10 : 0; + dut.out_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + std::cout << cycle << " " << unsigned(dut.out_valid) << " " + << dut.out_data << " " << unsigned(dut.in_ready) << "\\n"; + dut.clk = 0; + dut.eval(); + } +} +""", + encoding="utf-8", + ) + object_dir = first / "verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "pyc_queue_pipeline", + "--Mdir", + str(object_dir), + str(first / "verilog/pyc_primitives.v"), + str(first / "verilog/pyc_queue_pipeline.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vpyc_queue_pipeline"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + pyc_transactions = [ + int(fields[2]) + for line in cpp_run.stdout.splitlines() + if len(fields := line.split()) == 4 and fields[1] == "1" + ] + self.assertEqual([11], pyc_transactions) + + gfsim_model = first / "gfsim_model.cpp" + gfsim_harness = first / "gfsim_harness.cpp" + gfsim_executable = first / "gfsim_model" + gfsim_generated = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-queue-cxxgen"), str(frozen)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, gfsim_generated.returncode, gfsim_generated.stderr) + gfsim_model.write_text(gfsim_generated.stdout, encoding="utf-8") + gfsim_harness.write_text( + f'''#include "{gfsim_model.name}" +#include +#include + +int main() {{ + ac_generated::PycQueuePipeline model; + if (!model.input_queue().proposePush(10)) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 6; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + for (auto value : model.sink_0_values()) + std::cout << value << "\\n"; +}} +''', + encoding="utf-8", + ) + gfsim_build = subprocess.run( + ( + cxx, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(gfsim_harness), + "-o", + str(gfsim_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, gfsim_build.returncode, gfsim_build.stderr) + gfsim_run = subprocess.run( + (str(gfsim_executable),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, gfsim_run.returncode, gfsim_run.stderr) + gfsim_transactions = [ + int(value) for value in gfsim_run.stdout.splitlines() if value + ] + self.assertEqual(pyc_transactions, gfsim_transactions) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_queue_codegen.py b/components/agentic-circuit/tests/e2e/test_queue_codegen.py new file mode 100644 index 00000000..c7a39d61 --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_queue_codegen.py @@ -0,0 +1,592 @@ +from __future__ import annotations + +import os +import json +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +SOURCE = ROOT / "examples" / "pipelines" / "davincioo_queue_model.py" +CONDITIONAL_SOURCE = ROOT / "examples" / "pipelines" / "pyc_conditional_pipeline.py" +REORDER_SOURCE = ROOT / "examples" / "pipelines" / "pyc_reorder_pipeline.py" +DAVINCIOO_TRACE = ( + ROOT + / "references/davincioo-gfsim/upstream/tests/fixtures/traces" + / "examples_intermediate_softmax.pto.trace" +) +DAVINCIOO_PROJECTION = ROOT / "tests/goldens/davincioo/softmax-projection.json" + + +class QueueCodegenTest(unittest.TestCase): + def test_reorder_python_generates_and_runs_typed_cpp(self) -> None: + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + model = root / "reorder.cpp" + acir = root / "reorder.ac.mlir" + plan = root / "reorder.queue-plan.json" + generated = subprocess.run( + ( + str(ROOT / "tools/ac-queue-cxxgen.py"), + str(REORDER_SOURCE), + "--system", + "pyc_reorder_pipeline", + "--acir-output", + str(acir), + "--plan-output", + str(plan), + "--acir-opt", + str(ROOT / "build/dev-llvm22/bin/acir-opt"), + "--queue-plan-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-plan"), + "--queue-cxxgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-cxxgen"), + "--output", + str(model), + ), + cwd=ROOT, + env={**os.environ, "PYTHONPATH": str(ROOT / "src")}, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, generated.returncode, generated.stderr) + content = model.read_text(encoding="utf-8") + self.assertIn("gfsim::QueueReorder +#include + +int main() {{ + ac_generated::PycReorderPipeline model; + const std::array input{{ + ac_generated::Token{{2, 20}}, + ac_generated::Token{{0, 0}}, + ac_generated::Token{{1, 10}}, + }}; + for (const auto &item : input) + if (!model.completed().proposePush(item)) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 20; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + const auto &values = model.sink_0_values(); + if (values.size() != 3) + return 2; + for (std::size_t index = 0; index < values.size(); ++index) + if (values[index].sequence != index) + return 3; + return 0; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + def test_serial_runtime_if_generates_and_runs_common_blocks(self) -> None: + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + model = root / "conditional.cpp" + acir = root / "conditional.ac.mlir" + plan = root / "conditional.queue-plan.json" + generated = subprocess.run( + ( + str(ROOT / "tools/ac-queue-cxxgen.py"), + str(CONDITIONAL_SOURCE), + "--system", + "pyc_conditional_pipeline", + "--acir-output", + str(acir), + "--plan-output", + str(plan), + "--acir-opt", + str(ROOT / "build/dev-llvm22/bin/acir-opt"), + "--queue-plan-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-plan"), + "--queue-cxxgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-cxxgen"), + "--output", + str(model), + ), + cwd=ROOT, + env={**os.environ, "PYTHONPATH": str(ROOT / "src")}, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, generated.returncode, generated.stderr) + content = model.read_text(encoding="utf-8") + self.assertIn("gfsim::QueueRoute", content) + plan_document = json.loads(plan.read_text(encoding="utf-8")) + self.assertEqual("0.3", plan_document["contract_epoch"]) + + harness = root / "harness.cpp" + executable = root / "conditional" + harness.write_text( + f'''#include "{model.name}" +#include +#include + +int main() {{ + ac_generated::PycConditionalPipeline model; + if (!model.input_queue().proposePush(ac_generated::Item{{1, 0}}) || + !model.input_queue().proposePush(ac_generated::Item{{1, 1}})) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 16; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + const auto values = model.sink_0_values(); + if (values.size() != 2) + return 2; + return values[0].value == 11 && values[1].value == 21 ? 0 : 3; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + def test_native_frozen_acir_codegen_covers_common_state_blocks(self) -> None: + from tests.python_frontend.test_queue_codegen import ( + BROADCAST_SOURCE, + FEEDBACK_SOURCE, + ) + from tests.python_frontend.test_queue_frontend import ( + BARRIER_SOURCE, + CREDIT_SOURCE, + EXPECT_SOURCE, + FIRING_SOURCE, + LOOP_CONTROL_SOURCE, + MEMORY_SOURCE, + RECURSION_SOURCE, + SELECT_SOURCE, + ) + + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name, source, expected in ( + ("barrier", BARRIER_SOURCE, "gfsim::QueueBarrier"), + ("broadcast", BROADCAST_SOURCE, "gfsim::QueueBroadcast"), + ("credit", CREDIT_SOURCE, "gfsim::QueueCredit"), + ("expect", EXPECT_SOURCE, "gfsim::QueueExpect"), + ("feedback", FEEDBACK_SOURCE, "gfsim::QueueFeedback"), + ("firing", FIRING_SOURCE, "gfsim::QueueTransform"), + ("loop_control", LOOP_CONTROL_SOURCE, "gfsim::QueueFeedback"), + ("memory", MEMORY_SOURCE, "gfsim::QueueMemory"), + ("recursion", RECURSION_SOURCE, "gfsim::QueueTransform"), + ("select", SELECT_SOURCE, "gfsim::QueueSelect"), + ): + python = root / f"{name}.py" + model = root / f"{name}.cpp" + acir = root / f"{name}.ac.mlir" + plan = root / f"{name}.queue-plan.json" + python.write_text(source, encoding="utf-8") + generated = subprocess.run( + ( + str(ROOT / "tools" / "ac-queue-cxxgen.py"), + str(python), + "--system", + "pipeline", + "--acir-output", + str(acir), + "--plan-output", + str(plan), + "--acir-opt", + str(ROOT / "build/dev-llvm22/bin/acir-opt"), + "--queue-plan-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-plan"), + "--queue-cxxgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-cxxgen"), + "-o", + str(model), + ), + cwd=ROOT, + env={**os.environ, "PYTHONPATH": str(ROOT / "src")}, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, generated.returncode, generated.stderr) + self.assertIn(expected, model.read_text(encoding="utf-8")) + compiled = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + "-fsyntax-only", + str(model), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, compiled.returncode, compiled.stderr) + + def test_davincioo_like_python_generates_and_runs_typed_cpp(self) -> None: + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + model = root / "model.cpp" + acir = root / "model.ac.mlir" + plan = root / "model.queue-plan.json" + harness = root / "harness.cpp" + executable = root / "model" + generated = subprocess.run( + ( + str(ROOT / "tools" / "ac-queue-cxxgen.py"), + str(SOURCE), + "--system", + "davincioo_queue_model", + "--acir-output", + str(acir), + "--plan-output", + str(plan), + "--acir-opt", + str(ROOT / "build" / "dev-llvm22" / "bin" / "acir-opt"), + "--queue-plan-tool", + str( + ROOT + / "build" + / "dev-llvm22" + / "bin" + / "acir-queue-plan" + ), + "--queue-cxxgen-tool", + str( + ROOT + / "build" + / "dev-llvm22" + / "bin" + / "acir-queue-cxxgen" + ), + "-o", + str(model), + ), + cwd=ROOT, + env={**os.environ, "PYTHONPATH": str(ROOT / "src")}, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, generated.returncode, generated.stderr) + content = model.read_text(encoding="utf-8") + plan_document = json.loads(plan.read_text(encoding="utf-8")) + self.assertEqual("davincioo_queue_model", plan_document["system"]) + self.assertEqual(8, len(plan_document["scopes"])) + self.assertEqual(14, len(plan_document["queues"])) + self.assertEqual(16, len(plan_document["blocks"])) + copied_source = root / "copied_model.py" + copied_model = root / "copied_model.cpp" + copied_acir = root / "copied_model.ac.mlir" + copied_plan = root / "copied_model.queue-plan.json" + shutil.copyfile(SOURCE, copied_source) + regenerated = subprocess.run( + ( + str(ROOT / "tools" / "ac-queue-cxxgen.py"), + str(copied_source), + "--system", + "davincioo_queue_model", + "--acir-output", + str(copied_acir), + "--plan-output", + str(copied_plan), + "--acir-opt", + str(ROOT / "build" / "dev-llvm22" / "bin" / "acir-opt"), + "--queue-plan-tool", + str( + ROOT + / "build" + / "dev-llvm22" + / "bin" + / "acir-queue-plan" + ), + "--queue-cxxgen-tool", + str( + ROOT + / "build" + / "dev-llvm22" + / "bin" + / "acir-queue-cxxgen" + ), + "-o", + str(copied_model), + ), + cwd=root, + env={**os.environ, "PYTHONPATH": str(ROOT / "src")}, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, regenerated.returncode, regenerated.stderr) + self.assertEqual(content, copied_model.read_text(encoding="utf-8")) + for scope in ( + "frontend", + "dependency", + "dispatch", + "scalar_engine", + "vector_engine", + "cube_engine", + "tma_engine", + "retire", + ): + self.assertIn(f'("{scope}", gfsim::kInvalidObjectId', content) + self.assertIn("gfsim::QueueRoute", content) + self.assertNotIn("gfsim::QueueFeedback +#include +#include +#include + +int main() {{ + ac_generated::DavinciooQueueModel model; + const std::array input{{ + {input_rows}, + }}; + for (const auto &item : input) + if (!model.trace().proposePush(item)) + return 1; + auto rows = model.dispatch_rows(); + std::size_t simulatedCycles = 0; + std::size_t dependencyPeak = 0; + std::size_t reorderPeak = 0; + std::array resourcePeaks{{}}; + for (std::size_t tick = 0; tick < 600; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + dependencyPeak = std::max(dependencyPeak, model.dependency_0_active()); + reorderPeak = std::max(reorderPeak, model.reorder_0_active()); + for (std::size_t resource = 0; resource < resourcePeaks.size(); ++resource) + resourcePeaks[resource] = std::max( + resourcePeaks[resource], + model.dependency_0_resource_active(resource)); + if (model.sink_0_values().size() == input.size()) {{ + simulatedCycles = tick + 1; + break; + }} + }} + if (simulatedCycles != {projection["simulated_cycles"]}) {{ + std::cerr << "simulated_cycles=" << simulatedCycles << "\\n"; + return 2; + }} + const auto &values = model.sink_0_values(); + if (values.size() != input.size()) + return 3; + std::array opcodeCounts{{}}; + const std::array architecturalValues{{ + {architectural_values_text}}}; + for (std::size_t index = 0; index < values.size(); ++index) {{ + const auto &value = values[index]; + if (value.sequence_id != index || value.opcode != input[index].opcode || + value.waits_for != input[index].waits_for || + value.cycles != input[index].cycles) + return 4; + if (value.value != architecturalValues[index]) + return 5; + ++opcodeCounts[value.opcode]; + }} + const std::array expectedCounts{{{expected_counts_text}}}; + if (opcodeCounts != expectedCounts) + return 6; + const std::array completionOrder{{ + {completion_order_text}}}; + const auto &completed = model.observation_0_values(); + if (completed.size() != completionOrder.size()) + return 7; + for (std::size_t index = 0; index < completed.size(); ++index) + if (completed[index].sequence_id != completionOrder[index]) + return 8; + if (dependencyPeak != {occupancy["dependency_window_peak"]} || + reorderPeak != {occupancy["reorder_window_peak"]}) + return 9; + const std::array expectedResourcePeaks{{ + {resource_peaks_text}}}; + if (resourcePeaks != expectedResourcePeaks) + return 10; + return 0; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_select_backend.py b/components/agentic-circuit/tests/e2e/test_select_backend.py new file mode 100644 index 00000000..50460489 --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_select_backend.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +from agentic_circuit._queue_frontend import lower_queue_source + + +ROOT = Path(__file__).resolve().parents[2] +EXAMPLE = ROOT / "examples" / "pipelines" / "pyc_select_pipeline.py" +PYC_REPOSITORY = ROOT.parents[1] +DEFAULT_TOOLCHAIN = PYC_REPOSITORY / ".pycircuit_out/toolchain/install" + + +class SelectBackendTest(unittest.TestCase): + def test_select_is_cycle_equivalent_in_cpp_and_verilog(self) -> None: + toolchain = Path(os.environ.get("PYC_TOOLCHAIN_ROOT", DEFAULT_TOOLCHAIN)) + pycc = toolchain / "bin" / "pycc" + metadata = toolchain / "share" / "pycircuit" / "toolchain-metadata.json" + cxx = shutil.which("c++") + verilator = shutil.which("verilator") + if not pycc.is_file() or not metadata.is_file() or cxx is None or verilator is None: + self.skipTest("pinned pyCircuit toolchain, C++, or Verilator is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = root / "select.raw.ac.mlir" + frozen = root / "select.frozen.ac.mlir" + output = root / "output" + raw.write_text( + lower_queue_source( + EXAMPLE.read_text(encoding="utf-8"), "pyc_select_pipeline" + ), + encoding="utf-8", + ) + optimized = subprocess.run( + (str(ROOT / "build/dev-llvm22/bin/acir-opt"), str(raw)), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, optimized.returncode, optimized.stderr) + frozen.write_text(optimized.stdout, encoding="utf-8") + completed = subprocess.run( + ( + str(ROOT / "tools/ac-queue-pyc-build.py"), + str(frozen), + "--pycgen-tool", + str(ROOT / "build/dev-llvm22/bin/acir-queue-pycgen"), + "--pycc", + str(pycc), + "--toolchain-lock", + str(ROOT / "toolchains/pyc.lock.json"), + "--toolchain-metadata", + str(metadata), + "--cxx", + cxx, + "--verilator", + verilator, + "--pyc-output", + str(output / "model.pyc"), + "--cpp-output-dir", + str(output / "cpp"), + "--verilog-output-dir", + str(output / "verilog"), + "--manifest", + str(output / "manifest.json"), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + manifest = json.loads((output / "manifest.json").read_text()) + self.assertEqual( + ["ac.select", "ac.sink", "ac.source"], + manifest["opcode_lowering_inventory"], + ) + + cpp_harness = root / "cpp_harness.cpp" + cpp_executable = root / "cpp_model" + cpp_harness.write_text( + '''#include "pyc_select_pipeline.hpp" +#include +#include + +int main() { + pyc::gen::pyc_select_pipeline dut; + bool sent = false; + for (std::uint64_t cycle = 0; cycle < 14; ++cycle) { + const bool offering = cycle >= 1 && !sent; + dut.rst = pyc::cpp::Wire<1>(cycle == 0 ? 1 : 0); + dut.in0_valid = pyc::cpp::Wire<1>(offering ? 1 : 0); + dut.in0_data = pyc::cpp::Wire<1>(1); + dut.in1_valid = pyc::cpp::Wire<1>(offering ? 1 : 0); + dut.in1_data = pyc::cpp::Wire<64>(10); + dut.in2_valid = pyc::cpp::Wire<1>(offering ? 1 : 0); + dut.in2_data = pyc::cpp::Wire<64>(20); + dut.out_ready = pyc::cpp::Wire<1>(1); + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + dut.clk = pyc::cpp::Wire<1>(1); + dut.step(); + std::cout << cycle << " " << dut.out_valid.value() << " " + << dut.out_data.value() << " " << dut.in0_ready.value() << " " + << dut.in1_ready.value() << " " << dut.in2_ready.value() << "\\n"; + if (offering && dut.in0_ready.value() && dut.in2_ready.value()) + sent = true; + dut.clk = pyc::cpp::Wire<1>(0); + dut.step(); + } +} +''', + encoding="utf-8", + ) + cpp_build = subprocess.run( + ( + cxx, + "-std=c++17", + "-I", + str(output / "cpp"), + "-I", + str(toolchain / "include"), + str(output / "cpp/pyc_select_pipeline.cpp"), + str(cpp_harness), + str(toolchain / "lib/libpyc6_runtime.a"), + "-o", + str(cpp_executable), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, cpp_build.returncode, cpp_build.stderr) + cpp_run = subprocess.run( + (str(cpp_executable),), text=True, capture_output=True, check=False + ) + self.assertEqual(0, cpp_run.returncode, cpp_run.stderr) + + verilator_harness = root / "verilator_harness.cpp" + verilator_harness.write_text( + '''#include "Vpyc_select_pipeline.h" +#include +#include + +int main() { + Vpyc_select_pipeline dut; + bool sent = false; + for (std::uint64_t cycle = 0; cycle < 14; ++cycle) { + const bool offering = cycle >= 1 && !sent; + dut.rst = cycle == 0 ? 1 : 0; + dut.in0_valid = offering ? 1 : 0; + dut.in0_data = 1; + dut.in1_valid = offering ? 1 : 0; + dut.in1_data = 10; + dut.in2_valid = offering ? 1 : 0; + dut.in2_data = 20; + dut.out_ready = 1; + dut.clk = 0; + dut.eval(); + dut.clk = 1; + dut.eval(); + std::cout << cycle << " " << unsigned(dut.out_valid) << " " + << dut.out_data << " " << unsigned(dut.in0_ready) << " " + << unsigned(dut.in1_ready) << " " << unsigned(dut.in2_ready) + << "\\n"; + if (offering && dut.in0_ready && dut.in2_ready) + sent = true; + dut.clk = 0; + dut.eval(); + } +} +''', + encoding="utf-8", + ) + object_dir = root / "verilator_obj" + verilator_build = subprocess.run( + ( + verilator, + "--cc", + "--exe", + "--build", + "-Wno-fatal", + "--top-module", + "pyc_select_pipeline", + "--Mdir", + str(object_dir), + str(output / "verilog/pyc_primitives.v"), + str(output / "verilog/pyc_select_pipeline.v"), + str(verilator_harness), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_build.returncode, verilator_build.stderr) + verilator_run = subprocess.run( + (str(object_dir / "Vpyc_select_pipeline"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, verilator_run.returncode, verilator_run.stderr) + self.assertEqual(cpp_run.stdout, verilator_run.stdout) + transactions = [ + int(fields[2]) + for line in cpp_run.stdout.splitlines() + if len(fields := line.split()) == 6 and fields[1] == "1" + ] + self.assertEqual([20], transactions) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_workspace_examples.py b/components/agentic-circuit/tests/e2e/test_workspace_examples.py new file mode 100644 index 00000000..14c7e3e7 --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_workspace_examples.py @@ -0,0 +1,357 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).parents[2] +EXAMPLES = REPOSITORY / "examples" / "workspaces" +BUILD_DIRECTORY = Path( + os.environ.get( + "AGENTIC_CIRCUIT_TEST_BUILD_DIR", REPOSITORY / "build" / "dev-llvm22" + ) +).resolve() +SCENARIOS = ( + "producer_queue_consumer", + "backpressured_pipeline", + "request_response_memory", + "nested_arrays", + "multi_time_domain_bridge", + "suspended_process", +) +COMMON_FILES = ( + "architecture.py", + "agentic-circuit.toml", + "trace.pto.json", + "expected-result.json", + "expected-hierarchy.json", + "expected-stats.json", + "expected-events.jsonl", + "components/showcase.component.json", + "components/showcase.binding.json", +) +DAVINCIOO_SCENARIOS = frozenset( + {"producer_queue_consumer", "backpressured_pipeline", "request_response_memory"} +) +CANONICAL_ARTIFACTS = ( + "input/model.acpy.json", + "input/model.ac.mlir", + "frozen.ac.mlir", + "model.acsim.mlir", +) + + +def _environment() -> dict[str, str]: + environment = os.environ.copy() + entries = (REPOSITORY / "src", BUILD_DIRECTORY / "python") + environment["PYTHONPATH"] = os.pathsep.join( + [*(str(path) for path in entries), environment.get("PYTHONPATH", "")] + ).rstrip(os.pathsep) + return environment + + +def _run_cli(workspace: Path, *arguments: str) -> dict[str, object]: + completed = subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=workspace, + env=_environment(), + text=True, + capture_output=True, + check=False, + ) + if completed.returncode != 0: + raise AssertionError( + f"CLI failed ({completed.returncode}): {' '.join(arguments)}\n" + f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}" + ) + if completed.stderr: + raise AssertionError(f"structured CLI wrote stderr: {completed.stderr}") + return json.loads(completed.stdout) + + +def _selected_result(run_directory: Path) -> dict[str, object]: + result = json.loads((run_directory / "run-result.json").read_text()) + statistics = json.loads((run_directory / "stats.json").read_text()) + architectural_values = { + entry["name"].removeprefix("architectural_"): entry["value"] + for entry in statistics + if entry["name"].startswith("architectural_") + } + return { + "schema": "workspace-showcase-result", + "version": "0.1", + "status": result["status"], + "termination_reason": result["termination_reason"], + "simulated_ticks": result["simulated_ticks"], + "trace_position": result["trace_position"], + "architectural_values": architectural_values, + } + + +def _copy_workspace(source: Path, destination: Path) -> None: + shutil.copytree( + source, + destination, + ignore=shutil.ignore_patterns("build", "__pycache__", "*.pyc"), + ) + + +def _pack(events: Path, output: Path) -> None: + completed = subprocess.run( + [ + sys.executable, + os.fspath(REPOSITORY / "tools/pack-perfetto-trace.py"), + os.fspath(events), + os.fspath(output), + ], + cwd=REPOSITORY, + env=_environment(), + text=True, + capture_output=True, + check=False, + ) + if completed.returncode != 0: + raise AssertionError(completed.stderr) + + +class WorkspaceExampleTest(unittest.TestCase): + maxDiff = None + + def test_all_six_workspaces_own_the_complete_golden_set(self) -> None: + self.assertTrue(EXAMPLES.is_dir()) + self.assertEqual( + {*SCENARIOS, "npu"}, + {path.name for path in EXAMPLES.iterdir() if path.is_dir()}, + ) + for name in SCENARIOS: + with self.subTest(example=name): + root = EXAMPLES / name + for relative in COMMON_FILES: + self.assertTrue((root / relative).is_file(), relative) + if name in DAVINCIOO_SCENARIOS: + self.assertTrue((root / "source.davincioo.jsonl").is_file()) + manifest = (root / "agentic-circuit.toml").read_text() + self.assertIn(f'name = "workspace-{name.replace("_", "-")}"', manifest) + trace = json.loads((root / "trace.pto.json").read_text()) + self.assertEqual("pto-trace", trace["schema"]) + self.assertEqual("0.1", trace["version"]) + + def test_davincioo_sources_reproduce_the_checked_in_canonical_trace(self) -> None: + adapter = REPOSITORY / "tools" / "import-davincioo-pto-trace.py" + with tempfile.TemporaryDirectory(prefix="workspace-adapter-") as temporary: + output_root = Path(temporary) + for name in sorted(DAVINCIOO_SCENARIOS): + with self.subTest(example=name): + example = EXAMPLES / name + output = output_root / f"{name}.pto.json" + completed = subprocess.run( + [ + sys.executable, + os.fspath(adapter), + os.fspath(example / "source.davincioo.jsonl"), + os.fspath(output), + "--source-program", + "examples/beginner_matmul", + ], + cwd=REPOSITORY, + env=_environment(), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + self.assertEqual("", completed.stdout) + self.assertEqual( + (example / "trace.pto.json").read_bytes(), + output.read_bytes(), + ) + + def test_public_pipeline_goldens_replay_and_root_independence(self) -> None: + with tempfile.TemporaryDirectory(prefix="workspace-e2e-") as temporary: + temporary_root = Path(temporary) + for name in SCENARIOS: + with self.subTest(example=name): + source = EXAMPLES / name + first = temporary_root / "alpha" / name + second = temporary_root / "unrelated" / "omega" / name + _copy_workspace(source, first) + _copy_workspace(source, second) + first_artifacts = first / "artifacts" + second_artifacts = second / "artifacts" + + checked = _run_cli( + first, + "check", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--json", + ) + self.assertEqual("passed", checked["status"]) + _run_cli( + first, + "elaborate", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--emit", + "acir", + "-o", + os.fspath(first_artifacts / "elaborated.ac.mlir"), + "--json", + ) + _run_cli( + first, + "compile", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--emit", + "acpy,acir,frozen-acir,acsim,cpp", + "--output-dir", + os.fspath(first_artifacts / "compile"), + "--json", + ) + built = _run_cli( + first, + "build", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--output-dir", + os.fspath(first_artifacts / "build"), + "--json", + ) + self.assertEqual("passed", built["status"]) + ran = _run_cli( + first, + "run", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--trace", + "trace.pto.json", + "--stats-format", + "json", + "--event-log", + "jsonl", + "--expect-termination", + "--output-dir", + os.fspath(first_artifacts / "run"), + "--json", + ) + self.assertEqual("completed", ran["status"]) + hierarchy = _run_cli( + first, + "inspect", + "hierarchy", + "--project", + "agentic-circuit.toml", + "--format", + "json", + "--json", + ) + + expected_result = json.loads( + (source / "expected-result.json").read_text() + ) + expected_hierarchy = json.loads( + (source / "expected-hierarchy.json").read_text() + ) + self.assertEqual( + expected_result, _selected_result(first_artifacts / "run") + ) + self.assertEqual(expected_hierarchy, hierarchy) + self.assertEqual( + (source / "expected-stats.json").read_bytes(), + (first_artifacts / "run/stats.json").read_bytes(), + ) + self.assertEqual( + (source / "expected-events.jsonl").read_bytes(), + (first_artifacts / "run/events.jsonl").read_bytes(), + ) + + for replay_name in ("replay-one", "replay-two"): + replayed = _run_cli( + first, + "run", + "--replay-manifest", + os.fspath(first_artifacts / "run/run-manifest.json"), + "--output-dir", + os.fspath(first_artifacts / replay_name), + "--json", + ) + self.assertEqual("completed", replayed["status"]) + for relative in ( + "run-result.json", + "stats.json", + "events.jsonl", + ): + self.assertEqual( + (first_artifacts / "run" / relative).read_bytes(), + ( + first_artifacts / replay_name / relative + ).read_bytes(), + ) + + _run_cli( + second, + "compile", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--emit", + "acpy,acir,frozen-acir,acsim", + "--output-dir", + os.fspath(second_artifacts / "compile"), + "--json", + ) + _run_cli( + second, + "run", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--trace", + "trace.pto.json", + "--stats-format", + "json", + "--event-log", + "jsonl", + "--expect-termination", + "--output-dir", + os.fspath(second_artifacts / "run"), + "--json", + ) + for relative in CANONICAL_ARTIFACTS: + self.assertEqual( + (first_artifacts / "compile" / relative).read_bytes(), + (second_artifacts / "compile" / relative).read_bytes(), + ) + for relative in ("stats.json", "events.jsonl"): + self.assertEqual( + (first_artifacts / "run" / relative).read_bytes(), + (second_artifacts / "run" / relative).read_bytes(), + ) + _pack( + first_artifacts / "run/events.jsonl", + first_artifacts / "perfetto.json", + ) + _pack( + second_artifacts / "run/events.jsonl", + second_artifacts / "perfetto.json", + ) + self.assertEqual( + (first_artifacts / "perfetto.json").read_bytes(), + (second_artifacts / "perfetto.json").read_bytes(), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/e2e/test_workspace_npu.py b/components/agentic-circuit/tests/e2e/test_workspace_npu.py new file mode 100644 index 00000000..946b85bc --- /dev/null +++ b/components/agentic-circuit/tests/e2e/test_workspace_npu.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).parents[2] +EXAMPLE = REPOSITORY / "examples" / "workspaces" / "npu" +BUILD_DIRECTORY = Path( + os.environ.get( + "AGENTIC_CIRCUIT_TEST_BUILD_DIR", REPOSITORY / "build" / "dev-llvm22" + ) +).resolve() + + +def _environment() -> dict[str, str]: + environment = os.environ.copy() + entries = (REPOSITORY / "src", BUILD_DIRECTORY / "python") + environment["PYTHONPATH"] = os.pathsep.join( + [*(str(path) for path in entries), environment.get("PYTHONPATH", "")] + ).rstrip(os.pathsep) + return environment + + +def _run_cli(*arguments: str, cwd: Path = EXAMPLE) -> dict[str, object]: + completed = subprocess.run( + [sys.executable, "-m", "agentic_circuit._cli", *arguments], + cwd=cwd, + env=_environment(), + text=True, + capture_output=True, + check=False, + ) + if completed.returncode != 0: + raise AssertionError( + f"CLI failed ({completed.returncode}): {' '.join(arguments)}\n" + f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}" + ) + if completed.stderr: + raise AssertionError(f"structured CLI wrote stderr: {completed.stderr}") + return json.loads(completed.stdout) + + +def _copy_workspace(destination: Path) -> None: + shutil.copytree( + EXAMPLE, + destination, + ignore=shutil.ignore_patterns("build", "__pycache__", "*.pyc"), + ) + + +def _pack(events: Path, output: Path) -> None: + completed = subprocess.run( + [ + sys.executable, + os.fspath(REPOSITORY / "tools/pack-perfetto-trace.py"), + os.fspath(events), + os.fspath(output), + ], + cwd=REPOSITORY, + env=_environment(), + text=True, + capture_output=True, + check=False, + ) + if completed.returncode != 0: + raise AssertionError(completed.stderr) + + +def _selected_result(run_directory: Path) -> dict[str, object]: + result = json.loads((run_directory / "run-result.json").read_text()) + statistics = json.loads((run_directory / "stats.json").read_text()) + architectural_values = { + entry["name"].removeprefix("architectural_"): entry["value"] + for entry in statistics + if entry["name"].startswith("architectural_") + } + return { + "schema": "workspace-npu-result", + "version": "0.1", + "status": result["status"], + "termination_reason": result["termination_reason"], + "simulated_ticks": result["simulated_ticks"], + "trace_position": result["trace_position"], + "architectural_values": architectural_values, + } + + +class WorkspaceNpuTest(unittest.TestCase): + maxDiff = None + + def test_adapter_reproduces_checked_in_canonical_trace(self) -> None: + with tempfile.TemporaryDirectory(prefix="workspace-npu-adapter-") as temporary: + output = Path(temporary) / "pto-trace.json" + completed = subprocess.run( + [ + sys.executable, + os.fspath(REPOSITORY / "tools/import-davincioo-pto-trace.py"), + os.fspath(EXAMPLE / "traces/davincioo.jsonl"), + os.fspath(output), + "--source-program", + "examples/workspaces/npu", + ], + cwd=REPOSITORY, + env=_environment(), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + self.assertEqual( + (EXAMPLE / "traces/pto-trace.json").read_bytes(), + output.read_bytes(), + ) + trace = json.loads(output.read_text()) + self.assertEqual( + "davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb", + trace["metadata"]["producer"], + ) + self.assertEqual(6, trace["metadata"]["record_count"]) + + def test_hierarchical_npu_build_run_and_goldens(self) -> None: + checked = _run_cli( + "check", "architecture.py", "--project", "agentic-circuit.toml", "--json" + ) + self.assertEqual("passed", checked["status"]) + hierarchy = _run_cli( + "inspect", + "hierarchy", + "--project", + "agentic-circuit.toml", + "--format", + "json", + "--json", + ) + self.assertEqual( + json.loads((EXAMPLE / "expected-hierarchy.json").read_text()), hierarchy + ) + paths = {record["path"] for record in hierarchy["records"]} + for suffix in ( + "trace_source", + "frontend_decode", + "frontend_dispatch", + "backend_dependencies", + "backend_issue_scalar", + "backend_issue_vector", + "backend_issue_cube", + "backend_issue_tma", + "execution_scalar_unit_0", + "execution_vector_unit_0", + "execution_vector_unit_1", + "execution_cube_unit_0", + "execution_tma_unit_0", + "memory_load_store", + "memory_scratchpad", + "memory_controller", + "completion", + "retirement", + ): + self.assertIn(f"top.{suffix}", paths) + + with tempfile.TemporaryDirectory(prefix="workspace-npu-run-") as temporary: + output = Path(temporary) + built = _run_cli( + "build", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--output-dir", + os.fspath(output / "build"), + "--json", + ) + self.assertEqual("passed", built["status"]) + ran = _run_cli( + "run", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--trace", + "traces/pto-trace.json", + "--stats-format", + "json", + "--event-log", + "jsonl", + "--expect-termination", + "--output-dir", + os.fspath(output / "run"), + "--json", + ) + self.assertEqual("completed", ran["status"]) + stats = json.loads((output / "run/stats.json").read_text()) + events = (output / "run/events.jsonl").read_text().splitlines() + self.assertEqual( + json.loads((EXAMPLE / "expected-result.json").read_text()), + _selected_result(output / "run"), + ) + self.assertEqual( + (EXAMPLE / "expected-stats.json").read_bytes(), + (output / "run/stats.json").read_bytes(), + ) + self.assertEqual( + (EXAMPLE / "expected-events.jsonl").read_bytes(), + (output / "run/events.jsonl").read_bytes(), + ) + self.assertEqual( + 6, + next( + item["value"] + for item in stats + if item["name"] == "architectural_retired_instructions" + ), + ) + event_records = [json.loads(line) for line in events] + complete_order = [ + item["args"]["gfsim_root_sequence_id"] + for item in event_records + if item["name"] == "complete" + ] + retire_order = [ + item["args"]["gfsim_root_sequence_id"] + for item in event_records + if item["name"] == "retire" + ] + self.assertNotEqual(sorted(complete_order), complete_order) + self.assertEqual(list(range(6)), retire_order) + + _pack(output / "run/events.jsonl", output / "perfetto.json") + self.assertEqual( + (EXAMPLE / "expected-perfetto.json").read_bytes(), + (output / "perfetto.json").read_bytes(), + ) + + def test_replay_and_equivalent_roots_are_byte_identical(self) -> None: + with tempfile.TemporaryDirectory(prefix="workspace-npu-determinism-") as temporary: + root = Path(temporary) + first = root / "alpha" / "npu" + second = root / "unrelated" / "omega" / "npu" + _copy_workspace(first) + _copy_workspace(second) + + for workspace in (first, second): + _run_cli( + "run", + "architecture.py", + "--project", + "agentic-circuit.toml", + "--trace", + "traces/pto-trace.json", + "--stats-format", + "json", + "--event-log", + "jsonl", + "--expect-termination", + "--output-dir", + "artifacts/run", + "--json", + cwd=workspace, + ) + _pack( + workspace / "artifacts/run/events.jsonl", + workspace / "artifacts/perfetto.json", + ) + + for replay_name in ("replay-one", "replay-two"): + _run_cli( + "run", + "--replay-manifest", + "artifacts/run/run-manifest.json", + "--output-dir", + f"artifacts/{replay_name}", + "--json", + cwd=first, + ) + + for relative in ("run-result.json", "stats.json", "events.jsonl"): + expected = (first / "artifacts/run" / relative).read_bytes() + self.assertEqual( + expected, (second / "artifacts/run" / relative).read_bytes() + ) + self.assertEqual( + expected, + (first / "artifacts/replay-one" / relative).read_bytes(), + ) + self.assertEqual( + expected, + (first / "artifacts/replay-two" / relative).read_bytes(), + ) + self.assertEqual( + (first / "artifacts/perfetto.json").read_bytes(), + (second / "artifacts/perfetto.json").read_bytes(), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/goldens/README.md b/components/agentic-circuit/tests/goldens/README.md new file mode 100644 index 00000000..8259f598 --- /dev/null +++ b/components/agentic-circuit/tests/goldens/README.md @@ -0,0 +1,4 @@ +# Test goldens + +This directory owns generated outputs that tests compare byte-for-byte. Goldens +are verification artifacts, not public examples and not runtime resources. diff --git a/components/agentic-circuit/tests/goldens/davincioo/README.md b/components/agentic-circuit/tests/goldens/davincioo/README.md new file mode 100644 index 00000000..94304c61 --- /dev/null +++ b/components/agentic-circuit/tests/goldens/davincioo/README.md @@ -0,0 +1,9 @@ +# DavinciOO goldens + +- `softmax-projection.json` defines the committed behavioral projection used by + gfsim and PYC refinement tests. +- `davincioo-softmax-run.json` is the canonical generated run report. +- `davincioo-softmax-swimlane.svg` is the canonical trace visualization. + +Regenerate the run and visualization with `tools/run-davincioo.py`; tests reject +any byte-level drift. diff --git a/components/agentic-circuit/tests/goldens/davincioo/davincioo-softmax-run.json b/components/agentic-circuit/tests/goldens/davincioo/davincioo-softmax-run.json new file mode 100644 index 00000000..44ddc86d --- /dev/null +++ b/components/agentic-circuit/tests/goldens/davincioo/davincioo-softmax-run.json @@ -0,0 +1 @@ +{"architectural_values":[102,115,122,132,143,152,163,172,183,192,202,213,222,233,245],"completion_order":[0,2,3,5,7,9,10,12,1,4,6,8,11,13,14],"contract_epoch":"0.3","cycles":453,"record_count":15,"reference_cycles":453,"retired_count":15,"retirement_order":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14],"schema":"agentic-circuit-davincioo-run","spans":[{"begin":0,"end":1,"opcode":"TASSIGN","sequence":0,"stage":"incoming"},{"begin":0,"end":1,"opcode":"TLOAD","sequence":1,"stage":"source_wait"},{"begin":1,"end":2,"opcode":"TLOAD","sequence":1,"stage":"incoming"},{"begin":0,"end":2,"opcode":"TASSIGN","sequence":2,"stage":"source_wait"},{"begin":1,"end":3,"opcode":"TASSIGN","sequence":0,"stage":"decoded"},{"begin":2,"end":3,"opcode":"TASSIGN","sequence":2,"stage":"incoming"},{"begin":0,"end":3,"opcode":"TASSIGN","sequence":3,"stage":"source_wait"},{"begin":3,"end":4,"opcode":"TASSIGN","sequence":0,"stage":"pipelined"},{"begin":2,"end":4,"opcode":"TLOAD","sequence":1,"stage":"decoded"},{"begin":3,"end":4,"opcode":"TASSIGN","sequence":3,"stage":"incoming"},{"begin":0,"end":4,"opcode":"TROWMAX","sequence":4,"stage":"source_wait"},{"begin":4,"end":5,"opcode":"TASSIGN","sequence":0,"stage":"dispatch_scalar"},{"begin":4,"end":5,"opcode":"TLOAD","sequence":1,"stage":"pipelined"},{"begin":3,"end":5,"opcode":"TASSIGN","sequence":2,"stage":"decoded"},{"begin":4,"end":5,"opcode":"TROWMAX","sequence":4,"stage":"incoming"},{"begin":0,"end":5,"opcode":"TASSIGN","sequence":5,"stage":"source_wait"},{"begin":5,"end":6,"opcode":"TASSIGN","sequence":0,"stage":"dispatched"},{"begin":5,"end":6,"opcode":"TLOAD","sequence":1,"stage":"dispatch_tma"},{"begin":5,"end":6,"opcode":"TASSIGN","sequence":2,"stage":"pipelined"},{"begin":4,"end":6,"opcode":"TASSIGN","sequence":3,"stage":"decoded"},{"begin":5,"end":6,"opcode":"TASSIGN","sequence":5,"stage":"incoming"},{"begin":0,"end":6,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"source_wait"},{"begin":6,"end":7,"opcode":"TLOAD","sequence":1,"stage":"dispatched"},{"begin":6,"end":7,"opcode":"TASSIGN","sequence":2,"stage":"dispatch_scalar"},{"begin":6,"end":7,"opcode":"TASSIGN","sequence":3,"stage":"pipelined"},{"begin":5,"end":7,"opcode":"TROWMAX","sequence":4,"stage":"decoded"},{"begin":6,"end":7,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"incoming"},{"begin":0,"end":7,"opcode":"TASSIGN","sequence":7,"stage":"source_wait"},{"begin":7,"end":8,"opcode":"TASSIGN","sequence":2,"stage":"dispatched"},{"begin":7,"end":8,"opcode":"TASSIGN","sequence":3,"stage":"dispatch_scalar"},{"begin":7,"end":8,"opcode":"TROWMAX","sequence":4,"stage":"pipelined"},{"begin":6,"end":8,"opcode":"TASSIGN","sequence":5,"stage":"decoded"},{"begin":7,"end":8,"opcode":"TASSIGN","sequence":7,"stage":"incoming"},{"begin":0,"end":8,"opcode":"TEXP","sequence":8,"stage":"source_wait"},{"begin":6,"end":9,"opcode":"TASSIGN","sequence":0,"stage":"schedule_execute"},{"begin":8,"end":9,"opcode":"TASSIGN","sequence":3,"stage":"dispatched"},{"begin":8,"end":9,"opcode":"TROWMAX","sequence":4,"stage":"dispatch_vector"},{"begin":8,"end":9,"opcode":"TASSIGN","sequence":5,"stage":"pipelined"},{"begin":7,"end":9,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"decoded"},{"begin":8,"end":9,"opcode":"TEXP","sequence":8,"stage":"incoming"},{"begin":0,"end":9,"opcode":"TASSIGN","sequence":9,"stage":"source_wait"},{"begin":9,"end":10,"opcode":"TASSIGN","sequence":0,"stage":"completed"},{"begin":9,"end":10,"opcode":"TROWMAX","sequence":4,"stage":"dispatched"},{"begin":9,"end":10,"opcode":"TASSIGN","sequence":5,"stage":"dispatch_scalar"},{"begin":9,"end":10,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"pipelined"},{"begin":8,"end":10,"opcode":"TASSIGN","sequence":7,"stage":"decoded"},{"begin":9,"end":10,"opcode":"TASSIGN","sequence":9,"stage":"incoming"},{"begin":0,"end":10,"opcode":"TASSIGN","sequence":10,"stage":"source_wait"},{"begin":10,"end":11,"opcode":"TASSIGN","sequence":0,"stage":"rob_wait"},{"begin":8,"end":11,"opcode":"TASSIGN","sequence":2,"stage":"schedule_execute"},{"begin":10,"end":11,"opcode":"TASSIGN","sequence":5,"stage":"dispatched"},{"begin":10,"end":11,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"dispatch_vector"},{"begin":10,"end":11,"opcode":"TASSIGN","sequence":7,"stage":"pipelined"},{"begin":9,"end":11,"opcode":"TEXP","sequence":8,"stage":"decoded"},{"begin":10,"end":11,"opcode":"TASSIGN","sequence":10,"stage":"incoming"},{"begin":0,"end":11,"opcode":"TROWSUM","sequence":11,"stage":"source_wait"},{"begin":11,"end":12,"opcode":"TASSIGN","sequence":0,"stage":"retired"},{"begin":11,"end":12,"opcode":"TASSIGN","sequence":2,"stage":"completed"},{"begin":9,"end":12,"opcode":"TASSIGN","sequence":3,"stage":"schedule_execute"},{"begin":11,"end":12,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"dispatched"},{"begin":11,"end":12,"opcode":"TASSIGN","sequence":7,"stage":"dispatch_scalar"},{"begin":11,"end":12,"opcode":"TEXP","sequence":8,"stage":"pipelined"},{"begin":10,"end":12,"opcode":"TASSIGN","sequence":9,"stage":"decoded"},{"begin":11,"end":12,"opcode":"TROWSUM","sequence":11,"stage":"incoming"},{"begin":0,"end":12,"opcode":"TASSIGN","sequence":12,"stage":"source_wait"},{"begin":12,"end":13,"opcode":"TASSIGN","sequence":3,"stage":"completed"},{"begin":12,"end":13,"opcode":"TASSIGN","sequence":7,"stage":"dispatched"},{"begin":12,"end":13,"opcode":"TEXP","sequence":8,"stage":"dispatch_vector"},{"begin":12,"end":13,"opcode":"TASSIGN","sequence":9,"stage":"pipelined"},{"begin":11,"end":13,"opcode":"TASSIGN","sequence":10,"stage":"decoded"},{"begin":12,"end":13,"opcode":"TASSIGN","sequence":12,"stage":"incoming"},{"begin":0,"end":13,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"source_wait"},{"begin":11,"end":14,"opcode":"TASSIGN","sequence":5,"stage":"schedule_execute"},{"begin":13,"end":14,"opcode":"TEXP","sequence":8,"stage":"dispatched"},{"begin":13,"end":14,"opcode":"TASSIGN","sequence":9,"stage":"dispatch_scalar"},{"begin":13,"end":14,"opcode":"TASSIGN","sequence":10,"stage":"pipelined"},{"begin":12,"end":14,"opcode":"TROWSUM","sequence":11,"stage":"decoded"},{"begin":13,"end":14,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"incoming"},{"begin":0,"end":14,"opcode":"TSTORE","sequence":14,"stage":"source_wait"},{"begin":14,"end":15,"opcode":"TASSIGN","sequence":5,"stage":"completed"},{"begin":14,"end":15,"opcode":"TASSIGN","sequence":9,"stage":"dispatched"},{"begin":14,"end":15,"opcode":"TASSIGN","sequence":10,"stage":"dispatch_scalar"},{"begin":14,"end":15,"opcode":"TROWSUM","sequence":11,"stage":"pipelined"},{"begin":13,"end":15,"opcode":"TASSIGN","sequence":12,"stage":"decoded"},{"begin":14,"end":15,"opcode":"TSTORE","sequence":14,"stage":"incoming"},{"begin":13,"end":16,"opcode":"TASSIGN","sequence":7,"stage":"schedule_execute"},{"begin":15,"end":16,"opcode":"TASSIGN","sequence":10,"stage":"dispatched"},{"begin":15,"end":16,"opcode":"TROWSUM","sequence":11,"stage":"dispatch_vector"},{"begin":15,"end":16,"opcode":"TASSIGN","sequence":12,"stage":"pipelined"},{"begin":14,"end":16,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"decoded"},{"begin":16,"end":17,"opcode":"TASSIGN","sequence":7,"stage":"completed"},{"begin":16,"end":17,"opcode":"TROWSUM","sequence":11,"stage":"dispatched"},{"begin":16,"end":17,"opcode":"TASSIGN","sequence":12,"stage":"dispatch_scalar"},{"begin":16,"end":17,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"pipelined"},{"begin":15,"end":17,"opcode":"TSTORE","sequence":14,"stage":"decoded"},{"begin":15,"end":18,"opcode":"TASSIGN","sequence":9,"stage":"schedule_execute"},{"begin":17,"end":18,"opcode":"TASSIGN","sequence":12,"stage":"dispatched"},{"begin":17,"end":18,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"dispatch_vector"},{"begin":17,"end":18,"opcode":"TSTORE","sequence":14,"stage":"pipelined"},{"begin":18,"end":19,"opcode":"TASSIGN","sequence":9,"stage":"completed"},{"begin":16,"end":19,"opcode":"TASSIGN","sequence":10,"stage":"schedule_execute"},{"begin":18,"end":19,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"dispatched"},{"begin":18,"end":19,"opcode":"TSTORE","sequence":14,"stage":"dispatch_tma"},{"begin":19,"end":20,"opcode":"TASSIGN","sequence":10,"stage":"completed"},{"begin":19,"end":20,"opcode":"TSTORE","sequence":14,"stage":"dispatched"},{"begin":18,"end":21,"opcode":"TASSIGN","sequence":12,"stage":"schedule_execute"},{"begin":21,"end":22,"opcode":"TASSIGN","sequence":12,"stage":"completed"},{"begin":7,"end":132,"opcode":"TLOAD","sequence":1,"stage":"schedule_execute"},{"begin":132,"end":133,"opcode":"TLOAD","sequence":1,"stage":"completed"},{"begin":133,"end":134,"opcode":"TLOAD","sequence":1,"stage":"rob_wait"},{"begin":134,"end":135,"opcode":"TLOAD","sequence":1,"stage":"retired"},{"begin":12,"end":135,"opcode":"TASSIGN","sequence":2,"stage":"rob_wait"},{"begin":135,"end":136,"opcode":"TASSIGN","sequence":2,"stage":"retired"},{"begin":13,"end":136,"opcode":"TASSIGN","sequence":3,"stage":"rob_wait"},{"begin":136,"end":137,"opcode":"TASSIGN","sequence":3,"stage":"retired"},{"begin":10,"end":272,"opcode":"TROWMAX","sequence":4,"stage":"schedule_execute"},{"begin":272,"end":273,"opcode":"TROWMAX","sequence":4,"stage":"completed"},{"begin":273,"end":274,"opcode":"TROWMAX","sequence":4,"stage":"rob_wait"},{"begin":274,"end":275,"opcode":"TROWMAX","sequence":4,"stage":"retired"},{"begin":15,"end":275,"opcode":"TASSIGN","sequence":5,"stage":"rob_wait"},{"begin":275,"end":276,"opcode":"TASSIGN","sequence":5,"stage":"retired"},{"begin":12,"end":285,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"schedule_execute"},{"begin":285,"end":286,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"completed"},{"begin":286,"end":287,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"rob_wait"},{"begin":287,"end":288,"opcode":"TROWEXPANDSUB","sequence":6,"stage":"retired"},{"begin":17,"end":288,"opcode":"TASSIGN","sequence":7,"stage":"rob_wait"},{"begin":288,"end":289,"opcode":"TASSIGN","sequence":7,"stage":"retired"},{"begin":14,"end":298,"opcode":"TEXP","sequence":8,"stage":"schedule_execute"},{"begin":298,"end":299,"opcode":"TEXP","sequence":8,"stage":"completed"},{"begin":299,"end":300,"opcode":"TEXP","sequence":8,"stage":"rob_wait"},{"begin":300,"end":301,"opcode":"TEXP","sequence":8,"stage":"retired"},{"begin":19,"end":301,"opcode":"TASSIGN","sequence":9,"stage":"rob_wait"},{"begin":301,"end":302,"opcode":"TASSIGN","sequence":9,"stage":"retired"},{"begin":20,"end":302,"opcode":"TASSIGN","sequence":10,"stage":"rob_wait"},{"begin":302,"end":303,"opcode":"TASSIGN","sequence":10,"stage":"retired"},{"begin":17,"end":311,"opcode":"TROWSUM","sequence":11,"stage":"schedule_execute"},{"begin":311,"end":312,"opcode":"TROWSUM","sequence":11,"stage":"completed"},{"begin":312,"end":313,"opcode":"TROWSUM","sequence":11,"stage":"rob_wait"},{"begin":313,"end":314,"opcode":"TROWSUM","sequence":11,"stage":"retired"},{"begin":22,"end":314,"opcode":"TASSIGN","sequence":12,"stage":"rob_wait"},{"begin":314,"end":315,"opcode":"TASSIGN","sequence":12,"stage":"retired"},{"begin":19,"end":324,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"schedule_execute"},{"begin":324,"end":325,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"completed"},{"begin":325,"end":326,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"rob_wait"},{"begin":326,"end":327,"opcode":"TROWEXPANDDIV","sequence":13,"stage":"retired"},{"begin":20,"end":449,"opcode":"TSTORE","sequence":14,"stage":"schedule_execute"},{"begin":449,"end":450,"opcode":"TSTORE","sequence":14,"stage":"completed"},{"begin":450,"end":451,"opcode":"TSTORE","sequence":14,"stage":"rob_wait"},{"begin":451,"end":452,"opcode":"TSTORE","sequence":14,"stage":"retired"}],"specialization":"sha256:f11f0cd208553e37476bbffd572a6c87904f22a92a4ddc86507af063a2131502","trace_content_hash":"sha256:03de59fcc95cfccdfbbf220f19e41dc11971dc2b6a0b58e191c86a880e0d8118","version":"0.1"} diff --git a/components/agentic-circuit/tests/goldens/davincioo/davincioo-softmax-swimlane.svg b/components/agentic-circuit/tests/goldens/davincioo/davincioo-softmax-swimlane.svg new file mode 100644 index 00000000..7aaaf2f2 --- /dev/null +++ b/components/agentic-circuit/tests/goldens/davincioo/davincioo-softmax-swimlane.svg @@ -0,0 +1,217 @@ + + + +Generated DavinciOO PTO trace swimlane — 453 cycles + +0 + +45 + +90 + +135 + +180 + +225 + +270 + +315 + +360 + +405 + +450 +00 TASSIGN + +01 TLOAD + +02 TASSIGN + +03 TASSIGN + +04 TROWMAX + +05 TASSIGN + +06 TROWEXPANDSUB + +07 TASSIGN + +08 TEXP + +09 TASSIGN + +10 TASSIGN + +11 TROWSUM + +12 TASSIGN + +13 TROWEXPANDDIV + +14 TSTORE + +incoming [0,1) +source_wait [0,1) +incoming [1,2) +source_wait [0,2) +decoded [1,3) +incoming [2,3) +source_wait [0,3) +pipelined [3,4) +decoded [2,4) +incoming [3,4) +source_wait [0,4) +dispatch_scalar [4,5) +pipelined [4,5) +decoded [3,5) +incoming [4,5) +source_wait [0,5) +dispatched [5,6) +dispatch_tma [5,6) +pipelined [5,6) +decoded [4,6) +incoming [5,6) +source_wait [0,6) +dispatched [6,7) +dispatch_scalar [6,7) +pipelined [6,7) +decoded [5,7) +incoming [6,7) +source_wait [0,7) +dispatched [7,8) +dispatch_scalar [7,8) +pipelined [7,8) +decoded [6,8) +incoming [7,8) +source_wait [0,8) +schedule_execute [6,9) +dispatched [8,9) +dispatch_vector [8,9) +pipelined [8,9) +decoded [7,9) +incoming [8,9) +source_wait [0,9) +completed [9,10) +dispatched [9,10) +dispatch_scalar [9,10) +pipelined [9,10) +decoded [8,10) +incoming [9,10) +source_wait [0,10) +rob_wait [10,11) +schedule_execute [8,11) +dispatched [10,11) +dispatch_vector [10,11) +pipelined [10,11) +decoded [9,11) +incoming [10,11) +source_wait [0,11) +retired [11,12) +completed [11,12) +schedule_execute [9,12) +dispatched [11,12) +dispatch_scalar [11,12) +pipelined [11,12) +decoded [10,12) +incoming [11,12) +source_wait [0,12) +completed [12,13) +dispatched [12,13) +dispatch_vector [12,13) +pipelined [12,13) +decoded [11,13) +incoming [12,13) +source_wait [0,13) +schedule_execute [11,14) +dispatched [13,14) +dispatch_scalar [13,14) +pipelined [13,14) +decoded [12,14) +incoming [13,14) +source_wait [0,14) +completed [14,15) +dispatched [14,15) +dispatch_scalar [14,15) +pipelined [14,15) +decoded [13,15) +incoming [14,15) +schedule_execute [13,16) +dispatched [15,16) +dispatch_vector [15,16) +pipelined [15,16) +decoded [14,16) +completed [16,17) +dispatched [16,17) +dispatch_scalar [16,17) +pipelined [16,17) +decoded [15,17) +schedule_execute [15,18) +dispatched [17,18) +dispatch_vector [17,18) +pipelined [17,18) +completed [18,19) +schedule_execute [16,19) +dispatched [18,19) +dispatch_tma [18,19) +completed [19,20) +dispatched [19,20) +schedule_execute [18,21) +completed [21,22) +schedule_execute [7,132) +completed [132,133) +rob_wait [133,134) +retired [134,135) +rob_wait [12,135) +retired [135,136) +rob_wait [13,136) +retired [136,137) +schedule_execute [10,272) +completed [272,273) +rob_wait [273,274) +retired [274,275) +rob_wait [15,275) +retired [275,276) +schedule_execute [12,285) +completed [285,286) +rob_wait [286,287) +retired [287,288) +rob_wait [17,288) +retired [288,289) +schedule_execute [14,298) +completed [298,299) +rob_wait [299,300) +retired [300,301) +rob_wait [19,301) +retired [301,302) +rob_wait [20,302) +retired [302,303) +schedule_execute [17,311) +completed [311,312) +rob_wait [312,313) +retired [313,314) +rob_wait [22,314) +retired [314,315) +schedule_execute [19,324) +completed [324,325) +rob_wait [325,326) +retired [326,327) +schedule_execute [20,449) +completed [449,450) +rob_wait [450,451) +retired [451,452) + +frontend + +dispatch + +schedule/execute + +ROB wait + +retired +cycle + diff --git a/components/agentic-circuit/tests/goldens/davincioo/softmax-projection.json b/components/agentic-circuit/tests/goldens/davincioo/softmax-projection.json new file mode 100644 index 00000000..be602e5d --- /dev/null +++ b/components/agentic-circuit/tests/goldens/davincioo/softmax-projection.json @@ -0,0 +1,77 @@ +{ + "schema": "agentic-circuit-davincioo-projection", + "version": "0.3", + "contract_epoch": "0.3", + "source": { + "repository_commit": "a542b9cf705096288c615575be222b974b570a18", + "trace": "references/davincioo-gfsim/upstream/tests/fixtures/traces/examples_intermediate_softmax.pto.trace", + "record_count": 15 + }, + "opcode_ids": { + "TASSIGN": 0, + "TLOAD": 1, + "TROWMAX": 2, + "TROWEXPANDSUB": 3, + "TEXP": 4, + "TROWSUM": 5, + "TROWEXPANDDIV": 6, + "TSTORE": 7 + }, + "routes": { + "TASSIGN": 0, + "TLOAD": 3, + "TROWMAX": 1, + "TROWEXPANDSUB": 1, + "TEXP": 1, + "TROWSUM": 1, + "TROWEXPANDDIV": 1, + "TSTORE": 3 + }, + "execution_cycles": { + "TASSIGN": 1, + "TLOAD": 128, + "TROWMAX": 139, + "TROWEXPANDSUB": 12, + "TEXP": 12, + "TROWSUM": 12, + "TROWEXPANDDIV": 12, + "TSTORE": 128 + }, + "model_cost": { + "TASSIGN": 1, + "TLOAD": 123, + "TROWMAX": 139, + "TROWEXPANDSUB": 12, + "TEXP": 12, + "TROWSUM": 12, + "TROWEXPANDDIV": 12, + "TSTORE": 124 + }, + "boundary_cycle_compensation": { + "ingress": 5, + "drain": 4 + }, + "waits_for": [255, 255, 255, 255, 1, 255, 4, 255, 6, 255, 255, 8, 255, 11, 13], + "opcode_counts": { + "TASSIGN": 8, + "TEXP": 1, + "TLOAD": 1, + "TROWEXPANDDIV": 1, + "TROWEXPANDSUB": 1, + "TROWMAX": 1, + "TROWSUM": 1, + "TSTORE": 1 + }, + "completion_order": [0, 2, 3, 5, 7, 9, 10, 12, 1, 4, 6, 8, 11, 13, 14], + "retirement_order": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + "reorder_capacity": 64, + "dependency_capacity": 8, + "dependency_resources": 4, + "occupancy_projection": { + "dependency_window_peak": 8, + "resource_executing_peaks": [1, 1, 0, 1], + "reorder_window_peak": 8 + }, + "architectural_values": [102, 115, 122, 132, 143, 152, 163, 172, 183, 192, 202, 213, 222, 233, 245], + "simulated_cycles": 453 +} diff --git a/components/agentic-circuit/tests/install-consumer/CMakeLists.txt b/components/agentic-circuit/tests/install-consumer/CMakeLists.txt new file mode 100644 index 00000000..1ad56881 --- /dev/null +++ b/components/agentic-circuit/tests/install-consumer/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.25) + +project(AgenticCircuitInstallConsumer VERSION 0.1.0 LANGUAGES C CXX) + +# This consumer configures against the installed AgenticCircuit package only. +# It must not see source-tree or build-tree include paths; CI verifies that +# CMAKE_CURRENT_SOURCE_DIR stays this directory. +find_package(AgenticCircuit 0.1.0 EXACT CONFIG REQUIRED) + +execute_process( + COMMAND "${AgenticCircuit_EXECUTABLE}" schema capabilities --json + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE ACIR_SCHEMA_RESULT + OUTPUT_VARIABLE ACIR_SCHEMA_OUTPUT + ERROR_VARIABLE ACIR_SCHEMA_ERROR +) +if(NOT ACIR_SCHEMA_RESULT EQUAL 0) + message(FATAL_ERROR + "installed schema capabilities failed (${ACIR_SCHEMA_RESULT}): " + "${ACIR_SCHEMA_ERROR}${ACIR_SCHEMA_OUTPUT}") +endif() + +execute_process( + COMMAND "${AgenticCircuit_EXECUTABLE}" check architecture.py + --project "${CMAKE_CURRENT_SOURCE_DIR}/agentic-circuit.toml" --json + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE ACIR_CHECK_RESULT + OUTPUT_VARIABLE ACIR_CHECK_OUTPUT + ERROR_VARIABLE ACIR_CHECK_ERROR +) +if(NOT ACIR_CHECK_RESULT EQUAL 0) + message(FATAL_ERROR + "installed architecture check failed (${ACIR_CHECK_RESULT}): " + "${ACIR_CHECK_ERROR}${ACIR_CHECK_OUTPUT}") +endif() + +include_directories(SYSTEM ${LLVM_INCLUDE_DIRS} ${MLIR_INCLUDE_DIRS}) +add_definitions(${LLVM_DEFINITIONS}) + +add_executable(process-state-plan-consumer main.cpp) +target_compile_features(process-state-plan-consumer PRIVATE cxx_std_20) +set_target_properties(process-state-plan-consumer PROPERTIES CXX_EXTENSIONS OFF) +target_link_libraries( + process-state-plan-consumer + PRIVATE + AgenticCircuit::ACIRAnalysis + MLIRParser +) diff --git a/components/agentic-circuit/tests/install-consumer/agentic-circuit.toml b/components/agentic-circuit/tests/install-consumer/agentic-circuit.toml new file mode 100644 index 00000000..ae2b2ec1 --- /dev/null +++ b/components/agentic-circuit/tests/install-consumer/agentic-circuit.toml @@ -0,0 +1,26 @@ +contract_epoch = "0.3" + +[project] +name = "install-consumer" +version = "0.1.0" +architecture = "architecture.py" +system = "main" + +[providers] +standard_library = ["ac"] + +[build] +profile = "fast" +compiler = "c++" +standard_library = "libc++" +component_roots = ["components"] +protocol_roots = ["protocols"] +build_root = "build" +instrumentation_layers = [] + +[run] +trace_roots = ["traces"] +inputs = {} + +[diagnostics] +format = "text" diff --git a/components/agentic-circuit/tests/install-consumer/architecture.py b/components/agentic-circuit/tests/install-consumer/architecture.py new file mode 100644 index 00000000..73589ab8 --- /dev/null +++ b/components/agentic-circuit/tests/install-consumer/architecture.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from agentic_circuit import module, system + + +@module +def top() -> None: + return + + +@system(root="top") +def main() -> None: + return diff --git a/components/agentic-circuit/tests/install-consumer/main.cpp b/components/agentic-circuit/tests/install-consumer/main.cpp new file mode 100644 index 00000000..b5e43e77 --- /dev/null +++ b/components/agentic-circuit/tests/install-consumer/main.cpp @@ -0,0 +1,50 @@ +// Installed-tree consumer for the production ProcessStatePlan API. +#include "acir/Analysis/ProcessStatePlan.h" +#include "acir/Dialect/ACIR/ACIRDialect.h" + +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/DialectRegistry.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" + +int main() { + mlir::DialectRegistry registry; + registry.insert(); + mlir::MLIRContext context(registry); + context.loadAllAvailableDialects(); + + auto module = mlir::parseSourceString(R"mlir( +module attributes {ac.contract_epoch = "0.3", ac.freeze_epoch = "0.3", + ac.frozen_instrumentation = [], + ac.frozen_owners = [ + {kind = "ac.system_root", owner = @Top, path = "root", stable_id = "root"}, + {kind = "ac.process", owner = @Top::@workload, + path = "root.workload", stable_id = "root/workload"}], + ac.frozen_primary_workload = {path = "root.workload", + reference = @Top::@workload, stable_id = "root/workload"}, + ac.frozen_system = @soc, + ac.topology_digest = "0000000000000000000000000000000000000000000000000000000000000000", + ac.topology_frozen = true} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {format = "json", id = "default"} selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + ac.yield_sim + } {ac.frozen_process_skeleton = ["process/r0/b0/o0 ac.yield_sim{}props=<> operands= results= regions="]} + ac.return + } +} +)mlir", + &context); + if (!module) + return 1; + + auto plans = acir::planProcessState(*module); + if (mlir::failed(plans) || mlir::failed(acir::verifyProcessStatePlan(*plans))) + return 2; + auto serialized = acir::serializeProcessStatePlan(*plans); + if (!serialized || serialized->empty()) + return 3; + return plans->processes().size() == 1 ? 0 : 4; +} diff --git a/components/agentic-circuit/tests/python_frontend/__init__.py b/components/agentic-circuit/tests/python_frontend/__init__.py new file mode 100644 index 00000000..525a0511 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/__init__.py @@ -0,0 +1 @@ +"""Tests for the Agentic Circuit Python frontend.""" diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/acpy/minimal.acpy.json b/components/agentic-circuit/tests/python_frontend/fixtures/acpy/minimal.acpy.json new file mode 100644 index 00000000..646613af --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/acpy/minimal.acpy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","entities":[{"definition":null,"id":"e0","kind":"system","parent":null,"properties":[],"schema_ref":null,"scope":"Architecture","source":{"end_column":1,"end_line":2,"file":"architecture.py","start_column":1,"start_line":1},"type":null,"uses":[]}],"entry":"e0","schema":"agentic-circuit-acpy","sources":[{"path":"architecture.py","sha256":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}],"version":"0.1"} diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/definitions/basic.py b/components/agentic-circuit/tests/python_frontend/fixtures/definitions/basic.py new file mode 100644 index 00000000..b3b5665f --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/definitions/basic.py @@ -0,0 +1,11 @@ +from agentic_circuit import Static, module, system + + +@module +def worker(lanes: Static[int]) -> None: + return None + + +@system +def main(lanes: int = 4) -> None: + return None diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/definitions/invalid.py b/components/agentic-circuit/tests/python_frontend/fixtures/definitions/invalid.py new file mode 100644 index 00000000..9bc9d2d9 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/definitions/invalid.py @@ -0,0 +1,11 @@ +from agentic_circuit import system + + +@system +def invalid_defaults(lanes: int = object()) -> None: + return None + + +@system +def invalid_variadic(*lanes: int) -> None: + return None diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.ac.mlir b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.ac.mlir new file mode 100644 index 00000000..1a617a3f --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.ac.mlir @@ -0,0 +1,10 @@ +module attributes {ac.contract_epoch = "0.3"} { + ac.system @main root @pipeline as "root" tick 0 "cycle" seed {kind = "fixed", value = 0 : i64} instrumentation [] results {id = "default", format = "json"} selected true + ac.module @Refine(%input : i32) -> i32 parameters {} graph { + ac.return %input : i32 + } + ac.module @pipeline(%request : i32) -> i32 parameters {} graph { + %refined_0 = ac.instance @refined of @Refine(%request) static {} id "refined" path "refined" : (i32) -> i32 + ac.return %refined_0 : i32 + } +} diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.acpy.json b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.acpy.json new file mode 100644 index 00000000..89976a3a --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.acpy.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","entities":[{"definition":null,"id":"e0","kind":"system","parent":null,"properties":[{"name":"root","value":"pipeline"}],"schema_ref":null,"scope":"main","source":{"end_column":16,"end_line":18,"file":"hierarchy.py","start_column":1,"start_line":16},"type":null,"uses":[]},{"definition":null,"id":"e1","kind":"module","parent":"e0","properties":[{"name":"name","value":"pipeline"}],"schema_ref":null,"scope":"pipeline","source":{"end_column":19,"end_line":13,"file":"hierarchy.py","start_column":1,"start_line":10},"type":null,"uses":[]},{"definition":null,"id":"e2","kind":"arg","parent":"e1","properties":[{"name":"name","value":"request"}],"schema_ref":null,"scope":"pipeline","source":{"end_column":19,"end_line":13,"file":"hierarchy.py","start_column":1,"start_line":10},"type":"Flow[int, ReadyValid]","uses":[]},{"definition":null,"id":"e3","kind":"call","parent":"e1","properties":[{"name":"instance_name","value":"refined"}],"schema_ref":{"fingerprint":"sha256:0000000000000000000000000000000000000000000000000000000000000000","identity":"test.Refine"},"scope":"pipeline","source":{"end_column":30,"end_line":12,"file":"hierarchy.py","start_column":15,"start_line":12},"type":null,"uses":["e2"]},{"definition":null,"id":"e4","kind":"result","parent":"e3","properties":[{"name":"name","value":"refined#0"}],"schema_ref":null,"scope":"pipeline","source":{"end_column":30,"end_line":12,"file":"hierarchy.py","start_column":15,"start_line":12},"type":"i32","uses":["e3"]},{"definition":null,"id":"e5","kind":"return","parent":"e1","properties":[],"schema_ref":null,"scope":"pipeline","source":{"end_column":19,"end_line":13,"file":"hierarchy.py","start_column":1,"start_line":10},"type":null,"uses":["e4"]}],"entry":"e0","schema":"agentic-circuit-acpy","sources":[{"path":"hierarchy.py","sha256":"sha256:4c109d3b0ed78480ed694001f6c34d308184c60c31c67e4ae943c38dbf6de4e9"}],"version":"0.1"} diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.py b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.py new file mode 100644 index 00000000..e7090cb5 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/hierarchy.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from agentic_circuit import Flow, module, system + + +class ReadyValid: + pass + + +@module +def pipeline(request: Flow[int, ReadyValid]) -> Flow[int, ReadyValid]: + refined = Refine(request) + return refined + + +@system(root="pipeline") +def main() -> None: + return None diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/lowering/process.ac.mlir b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/process.ac.mlir new file mode 100644 index 00000000..b3972eaa --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/process.ac.mlir @@ -0,0 +1,9 @@ +module attributes {ac.contract_epoch = "0.3"} { + ac.system @main root @top as "root" tick 0 "cycle" workload @top::@workload seed {kind = "fixed", value = 0 : i64} instrumentation [] results {id = "default", format = "json"} selected true + ac.module @top() parameters {} graph { + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/lowering/process.py b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/process.py new file mode 100644 index 00000000..8ec36c8b --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/lowering/process.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from agentic_circuit import module, process, system + + +@module +def top() -> None: + return + + +@process(kind="workload") +def workload() -> None: + yield_sim() + + +@system(root="top") +def main() -> None: + return None diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/normalize/ambiguous.py b/components/agentic-circuit/tests/python_frontend/fixtures/normalize/ambiguous.py new file mode 100644 index 00000000..1bd71809 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/normalize/ambiguous.py @@ -0,0 +1,16 @@ +from agentic_circuit import Flow, module, system + + +class ReadyValid: + pass + + +@module +def pipeline(request: Flow[int, ReadyValid]) -> Flow[int, ReadyValid]: + decoded = Decode(request) + return decoded + + +@system +def main() -> None: + return None diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/normalize/calls.py b/components/agentic-circuit/tests/python_frontend/fixtures/normalize/calls.py new file mode 100644 index 00000000..3edec566 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/normalize/calls.py @@ -0,0 +1,19 @@ +from agentic_circuit import Flow, module, system + + +class ReadyValid: + pass + + +@module +def pipeline( + request: Flow[int, ReadyValid], +) -> tuple[Flow[int, ReadyValid], Flow[int, ReadyValid]]: + decoded, accepted = Decode(request, mode="strict") + decoded = Refine(decoded) + return decoded, accepted + + +@system +def main() -> None: + return None diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/process/invalid.py b/components/agentic-circuit/tests/python_frontend/fixtures/process/invalid.py new file mode 100644 index 00000000..9385478f --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/process/invalid.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from agentic_circuit import ResourceRef, process + + +class RuntimeBool: + pass + + +class RequestQueue: + pass + + +class Consumer: + pass + + +class TraceCursor: + pass + + +class CursorOwner: + pass + + +@process +async def coroutine_process(requests: ResourceRef[RequestQueue, Consumer]) -> None: + await receive(requests) + + +@process +def generator_process(requests: ResourceRef[RequestQueue, Consumer]) -> None: + yield requests + + +@process +def busy_process( + ready: RuntimeBool, requests: ResourceRef[RequestQueue, Consumer] +) -> None: + while ready: + try_recv(requests) + + +@process +def partial_busy_process( + ready: RuntimeBool, requests: ResourceRef[RequestQueue, Consumer] +) -> None: + while ready: + if ready: + wait_for(requests) + + +@process +def forked_cursor_process( + cursor: ResourceRef[TraceCursor, CursorOwner], +) -> None: + first = trace_next(cursor) + second = trace_next(cursor) + + +@process +def undeclared_effect_process( + requests: ResourceRef[RequestQueue, Consumer], +) -> None: + mutate_topology(requests) diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/process/suspended.py b/components/agentic-circuit/tests/python_frontend/fixtures/process/suspended.py new file mode 100644 index 00000000..c6758ae8 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/process/suspended.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from agentic_circuit import ResourceRef, process, system + + +class RuntimeBool: + pass + + +class Transaction: + pass + + +class RequestQueue: + pass + + +class Memory: + pass + + +class Consumer: + pass + + +class Target: + pass + + +@system +def main() -> None: + return None + + +@process +def controller( + ready: RuntimeBool, + requests: ResourceRef[RequestQueue, Consumer], + memory: ResourceRef[Memory, Target], +) -> None: + if ready: + message = try_recv(requests) + else: + message = schedule(requests) + wait_for(memory) + record_stat(message) + + +@process +def bounded_counter( + requests: ResourceRef[RequestQueue, Consumer], +) -> None: + for index in range(3): + record_stat(index) + wait_for(requests) + + +@process +def terminating_process(ready: RuntimeBool) -> None: + if ready: + return + else: + return diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/resources/invalid.py b/components/agentic-circuit/tests/python_frontend/fixtures/resources/invalid.py new file mode 100644 index 00000000..8b475b4a --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/resources/invalid.py @@ -0,0 +1,4 @@ +from agentic_circuit import ResourceRef + + +memory = ResourceRef("memory", "Memory", "target") diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/resources/valid.py b/components/agentic-circuit/tests/python_frontend/fixtures/resources/valid.py new file mode 100644 index 00000000..7c1663d1 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/resources/valid.py @@ -0,0 +1,13 @@ +from agentic_circuit import ResourceRef, address_map, address_space, queue + + +memory = ResourceRef("memory", "Memory", "target") +requests = queue( + "requests", + payload_type="Transaction", + protocol="ready_valid", + depth=8, + time_domain="core", +) +system = address_space("system", width=32) +mapping = address_map(system, (0x1000, 0x2000, memory, 0)) diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/scopes/invalid_escape.py b/components/agentic-circuit/tests/python_frontend/fixtures/scopes/invalid_escape.py new file mode 100644 index 00000000..a1847644 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/scopes/invalid_escape.py @@ -0,0 +1,21 @@ +from agentic_circuit import ResourceRef, module, scope, system + + +class PrivateState: + pass + + +class Owner: + pass + + +@module +def pipeline() -> ResourceRef[PrivateState, Owner]: + with scope("private"): + private = MakePrivate() + return private + + +@system +def main() -> None: + return None diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/scopes/nested.py b/components/agentic-circuit/tests/python_frontend/fixtures/scopes/nested.py new file mode 100644 index 00000000..802acc93 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/scopes/nested.py @@ -0,0 +1,30 @@ +from agentic_circuit import Endpoint, Flow, module, scope, system + + +class ReadyValid: + pass + + +class MemoryPort: + pass + + +class Target: + pass + + +@module +def pipeline( + requests: Flow[int, ReadyValid], + memory: Endpoint[MemoryPort, Target], +) -> Flow[int, ReadyValid]: + with scope("backend"): + scheduled = Schedule(requests) + with scope("memory"): + stored = Store(scheduled, memory) + return stored + + +@system +def main() -> None: + return None diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/source/basic.py b/components/agentic-circuit/tests/python_frontend/fixtures/source/basic.py new file mode 100644 index 00000000..ef48ac47 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/source/basic.py @@ -0,0 +1,11 @@ +from agentic_circuit import module, system + + +@module +def Worker(request: int) -> int: + return request + + +@system +def Architecture(lanes: int = 2) -> int: + return Worker(lanes) diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/static/supported.py b/components/agentic-circuit/tests/python_frontend/fixtures/static/supported.py new file mode 100644 index 00000000..858c925c --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/static/supported.py @@ -0,0 +1,9 @@ +from agentic_circuit import module + + +@module +def Supported(lanes: int, enabled: bool) -> tuple[int, ...]: + values = tuple(i * 2 for i in range(lanes)) + if enabled: + return values + return () diff --git a/components/agentic-circuit/tests/python_frontend/fixtures/static/unsupported.py b/components/agentic-circuit/tests/python_frontend/fixtures/static/unsupported.py new file mode 100644 index 00000000..b715804a --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/fixtures/static/unsupported.py @@ -0,0 +1,10 @@ +from agentic_circuit import Flow, module + +class ReadyValid: pass + +@module +def Unsupported(request: Flow[int, ReadyValid]) -> None: + if ( + request + ): + return None diff --git a/components/agentic-circuit/tests/python_frontend/test_acpy.py b/components/agentic-circuit/tests/python_frontend/test_acpy.py new file mode 100644 index 00000000..61fe995a --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_acpy.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import json +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator + + +REPOSITORY = Path(__file__).resolve().parents[2] +FIXTURES = Path(__file__).resolve().parent / "fixtures" +ZERO_DIGEST = "sha256:" + "0" * 64 + + +def minimal_document(): + from agentic_circuit._acpy import AcpyDocument, Entity, SourceFile + from agentic_circuit._diagnostics import SourceSpan + + return AcpyDocument( + entry="e0", + sources=(SourceFile("architecture.py", ZERO_DIGEST),), + entities=( + Entity( + id="e0", + kind="system", + source=SourceSpan("architecture.py", 1, 1, 2, 1), + parent=None, + scope="Architecture", + type=None, + definition=None, + uses=(), + schema_ref=None, + properties=(), + ), + ), + ) + + +class AcpyContractTest(unittest.TestCase): + def test_minimal_document_matches_golden_and_schema(self) -> None: + expected = (FIXTURES / "acpy" / "minimal.acpy.json").read_bytes().rstrip( + b"\n" + ) + + actual = minimal_document().canonical_bytes() + + self.assertEqual(expected, actual) + schema = json.loads( + (REPOSITORY / "schemas" / "acpy.schema.json").read_text( + encoding="utf-8" + ) + ) + Draft202012Validator(schema).validate(json.loads(actual)) + + def test_ids_are_dense_and_references_resolve(self) -> None: + from agentic_circuit._acpy import AcpyDocument, EntityAllocator, SourceFile + + allocator = EntityAllocator() + root = allocator.allocate(kind="system", scope="Architecture") + call = allocator.allocate(kind="call", scope="Architecture", parent=root.id) + allocator.allocate( + kind="result", scope="Architecture", parent=call.id, uses=(call.id,) + ) + document = AcpyDocument( + entry=root.id, + sources=(SourceFile("architecture.py", ZERO_DIGEST),), + entities=allocator.freeze(), + ) + + self.assertEqual( + [f"e{index}" for index in range(len(document.entities))], + [entity.id for entity in document.entities], + ) + self.assertEqual((), document.verify()) + + def test_rfc_8785_vector_and_utf16_key_order(self) -> None: + from agentic_circuit._canonical_json import canonical_json_bytes + + value = { + "numbers": [333333333.33333329, 1e30, 4.50, 2e-3, 1e-27], + "string": "€$\u000f\nA'B\"\\\"/", + "literals": [None, True, False], + } + expected = ( + "{\"literals\":[null,true,false]," + "\"numbers\":[333333333.3333333,1e+30,4.5,0.002,1e-27]," + "\"string\":\"€$\\u000f\\nA'B\\\"\\\\\\\"/\"}" + ).encode("utf-8") + self.assertEqual(expected, canonical_json_bytes(value)) + self.assertEqual( + "{\"€\":\"euro\",\"😀\":\"emoji\",\"דּ\":\"hebrew\"}".encode(), + canonical_json_bytes({"דּ": "hebrew", "😀": "emoji", "€": "euro"}), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_definitions.py b/components/agentic-circuit/tests/python_frontend/test_definitions.py new file mode 100644 index 00000000..fbdafa6f --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_definitions.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import dataclasses +import importlib.util +import inspect +import json +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).resolve().parents[2] +WORKSPACE = Path(__file__).resolve().parent / "fixtures" / "definitions" + + +def load_fixture(name: str): + path = WORKSPACE / name + module_name = f"agentic_circuit_test_{path.stem}" + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot import test fixture {path}") + loaded = importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded) + return loaded + + +def stdlib_registry(): + from agentic_circuit._schemas import SchemaRegistry + + return SchemaRegistry.from_catalog( + REPOSITORY / "schemas" / "stdlib" / "catalog.json", REPOSITORY + ) + + +class DefinitionCaptureTest(unittest.TestCase): + def test_decorated_definition_binds_without_executing_its_body(self) -> None: + loaded = load_fixture("basic.py") + + self.assertEqual("Definition(kind='system', qualified_name='main')", repr(loaded.main)) + self.assertEqual(["lanes"], list(inspect.signature(loaded.main).parameters)) + call = loaded.main(lanes=8) + + self.assertEqual("main", call.definition.qualified_name) + self.assertEqual((('lanes', 8),), call.arguments) + with self.assertRaisesRegex(TypeError, "ACPY-CALL-003"): + loaded.main(surprise=8) + + def test_definition_matches_ast_and_system_selection(self) -> None: + from agentic_circuit._frontend import CaptureRequest, capture_definitions + + loaded = load_fixture("basic.py") + result = capture_definitions( + CaptureRequest( + entry=WORKSPACE / "basic.py", + workspace=WORKSPACE, + system="main", + ), + vars(loaded), + stdlib_registry(), + ) + + self.assertEqual((), result.diagnostics) + self.assertIsNotNone(result.selected_system) + assert result.selected_system is not None + self.assertEqual("main", result.selected_system.qualified_name) + self.assertEqual( + ["worker", "main"], + [definition.qualified_name for definition in result.definitions], + ) + self.assertEqual("basic.py", result.source.path) + + def test_source_line_mismatch_rejects_fabricated_definition(self) -> None: + from agentic_circuit._frontend import CaptureRequest, capture_definitions + + loaded = load_fixture("basic.py") + loaded.main = dataclasses.replace(loaded.main, source_line=999) + result = capture_definitions( + CaptureRequest( + entry=WORKSPACE / "basic.py", + workspace=WORKSPACE, + system="main", + ), + vars(loaded), + stdlib_registry(), + ) + + self.assertIsNone(result.selected_system) + self.assertIn( + "ACPY-SYMBOL-DEFINITION", [item.code for item in result.diagnostics] + ) + + def test_non_static_defaults_and_variadic_parameters_are_rejected(self) -> None: + from agentic_circuit._frontend import CaptureRequest, capture_definitions + + loaded = load_fixture("invalid.py") + result = capture_definitions( + CaptureRequest( + entry=WORKSPACE / "invalid.py", + workspace=WORKSPACE, + system="invalid_defaults", + ), + vars(loaded), + stdlib_registry(), + ) + + codes = [item.code for item in result.diagnostics] + self.assertIn("ACPY-STATIC-DEFAULT", codes) + self.assertIn("ACPY-TYPE-SIGNATURE", codes) + + +class SchemaCallableTest(unittest.TestCase): + def test_recomputed_fingerprint_cannot_hide_invalid_closed_field(self) -> None: + from agentic_circuit._canonical_json import canonical_json_bytes, sha256_bytes + from agentic_circuit._schemas import SchemaError, SchemaRegistry + + record = json.loads( + (REPOSITORY / "schemas" / "stdlib" / "Queue.json").read_text() + ) + record["effect"]["kind"] = "ambient" + digest_record = dict(record) + digest_record.pop("schema_fingerprint") + record["schema_fingerprint"] = sha256_bytes( + canonical_json_bytes(digest_record) + ) + catalog = { + "catalog": "ac", + "version": "0.1", + "contract_epoch": "0.3", + "entries": [ + { + "canonical_name": "ac.Queue", + "availability": "available", + "schema_path": "schemas/stdlib/Queue.json", + "schema_fingerprint": record["schema_fingerprint"], + } + ], + } + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + stdlib = root / "schemas" / "stdlib" + stdlib.mkdir(parents=True) + (stdlib / "Queue.json").write_text(json.dumps(record)) + (stdlib / "catalog.json").write_text(json.dumps(catalog)) + + with self.assertRaisesRegex(SchemaError, "effect.kind"): + SchemaRegistry.from_catalog(stdlib / "catalog.json", root) + + def test_queue_signature_is_derived_from_the_closed_schema(self) -> None: + queue = stdlib_registry().callable("ac.Queue") + + signature = inspect.signature(queue) + + self.assertEqual("ComponentCallable('ac.Queue')", repr(queue)) + self.assertEqual( + ["input", "output", "T", "capacity", "byteCapacity", "name"], + list(signature.parameters), + ) + self.assertEqual( + [ + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + inspect.Parameter.KEYWORD_ONLY, + inspect.Parameter.KEYWORD_ONLY, + inspect.Parameter.KEYWORD_ONLY, + ], + [parameter.kind for parameter in signature.parameters.values()], + ) + + def test_callable_rejects_undocumented_keyword(self) -> None: + from agentic_circuit._types import _test_symbolic + + queue = stdlib_registry().callable("ac.Queue") + input_value = _test_symbolic("input", object()) + output_value = _test_symbolic("output", object()) + + with self.assertRaisesRegex(TypeError, "ACPY-CALL-003"): + queue( + input_value, + output_value, + T="Packet", + capacity=4, + surprise=True, + ) + + def test_valid_call_records_schema_defaults_in_signature_order(self) -> None: + from agentic_circuit._types import _test_symbolic + + queue = stdlib_registry().callable("ac.Queue") + call = queue( + _test_symbolic("input", object()), + _test_symbolic("output", object()), + T="Packet", + capacity=4, + ) + + self.assertEqual("ac.Queue", call.schema.identity) + self.assertEqual( + ["input", "output", "T", "capacity", "byteCapacity", "name"], + [name for name, _ in call.arguments], + ) + self.assertEqual("unbounded", dict(call.arguments)["byteCapacity"]) + self.assertIsNone(dict(call.arguments)["name"]) + + def test_declared_unavailable_component_has_no_callable(self) -> None: + registry = stdlib_registry() + + with self.assertRaisesRegex(LookupError, "declared unavailable"): + registry.callable("ac.Arbiter") + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_determinism.py b/components/agentic-circuit/tests/python_frontend/test_determinism.py new file mode 100644 index 00000000..e0caa6c2 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_determinism.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from dataclasses import dataclass +from pathlib import Path + + +REPOSITORY = Path(__file__).resolve().parents[2] +FIXTURES = Path(__file__).resolve().parent / "fixtures" / "lowering" + + +EXACT_PUBLIC_API = { + "system", + "module", + "extern_module", + "generated_module", + "struct", + "packet", + "transaction", + "protocol", + "interface", + "process", + "scope", + "array", + "instances", + "view", + "queue", + "ResourceRef", + "address_space", + "address_map", + "Static", + "Flow", + "Endpoint", + "source", + "sink", + "observe", + "atomic", + "u1", + "u2", + "u4", + "u8", + "u16", + "u32", + "u64", + "s8", + "s16", + "s32", + "s64", +} + + +@dataclass(frozen=True, slots=True) +class CoverageRow: + positive: tuple[str, ...] + negative: tuple[str, ...] + + +@dataclass(frozen=True, slots=True) +class CorpusArtifacts: + acpy_files: tuple[tuple[str, str], ...] + acir_files: tuple[tuple[str, str], ...] + + +def elaborate_corpus(root: Path, *, hash_seed: int) -> CorpusArtifacts: + for name in ("hierarchy.py", "process.py"): + shutil.copyfile(FIXTURES / name, root / name) + script = """ +import json +import sys +from pathlib import Path +from tests.python_frontend.test_lower_acir import elaborate + +root = Path(sys.argv[1]) +acpy = {} +acir = {} +for name in ("hierarchy", "process"): + result = elaborate(root / f"{name}.py", root) + if result.diagnostics or result.document is None or result.acir is None: + raise RuntimeError((name, result.diagnostics)) + acpy[f"{name}.acpy.json"] = result.document.canonical_bytes().decode("utf-8") + acir[f"{name}.ac.mlir"] = result.acir +print(json.dumps({"acpy": acpy, "acir": acir}, sort_keys=True, separators=(",", ":"))) +""" + environment = os.environ.copy() + environment["PYTHONHASHSEED"] = str(hash_seed) + environment["PYTHONPATH"] = os.pathsep.join( + (str(REPOSITORY / "src"), str(REPOSITORY)) + ) + completed = subprocess.run( + (sys.executable, "-c", script, str(root)), + cwd=REPOSITORY, + env=environment, + text=True, + capture_output=True, + check=False, + ) + if completed.returncode != 0: + raise AssertionError(completed.stderr) + payload = json.loads(completed.stdout) + return CorpusArtifacts( + acpy_files=tuple(sorted(payload["acpy"].items())), + acir_files=tuple(sorted(payload["acir"].items())), + ) + + +def frontend_test_ledger() -> dict[str, CoverageRow]: + public_import = ( + "tests.python_frontend.test_public_api." + "PublicApiTest.test_exact_public_inventory_is_importable" + ) + definition_positive = ( + "tests.python_frontend.test_definitions." + "DefinitionCaptureTest.test_definition_matches_ast_and_system_selection" + ) + definition_negative = ( + "tests.python_frontend.test_definitions." + "DefinitionCaptureTest.test_non_static_defaults_and_variadic_parameters_are_rejected" + ) + decorator_row = CoverageRow( + (public_import, definition_positive), (definition_negative,) + ) + symbolic_negative = ( + "tests.python_frontend.test_public_api." + "PublicApiTest.test_symbolic_values_reject_python_coercion" + ) + annotation_row = CoverageRow((public_import,), (symbolic_negative,)) + collection_negative = ( + "tests.python_frontend.test_scopes_collections." + "ScopeCollectionTest.test_ragged_collection_is_rejected" + ) + marker_negative = ( + "tests.python_frontend.test_public_api." + "PublicApiTest.test_ast_only_markers_reject_runtime_execution" + ) + return { + "system": decorator_row, + "module": decorator_row, + "extern_module": decorator_row, + "generated_module": decorator_row, + "struct": decorator_row, + "packet": decorator_row, + "transaction": decorator_row, + "protocol": decorator_row, + "interface": decorator_row, + "process": CoverageRow( + ( + "tests.python_frontend.test_process." + "ProcessFrontendTest.test_nested_control_and_suspension_build_closed_cfg", + ), + ( + "tests.python_frontend.test_process." + "ProcessFrontendTest.test_busy_wait_coroutine_generator_and_undeclared_effect_are_rejected", + ), + ), + "scope": CoverageRow( + ( + "tests.python_frontend.test_scopes_collections." + "ScopeCollectionTest.test_scope_signature_is_minimal_and_ordered", + ), + ( + "tests.python_frontend.test_scopes_collections." + "ScopeCollectionTest.test_owned_resource_cannot_escape_its_scope", + ), + ), + "array": CoverageRow( + ( + "tests.python_frontend.test_scopes_collections." + "ScopeCollectionTest.test_rectangular_homogeneous_collection_selects_array", + ), + (collection_negative,), + ), + "instances": CoverageRow( + ( + "tests.python_frontend.test_scopes_collections." + "ScopeCollectionTest.test_specialization_difference_selects_instances", + ), + (collection_negative,), + ), + "view": CoverageRow((public_import,), (marker_negative,)), + "queue": CoverageRow( + ( + "tests.python_frontend.test_resources." + "ResourceFrontendTest.test_queue_and_address_map_are_static_records", + ), + ( + "tests.python_frontend.test_resources." + "ResourceFrontendTest.test_dynamic_address_and_nonpositive_depth_are_rejected", + ), + ), + "ResourceRef": annotation_row, + "address_space": CoverageRow( + ( + "tests.python_frontend.test_resources." + "ResourceFrontendTest.test_queue_and_address_map_are_static_records", + ), + ( + "tests.python_frontend.test_resources." + "ResourceFrontendTest.test_dynamic_address_and_nonpositive_depth_are_rejected", + ), + ), + "address_map": CoverageRow( + ( + "tests.python_frontend.test_resources." + "ResourceFrontendTest.test_queue_and_address_map_are_static_records", + ), + ( + "tests.python_frontend.test_resources." + "ResourceFrontendTest.test_equal_priority_address_overlap_is_rejected", + ), + ), + "Static": annotation_row, + "Flow": annotation_row, + "Endpoint": annotation_row, + "source": CoverageRow((public_import,), (marker_negative,)), + "sink": CoverageRow((public_import,), (marker_negative,)), + "observe": CoverageRow((public_import,), (marker_negative,)), + "atomic": CoverageRow((public_import,), (marker_negative,)), + "u1": annotation_row, + "u2": annotation_row, + "u4": annotation_row, + "u8": annotation_row, + "u16": annotation_row, + "u32": annotation_row, + "u64": annotation_row, + "s8": annotation_row, + "s16": annotation_row, + "s32": annotation_row, + "s64": annotation_row, + } + + +class FrontendDeterminismTest(unittest.TestCase): + def test_corpus_is_identical_across_roots_and_hash_seeds(self) -> None: + with ( + tempfile.TemporaryDirectory() as first, + tempfile.TemporaryDirectory() as second, + ): + first_root = Path(first) + second_root = Path(second) + first_result = elaborate_corpus(first_root, hash_seed=1) + second_result = elaborate_corpus(second_root, hash_seed=99) + + self.assertEqual(first_result.acpy_files, second_result.acpy_files) + self.assertEqual(first_result.acir_files, second_result.acir_files) + serialized = repr((first_result.acpy_files, first_result.acir_files)) + self.assertNotIn(str(first_root), serialized) + self.assertNotIn(str(second_root), serialized) + + def test_every_public_name_has_positive_and_negative_coverage(self) -> None: + ledger = frontend_test_ledger() + + self.assertEqual(EXACT_PUBLIC_API, set(ledger)) + self.assertTrue( + all(row.positive and row.negative for row in ledger.values()) + ) + for public_name, row in ledger.items(): + for test_name in (*row.positive, *row.negative): + with self.subTest(public_name=public_name, test_name=test_name): + suite = unittest.defaultTestLoader.loadTestsFromName(test_name) + loaded = tuple(suite) + self.assertEqual(1, len(loaded)) + self.assertNotEqual("_FailedTest", type(loaded[0]).__name__) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_jit_blocks.py b/components/agentic-circuit/tests/python_frontend/test_jit_blocks.py new file mode 100644 index 00000000..ccc29162 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_jit_blocks.py @@ -0,0 +1,506 @@ +from __future__ import annotations + +from dataclasses import FrozenInstanceError +import importlib.util +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +JIT_SOURCE = """ +import agentic_circuit as ac + +@ac.config +class Config: + depth: int + bias: int + +@ac.system +def pipeline(cfg: ac.const[Config]) -> None: + incoming = ac.source(int, depth=cfg.depth, latency=1) + outgoing = ac.compute( + incoming, + lambda item: item + cfg.bias, + depth=cfg.depth, + latency=1, + ) + ac.sink(outgoing) +""" + + +HIGH_LEVEL_SOURCE = """ +import agentic_circuit as ac + +@ac.config +class Config: + engines: int + entries: int + +@ac.struct +class Token: + sequence: ac.u8 + waits_for: ac.u8 + engine: ac.u2 + cycles: ac.u16 + value: ac.u32 + +@ac.system +def core(cfg: ac.const[Config]) -> None: + incoming = ac.source(Token) + decoded = ac.compute( + incoming, + lambda item: item.with_fields(value=item.value + 1), + ) + scalar, vector, cube, tma = ac.route( + decoded, + by=Token.engine, + outputs=cfg.engines, + depth=2, + ) + dispatched = ac.merge( + scalar, + vector, + cube, + tma, + policy=ac.round_robin, + depth=4, + ) + completed = ac.schedule( + dispatched, + by=Token.sequence, + waits_for=Token.waits_for, + resource=Token.engine, + cost=Token.cycles, + entries=cfg.entries, + resources=cfg.engines, + no_dependency=255, + depth=8, + ) + engine_input = ac.source(Token) + engine_done = ac.engine( + engine_input, + cost=Token.cycles, + lanes=cfg.engines, + depth=4, + ) + engine_pipe = ac.pipeline(engine_done, stages=2, depth=2) + retired = ac.reorder( + completed, + by=Token.sequence, + entries=cfg.entries, + start=0, + depth=4, + ) + ac.sink(retired) + ac.sink(engine_pipe) +""" + + +STRUCTURAL_BLOCK_SOURCE = """ +import agentic_circuit as ac + +@ac.config +class Config: + outputs: int + entries: int + +@ac.struct +class Request: + address: ac.u8 + write: bool + data: ac.u16 + +@ac.system +def blocks(cfg: ac.const[Config]) -> None: + incoming = ac.source(Request) + left, right = ac.fork(incoming, outputs=cfg.outputs, depth=2) + left_ready, right_ready = ac.barrier(left, right, depth=2) + response = ac.table( + left_ready, + address=Request.address, + write=Request.write, + data=Request.data, + result=Request.data, + entries=cfg.entries, + depth=4, + ) + ac.sink(response) + ac.sink(right_ready) +""" + + +MULTIRATE_SOURCE = """ +import agentic_circuit as ac + +@ac.config +class Config: + rate: int + +@ac.system +def multirate(cfg: ac.const[Config]) -> None: + incoming = ac.source(int, depth=8, rate=cfg.rate) + computed = ac.compute( + incoming, lambda item: item + 1, depth=8, rate=cfg.rate + ) + pipelined = ac.pipeline(computed, stages=2, depth=8, rate=cfg.rate) + ac.sink(pipelined) +""" + + +REPOSITORY = Path(__file__).resolve().parents[2] + + +class ConfigAndJitTest(unittest.TestCase): + def test_config_is_an_immutable_closed_record(self) -> None: + import agentic_circuit as ac + + @ac.config + class Config: + lanes: int + enabled: bool = True + + value = Config(lanes=4) + + self.assertEqual(4, value.lanes) + self.assertTrue(value.enabled) + with self.assertRaises(FrozenInstanceError): + value.lanes = 8 + + def test_jit_canonicalizes_const_arguments_and_identity(self) -> None: + import agentic_circuit as ac + + @ac.config + class Config: + lanes: int + entries: int + + @ac.system + def core(cfg: ac.const[Config]) -> None: + raise AssertionError("JIT must not execute the system body") + + left = ac.jit(core, cfg=Config(lanes=4, entries=16)) + right = ac.jit(core, cfg=Config(entries=16, lanes=4)) + + self.assertEqual(left.fingerprint, right.fingerprint) + self.assertEqual( + (("cfg", (("entries", 16), ("lanes", 4))),), + left.canonical_arguments, + ) + self.assertIn("core", repr(left)) + + def test_jit_rejects_non_const_or_open_arguments(self) -> None: + import agentic_circuit as ac + + @ac.system + def runtime(value: int) -> None: + pass + + @ac.system + def templated(value: ac.const[int]) -> None: + pass + + with self.assertRaisesRegex(TypeError, "ACPY-JIT-001"): + ac.jit(runtime, value=4) + with self.assertRaisesRegex(TypeError, "ACPY-JIT-002"): + ac.jit(templated, value={"mutable"}) + + def test_checked_in_specialization_materializes_acir_and_cpp(self) -> None: + path = REPOSITORY / "examples" / "architecture" / "davincioo_jit.py" + spec = importlib.util.spec_from_file_location("ac_davincioo", path) + if spec is None or spec.loader is None: + raise RuntimeError("cannot load specialization example") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + self.addCleanup(sys.modules.pop, spec.name, None) + spec.loader.exec_module(module) + + acir = module.specialization.lower_acir() + cpp = module.specialization.lower_cpp() + + self.assertIn(module.specialization.fingerprint, acir) + self.assertIn(module.specialization.fingerprint, cpp) + self.assertIn("ac.dependency", acir) + self.assertIn("ac.reorder", acir) + self.assertIn("gfsim::Schedule", cpp) + self.assertIn("gfsim::Reorder None: + path = REPOSITORY / "examples" / "architecture" / "davincioo_jit.py" + spec = importlib.util.spec_from_file_location("ac_jit_cache", path) + if spec is None or spec.loader is None: + raise RuntimeError("cannot load specialization example") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + self.addCleanup(sys.modules.pop, spec.name, None) + spec.loader.exec_module(module) + + with tempfile.TemporaryDirectory() as directory: + first = module.specialization.materialize_cpp(directory) + second = module.specialization.materialize_cpp(directory) + + self.assertFalse(first.cache_hit) + self.assertTrue(second.cache_hit) + self.assertEqual(first.fingerprint, second.fingerprint) + self.assertTrue(second.source.is_file()) + self.assertTrue(second.artifact.is_file()) + self.assertTrue(second.manifest.is_file()) + self.assertEqual(first.artifact.read_bytes(), second.artifact.read_bytes()) + + def test_pyc_cpp_verilog_specialization_cache_when_toolchain_is_available( + self, + ) -> None: + root_value = os.environ.get("PYC_TOOLCHAIN_ROOT") + pycgen_value = os.environ.get("ACIR_QUEUE_PYCGEN") + if not root_value or not pycgen_value: + self.skipTest("pinned PYC toolchain integration paths are not configured") + root = Path(root_value) + path = REPOSITORY / "examples" / "architecture" / "davincioo_jit.py" + spec = importlib.util.spec_from_file_location("ac_pyc_cache", path) + if spec is None or spec.loader is None: + raise RuntimeError("cannot load specialization example") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + self.addCleanup(sys.modules.pop, spec.name, None) + spec.loader.exec_module(module) + + with tempfile.TemporaryDirectory() as directory: + kwargs = { + "pycgen_tool": pycgen_value, + "pycc": root / "bin" / "pycc", + "toolchain_metadata": root + / "share" + / "pycircuit" + / "toolchain-metadata.json", + } + first = module.specialization.materialize_pyc(directory, **kwargs) + second = module.specialization.materialize_pyc(directory, **kwargs) + + self.assertFalse(first.cache_hit) + self.assertTrue(second.cache_hit) + self.assertTrue(first.pyc.is_file()) + self.assertTrue((first.verilog / "davincioo.v").is_file()) + self.assertTrue(first.bundle_manifest.is_file()) + + +class JitQueueLoweringTest(unittest.TestCase): + def test_const_config_and_compute_lower_to_frozen_acir(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + from agentic_circuit._static_eval import FrozenMap + + constants = { + "cfg": FrozenMap((("bias", 3), ("depth", 4))), + } + lowered = lower_queue_source( + JIT_SOURCE, + "pipeline", + static_arguments=constants, + ) + + self.assertIn("%incoming = ac.source depth 4 latency 1", lowered) + self.assertIn( + "%outgoing = ac.transform %incoming depths [4] latencies [1]", + lowered, + ) + self.assertIn("ac.var.constant 3 : i64", lowered) + self.assertNotIn("cfg", lowered) + + def test_missing_or_non_const_system_parameter_is_rejected(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + with self.assertRaisesRegex(QueueFrontendError, "requires static argument"): + lower_queue_source(JIT_SOURCE, "pipeline") + with self.assertRaisesRegex(QueueFrontendError, "must use ac.const"): + lower_queue_source( + JIT_SOURCE.replace("cfg: ac.const[Config]", "cfg: Config"), + "pipeline", + static_arguments={"cfg": 4}, + ) + with self.assertRaisesRegex(QueueFrontendError, "fingerprint is invalid"): + lower_queue_source( + JIT_SOURCE, + "pipeline", + static_arguments={"cfg": 4}, + specialization_fingerprint="sha256:bad", + ) + + def test_only_compute_accepts_function_style_lambda(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + from agentic_circuit._static_eval import FrozenMap + + lowered = lower_queue_source( + JIT_SOURCE, + "pipeline", + static_arguments={ + "cfg": FrozenMap((("bias", 3), ("depth", 4))), + }, + ) + + self.assertEqual(1, lowered.count(" = ac.transform ")) + + def test_simple_high_level_blocks_lower_to_existing_acir(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + from agentic_circuit._static_eval import FrozenMap + + lowered = lower_queue_source( + HIGH_LEVEL_SOURCE, + "core", + static_arguments={ + "cfg": FrozenMap((("engines", 4), ("entries", 16))), + }, + ) + + self.assertIn("%decoded = ac.transform %incoming", lowered) + self.assertIn("%scalar, %vector, %cube, %tma = ac.route %decoded", lowered) + self.assertIn('field "engine"', lowered) + self.assertIn( + '%dispatched = ac.merge %scalar, %vector, %cube, %tma policy "round_robin"', + lowered, + ) + self.assertIn( + "%completed = ac.dependency %dispatched capacity 16 resources 4", + lowered, + ) + self.assertIn("%engine_done = ac.credit %engine_input credits 4", lowered) + self.assertIn( + "%engine_pipe = ac.transform %engine_done depths [2] latencies [2]", + lowered, + ) + self.assertIn("%retired = ac.reorder %completed capacity 16", lowered) + self.assertNotIn("lambda", lowered) + from agentic_circuit._queue_codegen import lower_queue_program_to_cpp + from agentic_circuit._queue_frontend import parse_queue_program + + cpp = lower_queue_program_to_cpp( + parse_queue_program( + HIGH_LEVEL_SOURCE, + "core", + static_arguments={ + "cfg": FrozenMap((("engines", 4), ("entries", 16))), + }, + ) + ) + self.assertIn("gfsim::Compute", cpp) + + def test_high_level_non_compute_lambda_is_rejected(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + from agentic_circuit._static_eval import FrozenMap + + with self.assertRaisesRegex(QueueFrontendError, "field descriptor"): + lower_queue_source( + HIGH_LEVEL_SOURCE.replace( + "by=Token.engine,", "by=lambda item: item.engine,", 1 + ), + "core", + static_arguments={ + "cfg": FrozenMap((("engines", 4), ("entries", 16))), + }, + ) + + def test_simple_structural_and_table_blocks_reuse_existing_acir(self) -> None: + from agentic_circuit._queue_codegen import lower_queue_program_to_cpp + from agentic_circuit._queue_frontend import ( + lower_queue_source, + parse_queue_program, + ) + from agentic_circuit._static_eval import FrozenMap + + lowered = lower_queue_source( + STRUCTURAL_BLOCK_SOURCE, + "blocks", + static_arguments={ + "cfg": FrozenMap((("entries", 32), ("outputs", 2))), + }, + ) + + self.assertIn("%left, %right = ac.fork %incoming", lowered) + self.assertIn("%left_ready, %right_ready = ac.barrier %left, %right", lowered) + self.assertIn( + "ac.memory.instance @response__table data i16 entries 32", lowered + ) + self.assertIn( + "%response = ac.memory.request @response__table, %left_ready", lowered + ) + self.assertIn('result_field "data"', lowered) + program = parse_queue_program( + STRUCTURAL_BLOCK_SOURCE, + "blocks", + static_arguments={ + "cfg": FrozenMap((("entries", 32), ("outputs", 2))), + }, + ) + cpp = lower_queue_program_to_cpp(program) + self.assertIn("gfsim::QueueFork", cpp) + self.assertIn("gfsim::QueueBarrier", cpp) + self.assertIn( + "gfsim::QueueMemoryArbiter None: + from agentic_circuit._queue_codegen import lower_queue_program_to_cpp + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + parse_queue_program, + ) + from agentic_circuit._static_eval import FrozenMap + + arguments = {"cfg": FrozenMap((("rate", 4),))} + lowered = lower_queue_source( + MULTIRATE_SOURCE, + "multirate", + static_arguments=arguments, + ) + self.assertEqual(3, lowered.count("ac.output_rates = array")) + cpp = lower_queue_program_to_cpp( + parse_queue_program( + MULTIRATE_SOURCE, + "multirate", + static_arguments=arguments, + ) + ) + self.assertIn("gfsim::Compute", cpp) + self.assertGreaterEqual(cpp.count(", nullptr, 1, 4)"), 2) + self.assertIn(", nullptr, 2, 4)", cpp) + with self.assertRaisesRegex( + QueueFrontendError, "rate must not exceed depth" + ): + lower_queue_source( + MULTIRATE_SOURCE.replace( + "depth=8, rate=cfg.rate", "depth=2, rate=cfg.rate", 1 + ), + "multirate", + static_arguments=arguments, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_lower_acir.py b/components/agentic-circuit/tests/python_frontend/test_lower_acir.py new file mode 100644 index 00000000..f28d810c --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_lower_acir.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import importlib.util +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).resolve().parents[2] +FIXTURES = Path(__file__).resolve().parent / "fixtures" / "lowering" +ZERO_DIGEST = "sha256:" + "0" * 64 + + +def load_fixture(path: Path): + spec = importlib.util.spec_from_file_location(f"lowering_{path.stem}", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot import lowering fixture {path}") + loaded = importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded) + return loaded + + +def registry(*, source_binding: str | None = "input"): + from agentic_circuit._schemas import ( + ComponentSchema, + PortSchema, + ResultSchema, + SchemaRegistry, + ) + + refine = ComponentSchema( + identity="test.Refine", + fingerprint=ZERO_DIGEST, + ports=(PortSchema("input", "flow", "i32", "in", "consumer", 1),), + results=(ResultSchema("output", "i32", source_binding),), + parameters=(), + availability="available", + effect_kind="stateful", + ) + return SchemaRegistry({refine.identity: refine}) + + +def elaborate(path: Path, workspace: Path, *, schemas=None): + from agentic_circuit._frontend import CaptureRequest, elaborate_frontend + + loaded = load_fixture(path) + return elaborate_frontend( + CaptureRequest(entry=path, workspace=workspace, system="main"), + vars(loaded), + schemas or registry(), + ) + + +def golden_bytes(name: str) -> bytes: + return (FIXTURES / name).read_bytes().rstrip(b"\n") + + +def golden_text(name: str) -> str: + return (FIXTURES / name).read_text(encoding="utf-8") + + +def acir_opt() -> Path | None: + roots = [REPOSITORY / "build"] + if REPOSITORY.parent.name == ".worktrees": + roots.append(REPOSITORY.parent.parent / "build") + for root in roots: + candidate = root / "dev-llvm22" / "bin" / "acir-opt" + if candidate.is_file(): + return candidate + return None + + +class AcirLoweringTest(unittest.TestCase): + def test_hierarchy_matches_goldens(self) -> None: + result = elaborate(FIXTURES / "hierarchy.py", FIXTURES) + + self.assertEqual((), result.diagnostics) + self.assertIsNotNone(result.document) + self.assertIsNotNone(result.acir) + assert result.document is not None and result.acir is not None + self.assertEqual( + golden_bytes("hierarchy.acpy.json"), result.document.canonical_bytes() + ) + self.assertEqual(golden_text("hierarchy.ac.mlir"), result.acir) + self.assertNotIn("ac.connect", result.acir) + + def test_process_matches_golden_and_native_verifier(self) -> None: + result = elaborate(FIXTURES / "process.py", FIXTURES) + + self.assertEqual((), result.diagnostics) + self.assertEqual(golden_text("process.ac.mlir"), result.acir) + verifier = acir_opt() + if verifier is None: + self.skipTest("acir-opt is not built") + completed = subprocess.run( + (str(verifier), "-o", "/dev/null", "-"), + input=result.acir, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + + def test_equivalent_roots_emit_identical_bytes(self) -> None: + with tempfile.TemporaryDirectory() as first_root, tempfile.TemporaryDirectory() as second_root: + first_path = Path(first_root) / "hierarchy.py" + second_path = Path(second_root) / "hierarchy.py" + shutil.copyfile(FIXTURES / "hierarchy.py", first_path) + shutil.copyfile(FIXTURES / "hierarchy.py", second_path) + + first = elaborate(first_path, Path(first_root)) + second = elaborate(second_path, Path(second_root)) + + assert first.document is not None and second.document is not None + self.assertEqual(first.document.canonical_bytes(), second.document.canonical_bytes()) + self.assertEqual(first.acir, second.acir) + + def test_lowering_failure_does_not_publish_partial_artifacts(self) -> None: + result = elaborate( + FIXTURES / "hierarchy.py", + FIXTURES, + schemas=registry(source_binding=None), + ) + + self.assertIsNone(result.document) + self.assertIsNone(result.acir) + self.assertEqual(("ACPY-VERIFY-001",), tuple(d.code for d in result.diagnostics)) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_normalization.py b/components/agentic-circuit/tests/python_frontend/test_normalization.py new file mode 100644 index 00000000..dcb8f90c --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_normalization.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + + +REPOSITORY = Path(__file__).resolve().parents[2] +WORKSPACE = Path(__file__).resolve().parent / "fixtures" / "normalize" +ZERO_DIGEST = "sha256:" + "0" * 64 + + +def load_fixture(name: str): + path = WORKSPACE / name + spec = importlib.util.spec_from_file_location(f"normalize_{path.stem}", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot import fixture {path}") + loaded = importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded) + return loaded + + +def component_schema( + identity: str, + *, + ports: tuple[str, ...], + results: tuple[str, ...], + parameters: tuple[tuple[str, bool, object], ...] = (), +): + from agentic_circuit._schemas import ( + ComponentSchema, + ParameterSchema, + PortSchema, + ResultSchema, + ) + + return ComponentSchema( + identity=identity, + fingerprint=ZERO_DIGEST, + ports=tuple( + PortSchema(name, "flow", f"!test.flow<{name}>", "in", "consumer", 1) + for name in ports + ), + results=tuple( + ResultSchema(name, f"!test.flow<{name}>", None) for name in results + ), + parameters=tuple( + ParameterSchema(name, "!test.static", required, default, None) + for name, required, default in parameters + ), + availability="available", + ) + + +def registry_for(name: str): + from agentic_circuit._schemas import SchemaRegistry + + decode = component_schema( + "test.Decode", + ports=("input",), + results=("decoded", "accepted"), + parameters=(("mode", False, "strict"),), + ) + if name == "ambiguous.py": + alternate = component_schema( + "alternate.Decode", ports=("input",), results=("decoded",) + ) + decode = component_schema("test.Decode", ports=("input",), results=("decoded",)) + return SchemaRegistry({decode.identity: decode, alternate.identity: alternate}) + refine = component_schema("test.Refine", ports=("input",), results=("output",)) + return SchemaRegistry({decode.identity: decode, refine.identity: refine}) + + +def normalize_fixture(name: str): + from agentic_circuit._frontend import CaptureRequest, capture_definitions + from agentic_circuit._normalize import normalize_program + + loaded = load_fixture(name) + registry = registry_for(name) + captured = capture_definitions( + CaptureRequest(entry=WORKSPACE / name, workspace=WORKSPACE, system="main"), + vars(loaded), + registry, + ) + if captured.diagnostics: + raise AssertionError(captured.diagnostics) + return normalize_program(captured, definition="pipeline") + + +class NormalizationTest(unittest.TestCase): + def test_calls_become_explicit_ssa_and_results(self) -> None: + program = normalize_fixture("calls.py") + + self.assertEqual( + ("decoded#0", "accepted#0", "decoded#1"), program.value_names() + ) + self.assertEqual(("input",), tuple(item.port for item in program.calls[0].inputs)) + self.assertEqual( + ("decoded", "accepted"), + tuple(item.result for item in program.calls[0].results), + ) + self.assertEqual("decoded#0", program.calls[1].inputs[0].value.name) + self.assertEqual(("decoded#1", "accepted#0"), program.return_names()) + + def test_ambiguity_reports_each_attempted_binding(self) -> None: + program = normalize_fixture("ambiguous.py") + + matches = [ + item for item in program.diagnostics if item.code == "ACPY-CALL-006" + ] + self.assertEqual(1, len(matches)) + self.assertEqual(2, len(matches[0].related)) + self.assertTrue( + all("attempted" in related.message for related in matches[0].related) + ) + + def test_naming_precedence_and_collision_are_explicit(self) -> None: + from agentic_circuit._naming import StableNameAllocator, StableNameError + + allocator = StableNameAllocator() + + self.assertEqual( + "front", allocator.allocate("test.Decode", "decoded", "front", (3, 5)) + ) + self.assertEqual( + "accepted", + allocator.allocate("test.Decode", "accepted", None, (4, 5)), + ) + self.assertEqual( + "Decode_5_7", allocator.allocate("test.Decode", None, None, (5, 7)) + ) + with self.assertRaisesRegex(StableNameError, "ACPY-NAME-002"): + allocator.allocate("test.Decode", "accepted", None, (6, 5)) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_process.py b/components/agentic-circuit/tests/python_frontend/test_process.py new file mode 100644 index 00000000..150cbb82 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_process.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + + +WORKSPACE = Path(__file__).resolve().parent / "fixtures" / "process" + + +def load_fixture(name: str): + path = WORKSPACE / name + spec = importlib.util.spec_from_file_location(f"process_{path.stem}", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot import process fixture {path}") + loaded = importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded) + return loaded + + +def process_site(fixture: str, name: str): + from agentic_circuit._source import load_source_unit + + unit = load_source_unit(WORKSPACE / fixture, WORKSPACE) + return next(site for site in unit.definitions if site.name == name) + + +def effect_registry(): + from agentic_circuit._process import EffectDeclaration, EffectRegistry + + return EffectRegistry( + ( + EffectDeclaration("try_recv", "queue"), + EffectDeclaration("schedule", "event"), + EffectDeclaration("wait_for", "suspension", suspension=True), + EffectDeclaration("record_stat", "statistics"), + EffectDeclaration("trace_next", "trace", linear_arguments=(0,)), + ) + ) + + +class ProcessFrontendTest(unittest.TestCase): + def test_nested_control_and_suspension_build_closed_cfg(self) -> None: + from agentic_circuit._process import construct_process + + process = construct_process( + process_site("suspended.py", "controller"), effect_registry() + ) + + self.assertEqual("entry", process.entry) + self.assertEqual( + ["entry", "then", "else", "resume", "done"], + [block.name for block in process.blocks], + ) + self.assertTrue(any(block.edge.kind == "suspend" for block in process.blocks)) + self.assertEqual( + ("ready", "requests", "memory"), + tuple(value.source_name for value in process.captures), + ) + self.assertEqual( + ("queue", "event", "suspension", "statistics"), + tuple(effect.kind for effect in process.effects), + ) + done = next(block for block in process.blocks if block.name == "done") + self.assertEqual( + ("message",), tuple(value.source_name for value in done.arguments) + ) + + def test_captured_program_constructs_selected_process(self) -> None: + from agentic_circuit._frontend import ( + CaptureRequest, + capture_definitions, + construct_captured_process, + ) + from agentic_circuit._schemas import SchemaRegistry + + loaded = load_fixture("suspended.py") + captured = capture_definitions( + CaptureRequest( + entry=WORKSPACE / "suspended.py", + workspace=WORKSPACE, + system="main", + ), + vars(loaded), + SchemaRegistry({}), + ) + self.assertEqual((), captured.diagnostics) + + process = construct_captured_process(captured, "controller", effect_registry()) + + self.assertEqual("controller", process.name) + + def test_bounded_for_is_an_explicit_closed_loop(self) -> None: + from agentic_circuit._process import construct_process + + process = construct_process( + process_site("suspended.py", "bounded_counter"), effect_registry() + ) + + self.assertEqual( + ["entry", "for_loop", "for_body", "after_for", "resume"], + [block.name for block in process.blocks], + ) + body = next(block for block in process.blocks if block.name == "for_body") + self.assertEqual( + ("index",), tuple(value.source_name for value in body.arguments) + ) + names = {block.name for block in process.blocks} + self.assertTrue( + all(target in names for block in process.blocks for target in block.edge.targets) + ) + + def test_fully_terminating_branch_has_no_unreachable_join(self) -> None: + from agentic_circuit._process import construct_process + + process = construct_process( + process_site("suspended.py", "terminating_process"), effect_registry() + ) + + self.assertEqual( + ["entry", "then", "else"], [block.name for block in process.blocks] + ) + + def test_busy_wait_coroutine_generator_and_undeclared_effect_are_rejected( + self, + ) -> None: + from agentic_circuit._process import ProcessConstructionError, construct_process + + expected = { + "coroutine_process": "ACPY-PROCESS-002", + "generator_process": "ACPY-PROCESS-002", + "busy_process": "ACPY-PROCESS-006", + "partial_busy_process": "ACPY-PROCESS-006", + "forked_cursor_process": "ACPY-PROCESS-007", + "undeclared_effect_process": "ACPY-EFFECT-003", + } + observed: set[str] = set() + for name, code in expected.items(): + with self.subTest(process=name): + with self.assertRaisesRegex(ProcessConstructionError, code) as raised: + construct_process(process_site("invalid.py", name), effect_registry()) + observed.add(raised.exception.code) + + self.assertEqual(set(expected.values()), observed) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_public_api.py b/components/agentic-circuit/tests/python_frontend/test_public_api.py new file mode 100644 index 00000000..0bfdd5de --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_public_api.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import importlib +import unittest +from dataclasses import FrozenInstanceError + + +PUBLIC = { + "system", + "module", + "extern_module", + "generated_module", + "struct", + "packet", + "transaction", + "protocol", + "interface", + "process", + "scope", + "array", + "map", + "set", + "instances", + "view", + "queue", + "ResourceRef", + "address_space", + "address_map", + "Static", + "Flow", + "Endpoint", + "source", + "popcount", + "memory", + "sink", + "observe", + "expect", + "atomic", + "compute", + "pipeline", + "config", + "const", + "jit", + "route", + "merge", + "schedule", + "engine", + "reorder", + "round_robin", + "priority", + "fork", + "barrier", + "table", + "u1", + "u2", + "u4", + "u8", + "u16", + "u32", + "u64", + "s8", + "s16", + "s32", + "s64", +} + + +class ReadyValid: + """Local schema marker used to form a public Flow annotation.""" + + +class PublicApiTest(unittest.TestCase): + def test_exact_public_inventory_is_importable(self) -> None: + api = importlib.import_module("agentic_circuit") + + self.assertEqual(PUBLIC, set(api.__all__)) + for name in PUBLIC: + self.assertIsNotNone(getattr(api, name)) + + def test_symbolic_values_reject_python_coercion(self) -> None: + types = importlib.import_module("agentic_circuit._types") + value = types._test_symbolic("request", types.Flow[int, ReadyValid]) + + for operation in (bool, int, hash, iter): + with self.subTest(operation=operation.__name__): + with self.assertRaisesRegex(TypeError, "ACPY-STATIC-002"): + operation(value) + + def test_symbolic_values_reject_python_equality(self) -> None: + types = importlib.import_module("agentic_circuit._types") + left = types._test_symbolic("left", object()) + right = types._test_symbolic("right", object()) + + with self.assertRaisesRegex(TypeError, "ACPY-STATIC-002"): + left == right + + def test_symbolic_value_repr_uses_only_stable_identity(self) -> None: + types = importlib.import_module("agentic_circuit._types") + + value = types._test_symbolic("request", object()) + + self.assertEqual("SymbolicValue('request')", repr(value)) + + def test_decorators_create_immutable_definition_metadata(self) -> None: + api = importlib.import_module("agentic_circuit") + + @api.module + def producer() -> None: + raise AssertionError("decorating a definition must not execute it") + + self.assertEqual("module", producer.kind) + self.assertEqual(producer.function.__qualname__, producer.qualified_name) + self.assertTrue(producer.qualified_name.endswith("..producer")) + self.assertEqual((), producer.explicit_options) + with self.assertRaises(FrozenInstanceError): + producer.kind = "system" + + def test_decorator_options_are_canonicalized(self) -> None: + api = importlib.import_module("agentic_circuit") + + @api.generated_module(zeta=2, alpha=1) + def generated() -> None: + pass + + self.assertEqual((("alpha", 1), ("zeta", 2)), generated.explicit_options) + + def test_ast_only_markers_reject_runtime_execution(self) -> None: + api = importlib.import_module("agentic_circuit") + + operations = ( + lambda: api.scope("nested"), + lambda: api.array(1, 2), + lambda: api.map({"a": object()}), + lambda: api.set({object()}), + lambda: api.instances(1, 2), + lambda: api.view(object(), "field"), + lambda: api.source(int), + lambda: api.popcount(object()), + lambda: api.sink(object()), + lambda: api.observe(object()), + lambda: api.expect( + object(), predicate=lambda value: True, message="expected" + ), + lambda: api.atomic(), + lambda: api.compute(object(), lambda value: value), + lambda: api.pipeline(object(), stages=2), + lambda: api.route(object(), by=object(), outputs=2), + lambda: api.merge(object(), object()), + lambda: api.schedule( + object(), + by=object(), + waits_for=object(), + resource=object(), + cost=object(), + ), + lambda: api.engine(object(), cost=object()), + lambda: api.reorder(object(), by=object()), + lambda: api.fork(object(), outputs=2), + lambda: api.barrier(object(), object()), + lambda: api.table( + object(), + address=object(), + write=object(), + data=object(), + result=object(), + ), + ) + for operation in operations: + with self.subTest(operation=operation): + with self.assertRaises(NotImplementedError): + operation() + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_queue_codegen.py b/components/agentic-circuit/tests/python_frontend/test_queue_codegen.py new file mode 100644 index 00000000..15a1c875 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_queue_codegen.py @@ -0,0 +1,589 @@ +from __future__ import annotations + +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] + +SOURCE = """ +from agentic_circuit import sink, source, struct, system + +@struct +class Item: + value: int + remaining: int + +@system +def pipeline() -> None: + input_queue = source(Item, depth=4, latency=1) + updated = input_queue.apply( + lambda item: item.with_fields( + value=item.value + 1, + remaining=item.remaining - 1, + ), + depth=8, + latency=2, + ) + sink(updated) +""" + +ROUTE_MERGE_SOURCE = """ +from agentic_circuit import sink, source, struct, system + +@struct +class Item: + value: int + route: int + +@system +def pipeline() -> None: + input_queue = source(Item, depth=2) + left, right = input_queue.route( + outputs=2, + key=lambda item: item.route, + depth=1, + latency=1, + ) + merged = left.merge(right, policy="round_robin", depth=2, latency=1) + sink(merged) +""" + +FEEDBACK_SOURCE = """ +from agentic_circuit import sink, source, struct, system + +@struct +class Item: + value: int + remaining: int + +@system +def pipeline() -> None: + current = source(Item) + while current.remaining > 0: + current = current.apply( + lambda item: item.with_fields( + value=item.value + 1, + remaining=item.remaining - 1, + ) + ) + sink(current) +""" + +BROADCAST_SOURCE = """ +from agentic_circuit import sink, source, system + +@system +def pipeline() -> None: + input_queue = source(int) + left = input_queue.apply(lambda item: item + 1) + right = input_queue.apply(lambda item: item + 2) + sink(left) + sink(right) +""" + +ARRAY_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + grid = ac.array( + 2, + lambda row: ac.array( + 2, + lambda column: ac.source(int, depth=row + column + 1), + ), + ) + ac.sink(grid[1][0]) +""" + +SCOPE_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + input_queue = ac.source(int) + with ac.scope("normalize"): + local = input_queue.apply(lambda item: item + 1) + exported = local.apply(lambda item: item * 2) + ac.sink(exported) +""" + + +class QueueCodegenTest(unittest.TestCase): + def test_serial_python_generates_typed_queue_wired_cpp(self) -> None: + from agentic_circuit._queue_codegen import lower_queue_source_to_cpp + + generated = lower_queue_source_to_cpp(SOURCE, "pipeline") + self.assertIn("struct Item", generated) + self.assertIn("gfsim::SimQueue input_queue_;", generated) + self.assertIn("gfsim::SimQueue updated_;", generated) + self.assertIn("gfsim::QueueTransform", generated) + self.assertIn("result.value = (item.value + 1);", generated) + self.assertIn("gfsim::QueueSink sink_0_;", generated) + self.assertEqual(generated, lower_queue_source_to_cpp(SOURCE, "pipeline")) + + def test_route_and_merge_generate_standard_typed_blocks(self) -> None: + from agentic_circuit._queue_codegen import lower_queue_source_to_cpp + + generated = lower_queue_source_to_cpp(ROUTE_MERGE_SOURCE, "pipeline") + self.assertIn("gfsim::QueueRoute", generated) + self.assertIn("return static_cast(item.route);", generated) + self.assertIn("gfsim::QueueMerge", generated) + self.assertIn("gfsim::QueueMergePolicy::RoundRobin", generated) + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "route_merge.cpp" + output.write_text(generated, encoding="utf-8") + compiled = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + "-fsyntax-only", + str(output), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, compiled.returncode, compiled.stderr) + harness = Path(directory) / "route_merge_harness.cpp" + executable = Path(directory) / "route_merge" + harness.write_text( + f'''#include "{output.name}" +#include + +int main() {{ + ac_generated::Pipeline model; + if (!model.input_queue().proposePush(ac_generated::Item{{7, 1}})) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 5; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + const auto &values = model.sink_0_values(); + return values.size() == 1 && values[0].value == 7 && values[0].route == 1 + ? 0 + : 2; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=Path(directory), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=Path(directory), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + def test_explicit_tool_writes_cpp_accepted_by_cxx20_compiler(self) -> None: + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "pipeline.py" + output = root / "pipeline.cpp" + source.write_text(SOURCE, encoding="utf-8") + environment = os.environ.copy() + environment["PYTHONPATH"] = str(ROOT / "src") + generated = subprocess.run( + ( + str(ROOT / "tools" / "ac-queue-cxxgen.py"), + str(source), + "--system", + "pipeline", + "-o", + str(output), + ), + cwd=ROOT, + env=environment, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, generated.returncode, generated.stderr) + compiled = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + "-fsyntax-only", + str(output), + ), + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, compiled.returncode, compiled.stderr) + + harness = root / "harness.cpp" + executable = root / "pipeline" + harness.write_text( + f'''#include "{output.name}" +#include + +int main() {{ + ac_generated::Pipeline model; + if (!model.input_queue().proposePush(ac_generated::Item{{41, 3}})) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 5; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + const auto &values = model.sink_0_values(); + return values.size() == 1 && values[0].value == 42 && + values[0].remaining == 2 + ? 0 + : 2; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + def test_serial_while_generates_parent_owned_feedback_queue(self) -> None: + from agentic_circuit._queue_codegen import lower_queue_source_to_cpp + + generated = lower_queue_source_to_cpp(FEEDBACK_SOURCE, "pipeline") + self.assertIn("gfsim::SimQueue>", generated) + self.assertIn("gfsim::QueueFeedback 0);", generated) + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + model = root / "feedback.cpp" + harness = root / "harness.cpp" + executable = root / "feedback" + model.write_text(generated, encoding="utf-8") + harness.write_text( + f'''#include "{model.name}" +#include + +int main() {{ + ac_generated::Pipeline model; + if (!model.current().proposePush(ac_generated::Item{{10, 3}})) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 8; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + const auto &values = model.sink_0_values(); + return values.size() == 1 && values[0].value == 13 && + values[0].remaining == 0 + ? 0 + : 2; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + def test_multiple_consumers_generate_strict_broadcast(self) -> None: + from agentic_circuit._queue_codegen import lower_queue_source_to_cpp + + generated = lower_queue_source_to_cpp(BROADCAST_SOURCE, "pipeline") + self.assertIn("gfsim::QueueBroadcast", generated) + self.assertIn("input_queue__fanout0_", generated) + self.assertIn("input_queue__fanout1_", generated) + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + model = root / "broadcast.cpp" + harness = root / "harness.cpp" + executable = root / "broadcast" + model.write_text(generated, encoding="utf-8") + harness.write_text( + f'''#include "{model.name}" +#include + +int main() {{ + ac_generated::Pipeline model; + if (!model.input_queue().proposePush(10)) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 6; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + return model.sink_0_values().size() == 1 && + model.sink_0_values()[0] == 11 && + model.sink_1_values().size() == 1 && + model.sink_1_values()[0] == 12 + ? 0 + : 2; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + def test_nested_array_generates_nested_simqueue_template(self) -> None: + from agentic_circuit._queue_codegen import lower_queue_source_to_cpp + + generated = lower_queue_source_to_cpp(ARRAY_SOURCE, "pipeline") + self.assertIn( + "std::array, 2>, 2> grid_;", + generated, + ) + self.assertIn('"grid__1__0"', generated) + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + model = root / "array.cpp" + harness = root / "harness.cpp" + executable = root / "array" + model.write_text(generated, encoding="utf-8") + harness.write_text( + f'''#include "{model.name}" +#include + +int main() {{ + ac_generated::Pipeline model; + if (!model.grid__1__0().proposePush(17)) + return 1; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 3; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + return model.sink_0_values().size() == 1 && + model.sink_0_values()[0] == 17 + ? 0 + : 2; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + def test_scope_generates_hierarchy_with_lca_queue_ownership(self) -> None: + from agentic_circuit._queue_codegen import lower_queue_source_to_cpp + + generated = lower_queue_source_to_cpp(SCOPE_SOURCE, "pipeline") + self.assertIn("gfsim::Module scope_normalize_;", generated) + self.assertIn("scope_normalize_.attachChild(local_);", generated) + self.assertIn("attachChild(exported_);", generated) + compiler = shutil.which("c++") + if compiler is None: + self.skipTest("C++ compiler is unavailable") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + model = root / "scope.cpp" + harness = root / "harness.cpp" + executable = root / "scope" + model.write_text(generated, encoding="utf-8") + harness.write_text( + f'''#include "{model.name}" +#include + +int main() {{ + ac_generated::Pipeline model; + auto *normalize = model.findChild("normalize"); + if (normalize == nullptr || normalize->asModule() == nullptr || + normalize->asModule()->findChild("local") == nullptr || + model.findChild("exported") == nullptr) + return 1; + if (!model.input_queue().proposePush(10)) + return 2; + auto rows = model.dispatch_rows(); + for (std::size_t tick = 0; tick < 6; ++tick) {{ + const gfsim::Epoch epoch{{tick, 0}}; + for (auto &row : rows) + row.work(row.object, epoch); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Arbitrate); + for (auto &row : rows) + row.xfer(row.object, epoch, gfsim::XferPhase::Commit); + }} + return model.sink_0_values().size() == 1 && + model.sink_0_values()[0] == 22 + ? 0 + : 3; +}} +''', + encoding="utf-8", + ) + linked = subprocess.run( + ( + compiler, + "-std=c++20", + "-I", + str(ROOT / "include"), + str(harness), + "-o", + str(executable), + ), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linked.returncode, linked.stderr) + executed = subprocess.run( + (str(executable),), + cwd=root, + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_queue_frontend.py b/components/agentic-circuit/tests/python_frontend/test_queue_frontend.py new file mode 100644 index 00000000..7d8bb4a4 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_queue_frontend.py @@ -0,0 +1,1170 @@ +from __future__ import annotations + +import unittest + + +SOURCE = """ +from agentic_circuit import sink, source, system + +@system +def pipeline() -> None: + input_queue = source(int, depth=4, latency=1) + output_queue = input_queue.apply(lambda item: item, depth=8, latency=2) + sink(output_queue) +""" + +POPCOUNT_SOURCE = """ +import agentic_circuit as ac +from agentic_circuit import sink, source, struct, system + +@struct +class Item: + value: u8 + count: u4 + +@system +def pipeline() -> None: + input_queue = source(Item) + output_queue = input_queue.apply( + lambda item: item.with_fields(count=ac.popcount(item.value)), + depth=2, + latency=1, + ) + sink(output_queue) +""" + +STRUCT_SOURCE = """ +from agentic_circuit import sink, source, struct, system + +@struct +class WorkItem: + value: int + remaining: int + +@system +def pipeline() -> None: + input_queue = source(WorkItem, depth=4, latency=1) + output_queue = input_queue.apply( + lambda item: item.with_fields( + value=(item.value + 1) * 2, + remaining=item.remaining - 1, + ), + depth=8, + latency=2, + ) + sink(output_queue) +""" + +SCOPE_SOURCE = """ +from agentic_circuit import scope, sink, source, system + +@system +def pipeline() -> None: + input_queue = source(int, depth=4, latency=1) + with scope("frontend"): + adjusted = input_queue.apply(lambda item: item + 1) + with scope("inner"): + completed = adjusted.apply(lambda item: item * 2) + sink(completed) +""" + +BROADCAST_SOURCE = """ +from agentic_circuit import sink, source, system + +@system +def pipeline() -> None: + input_queue = source(int) + left = input_queue.apply(lambda item: item + 1) + right = input_queue.apply(lambda item: item * 2) + sink(left) + sink(right) +""" + +CROSS_SCOPE_BROADCAST_SOURCE = """ +from agentic_circuit import scope, sink, source, system + +@system +def pipeline() -> None: + input_queue = source(int) + with scope("left"): + left = input_queue.apply(lambda item: item + 1) + with scope("right"): + right = input_queue.apply(lambda item: item * 2) + sink(left) + sink(right) +""" + +ROUTE_SOURCE = """ +from agentic_circuit import sink, source, struct, system + +@struct +class Item: + value: int + route: int + +@system +def pipeline() -> None: + input_queue = source(Item) + left, right = input_queue.route( + outputs=2, + key=lambda item: item.route, + depth=2, + latency=1, + ) + merged = left.merge(right, policy="round_robin", depth=3, latency=1) + sink(merged) +""" + +COLLECTION_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + lanes = ac.array(2, lambda lane: ac.source(int, depth=lane + 1)) + named = ac.map({"right": lanes[1], "left": lanes[0]}) + active = ac.set({named["right"], named["left"]}) + for lane in active: + ac.sink(lane) +""" + +NESTED_COLLECTION_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + grid = ac.array( + 2, + lambda row: ac.array( + 2, + lambda column: ac.source(int, depth=row + column + 1), + ), + ) + ac.sink(grid[1][0]) +""" + +FEEDBACK_SOURCE = """ +from agentic_circuit import sink, source, struct, system + +@struct +class Item: + value: int + remaining: int + +@system +def pipeline() -> None: + current = source(Item) + while current.remaining > 0: + current = current.apply( + lambda item: item.with_fields( + value=item.value + 1, + remaining=item.remaining - 1, + ), + depth=2, + latency=1, + ) + sink(current) +""" + +OBSERVE_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + input_queue = ac.source(int) + ac.observe(input_queue) + ac.sink(input_queue) +""" + +ATOMIC_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + left = ac.source(int) + right = ac.source(int) + with ac.atomic(): + left_next = left.apply(lambda item: item + 1) + right_next = right.apply(lambda item: item * 2) + ac.sink(left_next) + ac.sink(right_next) +""" + +STATIC_CONTROL_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + input_queue = ac.source(int) + if True: + selected = input_queue.apply(lambda item: item + 1) + else: + unreachable = input_queue.apply(lambda item: item + 99) + lanes = ac.array(2, lambda index: ac.source(int)) + for index in range(2): + ac.sink(lanes[index]) + ac.sink(selected) +""" + +CONST_KEY_MAP_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + one = ac.source(int) + two = ac.source(int) + lanes = ac.map({2: two, 1: one}) + ac.sink(lanes[1]) + ac.sink(lanes[2]) +""" + +WIDTH_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class Header: + value: ac.u32 + route: ac.u2 + remaining: ac.u16 + valid: bool + +@ac.system +def pipeline() -> None: + input_queue = ac.source(Header) + output_queue = input_queue.apply( + lambda item: item.with_fields( + value=item.value + 1, + remaining=item.remaining - 1, + ) + ) + ac.sink(output_queue) +""" + +FORK_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + input_queue = ac.source(int) + left, right = input_queue.fork(outputs=2, depth=2, latency=1) + ac.sink(left) + ac.sink(right) +""" + +RUNTIME_IF_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class Item: + value: int + route: int + +@ac.system +def pipeline() -> None: + input_queue = ac.source(Item) + if input_queue.route == 0: + output_queue = input_queue.apply( + lambda item: item.with_fields(value=item.value + 10) + ) + else: + output_queue = input_queue.apply( + lambda item: item.with_fields(value=item.value + 20) + ) + ac.sink(output_queue) +""" + +REORDER_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class Token: + sequence: ac.u64 + value: ac.u32 + +@ac.system +def pipeline() -> None: + completed = ac.source(Token) + retired = completed.reorder( + key=lambda item: item.sequence, + capacity=16, + start=0, + depth=4, + latency=1, + ) + ac.sink(retired) +""" + +DEPENDENCY_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class Token: + sequence: ac.u8 + waits_for: ac.u8 + resource: ac.u2 + cycles: ac.u16 + +@ac.system +def pipeline() -> None: + issued = ac.source(Token) + completed = issued.depend( + key=lambda item: item.sequence, + waits_for=lambda item: item.waits_for, + resource=lambda item: item.resource, + cost=lambda item: item.cycles, + capacity=16, + resources=4, + no_dependency=255, + depth=8, + latency=1, + ) + ac.sink(completed) +""" + +MEMORY_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class Request: + address: ac.u8 + write: bool + data: ac.u16 + +@ac.system +def pipeline() -> None: + sram = ac.memory(ac.u16, entries=16, init=0, latency=3) + requests = ac.source(Request) + responses = sram.request( + requests, + address=lambda item: item.address, + write=lambda item: item.write, + data=lambda item: item.data, + result_field="data", + depth=4, + ) + ac.sink(responses) +""" + +MEMORY_OWNED_SCOPE_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class Request: + address: ac.u8 + write: bool + data: ac.u16 + +@ac.system +def pipeline() -> None: + with ac.scope("owner"): + sram = ac.memory(ac.u16, entries=16, init=0, latency=3) + requests = ac.source(Request) + responses = sram.request( + requests, + address=lambda item: item.address, + write=lambda item: item.write, + data=lambda item: item.data, + result_field="data", + depth=4, + ) + ac.sink(responses) +""" + +MEMORY_ARRAY_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class BankRequest: + bank: ac.u2 + offset: ac.u4 + write: ac.u1 + data: ac.u16 + tag: ac.u8 + +@ac.system +def pipeline() -> None: + requests = ac.source(BankRequest, depth=8, latency=1) + with ac.scope("sram"): + banks = ac.array( + 4, + lambda _: ac.memory(ac.u16, entries=16, init=0, latency=2), + ) + selected = banks.select( + requests, + key=lambda request: request.bank, + depth=2, + latency=1, + ) + responses = selected.request( + address=lambda request: request.offset, + write=lambda request: request.write, + data=lambda request: request.data, + result_field="data", + depth=2, + merge_policy="priority", + merge_depth=3, + merge_latency=1, + ) + ac.sink(responses) +""" + +CREDIT_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + issued = ac.source(int) + completed = issued.credit( + cost=lambda item: item, + credits=4, + depth=4, + latency=1, + ) + ac.sink(completed) +""" + +BARRIER_SOURCE = """ +import agentic_circuit as ac + +@ac.system +def pipeline() -> None: + left = ac.source(int) + right = ac.source(int) + left_ready, right_ready = left.barrier(right, depth=2, latency=1) + ac.sink(left_ready) + ac.sink(right_ready) +""" + +SELECT_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class Control: + route: ac.u1 + +@ac.system +def pipeline() -> None: + control = ac.source(Control) + lanes = ac.array(2, lambda index: ac.source(int)) + selected = lanes.select( + control, + key=lambda item: item.route, + depth=2, + latency=1, + ) + ac.sink(selected) +""" + +FIRING_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class FiringItem: + value: ac.u16 + +@ac.system +def pipeline() -> None: + incoming = ac.source(FiringItem) + outgoing = incoming.firing( + lambda queue: queue.push( + queue.pop().with_fields(value=queue.peek().value + 1) + ) + ) + ac.sink(outgoing) +""" + +LOOP_CONTROL_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class LoopItem: + remaining: ac.u4 + stop: bool + skip: bool + +@ac.system +def pipeline() -> None: + current = ac.source(LoopItem) + while current.remaining > 0: + if current.stop: + break + current = current.apply( + lambda item: item.with_fields(remaining=item.remaining - 1) + ) + if current.skip: + continue + ac.sink(current) +""" + +RECURSION_SOURCE = """ +import agentic_circuit as ac + +def stages(queue, count): + if count == 0: + return queue + return stages( + queue.apply(lambda item: item + 1, depth=2, latency=1), + count - 1, + ) + +@ac.system +def pipeline() -> None: + incoming = ac.source(int) + outgoing = stages(incoming, 3) + ac.sink(outgoing) +""" + +EXPECT_SOURCE = """ +import agentic_circuit as ac + +@ac.struct +class ExpectedItem: + value: ac.u16 + +@ac.system +def pipeline() -> None: + incoming = ac.source(ExpectedItem) + ac.expect( + incoming, + predicate=lambda item: item.value > 0, + message="value must be positive", + ) + ac.sink(incoming) +""" + + +class QueueFrontendTest(unittest.TestCase): + def test_popcount_lowers_to_width_checked_var_operation(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(POPCOUNT_SOURCE, "pipeline") + self.assertIn( + "ac.var.popcount %v0 : !ac.var -> !ac.var", + lowered, + ) + + def test_verification_expect_is_non_consuming_and_role_explicit(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(EXPECT_SOURCE, "pipeline") + self.assertIn("ac.expect %incoming message", lowered) + self.assertIn("ac.expect.yield", lowered) + self.assertIn("ac.sink %incoming", lowered) + with self.assertRaisesRegex(QueueFrontendError, "predicate must lower to bool"): + lower_queue_source( + EXPECT_SOURCE.replace("item.value > 0", "item.value"), "pipeline" + ) + + def test_compile_time_recursion_expands_to_frozen_queue_chain(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(RECURSION_SOURCE, "pipeline") + self.assertEqual(3, lowered.count(" = ac.transform ")) + self.assertIn("%outgoing__rec0 = ac.transform %incoming", lowered) + self.assertIn("%outgoing__rec1 = ac.transform %outgoing__rec0", lowered) + self.assertIn("%outgoing = ac.transform %outgoing__rec1", lowered) + self.assertEqual(lowered, lower_queue_source(RECURSION_SOURCE, "pipeline")) + with self.assertRaisesRegex(QueueFrontendError, "recursion depth"): + lower_queue_source( + RECURSION_SOURCE.replace("stages(incoming, 3)", "stages(incoming, runtime)"), + "pipeline", + ) + + def test_bounded_loop_break_and_tail_continue_lower_to_feedback_edges(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(LOOP_CONTROL_SOURCE, "pipeline") + self.assertIn("ac.feedback", lowered) + self.assertIn('field "stop"', lowered) + self.assertIn('field "skip"', lowered) + self.assertIn('ac.var.cmp "eq"', lowered) + self.assertIn("ac.var.mul", lowered) + self.assertIn("ac.feedback.yield", lowered) + + def test_python_firing_effects_normalize_to_standard_atomic_transform(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(FIRING_SOURCE, "pipeline") + self.assertEqual(lowered, lower_queue_source(FIRING_SOURCE, "pipeline")) + self.assertIn("%outgoing = ac.transform %incoming", lowered) + self.assertIn('ac.var.get %item field "value"', lowered) + self.assertIn("ac.transform.yield", lowered) + with self.assertRaisesRegex(QueueFrontendError, "exactly one pop and one push"): + lower_queue_source( + FIRING_SOURCE.replace("queue.pop()", "queue.peek()"), "pipeline" + ) + with self.assertRaisesRegex(QueueFrontendError, "queue effects require firing"): + lower_queue_source(FIRING_SOURCE.replace(".firing(", ".apply("), "pipeline") + + def test_runtime_queue_collection_index_lowers_to_official_select(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(SELECT_SOURCE, "pipeline") + self.assertIn( + "%selected = ac.select %control, %lanes__0, %lanes__1 " + "depth 2 latency 1 key", + lowered, + ) + self.assertIn('ac.var.get %item field "route"', lowered) + self.assertIn("ac.select.yield", lowered) + self.assertNotIn("dynamic", lowered) + + def test_barrier_lowers_multi_queue_atomic_synchronization(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(BARRIER_SOURCE, "pipeline") + self.assertIn( + "%left_ready, %right_ready = ac.barrier %left, %right " + "depths [2, 2] latencies [1, 1]", + lowered, + ) + with self.assertRaisesRegex(QueueFrontendError, "inputs must be unique"): + lower_queue_source( + BARRIER_SOURCE.replace("left.barrier(right", "left.barrier(left"), + "pipeline", + ) + with self.assertRaisesRegex(QueueFrontendError, "matching input/output arity"): + lower_queue_source( + BARRIER_SOURCE.replace( + "left_ready, right_ready =", "left_ready, right_ready, extra =" + ), + "pipeline", + ) + + def test_credit_lowers_bounded_parallel_completion_window(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(CREDIT_SOURCE, "pipeline") + self.assertIn( + "%completed = ac.credit %issued credits 4 depth 4 latency 1 cost", + lowered, + ) + self.assertIn("ac.credit.yield %item : !ac.var", lowered) + with self.assertRaisesRegex(QueueFrontendError, "credits must be positive"): + lower_queue_source( + CREDIT_SOURCE.replace("credits=4", "credits=0"), "pipeline" + ) + + def test_memory_lowers_old_data_request_response_contract(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(MEMORY_SOURCE, "pipeline") + self.assertIn( + "ac.memory.instance @sram data i16 entries 16 init 0 latency 3", + lowered, + ) + self.assertIn( + "%responses = ac.memory.request @sram, %requests ordinal 0 " + 'result_field "data" depth 4 address', + lowered, + ) + self.assertIn("} write {", lowered) + self.assertIn("} data {", lowered) + self.assertEqual(3, lowered.count("ac.memory.yield")) + with self.assertRaisesRegex(QueueFrontendError, "memory init must be zero"): + lower_queue_source(MEMORY_SOURCE.replace("init=0", "init=1"), "pipeline") + with self.assertRaisesRegex(QueueFrontendError, "latency must be positive"): + lower_queue_source(MEMORY_SOURCE.replace("latency=3", "latency=0"), "pipeline") + with self.assertRaisesRegex(QueueFrontendError, "unsupported keyword"): + lower_queue_source( + MEMORY_SOURCE.replace("depth=4,", "depth=4,\n latency=1,"), + "pipeline", + ) + + def test_memory_instance_freezes_multiple_endpoint_priority(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + source = MEMORY_SOURCE.replace( + "requests = ac.source(Request)", + "left = ac.source(Request)\n right = ac.source(Request)", + ).replace( + "responses = sram.request(\n requests,", + "responses = sram.request(\n left,", + ).replace( + " ac.sink(responses)", + " other = sram.request(\n" + " right, address=lambda item: item.address,\n" + " write=lambda item: item.write, data=lambda item: item.data,\n" + " result_field=\"data\", depth=2,\n" + " )\n" + " ac.sink(responses)\n ac.sink(other)", + ) + lowered = lower_queue_source(source, "pipeline") + self.assertEqual(1, lowered.count("ac.memory.instance")) + self.assertEqual(2, lowered.count("ac.memory.request")) + self.assertIn("@sram, %left ordinal 0", lowered) + self.assertIn("@sram, %right ordinal 1", lowered) + + def test_memory_is_visible_in_its_declaration_scope(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(MEMORY_OWNED_SCOPE_SOURCE, "pipeline") + self.assertIn( + 'ac.memory.instance @sram data i16 entries 16 init 0 latency 3 owner "/owner" ' + 'stable_id "memory/owner/sram"', + lowered, + ) + self.assertIn("ac.scope @owner()", lowered) + self.assertIn( + 'ac.endpoint_path = "/owner/responses", ac.name = "responses"', + lowered, + ) + + def test_memory_array_select_statically_expands_existing_ops(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(MEMORY_ARRAY_SOURCE, "pipeline") + self.assertEqual(4, lowered.count("ac.memory.instance")) + self.assertEqual(4, lowered.count("ac.memory.request")) + self.assertEqual(1, lowered.count("ac.route ")) + self.assertEqual(1, lowered.count("ac.merge ")) + for bank in range(4): + self.assertIn( + f'ac.memory.instance @banks__{bank} data i16 entries 16 init 0 latency 2 ' + f'owner "/sram" stable_id "memory/sram/banks__{bank}"', + lowered, + ) + self.assertIn( + f"ac.memory.request @banks__{bank}, " + f"%selected__bank{bank}_request__local ordinal 0", + lowered, + ) + self.assertIn("depths [2, 2, 2, 2] latencies [1, 1, 1, 1]", lowered) + self.assertIn('ac.var.get %item field "bank"', lowered) + self.assertIn( + '%responses__local = ac.merge %responses__bank0__local, ' + '%responses__bank1__local, %responses__bank2__local, ' + '%responses__bank3__local policy "priority" depth 3 latency 1', + lowered, + ) + self.assertEqual(lowered, lower_queue_source(MEMORY_ARRAY_SOURCE, "pipeline")) + + def test_memory_array_select_rejects_invalid_elaboration(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + heterogeneous = MEMORY_ARRAY_SOURCE.replace( + "entries=16", "entries=_ + 1" + ) + with self.assertRaisesRegex(QueueFrontendError, "must be homogeneous"): + lower_queue_source(heterogeneous, "pipeline") + + noninteger_key = MEMORY_ARRAY_SOURCE.replace( + "key=lambda request: request.bank", "key=lambda request: request" + ) + with self.assertRaisesRegex(QueueFrontendError, "route key must lower"): + lower_queue_source(noninteger_key, "pipeline") + + request_start = MEMORY_ARRAY_SOURCE.index( + " responses = selected.request(" + ) + unused = MEMORY_ARRAY_SOURCE[:request_start] + " ac.sink(requests)\n" + with self.assertRaisesRegex( + QueueFrontendError, "selected memory is not requested" + ): + lower_queue_source(unused, "pipeline") + + wrong_scope = MEMORY_ARRAY_SOURCE.replace( + " responses = selected.request(", + " responses = selected.request(", + ) + with self.assertRaisesRegex(QueueFrontendError, "same lexical scope"): + lower_queue_source(wrong_scope, "pipeline") + + def test_memory_rejects_legacy_unconnected_type_and_scope_forms(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + legacy = MEMORY_SOURCE.replace( + "sram = ac.memory(ac.u16, entries=16, init=0, latency=3)\n ", "" + ).replace("sram.request(\n requests,", "requests.memory(") + with self.assertRaisesRegex(QueueFrontendError, "Queue.memory was removed"): + lower_queue_source(legacy, "pipeline") + unconnected = MEMORY_SOURCE.replace( + "responses = sram.request(", "other = ac.memory(ac.u16)\n responses = sram.request(" + ) + with self.assertRaisesRegex(QueueFrontendError, "is not connected"): + lower_queue_source(unconnected, "pipeline") + mismatch = MEMORY_SOURCE.replace("ac.memory(ac.u16", "ac.memory(ac.u8") + with self.assertRaisesRegex(QueueFrontendError, "must match instance"): + lower_queue_source(mismatch, "pipeline") + illegal_scope = MEMORY_SOURCE.replace( + "sram = ac.memory(ac.u16, entries=16, init=0, latency=3)", + 'with ac.scope("owner"):\n sram = ac.memory(ac.u16, entries=16, init=0, latency=3)', + ) + with self.assertRaisesRegex(QueueFrontendError, "declaration scope"): + lower_queue_source(illegal_scope, "pipeline") + rebound = MEMORY_SOURCE.replace( + "requests = ac.source(Request)", + "sram = ac.source(Request)\n requests = ac.source(Request)", + ) + with self.assertRaisesRegex(QueueFrontendError, "cannot be rebound"): + lower_queue_source(rebound, "pipeline") + + def test_dependency_lowers_four_pure_policies(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(DEPENDENCY_SOURCE, "pipeline") + self.assertIn( + "%completed = ac.dependency %issued capacity 16 resources 4 " + "no_dependency 255 depth 8 latency 1 key", + lowered, + ) + self.assertIn("} waits_for {", lowered) + self.assertIn("} resource {", lowered) + self.assertIn("} cost {", lowered) + self.assertEqual(4, lowered.count("ac.dependency.yield")) + with self.assertRaisesRegex(QueueFrontendError, "requires one cost lambda"): + lower_queue_source( + DEPENDENCY_SOURCE.replace("cost=lambda item: item.cycles,", ""), + "pipeline", + ) + + def test_reorder_lowers_sequence_key_and_static_capacity(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(REORDER_SOURCE, "pipeline") + self.assertIn( + "%retired = ac.reorder %completed capacity 16 start 0 depth 4 latency 1", + lowered, + ) + self.assertIn('ac.var.get %item field "sequence"', lowered) + self.assertIn("ac.reorder.yield", lowered) + with self.assertRaisesRegex(QueueFrontendError, "capacity must be positive"): + lower_queue_source( + REORDER_SOURCE.replace("capacity=16", "capacity=0"), "pipeline" + ) + + def test_simple_serial_python_lowers_to_typed_queue_graph(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + self.assertEqual( + """module attributes {ac.contract_epoch = "0.3", ac.system = "pipeline"} { + %input_queue = ac.source depth 4 latency 1 {ac.name = "input_queue"} : !ac.queue + %output_queue = ac.transform %input_queue depths [8] latencies [2] { + ^transform(%item: !ac.var): + ac.transform.yield %item : !ac.var + } {ac.name = "output_queue"} : (!ac.queue) -> !ac.queue + ac.sink %output_queue {ac.name = "sink_2"} : !ac.queue +} +""", + lower_queue_source(SOURCE, "pipeline"), + ) + + def test_repeated_lowering_is_byte_identical(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + self.assertEqual( + lower_queue_source(SOURCE, "pipeline"), + lower_queue_source(SOURCE, "pipeline"), + ) + + def test_struct_and_immutable_lambda_lower_to_var_operations(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(STRUCT_SOURCE, "pipeline") + self.assertIn("ac.type_scope @types", lowered) + self.assertIn("ac.struct @WorkItem", lowered) + self.assertIn("!ac.queue>", lowered) + self.assertIn('ac.var.get %item field "value"', lowered) + self.assertIn("ac.var.constant 1 : i64", lowered) + self.assertIn("ac.var.add", lowered) + self.assertIn("ac.var.mul", lowered) + self.assertIn("ac.var.sub", lowered) + self.assertIn("ac.var.with", lowered) + self.assertIn('field "remaining"', lowered) + + def test_nested_scope_infers_borrowed_local_and_exported_queues(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(SCOPE_SOURCE, "pipeline") + self.assertIn("%completed = ac.scope @frontend(%input_queue)", lowered) + self.assertIn("^body(%input_queue__in: !ac.queue):", lowered) + self.assertIn( + "%completed__inner = ac.scope @inner(%adjusted__local)", + lowered, + ) + self.assertIn("ac.scope.yield %completed__local", lowered) + self.assertIn( + 'ac.sink %completed {ac.name = "sink_5"} : !ac.queue', lowered + ) + + def test_multiple_consumers_insert_strict_atomic_broadcast(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(BROADCAST_SOURCE, "pipeline") + self.assertIn( + "%input_queue__fanout0, %input_queue__fanout1 = ac.broadcast " + "%input_queue depths [1, 1] latencies [1, 1]", + lowered, + ) + self.assertIn("ac.transform %input_queue__fanout0", lowered) + self.assertIn("ac.transform %input_queue__fanout1", lowered) + + def test_cross_scope_broadcast_is_placed_at_lexical_lca(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(CROSS_SCOPE_BROADCAST_SOURCE, "pipeline") + broadcast = lowered.index("ac.broadcast %input_queue") + left_scope = lowered.index("ac.scope @left(%input_queue__fanout0)") + right_scope = lowered.index("ac.scope @right(%input_queue__fanout1)") + self.assertLess(broadcast, left_scope) + self.assertLess(broadcast, right_scope) + self.assertIn("^body(%input_queue__fanout0__in: !ac.queue):", lowered) + + def test_tuple_route_lowers_selector_to_var_region(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(ROUTE_SOURCE, "pipeline") + self.assertIn("%left, %right = ac.route %input_queue", lowered) + self.assertIn("depths [2, 2] latencies [1, 1]", lowered) + self.assertIn('ac.var.get %item field "route"', lowered) + self.assertIn("ac.route.yield", lowered) + self.assertIn('%merged = ac.merge %left, %right policy "round_robin"', lowered) + self.assertIn("depth 3 latency 1", lowered) + self.assertIn("ac.sink %merged", lowered) + + def test_static_queue_collections_flatten_in_canonical_order(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(COLLECTION_SOURCE, "pipeline") + self.assertIn("%lanes__0 = ac.source depth 1", lowered) + self.assertIn("%lanes__1 = ac.source depth 2", lowered) + self.assertLess( + lowered.index("ac.sink %lanes__0"), + lowered.index("ac.sink %lanes__1"), + ) + self.assertNotIn("dynamic", lowered) + reordered = COLLECTION_SOURCE.replace( + '{named["right"], named["left"]}', + '{named["left"], named["right"]}', + ) + self.assertEqual(lowered, lower_queue_source(reordered, "pipeline")) + + def test_dynamic_or_duplicate_collection_shape_is_rejected(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + with self.assertRaisesRegex(QueueFrontendError, "positive compile-time extent"): + lower_queue_source( + COLLECTION_SOURCE.replace("ac.array(2", "ac.array(runtime"), + "pipeline", + ) + with self.assertRaisesRegex(QueueFrontendError, "members must be unique"): + lower_queue_source( + COLLECTION_SOURCE.replace( + '{named["right"], named["left"]}', + '{named["left"], named["left"]}', + ), + "pipeline", + ) + + def test_nested_collection_shape_is_statically_flattened(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(NESTED_COLLECTION_SOURCE, "pipeline") + self.assertIn("%grid__0__0 = ac.source depth 1", lowered) + self.assertIn("%grid__0__1 = ac.source depth 2", lowered) + self.assertIn("%grid__1__0 = ac.source depth 2", lowered) + self.assertIn("%grid__1__1 = ac.source depth 3", lowered) + self.assertIn("ac.sink %grid__1__0", lowered) + + def test_serial_while_lowers_to_bounded_feedback(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(FEEDBACK_SOURCE, "pipeline") + self.assertIn("ac.feedback %current depth 2 latency 1", lowered) + self.assertIn("max_iterations 1024", lowered) + self.assertIn('ac.var.cmp "sgt"', lowered) + self.assertIn("ac.feedback.yield", lowered) + self.assertIn("ac.sink %current__feedback0", lowered) + + def test_observation_only_use_does_not_insert_broadcast(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(OBSERVE_SOURCE, "pipeline") + self.assertIn('ac.observe %input_queue name "observe_1"', lowered) + self.assertIn("ac.sink %input_queue", lowered) + self.assertNotIn("ac.broadcast", lowered) + + def test_explicit_atomic_groups_multiple_queue_updates(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(ATOMIC_SOURCE, "pipeline") + self.assertIn("%left_next, %right_next = ac.transform %left, %right", lowered) + self.assertIn( + "^transform(%item0: !ac.var, %item1: !ac.var):", lowered + ) + self.assertIn("ac.transform.yield", lowered) + self.assertIn('ac.output_names = ["left_next", "right_next"]', lowered) + with self.assertRaisesRegex(QueueFrontendError, "inputs must be unique"): + lower_queue_source( + ATOMIC_SOURCE.replace( + "right_next = right.apply", "right_next = left.apply" + ), + "pipeline", + ) + + def test_static_if_and_range_are_fully_expanded(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + lowered = lower_queue_source(STATIC_CONTROL_SOURCE, "pipeline") + self.assertIn('ac.name = "selected"', lowered) + self.assertNotIn("unreachable", lowered) + self.assertIn("ac.sink %lanes__0", lowered) + self.assertIn("ac.sink %lanes__1", lowered) + with self.assertRaisesRegex(QueueFrontendError, "one result name"): + lower_queue_source( + STATIC_CONTROL_SOURCE.replace("if True:", "if input_queue:"), + "pipeline", + ) + + def test_user_opcode_definition_is_rejected(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + illegal = """ +import agentic_circuit as ac + +@ac.opcode +def private_block(): + pass + +@ac.system +def pipeline() -> None: + queue = ac.source(int) + ac.sink(queue) +""" + with self.assertRaisesRegex(QueueFrontendError, "providers are forbidden"): + lower_queue_source(illegal, "pipeline") + + def test_static_map_accepts_canonical_integer_keys(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(CONST_KEY_MAP_SOURCE, "pipeline") + self.assertLess(lowered.index("ac.sink %one"), lowered.index("ac.sink %two")) + + def test_explicit_integer_widths_freeze_payload_layout(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(WIDTH_SOURCE, "pipeline") + self.assertIn('{name = "value", type = i32}', lowered) + self.assertIn('{name = "route", type = i2}', lowered) + self.assertIn('{name = "remaining", type = i16}', lowered) + self.assertIn('{name = "valid", type = i1}', lowered) + self.assertIn("size = 12 : i64", lowered) + + def test_explicit_fork_lowers_to_decoupled_fanout(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(FORK_SOURCE, "pipeline") + self.assertIn( + "%left, %right = ac.fork %input_queue depths [2, 2] latencies [1, 1]", + lowered, + ) + + def test_runtime_if_infers_route_branch_transforms_and_merge(self) -> None: + from agentic_circuit._queue_frontend import lower_queue_source + + lowered = lower_queue_source(RUNTIME_IF_SOURCE, "pipeline") + self.assertEqual(lowered, lower_queue_source(RUNTIME_IF_SOURCE, "pipeline")) + self.assertIn( + "%output_queue__if_false0_in, %output_queue__if_true0_in = " + "ac.route %input_queue", + lowered, + ) + self.assertIn('ac.var.cmp "eq"', lowered) + self.assertIn("ac.route.yield", lowered) + self.assertIn("ac.transform %output_queue__if_false0_in", lowered) + self.assertIn("ac.transform %output_queue__if_true0_in", lowered) + self.assertIn( + "%output_queue = ac.merge %output_queue__if_false0, " + '%output_queue__if_true0 policy "priority"', + lowered, + ) + + def test_runtime_if_requires_symmetric_queue_assignment(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + with self.assertRaisesRegex( + QueueFrontendError, "requires one apply assignment in each branch" + ): + lower_queue_source( + RUNTIME_IF_SOURCE.replace(" else:\n", " else:\n pass\n"), + "pipeline", + ) + with self.assertRaisesRegex(QueueFrontendError, "one result name"): + lower_queue_source( + RUNTIME_IF_SOURCE.replace( + " output_queue = input_queue.apply(\n" + " lambda item: item.with_fields(value=item.value + 20)", + " other_queue = input_queue.apply(\n" + " lambda item: item.with_fields(value=item.value + 20)", + ), + "pipeline", + ) + with self.assertRaisesRegex(QueueFrontendError, "must lower to bool"): + lower_queue_source( + RUNTIME_IF_SOURCE.replace( + "if input_queue.route == 0:", "if input_queue.route:" + ), + "pipeline", + ) + + def test_latency_zero_and_unsupported_lambda_are_rejected(self) -> None: + from agentic_circuit._queue_frontend import ( + QueueFrontendError, + lower_queue_source, + ) + + with self.assertRaisesRegex(QueueFrontendError, "latency must be positive"): + lower_queue_source(SOURCE.replace("latency=2", "latency=0"), "pipeline") + with self.assertRaisesRegex(QueueFrontendError, "unsupported lambda"): + lower_queue_source( + SOURCE.replace("lambda item: item", "lambda item: unknown(item)"), + "pipeline", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_resources.py b/components/agentic-circuit/tests/python_frontend/test_resources.py new file mode 100644 index 00000000..b06d50cd --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_resources.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + + +WORKSPACE = Path(__file__).resolve().parent / "fixtures" / "resources" + + +def load_fixture(name: str): + path = WORKSPACE / name + spec = importlib.util.spec_from_file_location(f"resources_{path.stem}", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot import resource fixture {path}") + loaded = importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded) + return loaded + + +class ResourceFrontendTest(unittest.TestCase): + def test_queue_and_address_map_are_static_records(self) -> None: + loaded = load_fixture("valid.py") + + self.assertEqual("ready_valid", loaded.requests.protocol) + self.assertEqual(8, loaded.requests.depth) + self.assertEqual("core", loaded.requests.time_domain) + self.assertEqual((0x1000, 0x2000), loaded.mapping.entries[0].range) + self.assertEqual("memory", loaded.mapping.entries[0].target.stable_name) + + def test_protocol_roles_must_be_complementary(self) -> None: + from agentic_circuit._resources import ( + FrontendRuleError, + ProtocolContract, + verify_protocol_roles, + ) + + contract = ProtocolContract( + "ready_valid", "producer", "consumer", "Transaction", "core" + ) + + with self.assertRaisesRegex(FrontendRuleError, "ACPY-PROTOCOL-004"): + verify_protocol_roles(contract, "producer", "producer") + + def test_equal_priority_address_overlap_is_rejected(self) -> None: + from agentic_circuit import address_map, address_space + from agentic_circuit._resources import FrontendRuleError + + memory = load_fixture("invalid.py").memory + space = address_space("system", width=32) + + with self.assertRaisesRegex(FrontendRuleError, "ACPY-ADDRESS-003"): + address_map( + space, + (0x1000, 0x2000, memory, 0), + (0x1800, 0x2800, memory, 0), + ) + + def test_dynamic_address_and_nonpositive_depth_are_rejected(self) -> None: + from agentic_circuit import address_map, address_space, queue + from agentic_circuit._resources import FrontendRuleError + from agentic_circuit._types import _test_symbolic + + memory = load_fixture("invalid.py").memory + space = address_space("system", width=32) + dynamic = _test_symbolic("base", int) + + with self.assertRaisesRegex(FrontendRuleError, "ACPY-STATIC-002"): + address_map(space, (dynamic, 0x2000, memory, 0)) + with self.assertRaisesRegex(FrontendRuleError, "ACPY-RESOURCE-002"): + queue( + "requests", + payload_type="Transaction", + protocol="ready_valid", + depth=0, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_scopes_collections.py b/components/agentic-circuit/tests/python_frontend/test_scopes_collections.py new file mode 100644 index 00000000..f3bc8a4d --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_scopes_collections.py @@ -0,0 +1,172 @@ +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + + +WORKSPACE = Path(__file__).resolve().parent / "fixtures" / "scopes" +ZERO_DIGEST = "sha256:" + "0" * 64 + + +def schema(identity: str, ports, results, *, static=()): + from agentic_circuit._schemas import ( + ComponentSchema, + ParameterSchema, + PortSchema, + ResultSchema, + ) + + return ComponentSchema( + identity=identity, + fingerprint=ZERO_DIGEST, + ports=tuple( + PortSchema(name, kind, type_key, "in", role, 1) + for name, kind, type_key, role in ports + ), + results=tuple(ResultSchema(name, type_key, None) for name, type_key in results), + parameters=tuple( + ParameterSchema(name, "index", True, None, None) for name in static + ), + availability="available", + ) + + +def normalize_scope_fixture(name: str = "nested.py"): + from agentic_circuit._frontend import CaptureRequest, capture_definitions + from agentic_circuit._normalize import normalize_program + from agentic_circuit._schemas import SchemaRegistry + + path = WORKSPACE / name + spec = importlib.util.spec_from_file_location(f"scope_{path.stem}", path) + if spec is None or spec.loader is None: + raise RuntimeError("cannot import scope fixture") + loaded = importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded) + if name == "invalid_escape.py": + make_private = schema( + "test.MakePrivate", (), (("private", "!test.resource"),) + ) + registry = SchemaRegistry({make_private.identity: make_private}) + else: + schedule = schema( + "test.Schedule", + (("input", "flow", "!test.flow", "consumer"),), + (("scheduled", "!test.flow"),), + ) + store = schema( + "test.Store", + ( + ("input", "flow", "!test.flow", "consumer"), + ("memory", "endpoint", "!test.endpoint", "target"), + ), + (("stored", "!test.flow"),), + ) + registry = SchemaRegistry({schedule.identity: schedule, store.identity: store}) + captured = capture_definitions( + CaptureRequest(entry=path, workspace=WORKSPACE, system="main"), + vars(loaded), + registry, + ) + if captured.diagnostics: + raise AssertionError(captured.diagnostics) + return normalize_program(captured, definition="pipeline") + + +def resolved_call(key: str, component, *, lanes: int): + from agentic_circuit._diagnostics import SourceSpan + from agentic_circuit._resolve import ResolvedCall + + return ResolvedCall( + entity_key=key, + schema=component, + instance_name=key, + static_arguments=(("lanes", lanes),), + inputs=(), + results=(), + source=SourceSpan("nested.py", 1, 1, 1, 2), + ) + + +class ScopeCollectionTest(unittest.TestCase): + def test_rectangular_homogeneous_collection_selects_array(self) -> None: + from agentic_circuit._collections import classify_collection + + compute = schema("test.Compute", (), (), static=("lanes",)) + elements = tuple( + tuple( + resolved_call(f"worker_{row}_{column}", compute, lanes=2) + for column in range(4) + ) + for row in range(2) + ) + + collection = classify_collection(elements) + + self.assertEqual("array", collection.kind) + self.assertEqual((2, 4), collection.shape) + self.assertEqual("test.Compute", collection.element_schema) + self.assertEqual( + tuple(f"worker_{row}_{column}" for row in range(2) for column in range(4)), + collection.elements, + ) + + def test_specialization_difference_selects_instances(self) -> None: + from agentic_circuit._collections import classify_collection + + compute = schema("test.Compute", (), (), static=("lanes",)) + collection = classify_collection( + ( + resolved_call("worker_0", compute, lanes=2), + resolved_call("worker_1", compute, lanes=4), + ) + ) + + self.assertEqual("instances", collection.kind) + self.assertIsNone(collection.element_schema) + + def test_ragged_collection_is_rejected(self) -> None: + from agentic_circuit._collections import CollectionError, classify_collection + + compute = schema("test.Compute", (), (), static=("lanes",)) + element = resolved_call("worker", compute, lanes=2) + + with self.assertRaisesRegex(CollectionError, "ragged collection"): + classify_collection(((element,), (element, element))) + + def test_scope_signature_is_minimal_and_ordered(self) -> None: + from agentic_circuit._scopes import outline_scopes + + program = normalize_scope_fixture() + self.assertEqual((), program.diagnostics) + scopes = outline_scopes(program) + backend = next(scope for scope in scopes if scope.name == "backend") + memory = next(scope for scope in scopes if scope.name == "memory") + + self.assertEqual( + ("requests", "memory"), + tuple(binding.value.source_name for binding in backend.captures), + ) + self.assertEqual( + ("stored",), tuple(binding.value.source_name for binding in backend.escapes) + ) + self.assertEqual( + ("scheduled", "memory"), + tuple(binding.value.source_name for binding in memory.captures), + ) + self.assertEqual( + ("stored",), tuple(binding.value.source_name for binding in memory.escapes) + ) + + def test_owned_resource_cannot_escape_its_scope(self) -> None: + from agentic_circuit._scopes import ScopeError, outline_scopes + + program = normalize_scope_fixture("invalid_escape.py") + self.assertEqual((), program.diagnostics) + + with self.assertRaisesRegex(ScopeError, "ACPY-SCOPE-004"): + outline_scopes(program) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_source_capture.py b/components/agentic-circuit/tests/python_frontend/test_source_capture.py new file mode 100644 index 00000000..0a7fbad5 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_source_capture.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import hashlib +import json +import tempfile +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator + + +REPOSITORY = Path(__file__).resolve().parents[2] +WORKSPACE = Path(__file__).resolve().parent / "fixtures" / "source" + + +class SourceCaptureTest(unittest.TestCase): + def test_identity_is_workspace_relative_and_hashed(self) -> None: + from agentic_circuit._source import load_source_unit + + entry = WORKSPACE / "basic.py" + unit = load_source_unit(entry, WORKSPACE) + + self.assertEqual("basic.py", unit.path) + self.assertEqual( + "sha256:" + hashlib.sha256(entry.read_bytes()).hexdigest(), unit.sha256 + ) + self.assertEqual( + ["Worker", "Architecture"], + [site.qualified_name for site in unit.definitions], + ) + self.assertEqual([0, 1], [site.lexical_index for site in unit.definitions]) + self.assertEqual(4, unit.definitions[0].span.start_line) + + def test_source_outside_workspace_is_rejected(self) -> None: + from agentic_circuit._source import SourceCaptureError, load_source_unit + + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) / "workspace" + workspace.mkdir() + outside = Path(temporary) / "outside.py" + outside.write_text("value = 1\n", encoding="utf-8") + + with self.assertRaisesRegex(SourceCaptureError, "outside workspace"): + load_source_unit(outside, workspace) + + def test_non_utf8_source_is_rejected(self) -> None: + from agentic_circuit._source import SourceCaptureError, load_source_unit + + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + entry = workspace / "invalid.py" + entry.write_bytes(b"\xff\n") + + with self.assertRaisesRegex(SourceCaptureError, "not UTF-8"): + load_source_unit(entry, workspace) + + def test_duplicate_qualified_definitions_are_rejected(self) -> None: + from agentic_circuit._source import SourceCaptureError, load_source_unit + + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + entry = workspace / "duplicate.py" + entry.write_text( + "def repeated():\n pass\n\ndef repeated():\n pass\n", + encoding="utf-8", + ) + + with self.assertRaisesRegex(SourceCaptureError, "duplicate definition"): + load_source_unit(entry, workspace) + + def test_nested_definition_keeps_qualified_name_and_column(self) -> None: + from agentic_circuit._source import load_source_unit + + with tempfile.TemporaryDirectory() as temporary: + workspace = Path(temporary) + entry = workspace / "nested.py" + entry.write_text( + "def outer():\n" + " @module\n" + " def inner():\n" + " pass\n", + encoding="utf-8", + ) + + unit = load_source_unit(entry, workspace) + + inner = unit.definitions[1] + self.assertEqual("outer..inner", inner.qualified_name) + self.assertEqual(2, inner.span.start_line) + self.assertEqual(5, inner.span.start_column) + + +class DiagnosticTest(unittest.TestCase): + def test_diagnostic_contract_identity_cannot_be_overridden(self) -> None: + from agentic_circuit._diagnostics import Diagnostic + + with self.assertRaisesRegex(ValueError, "schema identity"): + Diagnostic( + stage="source-capture", + code="ACPY-SYNTAX-SOURCE", + severity="error", + message="source cannot be captured", + schema="different-schema", + ) + + def test_diagnostics_sort_by_source_then_code(self) -> None: + from agentic_circuit._diagnostics import ( + Diagnostic, + DiagnosticBag, + SourceSpan, + ) + + bag = DiagnosticBag() + bag.add( + Diagnostic( + stage="type-check", + code="ACPY-TYPE-002", + severity="error", + message="second", + source=SourceSpan("basic.py", 8, 1, 8, 4), + ) + ) + bag.add( + Diagnostic( + stage="type-check", + code="ACPY-TYPE-001", + severity="error", + message="first", + source=SourceSpan("basic.py", 3, 1, 3, 4), + ) + ) + + self.assertEqual( + ["ACPY-TYPE-001", "ACPY-TYPE-002"], + [item.code for item in bag.freeze()], + ) + + def test_diagnostic_json_matches_the_closed_public_schema(self) -> None: + from agentic_circuit._diagnostics import ( + Diagnostic, + FixIt, + RelatedLocation, + SourceSpan, + ) + + source = SourceSpan("basic.py", 9, 5, 9, 12) + diagnostic = Diagnostic( + stage="source-capture", + code="ACPY-SYNTAX-SOURCE", + severity="error", + message="source cannot be captured", + source=source, + related=(RelatedLocation("definition", source, None),), + fixits=(FixIt("Keep the definition in the workspace"),), + ) + schema = json.loads( + (REPOSITORY / "schemas" / "diagnostic.schema.json").read_text( + encoding="utf-8" + ) + ) + + value = diagnostic.to_json() + Draft202012Validator(schema).validate(value) + self.assertEqual( + {"file": "basic.py", "line": 9, "column": 5}, value["source"] + ) + self.assertIsNone(value["expected"]) + self.assertIsNone(value["actual"]) + self.assertEqual( + {"file": "basic.py", "line": 9, "column": 5}, + value["related"][0]["source"], + ) + self.assertIsNone(value["related"][0]["object_path"]) + self.assertEqual( + [{"message": "Keep the definition in the workspace"}], value["fixits"] + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/python_frontend/test_static_eval.py b/components/agentic-circuit/tests/python_frontend/test_static_eval.py new file mode 100644 index 00000000..c392fc80 --- /dev/null +++ b/components/agentic-circuit/tests/python_frontend/test_static_eval.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import ast +import unittest +from pathlib import Path + + +WORKSPACE = Path(__file__).resolve().parent / "fixtures" / "static" + + +def parse_expr(text: str) -> ast.expr: + return ast.parse(text, mode="eval").body + + +class StaticEvaluationTest(unittest.TestCase): + def test_closed_expression_is_deterministic(self) -> None: + from agentic_circuit._static_eval import StaticEnvironment, evaluate_static + + expression = parse_expr("tuple(i * 2 for i in range(lanes))") + self.assertEqual( + (0, 2, 4, 6), + evaluate_static(expression, StaticEnvironment({"lanes": 4})), + ) + + def test_unapproved_call_is_rejected(self) -> None: + from agentic_circuit._static_eval import StaticEnvironment, StaticEvalError + from agentic_circuit._static_eval import evaluate_static + + with self.assertRaisesRegex(StaticEvalError, "unapproved call"): + evaluate_static(parse_expr("open('input.txt')"), StaticEnvironment({})) + + def test_non_finite_float_is_rejected(self) -> None: + from agentic_circuit._static_eval import StaticEnvironment, StaticEvalError + from agentic_circuit._static_eval import evaluate_static + + with self.assertRaisesRegex(StaticEvalError, "finite"): + evaluate_static(parse_expr("1e309"), StaticEnvironment({})) + + def test_comprehension_target_does_not_escape(self) -> None: + from agentic_circuit._static_eval import StaticEnvironment, StaticEvalError + from agentic_circuit._static_eval import evaluate_static + + expression = parse_expr("(tuple(i for i in range(2)), i)") + with self.assertRaisesRegex(StaticEvalError, "unknown static name 'i'"): + evaluate_static(expression, StaticEnvironment({})) + + +class ValidationTest(unittest.TestCase): + def test_supported_static_control_has_no_diagnostics(self) -> None: + from agentic_circuit._diagnostics import DiagnosticBag + from agentic_circuit._source import load_source_unit + from agentic_circuit._validate import ValidationContext, validate_definition + + unit = load_source_unit(WORKSPACE / "supported.py", WORKSPACE) + site = next(item for item in unit.definitions if item.name == "Supported") + diagnostics = DiagnosticBag() + validate_definition( + site, + ValidationContext( + definition_kind="module", + static_names=frozenset({"lanes", "enabled"}), + symbolic_names=frozenset(), + approved_helpers=frozenset({"tuple", "range"}), + ), + diagnostics, + ) + + self.assertEqual((), diagnostics.freeze()) + + def test_dynamic_truthiness_is_rejected_at_operand(self) -> None: + from agentic_circuit._diagnostics import DiagnosticBag + from agentic_circuit._source import load_source_unit + from agentic_circuit._validate import ValidationContext, validate_definition + + unit = load_source_unit(WORKSPACE / "unsupported.py", WORKSPACE) + site = next(item for item in unit.definitions if item.name == "Unsupported") + diagnostics = DiagnosticBag() + validate_definition( + site, + ValidationContext( + definition_kind="module", + static_names=frozenset(), + symbolic_names=frozenset({"request"}), + approved_helpers=frozenset(), + ), + diagnostics, + ) + + matches = [item for item in diagnostics.freeze() if item.code == "ACPY-STATIC-002"] + self.assertEqual(1, len(matches)) + item = matches[0] + self.assertIsNotNone(item.source) + assert item.source is not None + self.assertEqual((8, 7), (item.source.start_line, item.source.start_column)) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/test_build_config.py b/components/agentic-circuit/tests/test_build_config.py new file mode 100644 index 00000000..3a7a336d --- /dev/null +++ b/components/agentic-circuit/tests/test_build_config.py @@ -0,0 +1,142 @@ +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + + +class BuildConfigurationTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.repo_root = Path(__file__).resolve().parents[1] + + def test_production_configure_does_not_require_lit(self): + mlir_dir = os.environ.get("MLIR_DIR") + if not mlir_dir: + self.skipTest("MLIR_DIR is required for the configure regression test") + + cmake = shutil.which("cmake") + ninja = shutil.which("ninja") + self.assertIsNotNone(cmake) + self.assertIsNotNone(ninja) + + ignored_lit_dirs = {str(self.repo_root / ".venv" / "bin")} + for executable in ("lit", "llvm-lit"): + executable_path = shutil.which(executable) + if executable_path: + ignored_lit_dirs.add(str(Path(executable_path).resolve().parent)) + + with tempfile.TemporaryDirectory() as temp_dir: + result = subprocess.run( + [ + cmake, + "-S", + str(self.repo_root), + "-B", + str(Path(temp_dir) / "build"), + "-G", + "Ninja", + f"-DCMAKE_MAKE_PROGRAM={ninja}", + f"-DMLIR_DIR={mlir_dir}", + "-DBUILD_TESTING=OFF", + f"-DCMAKE_IGNORE_PATH={';'.join(sorted(ignored_lit_dirs))}", + ], + capture_output=True, + env=os.environ, + text=True, + ) + + self.assertEqual( + result.returncode, + 0, + msg=f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}", + ) + + def test_standalone_lit_requires_explicit_tool_paths(self): + lit = shutil.which("lit") or str(Path(sys.executable).with_name("lit")) + self.assertTrue(Path(lit).is_file(), f"lit executable not found: {lit}") + + environment = os.environ.copy() + environment.pop("ACIR_TEST_EXEC_ROOT", None) + environment.pop("ACIR_TOOLS_DIR", None) + environment.pop("LLVM_TOOLS_DIR", None) + result = subprocess.run( + [lit, "-sv", str(self.repo_root / "test" / "ACIR" / "dialect-smoke.mlir")], + capture_output=True, + env=environment, + text=True, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("ACIR_TEST_EXEC_ROOT", result.stderr) + self.assertIn("ACIR_TOOLS_DIR", result.stderr) + self.assertIn("LLVM_TOOLS_DIR", result.stderr) + + def test_binding_unit_target_configures_with_portable_gtest_only(self): + cmake = shutil.which("cmake") + ninja = shutil.which("ninja") + self.assertIsNotNone(cmake) + self.assertIsNotNone(ninja) + + binding_source = (self.repo_root / "unittests" / "Bindings").as_posix() + support_targets = "\n".join( + f"add_library({target} INTERFACE)" + for target in ( + "ACIRBindings", + "ACIRDialect", + "ACIRTransforms", + "ACSimDialect", + "LLVMSupport", + "MLIRFuncDialect", + "MLIRIndexDialect", + "MLIRParser", + "MLIRSCFDialect", + ) + ) + + with tempfile.TemporaryDirectory() as temp_dir: + source_dir = Path(temp_dir) / "source" + build_dir = Path(temp_dir) / "build" + gtest_dir = source_dir / "gtest-only" + gtest_dir.mkdir(parents=True) + (gtest_dir / "GTestConfig.cmake").write_text( + "add_library(GTest::gtest_main INTERFACE IMPORTED)\n" + "set(GTest_FOUND TRUE)\n", + encoding="utf-8", + ) + (source_dir / "CMakeLists.txt").write_text( + "cmake_minimum_required(VERSION 3.20)\n" + "project(BindingUnitConfigure LANGUAGES CXX)\n" + "enable_testing()\n" + f"{support_targets}\n" + f'add_subdirectory("{binding_source}" bindings)\n', + encoding="utf-8", + ) + + result = subprocess.run( + [ + cmake, + "-S", + str(source_dir), + "-B", + str(build_dir), + "-G", + "Ninja", + f"-DCMAKE_MAKE_PROGRAM={ninja}", + f"-DGTest_DIR={gtest_dir}", + ], + capture_output=True, + text=True, + ) + + self.assertEqual( + result.returncode, + 0, + msg=f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/tools/__init__.py b/components/agentic-circuit/tests/tools/__init__.py new file mode 100644 index 00000000..7d3ea0cf --- /dev/null +++ b/components/agentic-circuit/tests/tools/__init__.py @@ -0,0 +1 @@ +"""Repository-local tool tests.""" diff --git a/components/agentic-circuit/tests/tools/fixtures/davincioo-fixture-provenance.md b/components/agentic-circuit/tests/tools/fixtures/davincioo-fixture-provenance.md new file mode 100644 index 00000000..b384f67d --- /dev/null +++ b/components/agentic-circuit/tests/tools/fixtures/davincioo-fixture-provenance.md @@ -0,0 +1,15 @@ +# DavinciOO trace fixture provenance + +`davincioo-valid.jsonl` contains the first two records of +`model/tests/fixtures/traces/examples_beginner_matmul.pto.trace` from DavinciOO +commit `e73633301cabed0d871ea5ff66e76a91df870aeb`. That checkout pins PTO-ISA +commit `f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3`. + +`davincioo-valid.pto-trace.json` is the deterministic output of: + +```text +python tools/import-davincioo-pto-trace.py \ + tests/tools/fixtures/davincioo-valid.jsonl \ + tests/tools/fixtures/davincioo-valid.pto-trace.json \ + --source-program examples/beginner_matmul +``` diff --git a/components/agentic-circuit/tests/tools/fixtures/davincioo-invalid.jsonl b/components/agentic-circuit/tests/tools/fixtures/davincioo-invalid.jsonl new file mode 100644 index 00000000..50180213 --- /dev/null +++ b/components/agentic-circuit/tests/tools/fixtures/davincioo-invalid.jsonl @@ -0,0 +1 @@ +{"block_idx":0,"sequence_id":0,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[],"output_tiles":[],"unexpected":true} diff --git a/components/agentic-circuit/tests/tools/fixtures/davincioo-valid.jsonl b/components/agentic-circuit/tests/tools/fixtures/davincioo-valid.jsonl new file mode 100644 index 00000000..d060f19a --- /dev/null +++ b/components/agentic-circuit/tests/tools/fixtures/davincioo-valid.jsonl @@ -0,0 +1,2 @@ +{"block_idx":0,"sequence_id":0,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[{"dtype":"uint64","value":"0"}],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"outer=col_major,inner=row_major,fractal_size=512","dtype":"float32"}]} +{"block_idx":0,"sequence_id":1,"opcode":"TLOAD","input_tiles":[{"address":"0xfffd835fd000","shape":[1,1,1,64,256],"layout":"ND","dtype":"float32"}],"scalar_inputs":[],"output_tiles":[{"address":"0x0","shape":[64,256],"layout":"outer=col_major,inner=row_major,fractal_size=512","dtype":"float32"}]} diff --git a/components/agentic-circuit/tests/tools/fixtures/davincioo-valid.pto-trace.json b/components/agentic-circuit/tests/tools/fixtures/davincioo-valid.pto-trace.json new file mode 100644 index 00000000..0cc918ce --- /dev/null +++ b/components/agentic-circuit/tests/tools/fixtures/davincioo-valid.pto-trace.json @@ -0,0 +1 @@ +{"contract_epoch":"0.3","metadata":{"content_hash":"sha256:4ad35c1b1106c2cae0b56e0c7233a019d85e3387653e6e886f3765e0a966f179","data_layout":"davincioo-tile-address-v1","producer":"davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb","pto_identity":"pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3","record_count":2,"source_program":"examples/beginner_matmul"},"records":[{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[],"operand_roles":["scalar_input","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[{"dtype":"uint64","value":"0"}]}},"dependencies":[],"opcode":"TASSIGN","operands":[{"kind":"immediate","type":"uint64","value":"0"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":0},{"attributes":{"davincioo":{"block_idx":0,"input_tiles":[{"address":"0xfffd835fd000","dtype":"float32","layout":"ND","shape":[1,1,1,64,256]}],"operand_roles":["input_tile","output_tile"],"output_tiles":[{"address":"0x0","dtype":"float32","layout":"outer=col_major,inner=row_major,fractal_size=512","shape":[64,256]}],"scalar_inputs":[]}},"dependencies":[],"opcode":"TLOAD","operands":[{"id":"block/0/tile/0xfffd835fd000","kind":"tile"},{"id":"block/0/tile/0x0","kind":"tile"}],"sequence_id":1}],"schema":"pto-trace","version":"0.1"} diff --git a/components/agentic-circuit/tests/tools/test_perfetto_trace.py b/components/agentic-circuit/tests/tools/test_perfetto_trace.py new file mode 100644 index 00000000..b7139504 --- /dev/null +++ b/components/agentic-circuit/tests/tools/test_perfetto_trace.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from agentic_circuit._canonical_json import canonical_json_bytes +from tools.perfetto_trace import PerfettoTraceError, pack_event_jsonl + + +ROOT = Path(__file__).resolve().parents[2] + + +def encoded(*events: dict[str, object]) -> bytes: + return b"".join( + json.dumps(event, separators=(",", ":")).encode("utf-8") + b"\n" + for event in events + ) + + +def event( + phase: object, + *, + time: int = 2, + delta: int = 3, + owner: int = 5, + local_index: int = 0, + **fields: object, +) -> dict[str, object]: + value: dict[str, object] = { + "name": "work", + "cat": "execution", + "ph": phase, + "ts": time * 1024 + delta, + "pid": 0, + "tid": owner, + "args": { + "gfsim_epoch_time": time, + "gfsim_epoch_delta": delta, + "gfsim_object_id": owner, + "gfsim_local_committed_index": local_index, + "gfsim_root_sequence_id": 17, + "engine": "cube", + }, + } + value.update(fields) + return value + + +class PerfettoTraceLibraryTest(unittest.TestCase): + def test_empty_input_is_one_canonical_trace_document(self) -> None: + self.assertEqual(b'{"traceEvents":[]}\n', pack_event_jsonl(b"")) + + def test_process_and_thread_metadata_are_preserved_exactly(self) -> None: + process = { + "name": "process_name", + "ph": "M", + "pid": 0, + "args": {"name": "gfsim"}, + } + thread = { + "name": "thread_name", + "ph": "M", + "pid": 0, + "tid": 5, + "args": {"name": "/generated/root/cube"}, + } + output = pack_event_jsonl(encoded(process, thread)) + self.assertEqual( + canonical_json_bytes({"traceEvents": [process, thread]}) + b"\n", + output, + ) + self.assertEqual([process, thread], json.loads(output)["traceEvents"]) + + def test_all_runtime_phases_preserve_input_order(self) -> None: + instant = event("i", s="t") + complete = event("X", local_index=1, dur=9) + counter = event( + "C", + local_index=2, + args={ + "gfsim_epoch_time": 2, + "gfsim_epoch_delta": 3, + "gfsim_object_id": 5, + "gfsim_local_committed_index": 2, + "occupancy": 4, + }, + ) + flow_start = event("s", local_index=3, id=17) + flow_end = event("f", local_index=4, id=17, bp="e") + events = [instant, complete, counter, flow_start, flow_end] + + output = pack_event_jsonl(encoded(*events)) + + self.assertEqual(events, json.loads(output)["traceEvents"]) + self.assertEqual( + canonical_json_bytes({"traceEvents": events}) + b"\n", output + ) + + def test_malformed_duplicate_and_unknown_fields_are_rejected(self) -> None: + duplicate = ( + b'{"name":"work","name":"again","cat":"execution","ph":"C",' + b'"ts":0,"pid":0,"tid":0,"args":{"gfsim_epoch_time":0,' + b'"gfsim_epoch_delta":0,"gfsim_object_id":0,' + b'"gfsim_local_committed_index":0}}\n' + ) + for source in ( + duplicate, + b"not-json\n", + b"\xff\n", + encoded(event("C", unknown=True)), + encoded(event("B")), + encoded(event(["C"])), + b"\n", + ): + with self.subTest(source=source): + with self.assertRaises(PerfettoTraceError): + pack_event_jsonl(source) + + def test_phase_specific_fields_and_runtime_identity_are_exact(self) -> None: + cases = ( + event("i"), + event("i", s="g"), + event("X"), + event("X", dur=-1), + event("C", s="t"), + event("s"), + event("f", id=1), + event("f", id=1, bp="x"), + event("C", pid=1), + event("C", pid=False), + event("C", tid=6), + event("C", ts=0), + ) + for value in cases: + with self.subTest(value=value): + with self.assertRaisesRegex( + PerfettoTraceError, "ACPERFETTO-SCHEMA" + ): + pack_event_jsonl(encoded(value)) + + def test_committed_keys_must_be_strictly_increasing(self) -> None: + later = event("C", time=3, owner=1, local_index=0) + earlier = event("C", time=2, owner=9, local_index=0) + duplicate = event("C", time=3, owner=1, local_index=0) + for source in (encoded(later, earlier), encoded(later, duplicate)): + with self.subTest(source=source): + with self.assertRaisesRegex( + PerfettoTraceError, "ACPERFETTO-ORDER" + ): + pack_event_jsonl(source) + + def test_repeated_packing_is_byte_identical(self) -> None: + source = encoded(event("i", s="t"), event("X", local_index=1, dur=4)) + self.assertEqual(pack_event_jsonl(source), pack_event_jsonl(source)) + + +class PerfettoTraceCommandTest(unittest.TestCase): + command = ROOT / "tools" / "pack-perfetto-trace.py" + + def run_command( + self, *arguments: object, cwd: Path | None = None + ) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( + [ + sys.executable, + os.fspath(self.command), + *(os.fspath(arg) for arg in arguments), + ], + cwd=cwd or ROOT, + env={**os.environ, "PYTHONPATH": os.fspath(ROOT / "src")}, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + def test_help_and_exact_argument_surface(self) -> None: + result = self.run_command("--help") + self.assertEqual(0, result.returncode) + self.assertIn(b"INPUT OUTPUT", result.stdout) + self.assertEqual(b"", result.stderr) + self.assertEqual(2, self.run_command().returncode) + + def test_command_atomically_publishes_library_bytes(self) -> None: + source = encoded(event("C")) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + input_path = root / "events.jsonl" + output_path = root / "perfetto.json" + input_path.write_bytes(source) + + result = self.run_command(input_path, output_path, cwd=root) + + self.assertEqual(0, result.returncode, result.stderr.decode()) + self.assertEqual(b"", result.stdout) + self.assertEqual(b"", result.stderr) + self.assertEqual(pack_event_jsonl(source), output_path.read_bytes()) + self.assertEqual([], list(root.glob(".perfetto.json.*.tmp"))) + + def test_failure_preserves_existing_output_and_cleans_stage(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + input_path = root / "events.jsonl" + output_path = root / "perfetto.json" + input_path.write_bytes(b"not-json\n") + output_path.write_bytes(b"prior\n") + + result = self.run_command(input_path, output_path, cwd=root) + + self.assertEqual(2, result.returncode) + self.assertEqual(b"", result.stdout) + self.assertIn(b"ACPERFETTO-JSON", result.stderr) + self.assertEqual(b"prior\n", output_path.read_bytes()) + self.assertEqual([], list(root.glob(".perfetto.json.*.tmp"))) + + def test_alias_and_missing_parent_are_rejected(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + input_path = root / "events.jsonl" + input_path.write_bytes(encoded(event("C"))) + alias = self.run_command(input_path, input_path, cwd=root) + self.assertEqual(2, alias.returncode) + self.assertIn(b"ACPERFETTO-IO", alias.stderr) + self.assertEqual(encoded(event("C")), input_path.read_bytes()) + + missing = self.run_command(input_path, root / "missing" / "out.json") + self.assertEqual(2, missing.returncode) + self.assertIn(b"ACPERFETTO-IO", missing.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/tools/test_pto_trace_adapter.py b/components/agentic-circuit/tests/tools/test_pto_trace_adapter.py new file mode 100644 index 00000000..6bb6fe87 --- /dev/null +++ b/components/agentic-circuit/tests/tools/test_pto_trace_adapter.py @@ -0,0 +1,323 @@ +from __future__ import annotations + +import hashlib +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from agentic_circuit._canonical_json import canonical_json_bytes +from tools.pto_trace_adapter import ( + AdapterError, + AdapterLimits, + convert_davincioo_trace, + parse_davincioo_jsonl, +) + + +ROOT = Path(__file__).resolve().parents[2] +FIXTURES = Path(__file__).with_name("fixtures") +PRODUCER = "davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb" +PTO = "pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3" + + +def row(**overrides: object) -> dict[str, object]: + value: dict[str, object] = { + "block_idx": 3, + "sequence_id": 7, + "opcode": "TADDS", + "input_tiles": [ + {"address": "0x20", "shape": [2, 4], "layout": "ND", "dtype": "f32"} + ], + "scalar_inputs": [{"dtype": "f32", "value": "1.25"}], + "output_tiles": [ + {"address": "0x40", "shape": [2, 4], "layout": "ND", "dtype": "f32"} + ], + } + value.update(overrides) + return value + + +def jsonl(*rows: dict[str, object]) -> bytes: + return b"".join( + json.dumps(item, separators=(",", ":")).encode("utf-8") + b"\n" + for item in rows + ) + + +class DavinciOOAdapterTest(unittest.TestCase): + def test_pinned_fixture_maps_losslessly_to_canonical_trace(self) -> None: + source = (FIXTURES / "davincioo-valid.jsonl").read_bytes() + encoded = convert_davincioo_trace(source, source_program="examples/beginner_matmul") + document = json.loads(encoded) + + self.assertEqual(encoded, canonical_json_bytes(document) + b"\n") + self.assertEqual( + {"schema", "version", "contract_epoch", "metadata", "records"}, + set(document), + ) + self.assertEqual("pto-trace", document["schema"]) + self.assertEqual("0.1", document["version"]) + self.assertEqual("0.3", document["contract_epoch"]) + self.assertEqual( + { + "producer": PRODUCER, + "pto_identity": PTO, + "source_program": "examples/beginner_matmul", + "data_layout": "davincioo-tile-address-v1", + "record_count": 2, + "content_hash": "sha256:" + + hashlib.sha256(canonical_json_bytes(document["records"])).hexdigest(), + }, + document["metadata"], + ) + + first = document["records"][0] + self.assertEqual(0, first["sequence_id"]) + self.assertEqual("TASSIGN", first["opcode"]) + self.assertEqual([], first["dependencies"]) + self.assertEqual( + [ + {"kind": "immediate", "type": "uint64", "value": "0"}, + {"kind": "tile", "id": "block/0/tile/0x0"}, + ], + first["operands"], + ) + retained = first["attributes"]["davincioo"] + self.assertEqual(0, retained["block_idx"]) + self.assertEqual(["scalar_input", "output_tile"], retained["operand_roles"]) + self.assertEqual( + row( + block_idx=0, + sequence_id=0, + opcode="TASSIGN", + input_tiles=[], + scalar_inputs=[{"dtype": "uint64", "value": "0"}], + output_tiles=[ + { + "address": "0x0", + "shape": [64, 256], + "layout": "outer=col_major,inner=row_major,fractal_size=512", + "dtype": "float32", + } + ], + ), + { + "block_idx": retained["block_idx"], + "sequence_id": first["sequence_id"], + "opcode": first["opcode"], + "input_tiles": retained["input_tiles"], + "scalar_inputs": retained["scalar_inputs"], + "output_tiles": retained["output_tiles"], + }, + ) + + def test_default_source_identity_is_raw_input_hash(self) -> None: + source = jsonl(row()) + document = json.loads(convert_davincioo_trace(source)) + self.assertEqual( + "sha256:" + hashlib.sha256(source).hexdigest(), + document["metadata"]["source_program"], + ) + + def test_output_is_independent_of_repeated_conversion(self) -> None: + source = jsonl(row()) + self.assertEqual( + convert_davincioo_trace(source, source_program="kernel.pto"), + convert_davincioo_trace(source, source_program="kernel.pto"), + ) + + def test_blank_lines_are_ignored_but_empty_trace_is_valid(self) -> None: + self.assertEqual((), parse_davincioo_jsonl(b"\n \n")) + document = json.loads(convert_davincioo_trace(b"\n")) + self.assertEqual([], document["records"]) + + def test_duplicate_key_and_unknown_field_are_rejected(self) -> None: + duplicate = ( + b'{"block_idx":0,"block_idx":1,"sequence_id":0,"opcode":"T",' + b'"input_tiles":[],"scalar_inputs":[],"output_tiles":[]}\n' + ) + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-JSON"): + parse_davincioo_jsonl(duplicate) + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-SCHEMA"): + parse_davincioo_jsonl((FIXTURES / "davincioo-invalid.jsonl").read_bytes()) + + def test_invalid_utf8_and_trailing_json_are_rejected(self) -> None: + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-JSON"): + parse_davincioo_jsonl(b"\xff\n") + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-JSON"): + parse_davincioo_jsonl(jsonl(row()).rstrip() + b" trailing\n") + + def test_sequence_ids_are_unique_strictly_increasing_safe_integers(self) -> None: + for rows in ( + (row(sequence_id=7), row(sequence_id=7)), + (row(sequence_id=8), row(sequence_id=7)), + (row(sequence_id=(1 << 53)),), + (row(sequence_id=-1),), + (row(sequence_id=True),), + ): + with self.subTest(rows=rows): + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-SEQUENCE"): + convert_davincioo_trace(jsonl(*rows)) + + def test_tile_and_scalar_shapes_are_closed_and_canonical(self) -> None: + invalid_tiles = ( + {"address": "0X20", "shape": [2], "layout": "ND", "dtype": "f32"}, + {"address": "0x020", "shape": [2], "layout": "ND", "dtype": "f32"}, + {"address": "0x20", "shape": [0], "layout": "ND", "dtype": "f32"}, + {"address": "0x20", "shape": [True], "layout": "ND", "dtype": "f32"}, + {"address": "0x20", "shape": [2], "layout": "", "dtype": "f32"}, + {"address": "0x20", "shape": [2], "layout": "ND", "dtype": "f32", "x": 1}, + ) + for tile in invalid_tiles: + with self.subTest(tile=tile): + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-SCHEMA"): + convert_davincioo_trace(jsonl(row(input_tiles=[tile]))) + for scalar in ( + {"dtype": "", "value": "1"}, + {"dtype": "u32", "value": 1}, + {"dtype": "u32", "value": "1", "x": 1}, + ): + with self.subTest(scalar=scalar): + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-SCHEMA"): + convert_davincioo_trace(jsonl(row(scalar_inputs=[scalar]))) + + def test_limits_fail_before_partial_conversion(self) -> None: + source = jsonl(row()) + cases = ( + AdapterLimits(max_document_bytes=len(source) - 1), + AdapterLimits(max_line_bytes=8), + AdapterLimits(max_record_count=0), + AdapterLimits(max_tiles_per_record=0), + AdapterLimits(max_scalars_per_record=0), + AdapterLimits(max_shape_rank=1), + AdapterLimits(max_string_bytes=2), + ) + for limits in cases: + with self.subTest(limits=limits): + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-LIMIT"): + convert_davincioo_trace(source, limits=limits) + + def test_source_program_identity_is_nonempty_and_path_independent(self) -> None: + source = jsonl(row()) + with self.assertRaisesRegex(AdapterError, "ACTRACE-ADAPTER-SCHEMA"): + convert_davincioo_trace(source, source_program="") + left = convert_davincioo_trace(source, source_program="suite/kernel") + right = convert_davincioo_trace(source, source_program="suite/kernel") + self.assertEqual(left, right) + + +class DavinciOOAdapterCommandTest(unittest.TestCase): + command = ROOT / "tools" / "import-davincioo-pto-trace.py" + + def run_command( + self, *arguments: object, cwd: Path | None = None + ) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( + [sys.executable, os.fspath(self.command), *(os.fspath(arg) for arg in arguments)], + cwd=cwd or ROOT, + env={**os.environ, "PYTHONPATH": os.fspath(ROOT / "src")}, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + def test_help_and_exact_argument_surface(self) -> None: + help_result = self.run_command("--help") + self.assertEqual(0, help_result.returncode) + self.assertIn(b"INPUT OUTPUT", help_result.stdout) + self.assertEqual(b"", help_result.stderr) + + missing = self.run_command() + self.assertEqual(2, missing.returncode) + self.assertEqual(b"", missing.stdout) + + def test_command_atomically_publishes_exact_library_bytes(self) -> None: + source = (FIXTURES / "davincioo-valid.jsonl").read_bytes() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + input_path = root / "input.pto.trace" + output_path = root / "canonical.json" + input_path.write_bytes(source) + + result = self.run_command( + input_path, + output_path, + "--source-program", + "examples/beginner_matmul", + cwd=root, + ) + + self.assertEqual(0, result.returncode, result.stderr.decode()) + self.assertEqual(b"", result.stdout) + self.assertEqual(b"", result.stderr) + self.assertEqual( + convert_davincioo_trace( + source, source_program="examples/beginner_matmul" + ), + output_path.read_bytes(), + ) + self.assertEqual([], list(root.glob(".canonical.json.*.tmp"))) + + def test_failure_preserves_existing_output_and_cleans_stage(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + input_path = root / "invalid.pto.trace" + output_path = root / "canonical.json" + input_path.write_bytes((FIXTURES / "davincioo-invalid.jsonl").read_bytes()) + output_path.write_bytes(b"prior\n") + + result = self.run_command(input_path, output_path, cwd=root) + + self.assertEqual(2, result.returncode) + self.assertEqual(b"", result.stdout) + self.assertIn(b"ACTRACE-ADAPTER-SCHEMA", result.stderr) + self.assertEqual(b"prior\n", output_path.read_bytes()) + self.assertEqual([], list(root.glob(".canonical.json.*.tmp"))) + + def test_command_rejects_aliasing_input_and_missing_parent(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + input_path = root / "input.pto.trace" + input_path.write_bytes(jsonl(row())) + alias = self.run_command(input_path, input_path, cwd=root) + self.assertEqual(2, alias.returncode) + self.assertIn(b"ACTRACE-ADAPTER-IO", alias.stderr) + self.assertEqual(jsonl(row()), input_path.read_bytes()) + + missing = self.run_command(input_path, root / "missing" / "out.json", cwd=root) + self.assertEqual(2, missing.returncode) + self.assertIn(b"ACTRACE-ADAPTER-IO", missing.stderr) + + def test_output_bytes_do_not_depend_on_root_or_umask(self) -> None: + source = (FIXTURES / "davincioo-valid.jsonl").read_bytes() + outputs: list[bytes] = [] + for mask in (0o022, 0o077): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + input_path = root / "nested" / "input.pto.trace" + output_path = root / "out.json" + input_path.parent.mkdir() + input_path.write_bytes(source) + previous = os.umask(mask) + try: + result = self.run_command( + input_path, + output_path, + "--source-program", + "fixture/stable", + cwd=root, + ) + finally: + os.umask(previous) + self.assertEqual(0, result.returncode, result.stderr.decode()) + outputs.append(output_path.read_bytes()) + self.assertEqual(outputs[0], outputs[1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/tests/tools/test_pyc_verilog_backend.py b/components/agentic-circuit/tests/tools/test_pyc_verilog_backend.py new file mode 100644 index 00000000..06b71f53 --- /dev/null +++ b/components/agentic-circuit/tests/tools/test_pyc_verilog_backend.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import shutil +import subprocess +import tempfile +import sys +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +TOOL = ROOT / "tools/acir-queue-veriloggen.py" + + +def load_tool(): + spec = importlib.util.spec_from_file_location("acir_queue_veriloggen", TOOL) + if spec is None or spec.loader is None: + raise RuntimeError("cannot load PYC Verilog bridge") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +PYC_ASSERT = """ +func.func @guard(%cond: i1) -> (i1) attributes {result_names = ["out"]} { + pyc.assert %cond {msg = "guard_failed"} + func.return %cond : i1 +} +""" + +PYC_COMPARE = """ +func.func @compare(%left: i8, %right: i8) -> (i1) attributes {result_names = ["equal"]} { + %same = pyc.eq %left, %right : i8, i8 -> i1 + func.return %same : i1 +} +""" + + +class PycVerilogBackendTest(unittest.TestCase): + def test_assertion_is_preserved_in_simulation_only_rtl(self) -> None: + tool = load_tool() + module = tool.parse_pyc_module(PYC_ASSERT) + verilog = tool.emit_verilog(module, ROOT / "resources/pyc_runtime/verilog") + self.assertIn("// synthesis translate_off", verilog) + self.assertIn("if (!cond)", verilog) + self.assertIn('$display("guard_failed")', verilog) + self.assertNotIn("assertion omitted", verilog) + verilator = shutil.which("verilator") + if verilator is not None: + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "guard.v" + source.write_text(verilog, encoding="utf-8") + linted = subprocess.run( + ( + verilator, + "--lint-only", + "--timing", + "--top-module", + "guard", + str(source), + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, linted.returncode, linted.stderr) + + def test_result_names_are_unique_safe_verilog_identifiers(self) -> None: + tool = load_tool() + duplicate = PYC_ASSERT.replace("-> (i1)", "-> (i1, i1)").replace( + '["out"]', '["out", "out"]' + ) + with self.assertRaisesRegex(tool.PYCVerilogError, "identifier list"): + tool.parse_pyc_module(duplicate) + injected = PYC_ASSERT.replace('["out"]', '["out; wire injected"]') + with self.assertRaisesRegex(tool.PYCVerilogError, "identifier list"): + tool.parse_pyc_module(injected) + + def test_comparison_annotation_describes_operands_and_result_is_i1(self) -> None: + tool = load_tool() + verilog = tool.emit_verilog( + tool.parse_pyc_module(PYC_COMPARE), + ROOT / "resources/pyc_runtime/verilog", + ) + self.assertIn("wire same;", verilog) + self.assertNotIn("wire [7:0] same;", verilog) + self.assertIn("assign same = left == right;", verilog) + + def test_cli_rejects_invalid_timeout_and_path_aliases(self) -> None: + tool = load_tool() + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "model.pyc" + source.write_text(PYC_ASSERT, encoding="utf-8") + with self.assertRaises(SystemExit) as timeout: + tool.main([str(source), "--pyc-input", "--timeout", "nan"]) + self.assertEqual(2, timeout.exception.code) + original = source.read_bytes() + with self.assertRaises(SystemExit) as alias: + tool.main([str(source), "--pyc-input", "-o", str(source)]) + self.assertEqual(2, alias.exception.code) + self.assertEqual(original, source.read_bytes()) + + def test_runtime_primitives_pass_bounded_verilator_smoke(self) -> None: + verilator = shutil.which("verilator") + if verilator is None: + self.skipTest("Verilator is unavailable") + runtime = ROOT / "resources/pyc_runtime/verilog" + with tempfile.TemporaryDirectory() as directory: + object_dir = Path(directory) / "obj" + completed = subprocess.run( + ( + verilator, + "--binary", + "--timing", + "--top-module", + "pyc_primitives_smoke", + "-Mdir", + str(object_dir), + str(ROOT / "test/CodeGen/pyc-primitives-smoke.sv"), + str(runtime / "pyc_fifo.v"), + str(runtime / "pyc_reg.v"), + str(runtime / "pyc_popcount.v"), + str(runtime / "pyc_rr_arbiter.v"), + "-o", + "pyc-primitives-smoke", + ), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, completed.returncode, completed.stderr) + executed = subprocess.run( + (str(object_dir / "pyc-primitives-smoke"),), + text=True, + capture_output=True, + check=False, + ) + self.assertEqual(0, executed.returncode, executed.stderr) + self.assertIn("PYC_PRIMITIVES_SMOKE PASS", executed.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/components/agentic-circuit/toolchains/llvm.lock.json b/components/agentic-circuit/toolchains/llvm.lock.json new file mode 100644 index 00000000..d5410c22 --- /dev/null +++ b/components/agentic-circuit/toolchains/llvm.lock.json @@ -0,0 +1,15 @@ +{ + "lock_version": 1, + "llvm": { + "release": "22.1.8", + "upstream_commit": "ca7933e47d3a3451d81e72ac174dcb5aa28b59d1", + "source_url": "https://github.com/llvm/llvm-project/releases/download/llvmorg-22.1.8/llvm-project-22.1.8.src.tar.xz", + "source_sha256": "922f1817a0df7b1489272d18134ee0087a8b068828f87ac63b9861b1a9965888", + "local_prefix": "/opt/homebrew/opt/llvm", + "supported_host_triples": [ + "arm64-apple-darwin", + "x86_64-linux-gnu" + ], + "package_version_policy": "exact" + } +} diff --git a/components/agentic-circuit/toolchains/pyc.lock.json b/components/agentic-circuit/toolchains/pyc.lock.json new file mode 100644 index 00000000..be4f25f5 --- /dev/null +++ b/components/agentic-circuit/toolchains/pyc.lock.json @@ -0,0 +1,9 @@ +{ + "frontend_contract": "pycircuit", + "llvm_version": "22.1.8", + "pyc_interface": "pyc6", + "pycircuit_commit": "repo-local", + "repository": "https://github.com/PTO-ISA/pyCircuit", + "schema": "agentic-circuit-pyc-toolchain-lock", + "version": "0.3" +} diff --git a/components/agentic-circuit/tools/CMakeLists.txt b/components/agentic-circuit/tools/CMakeLists.txt new file mode 100644 index 00000000..63bbbe7f --- /dev/null +++ b/components/agentic-circuit/tools/CMakeLists.txt @@ -0,0 +1,47 @@ +# Match a no-RTTI LLVM build: acir-opt subclasses MLIR passes and LLVM +# command-line options, whose vtables must not reference typeinfo symbols +# that no-RTTI LLVM libraries do not export. +if(NOT LLVM_ENABLE_RTTI) + add_compile_options($<$:-fno-rtti>) +endif() + +add_subdirectory(acir-cxxgen) +add_subdirectory(acir-opcode-catalog) +add_subdirectory(acir-queue-cxxgen) +add_subdirectory(acir-queue-plan) +add_subdirectory(acir-queue-pycgen) +add_subdirectory(acir-opt) + +install(TARGETS + acir-cxxgen + acir-opcode-catalog + acir-opt + acir-queue-cxxgen + acir-queue-plan + acir-queue-pycgen + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +# The integrated ACIR-to-PYC lane always consumes the pyc6 targets built from +# this checkout. Keeping the dependency explicit prevents a stale external +# pyCircuit install from becoming an accidental build input. +if(TARGET pycc AND TARGET pyc6_runtime) + add_custom_target(acir-pyc-toolchain + DEPENDS acir-queue-pycgen pycc pyc6_runtime + ) +elseif(NOT PROJECT_IS_TOP_LEVEL) + message(FATAL_ERROR + "Agentic Circuit integration requires repo-local pycc and pyc6_runtime targets") +else() + message(STATUS + "Standalone Agentic Circuit build: ACIR-to-PYC toolchain target is disabled") +endif() + +# The PYC-compatible Verilog bridge is intentionally a Python entrypoint: it +# invokes the in-tree ACIR→PYC generator and lowers the canonical textual PYC +# contract to a self-contained RTL artifact. +install(PROGRAMS + "${CMAKE_CURRENT_SOURCE_DIR}/ac-queue-pyc-build.py" + "${CMAKE_CURRENT_SOURCE_DIR}/acir-queue-veriloggen.py" + DESTINATION bin +) diff --git a/components/agentic-circuit/tools/ac-queue-cxxgen.py b/components/agentic-circuit/tools/ac-queue-cxxgen.py new file mode 100755 index 00000000..beb683ad --- /dev/null +++ b/components/agentic-circuit/tools/ac-queue-cxxgen.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Generate one canonical typed gfsim C++ file from serial Queue Python.""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +import subprocess +import tempfile + +from agentic_circuit._queue_codegen import lower_queue_program_to_cpp +from agentic_circuit._queue_frontend import lower_queue_program, parse_queue_program + + +def _write_atomic(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary = tempfile.mkstemp( + dir=path.parent, prefix=f".{path.name}.", text=True + ) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +def main() -> int: + parser = argparse.ArgumentParser(allow_abbrev=False) + parser.add_argument("source", type=Path) + parser.add_argument("--system", required=True) + parser.add_argument("-o", "--output", required=True, type=Path) + parser.add_argument("--acir-output", type=Path) + parser.add_argument("--plan-output", type=Path) + parser.add_argument("--acir-opt", type=Path) + parser.add_argument("--queue-plan-tool", type=Path) + parser.add_argument("--queue-cxxgen-tool", type=Path) + arguments = parser.parse_args() + artifact_options = ( + arguments.acir_output, + arguments.plan_output, + arguments.acir_opt, + arguments.queue_plan_tool, + arguments.queue_cxxgen_tool, + ) + if any(value is not None for value in artifact_options) and any( + value is None for value in artifact_options + ): + parser.error( + "--acir-output, --plan-output, --acir-opt, --queue-plan-tool, and " + "--queue-cxxgen-tool must be provided together" + ) + program = parse_queue_program( + arguments.source.read_text(encoding="utf-8"), arguments.system + ) + generated = lower_queue_program_to_cpp(program) + canonical_acir: str | None = None + queue_plan: str | None = None + if arguments.acir_output is not None: + assert arguments.acir_opt is not None + assert arguments.queue_plan_tool is not None + assert arguments.queue_cxxgen_tool is not None + with tempfile.TemporaryDirectory() as directory: + raw = Path(directory) / "model.ac.mlir" + raw.write_text(lower_queue_program(program), encoding="utf-8") + optimized = subprocess.run( + ( + str(arguments.acir_opt), + "--canonicalize", + "--cse", + str(raw), + ), + text=True, + capture_output=True, + check=False, + ) + if optimized.returncode != 0: + parser.error(f"acir-opt rejected generated ACIR: {optimized.stderr}") + canonical_acir = optimized.stdout + frozen = Path(directory) / "model.frozen.ac.mlir" + frozen.write_text(canonical_acir, encoding="utf-8") + planned = subprocess.run( + (str(arguments.queue_plan_tool), str(frozen)), + text=True, + capture_output=True, + check=False, + ) + if planned.returncode != 0: + parser.error(f"QueueGraph planning failed: {planned.stderr}") + queue_plan = planned.stdout + emitted = subprocess.run( + (str(arguments.queue_cxxgen_tool), str(frozen)), + text=True, + capture_output=True, + check=False, + ) + if emitted.returncode != 0: + parser.error(f"native Queue C++ generation failed: {emitted.stderr}") + generated = emitted.stdout + _write_atomic(arguments.output, generated) + if canonical_acir is not None and queue_plan is not None: + assert arguments.acir_output is not None + assert arguments.plan_output is not None + _write_atomic(arguments.acir_output, canonical_acir) + _write_atomic(arguments.plan_output, queue_plan) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/tools/ac-queue-pyc-build.py b/components/agentic-circuit/tools/ac-queue-pyc-build.py new file mode 100755 index 00000000..c1789e0a --- /dev/null +++ b/components/agentic-circuit/tools/ac-queue-pyc-build.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +"""Build canonical PYC, C++, and Verilog artifacts from frozen Queue ACIR.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile + + +def _sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _read_json(path: Path) -> dict[str, object]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"JSON document must be an object: {path}") + return value + + +def _canonical_json(value: object) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n" + + +def _opcode_inventory(acir: str, catalog: dict[str, object]) -> list[str]: + entries = catalog.get("entries") + if not isinstance(entries, list): + raise ValueError("official opcode catalog entries are invalid") + official = { + entry["operation"] + for entry in entries + if isinstance(entry, dict) and isinstance(entry.get("operation"), str) + } + mentioned = set(re.findall(r"\bac\.[a-z][a-z0-9_.]*\b", acir)) + return sorted(official & mentioned) + + +def _write_atomic(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary = tempfile.mkstemp( + dir=path.parent, prefix=f".{path.name}.", text=True + ) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +def _run(command: tuple[str, ...], *, stage: str) -> subprocess.CompletedProcess[str]: + completed = subprocess.run( + command, + text=True, + capture_output=True, + check=False, + ) + if completed.returncode != 0: + raise RuntimeError( + f"{stage} failed with exit {completed.returncode}:\n{completed.stderr}" + ) + return completed + + +def _normalize_cpp_manifest(path: Path) -> None: + manifest = _read_json(path) + manifest["include_dirs"] = ["."] + profile = manifest.get("profile_summary") + if isinstance(profile, dict): + profile.pop("pass_time_ms", None) + profile.pop("pycc_peak_rss_bytes", None) + runtime = manifest.get("runtime") + if isinstance(runtime, dict): + runtime["cmake_config_dir"] = "${PYC_TOOLCHAIN_ROOT}/share/pycircuit/cmake" + runtime["include_dirs"] = ["${PYC_TOOLCHAIN_ROOT}/include"] + runtime["lib_dirs"] = ["${PYC_TOOLCHAIN_ROOT}/lib"] + runtime["library_files"] = ["${PYC_TOOLCHAIN_ROOT}/lib/libpyc6_runtime.a"] + runtime["toolchain_root_hint"] = "${PYC_TOOLCHAIN_ROOT}" + path.write_text( + json.dumps(manifest, sort_keys=True, indent=2) + "\n", encoding="utf-8" + ) + + +def _artifacts(root: Path, backend: str) -> list[dict[str, object]]: + result: list[dict[str, object]] = [] + for path in sorted( + candidate for candidate in root.rglob("*") if candidate.is_file() + ): + data = path.read_bytes() + result.append( + { + "backend": backend, + "path": path.relative_to(root).as_posix(), + "sha256": _sha256(data), + "size": len(data), + } + ) + return result + + +def main() -> int: + parser = argparse.ArgumentParser(allow_abbrev=False) + parser.add_argument("frozen_acir", type=Path) + parser.add_argument("--pycgen-tool", required=True, type=Path) + parser.add_argument("--pycc", required=True, type=Path) + parser.add_argument("--toolchain-lock", required=True, type=Path) + parser.add_argument("--toolchain-metadata", required=True, type=Path) + parser.add_argument("--cxx", required=True, type=Path) + parser.add_argument("--verilator", required=True, type=Path) + parser.add_argument("--pyc-output", required=True, type=Path) + parser.add_argument("--cpp-output-dir", required=True, type=Path) + parser.add_argument("--verilog-output-dir", required=True, type=Path) + parser.add_argument("--manifest", required=True, type=Path) + arguments = parser.parse_args() + + for target in ( + arguments.pyc_output, + arguments.cpp_output_dir, + arguments.verilog_output_dir, + arguments.manifest, + ): + if target.exists(): + parser.error(f"output already exists: {target}") + + lock = _read_json(arguments.toolchain_lock) + metadata = _read_json(arguments.toolchain_metadata) + catalog_path = Path(__file__).resolve().parents[1] / "schemas/opcodes.json" + catalog = _read_json(catalog_path) + locked_commit = lock.get("pycircuit_commit") + if locked_commit != "repo-local" and metadata.get("git_sha") != locked_commit: + parser.error("pyCircuit commit does not match toolchain lock") + if locked_commit == "repo-local" and not metadata.get("git_sha"): + parser.error("repo-local pyCircuit metadata has no git revision") + if metadata.get("llvm_version") != lock.get("llvm_version"): + parser.error("pycc LLVM version does not match toolchain lock") + + with tempfile.TemporaryDirectory() as directory: + temporary = Path(directory) + pyc = temporary / "model.pyc" + cpp = temporary / "cpp" + verilog = temporary / "verilog" + emitted = _run( + (str(arguments.pycgen_tool), str(arguments.frozen_acir)), + stage="ACIR-to-PYC", + ) + pyc.write_text(emitted.stdout, encoding="utf-8") + _run( + ( + str(arguments.pycc), + str(pyc), + "--emit=cpp", + f"--out-dir={cpp}", + "--cpp-split=module", + "--cpp-shard-max-ast-nodes=512", + "--cpp-shard-threshold-lines=1000", + "--cpp-shard-threshold-bytes=65536", + "--hierarchy-policy=strict", + "--inline-policy=off", + "--build-profile=dev-fast", + ), + stage="pycc C++", + ) + _run( + ( + str(arguments.pycc), + str(pyc), + "--emit=verilog", + f"--out-dir={verilog}", + "--hierarchy-policy=strict", + "--inline-policy=off", + "--build-profile=dev-fast", + "--include-primitives", + ), + stage="pycc Verilog", + ) + cpp_manifest = _read_json(cpp / "cpp_compile_manifest.json") + sources = cpp_manifest.get("sources") + if not isinstance(sources, list) or not sources: + raise RuntimeError("pycc C++ manifest contains no sources") + source_paths = [ + cpp / str(source["path"]) for source in sources if isinstance(source, dict) + ] + include_root = arguments.toolchain_metadata.parents[2] / "include" + _run( + ( + str(arguments.cxx), + "-std=c++17", + "-I", + str(cpp), + "-I", + str(include_root), + "-fsyntax-only", + *(str(path) for path in source_paths), + ), + stage="generated PYC C++ syntax", + ) + + verilog_manifest = _read_json(verilog / "manifest.json") + top = str(verilog_manifest.get("top", "")) + modules = verilog_manifest.get("verilog_modules") + if not top or not isinstance(modules, list) or not modules: + raise RuntimeError("pycc Verilog manifest is incomplete") + _run( + ( + str(arguments.verilator), + "--lint-only", + "--timing", + "-Wall", + "-Wno-fatal", + "--top-module", + top, + *(str(verilog / str(module)) for module in modules), + ), + stage="Verilator lint", + ) + + # Compile with pycc's real toolchain paths, then remove host-specific + # paths only from the manifest that is published and fingerprinted. + _normalize_cpp_manifest(cpp / "cpp_compile_manifest.json") + + pyc_bytes = pyc.read_bytes() + frozen_acir_bytes = arguments.frozen_acir.read_bytes() + manifest = { + "artifacts": [ + *_artifacts(cpp, "cpp"), + *_artifacts(verilog, "verilog"), + ], + "contract_epoch": "0.3", + "build_profile": "dev-fast", + "frozen_acir_sha256": _sha256(frozen_acir_bytes), + "gates": ["pycc-cpp", "pycc-verilog", "cxx-syntax", "verilator-lint"], + "hierarchy_policy": "strict", + "inline_policy": "off", + "llvm_version": metadata["llvm_version"], + "opcode_catalog_sha256": _sha256(catalog_path.read_bytes()), + "opcode_lowering_inventory": _opcode_inventory( + frozen_acir_bytes.decode("utf-8"), catalog + ), + "pyc_interface": lock["pyc_interface"], + "pyc_ir_sha256": _sha256(pyc_bytes), + "pycc_sha256": _sha256(arguments.pycc.read_bytes()), + "pycircuit_commit": metadata["git_sha"], + "schema": "agentic-circuit-pyc-build-manifest", + "targets": ["cpp", "verilog"], + "version": "0.3", + } + + arguments.cpp_output_dir.parent.mkdir(parents=True, exist_ok=True) + arguments.verilog_output_dir.parent.mkdir(parents=True, exist_ok=True) + os.replace(cpp, arguments.cpp_output_dir) + os.replace(verilog, arguments.verilog_output_dir) + _write_atomic(arguments.pyc_output, pyc.read_text(encoding="utf-8")) + _write_atomic(arguments.manifest, _canonical_json(manifest)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/tools/acir-cxxgen/CMakeLists.txt b/components/agentic-circuit/tools/acir-cxxgen/CMakeLists.txt new file mode 100644 index 00000000..5c785f36 --- /dev/null +++ b/components/agentic-circuit/tools/acir-cxxgen/CMakeLists.txt @@ -0,0 +1,10 @@ +add_executable(acir-cxxgen acir-cxxgen.cpp) +target_include_directories(acir-cxxgen PRIVATE + ${PROJECT_SOURCE_DIR}/lib/CodeGen +) +target_link_libraries(acir-cxxgen PRIVATE + AgenticCircuit::ProjectOptions + ACIRCodeGen + ACSimDialect + MLIRParser +) diff --git a/components/agentic-circuit/tools/acir-cxxgen/acir-cxxgen.cpp b/components/agentic-circuit/tools/acir-cxxgen/acir-cxxgen.cpp new file mode 100644 index 00000000..e03b1917 --- /dev/null +++ b/components/agentic-circuit/tools/acir-cxxgen/acir-cxxgen.cpp @@ -0,0 +1,294 @@ +#include "BuildInternal.h" + +#include "acir/CodeGen/Build.h" +#include "acir/CodeGen/Generator.h" +#include "acir/CodeGen/ModelPlan.h" +#include "acir/Dialect/ACSim/ACSimDialect.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Index/IR/IndexDialect.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Program.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +llvm::cl::opt inputFile(llvm::cl::Positional, llvm::cl::Required, + llvm::cl::desc("")); +llvm::cl::opt stopAfter("stop-after", llvm::cl::Required); +llvm::cl::opt outputRoot("output-root"); +llvm::cl::opt frozenAcir("frozen-acir"); +llvm::cl::opt bindingLock("binding-lock"); +llvm::cl::opt projectName("project-name"); +llvm::cl::opt projectIdentity("project-identity"); +llvm::cl::opt systemName("system-name"); +llvm::cl::opt systemIdentity("system-identity"); +llvm::cl::opt profile("profile"); +llvm::cl::opt compiler("compiler"); +llvm::cl::opt standardLibrary("standard-library"); +llvm::cl::opt abiMode("abi-mode"); +llvm::cl::opt objectFormat("object-format"); +llvm::cl::list contractFlags("contract-flag"); +llvm::cl::list includeRoots("include-root"); +llvm::cl::list definitions("definition"); +llvm::cl::list compilerFlags("compiler-flag"); +llvm::cl::list linkerFlags("linker-flag"); +llvm::cl::list linkInputs("link-input"); +llvm::cl::list providerInputs("provider-input"); +llvm::cl::list instrumentation("instrumentation"); + +constexpr std::array stages = { + "model-plan", "acsim-emit-cxx", "acsim-check-cxx-contract", + "compile", "link", "publish"}; + +int fail(llvm::StringRef stage, llvm::Error error) { + llvm::errs() << "stage=" << stage + << " status=failed error=" << llvm::toString(std::move(error)) + << '\n'; + return EXIT_FAILURE; +} + +llvm::Error driverError(llvm::StringRef message) { + return llvm::createStringError( + std::make_error_code(std::errc::invalid_argument), + "ACLOWER-FINGERPRINT: " + message); +} + +llvm::Expected readFile(llvm::StringRef path) { + auto buffer = llvm::MemoryBuffer::getFile(path); + if (!buffer) + return llvm::createStringError(buffer.getError(), "cannot read input file"); + return buffer.get()->getBuffer().str(); +} + +llvm::Error requireOutputRoot() { + if (outputRoot.empty()) + return driverError("--output-root is required for this stage"); + if (std::error_code error = llvm::sys::fs::create_directories(outputRoot)) + return llvm::createStringError(error, "cannot create output root"); + return llvm::Error::success(); +} + +llvm::Error writeBundle(const acir::codegen::SourceBundle &bundle) { + if (auto error = requireOutputRoot()) + return error; + for (const acir::codegen::GeneratedFile &file : bundle.files) + if (auto error = acir::codegen::writeFileExclusive( + outputRoot, file.relativePath, file.content)) + return error; + return llvm::Error::success(); +} + +llvm::Expected makeCompileRequest() { + if (compiler.empty() || projectName.empty() || projectIdentity.empty() || + systemName.empty() || systemIdentity.empty() || profile.empty() || + standardLibrary.empty() || abiMode.empty() || objectFormat.empty() || + contractFlags.empty()) + return driverError( + "compile stages require explicit identity and toolchain flags"); + auto toolchain = acir::codegen::identifyToolchain( + compiler, standardLibrary, abiMode, objectFormat, + std::vector(contractFlags.begin(), contractFlags.end())); + if (!toolchain) + return toolchain.takeError(); + acir::codegen::BuildRequest request; + request.project = {projectName, projectIdentity}; + request.system = {systemName, systemIdentity}; + request.profile = profile; + request.passPipeline = {"acsim-emit-cxx", "acsim-check-cxx-contract", + "compile", "link"}; + request.toolchain = std::move(*toolchain); + request.includeRoots.assign(includeRoots.begin(), includeRoots.end()); + request.definitions.assign(definitions.begin(), definitions.end()); + request.compilerFlags.assign(compilerFlags.begin(), compilerFlags.end()); + request.linkerFlags.assign(linkerFlags.begin(), linkerFlags.end()); + request.linkInputs.assign(linkInputs.begin(), linkInputs.end()); + request.providerInputs.assign(providerInputs.begin(), providerInputs.end()); + request.instrumentationLayers.assign(instrumentation.begin(), + instrumentation.end()); + request.outputRoot = outputRoot; + return request; +} + +std::string resolvePath(llvm::StringRef path) { + if (llvm::sys::path::is_absolute(path)) + return path.str(); + llvm::SmallString<256> result(outputRoot); + llvm::sys::path::append(result, path); + return result.str().str(); +} + +std::vector resolveCommand(const acir::codegen::CompilePlan &plan, + llvm::ArrayRef arguments) { + std::set generated(plan.sourceUnits.begin(), + plan.sourceUnits.end()); + generated.insert(plan.objectOutputs.begin(), plan.objectOutputs.end()); + generated.insert(plan.executablePath); + std::vector result; + for (const std::string &argument : arguments) { + if (generated.contains(argument)) { + result.push_back(resolvePath(argument)); + continue; + } + llvm::StringRef value(argument); + if (value.starts_with("-I") && + !llvm::sys::path::is_absolute(value.drop_front(2))) { + result.push_back("-I" + resolvePath(value.drop_front(2))); + continue; + } + result.push_back(argument); + } + return result; +} + +llvm::Error runCommand(llvm::ArrayRef ownedArguments) { + llvm::SmallVector arguments; + for (const std::string &argument : ownedArguments) + arguments.push_back(argument); + const int status = llvm::sys::ExecuteAndWait(arguments.front(), arguments, + std::nullopt, {}, 120); + return status == 0 ? llvm::Error::success() + : driverError("compiler or linker command failed"); +} + +llvm::Error compileOrLink(const acir::codegen::SourceBundle &bundle, + bool link) { + auto request = makeCompileRequest(); + if (!request) + return request.takeError(); + auto plan = acir::codegen::createCompilePlan(*request, bundle); + if (!plan) + return plan.takeError(); + if (auto error = writeBundle(bundle)) + return error; + auto planBytes = plan->canonicalJson(); + if (!planBytes) + return planBytes.takeError(); + if (auto error = acir::codegen::writeFileExclusive( + outputRoot, "compile-plan.json", *planBytes)) + return error; + for (const std::string &output : plan->objectOutputs) { + llvm::SmallString<256> parent(resolvePath(output)); + llvm::sys::path::remove_filename(parent); + if (std::error_code error = llvm::sys::fs::create_directories(parent)) + return llvm::createStringError(error, "cannot create object directory"); + } + for (const acir::codegen::CompileCommand &command : plan->compileCommands) + if (auto error = runCommand(resolveCommand(*plan, command.arguments))) + return error; + if (!link) + return llvm::Error::success(); + llvm::SmallString<256> executableParent(resolvePath(plan->executablePath)); + llvm::sys::path::remove_filename(executableParent); + if (std::error_code error = + llvm::sys::fs::create_directories(executableParent)) + return llvm::createStringError(error, "cannot create executable directory"); + return runCommand(resolveCommand(*plan, plan->linkCommand.arguments)); +} + +int runDriver() { + if (std::find(stages.begin(), stages.end(), stopAfter) == stages.end()) { + llvm::errs() << "unknown --stop-after stage '" << stopAfter << "'\n"; + return EXIT_FAILURE; + } + + mlir::MLIRContext context; + context + .loadDialect(); + auto module = mlir::parseSourceFile(inputFile, &context); + if (!module) + return fail(stopAfter, driverError("canonical ACSim parsing failed")); + auto model = acir::codegen::buildModelPlan(*module); + if (!model) + return fail(stopAfter, model.takeError()); + if (stopAfter == "model-plan") { + llvm::outs() << "stage=model-plan status=passed model=" + << model->modelSymbol << " modules=" << model->modules.size() + << " runtime_objects=" << model->runtimeObjects.size() << '\n'; + return EXIT_SUCCESS; + } + + auto bundle = acir::codegen::generateModelSources(*model); + if (!bundle) + return fail(stopAfter, bundle.takeError()); + if (stopAfter == "acsim-emit-cxx") { + if (auto error = writeBundle(*bundle)) + return fail(stopAfter, std::move(error)); + llvm::outs() << "stage=acsim-emit-cxx status=passed files=" + << bundle->files.size() << '\n'; + return EXIT_SUCCESS; + } + if (stopAfter == "acsim-check-cxx-contract") { + if (auto error = writeBundle(*bundle)) + return fail(stopAfter, std::move(error)); + if (auto error = acir::codegen::validateSourceBundle(*model, *bundle)) + return fail(stopAfter, std::move(error)); + if (auto error = acir::codegen::writeFileExclusive( + outputRoot, "reports/source-contract.txt", "passed\n")) + return fail(stopAfter, std::move(error)); + llvm::outs() << "stage=acsim-check-cxx-contract status=passed\n"; + return EXIT_SUCCESS; + } + if (stopAfter == "compile" || stopAfter == "link") { + if (auto error = compileOrLink(*bundle, stopAfter == "link")) + return fail(stopAfter, std::move(error)); + llvm::outs() << "stage=" << stopAfter << " status=passed\n"; + return EXIT_SUCCESS; + } + + auto request = makeCompileRequest(); + if (!request) + return fail(stopAfter, request.takeError()); + auto frozen = readFile(frozenAcir); + if (!frozen) + return fail(stopAfter, frozen.takeError()); + auto lock = readFile(bindingLock); + if (!lock) + return fail(stopAfter, lock.takeError()); + std::string acsimBytes; + llvm::raw_string_ostream acsimStream(acsimBytes); + module->print(acsimStream); + acsimStream.flush(); + request->canonicalACSim = *module; + request->frozenAcirBytes = std::move(*frozen); + request->bindingLockBytes = std::move(*lock); + request->canonicalACSimBytes = std::move(acsimBytes); + auto result = acir::codegen::buildGeneratedModel(*request); + if (!result) + return fail(stopAfter, result.takeError()); + llvm::outs() << "stage=publish status=passed build=" + << result->buildFingerprint << '\n'; + return EXIT_SUCCESS; +} + +} // namespace + +int main(int argc, char **argv) { + try { + llvm::cl::ParseCommandLineOptions( + argc, argv, "Agentic Circuit internal C++ generator\n"); + return runDriver(); + } catch (const std::exception &exception) { + llvm::errs() << "ACLOWER-FINGERPRINT: internal driver exception: " + << exception.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/components/agentic-circuit/tools/acir-opcode-catalog/CMakeLists.txt b/components/agentic-circuit/tools/acir-opcode-catalog/CMakeLists.txt new file mode 100644 index 00000000..509e07de --- /dev/null +++ b/components/agentic-circuit/tools/acir-opcode-catalog/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(acir-opcode-catalog acir-opcode-catalog.cpp) +target_link_libraries(acir-opcode-catalog PRIVATE + AgenticCircuit::ProjectOptions + ACIRCodeGen +) diff --git a/components/agentic-circuit/tools/acir-opcode-catalog/acir-opcode-catalog.cpp b/components/agentic-circuit/tools/acir-opcode-catalog/acir-opcode-catalog.cpp new file mode 100644 index 00000000..eb9d1213 --- /dev/null +++ b/components/agentic-circuit/tools/acir-opcode-catalog/acir-opcode-catalog.cpp @@ -0,0 +1,15 @@ +#include "acir/CodeGen/QueueBlockContract.h" + +#include "llvm/Support/raw_ostream.h" + +#include + +int main() { + auto catalog = acir::codegen::canonicalQueueBlockCatalogJson(); + if (!catalog) { + llvm::errs() << llvm::toString(catalog.takeError()) << '\n'; + return EXIT_FAILURE; + } + llvm::outs() << *catalog << '\n'; + return EXIT_SUCCESS; +} diff --git a/components/agentic-circuit/tools/acir-opt/BindingOptions.cpp b/components/agentic-circuit/tools/acir-opt/BindingOptions.cpp new file mode 100644 index 00000000..ad0881cb --- /dev/null +++ b/components/agentic-circuit/tools/acir-opt/BindingOptions.cpp @@ -0,0 +1,143 @@ +#include "BindingOptions.h" + +#include "acir/Bindings/Registry.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/Errc.h" +#include "llvm/Support/MemoryBuffer.h" + +#include +#include + +namespace acir::opt { +namespace { + +llvm::cl::OptionCategory BindingCategory("Exact binding resolution options"); + +llvm::cl::opt ResolveBindings( + "ac-resolve-gfsim-bindings", + llvm::cl::desc("Run exact frozen-ACIR gfsim binding resolution"), + llvm::cl::init(false), llvm::cl::cat(BindingCategory)); + +llvm::cl::opt + LowerToACSim("ac-lower-to-acsim", + llvm::cl::desc("Atomically lower the frozen ACIR model to " + "canonical ACSim"), + llvm::cl::init(false), llvm::cl::cat(BindingCategory)); + +llvm::cl::list BindingRegistries( + "ac-binding-registry", + llvm::cl::desc( + "Closed binding candidate/request registry JSON file (repeatable)"), + llvm::cl::ZeroOrMore, llvm::cl::value_desc("file"), + llvm::cl::cat(BindingCategory)); + +llvm::cl::opt BindingLockOutput( + "ac-binding-lock-output", + llvm::cl::desc("Required atomic output path for acsim-bindings.lock.json"), + llvm::cl::value_desc("file"), llvm::cl::init(""), + llvm::cl::cat(BindingCategory)); + +llvm::cl::opt + BindingProfile("ac-binding-profile", + llvm::cl::desc("Exact static build profile identity"), + llvm::cl::value_desc("profile"), llvm::cl::init(""), + llvm::cl::cat(BindingCategory)); + +llvm::cl::opt + BindingTarget("ac-binding-target", + llvm::cl::desc("Exact toolchain target identity"), + llvm::cl::value_desc("target"), llvm::cl::init(""), + llvm::cl::cat(BindingCategory)); + +llvm::Error optionError(const llvm::Twine &message) { + return llvm::createStringError(llvm::errc::invalid_argument, + "ACLOWER-BINDING-OPTIONS: %s", + message.str().c_str()); +} + +bool hasRelatedOption() { + return !BindingRegistries.empty() || !BindingLockOutput.empty() || + !BindingProfile.empty() || !BindingTarget.empty(); +} + +struct RegistryInputs { + std::vector candidates; + std::vector requests; +}; + +llvm::Expected loadRegistryInputs() { + std::vector registryPaths(BindingRegistries.begin(), + BindingRegistries.end()); + llvm::sort(registryPaths); + RegistryInputs inputs; + for (const std::string &path : registryPaths) { + auto buffer = llvm::MemoryBuffer::getFile(path, false, false); + if (!buffer) + return optionError(llvm::Twine("cannot read registry '") + path + + "': " + buffer.getError().message()); + auto document = bindings::parseBindingRegistry((*buffer)->getBuffer()); + if (!document) + return document.takeError(); + inputs.candidates.insert( + inputs.candidates.end(), + std::make_move_iterator(document->candidates.begin()), + std::make_move_iterator(document->candidates.end())); + inputs.requests.insert(inputs.requests.end(), + std::make_move_iterator(document->requests.begin()), + std::make_move_iterator(document->requests.end())); + } + return inputs; +} + +} // namespace + +llvm::Expected> +loadBindingCommandLineOptions() { + if (!ResolveBindings) { + if (hasRelatedOption() && !LowerToACSim) + return optionError("binding options require --ac-resolve-gfsim-bindings " + "or --ac-lower-to-acsim"); + return std::optional(); + } + if (BindingLockOutput.empty()) + return optionError("--ac-binding-lock-output is required"); + if (BindingProfile.empty()) + return optionError("--ac-binding-profile is required"); + if (BindingTarget.empty()) + return optionError("--ac-binding-target is required"); + + auto inputs = loadRegistryInputs(); + if (!inputs) + return inputs.takeError(); + ResolveBindingsPassOptions options; + options.profile = BindingProfile; + options.target = BindingTarget; + options.lockOutputPath = BindingLockOutput; + options.candidates = std::move(inputs->candidates); + options.requests = std::move(inputs->requests); + return std::optional(std::move(options)); +} + +llvm::Expected> +loadLoweringCommandLineOptions() { + if (!LowerToACSim) + return std::optional(); + if (BindingProfile.empty()) + return optionError("--ac-lower-to-acsim requires --ac-binding-profile"); + if (BindingTarget.empty()) + return optionError("--ac-lower-to-acsim requires --ac-binding-target"); + + auto inputs = loadRegistryInputs(); + if (!inputs) + return inputs.takeError(); + ACIRToACSimPassOptions options; + options.profile = BindingProfile; + options.target = BindingTarget; + options.candidates = std::move(inputs->candidates); + options.requests = std::move(inputs->requests); + return std::optional(std::move(options)); +} + +} // namespace acir::opt diff --git a/components/agentic-circuit/tools/acir-opt/BindingOptions.h b/components/agentic-circuit/tools/acir-opt/BindingOptions.h new file mode 100644 index 00000000..cc7f01e7 --- /dev/null +++ b/components/agentic-circuit/tools/acir-opt/BindingOptions.h @@ -0,0 +1,21 @@ +#ifndef ACIR_OPT_BINDINGOPTIONS_H +#define ACIR_OPT_BINDINGOPTIONS_H + +#include "acir/Conversion/ACIRToACSim/ACIRToACSim.h" +#include "acir/Transforms/ResolveBindings.h" + +#include "llvm/Support/Error.h" + +#include + +namespace acir::opt { + +llvm::Expected> +loadBindingCommandLineOptions(); + +llvm::Expected> +loadLoweringCommandLineOptions(); + +} // namespace acir::opt + +#endif // ACIR_OPT_BINDINGOPTIONS_H diff --git a/components/agentic-circuit/tools/acir-opt/CMakeLists.txt b/components/agentic-circuit/tools/acir-opt/CMakeLists.txt new file mode 100644 index 00000000..029ffc78 --- /dev/null +++ b/components/agentic-circuit/tools/acir-opt/CMakeLists.txt @@ -0,0 +1,13 @@ +set(ACIR_OPT_SOURCES acir-opt.cpp BindingOptions.cpp) +add_executable(acir-opt ${ACIR_OPT_SOURCES}) +set(ACIR_OPT_LIBRARIES + AgenticCircuit::ProjectOptions ACIRAnalysis ACIRTransforms ACIRToACSim + ACIRDialect ACSimDialect MLIRFuncDialect MLIRIndexDialect MLIRSCFDialect + MLIRTransforms MLIROptLib) +target_link_libraries(acir-opt PRIVATE ${ACIR_OPT_LIBRARIES}) + +if(ACIR_BUILD_TESTING) + add_executable(acir-opt-internal ${ACIR_OPT_SOURCES}) + target_compile_definitions(acir-opt-internal PRIVATE ACIR_INTERNAL_TEST_TOOL=1) + target_link_libraries(acir-opt-internal PRIVATE ${ACIR_OPT_LIBRARIES}) +endif() diff --git a/components/agentic-circuit/tools/acir-opt/acir-opt.cpp b/components/agentic-circuit/tools/acir-opt/acir-opt.cpp new file mode 100644 index 00000000..76058ee5 --- /dev/null +++ b/components/agentic-circuit/tools/acir-opt/acir-opt.cpp @@ -0,0 +1,247 @@ +#include "BindingOptions.h" +#include "acir/Dialect/ACIR/ACIRDialect.h" +#include "acir/Dialect/ACIR/GraphRegion.h" +#include "acir/InitAllDialects.h" +#include "acir/InitAllPasses.h" +#include "acir/Transforms/ResolveBindings.h" +#include "mlir/AsmParser/AsmParser.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassInstrumentation.h" +#include "mlir/Pass/PassManager.h" +#include "mlir/Support/FileUtilities.h" +#include "mlir/Tools/mlir-opt/MlirOptMain.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/ToolOutputFile.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include + +namespace { + +enum class CanonicalScanResult { Canonical, GenericOperation, MalformedEscape }; + +CanonicalScanResult scanCanonicalAssembly(llvm::StringRef input) { + mlir::MLIRContext stringContext; + mlir::ScopedDiagnosticHandler suppressStringDiagnostics( + &stringContext, [](mlir::Diagnostic &) { return mlir::success(); }); + for (size_t index = 0; index < input.size();) { + if (input.substr(index).starts_with("//")) { + index = input.find('\n', index); + if (index == llvm::StringRef::npos) + return CanonicalScanResult::Canonical; + continue; + } + if (input.substr(index).starts_with("/*")) { + unsigned depth = 1; + index += 2; + while (index < input.size() && depth) { + if (input.substr(index).starts_with("/*")) { + ++depth; + index += 2; + } else if (input.substr(index).starts_with("*/")) { + --depth; + index += 2; + } else { + ++index; + } + } + continue; + } + if (input[index] != '"') { + ++index; + continue; + } + size_t start = index++; + bool escaped = false; + while (index < input.size()) { + char value = input[index++]; + if (!escaped && value == '"') + break; + escaped = !escaped && value == '\\'; + if (value != '\\') + escaped = false; + } + llvm::StringRef token = input.slice(start, index); + size_t next = index; + while (next < input.size() && llvm::isSpace(input[next])) + ++next; + if (next >= input.size() || input[next] != '(') + continue; + auto parsed = mlir::parseAttribute(token, &stringContext); + auto spelling = mlir::dyn_cast_or_null(parsed); + if (!spelling) + return CanonicalScanResult::MalformedEscape; + llvm::StringRef value = spelling.getValue(); + if (value.starts_with("ac.") || value.starts_with("acsim.")) + return CanonicalScanResult::GenericOperation; + } + return CanonicalScanResult::Canonical; +} + +#ifdef ACIR_INTERNAL_TEST_TOOL +llvm::cl::opt testRawDepth( + "acir-test-raw-depth", llvm::cl::Hidden, llvm::cl::init(0), + llvm::cl::desc("materialize hostile nested IR after shallow parsing")); +llvm::cl::opt testRawMalformed("acir-test-raw-malformed", + llvm::cl::Hidden, llvm::cl::init(false)); +llvm::cl::opt testPassTrace("acir-test-pass-trace", llvm::cl::Hidden, + llvm::cl::init(false)); + +class MaterializeRawDepthPass final + : public mlir::PassWrapper> { +public: + llvm::StringRef getArgument() const final { + return "acir-test-materialize-raw-depth"; + } + void runOnOperation() final { + mlir::ModuleOp parent = getOperation(); + for (unsigned index = 0; index < testRawDepth; ++index) { + auto nested = mlir::ModuleOp::create(parent.getLoc()); + parent.getBody()->push_back(nested); + parent = nested; + } + if (testRawMalformed) { + mlir::OperationState state(parent.getLoc(), "scf.yield"); + parent.getBody()->push_back(mlir::Operation::create(state)); + } + } +}; + +class TestPassTrace final : public mlir::PassInstrumentation { +public: + void runBeforePass(mlir::Pass *pass, mlir::Operation *) final { + llvm::errs() << "enter:" << pass->getArgument() << '\n'; + } + void runAfterPass(mlir::Pass *pass, mlir::Operation *) final { + llvm::errs() << "complete:" << pass->getArgument() << '\n'; + } + void runAfterPassFailed(mlir::Pass *pass, mlir::Operation *) final { + llvm::errs() << "fail:" << pass->getArgument() << '\n'; + } +}; +#endif + +int runDriver(int argc, char **argv) { + mlir::DialectRegistry registry; + acir::registerAllDialects(registry); +#ifdef ACIR_INTERNAL_TEST_TOOL + registry.addExtension( + +[](mlir::MLIRContext *context, acir::ac::ACIRDialect *) { + auto &providers = acir::ac::getStructuralProviderRegistry(context); + for (llvm::StringRef name : + {"A", "B", "Consumer", "Empty", "Ext", "Leaf", "Producer", "Top"}) + providers.registerExternal(name); + providers.registerGenerator("Gen"); + }); +#endif + acir::registerAllPasses(); + + auto [inputFilename, outputFilename] = mlir::registerAndParseCLIOptions( + argc, argv, "Agentic Circuit optimizer driver\n", registry); + auto bindingOptions = acir::opt::loadBindingCommandLineOptions(); + if (!bindingOptions) { + llvm::errs() << "error: " << llvm::toString(bindingOptions.takeError()) + << '\n'; + return EXIT_FAILURE; + } + auto loweringOptions = acir::opt::loadLoweringCommandLineOptions(); + if (!loweringOptions) { + llvm::errs() << "error: " << llvm::toString(loweringOptions.takeError()) + << '\n'; + return EXIT_FAILURE; + } + mlir::MlirOptMainConfig config = + mlir::MlirOptMainConfig::createFromCLOptions(); + mlir::MlirOptMainConfig commandLineConfig = config; + config.allowUnregisteredDialects(false) + .useExplicitModule(true) + .setPassPipelineSetupFn([commandLineConfig, + bindingOptions = std::move(*bindingOptions), + loweringOptions = std::move(*loweringOptions)]( + mlir::PassManager &passManager) { +#ifdef ACIR_INTERNAL_TEST_TOOL + if (testPassTrace) + passManager.addInstrumentation(std::make_unique()); + if (testRawDepth) + passManager.addPass(std::make_unique()); +#endif + passManager.addPass(acir::createNormalizeACIRFilePass()); + passManager.addPass(acir::createVerifyACIRFilePass()); + if (mlir::failed(commandLineConfig.setupPassPipeline(passManager))) + return mlir::failure(); + if (bindingOptions) + passManager.addPass(acir::createResolveBindingsPass(*bindingOptions)); + if (loweringOptions) { + // Atomic whole-model lowering publishes canonical ACSim, so the + // trailing ACIR whole-model gate does not apply to its output. + passManager.addPass(acir::createACIRToACSimPass(*loweringOptions)); + return mlir::success(); + } + // The final whole-model gate makes a persisted freeze digest effective + // across every user-supplied pipeline: any topology mutation after + // ac-freeze-topology is diagnosed before output is committed. + passManager.addPass(acir::createVerifyModelPass()); + return mlir::success(); + }); + + std::string errorMessage; + std::unique_ptr input = + mlir::openInputFile(inputFilename, &errorMessage); + if (!input) { + llvm::errs() << errorMessage << '\n'; + return EXIT_FAILURE; + } + +#ifndef ACIR_INTERNAL_TEST_TOOL + llvm::StringRef contents = input->getBuffer(); + bool isBytecode = contents.size() >= 4 && + contents.take_front(4) == llvm::StringRef("ML\xefR", 4); + if (!isBytecode) { + switch (scanCanonicalAssembly(contents)) { + case CanonicalScanResult::Canonical: + break; + case CanonicalScanResult::GenericOperation: + llvm::errs() + << "error: generic ACIR operation spelling is internal-only; " + "use canonical ACIR assembly\n"; + return EXIT_FAILURE; + case CanonicalScanResult::MalformedEscape: + llvm::errs() << "error: malformed quoted operation name escape\n"; + return EXIT_FAILURE; + } + } +#endif + + std::unique_ptr output = + mlir::openOutputFile(outputFilename, &errorMessage); + if (!output) { + llvm::errs() << errorMessage << '\n'; + return EXIT_FAILURE; + } + + mlir::LogicalResult result = + mlir::MlirOptMain(output->os(), std::move(input), registry, config); + if (mlir::succeeded(result)) + output->keep(); + return mlir::asMainReturnCode(result); +} + +} // namespace + +int main(int argc, char **argv) { + try { + return runDriver(argc, argv); + } catch (const std::exception &error) { + llvm::errs() << "error: unhandled exception: " << error.what() << '\n'; + return EXIT_FAILURE; + } catch (...) { + llvm::errs() << "error: unhandled unknown exception\n"; + return EXIT_FAILURE; + } +} diff --git a/components/agentic-circuit/tools/acir-queue-cxxgen/CMakeLists.txt b/components/agentic-circuit/tools/acir-queue-cxxgen/CMakeLists.txt new file mode 100644 index 00000000..4bc780cf --- /dev/null +++ b/components/agentic-circuit/tools/acir-queue-cxxgen/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(acir-queue-cxxgen acir-queue-cxxgen.cpp) +target_link_libraries(acir-queue-cxxgen PRIVATE + AgenticCircuit::ProjectOptions + ACIRCodeGen + ACIRDialect + MLIRParser +) diff --git a/components/agentic-circuit/tools/acir-queue-cxxgen/acir-queue-cxxgen.cpp b/components/agentic-circuit/tools/acir-queue-cxxgen/acir-queue-cxxgen.cpp new file mode 100644 index 00000000..12fb41bc --- /dev/null +++ b/components/agentic-circuit/tools/acir-queue-cxxgen/acir-queue-cxxgen.cpp @@ -0,0 +1,44 @@ +#include "acir/CodeGen/QueueGraphGenerator.h" +#include "acir/CodeGen/QueueGraphPlan.h" +#include "acir/InitAllDialects.h" + +#include "mlir/IR/DialectRegistry.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include + +namespace { + +llvm::cl::opt inputFile(llvm::cl::Positional, llvm::cl::Required, + llvm::cl::desc("")); + +} // namespace + +int main(int argc, char **argv) { + llvm::cl::ParseCommandLineOptions( + argc, argv, "Generate typed Queue-wired gfsim C++ from frozen ACIR\n"); + mlir::DialectRegistry registry; + acir::registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = mlir::parseSourceFile(inputFile, &context); + if (!module) { + llvm::errs() << "ACLOWER-QUEUE-CXX: frozen ACIR parsing failed\n"; + return EXIT_FAILURE; + } + auto plan = acir::codegen::buildQueueGraphPlan(*module); + if (!plan) { + llvm::errs() << llvm::toString(plan.takeError()) << '\n'; + return EXIT_FAILURE; + } + auto source = acir::codegen::generateQueueGraphCpp(*plan); + if (!source) { + llvm::errs() << llvm::toString(source.takeError()) << '\n'; + return EXIT_FAILURE; + } + llvm::outs() << *source; + return EXIT_SUCCESS; +} diff --git a/components/agentic-circuit/tools/acir-queue-plan/CMakeLists.txt b/components/agentic-circuit/tools/acir-queue-plan/CMakeLists.txt new file mode 100644 index 00000000..e9a7ec0d --- /dev/null +++ b/components/agentic-circuit/tools/acir-queue-plan/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(acir-queue-plan acir-queue-plan.cpp) +target_link_libraries(acir-queue-plan PRIVATE + AgenticCircuit::ProjectOptions + ACIRCodeGen + ACIRDialect + MLIRParser +) diff --git a/components/agentic-circuit/tools/acir-queue-plan/acir-queue-plan.cpp b/components/agentic-circuit/tools/acir-queue-plan/acir-queue-plan.cpp new file mode 100644 index 00000000..35720a30 --- /dev/null +++ b/components/agentic-circuit/tools/acir-queue-plan/acir-queue-plan.cpp @@ -0,0 +1,43 @@ +#include "acir/CodeGen/QueueGraphPlan.h" +#include "acir/InitAllDialects.h" + +#include "mlir/IR/DialectRegistry.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include + +namespace { + +llvm::cl::opt inputFile(llvm::cl::Positional, llvm::cl::Required, + llvm::cl::desc("")); + +} // namespace + +int main(int argc, char **argv) { + llvm::cl::ParseCommandLineOptions( + argc, argv, "Extract canonical QueueGraph plan from frozen ACIR\n"); + mlir::DialectRegistry registry; + acir::registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = mlir::parseSourceFile(inputFile, &context); + if (!module) { + llvm::errs() << "ACLOWER-QUEUE-PLAN: frozen ACIR parsing failed\n"; + return EXIT_FAILURE; + } + auto plan = acir::codegen::buildQueueGraphPlan(*module); + if (!plan) { + llvm::errs() << llvm::toString(plan.takeError()) << '\n'; + return EXIT_FAILURE; + } + auto json = plan->canonicalJson(); + if (!json) { + llvm::errs() << llvm::toString(json.takeError()) << '\n'; + return EXIT_FAILURE; + } + llvm::outs() << *json << '\n'; + return EXIT_SUCCESS; +} diff --git a/components/agentic-circuit/tools/acir-queue-pycgen/CMakeLists.txt b/components/agentic-circuit/tools/acir-queue-pycgen/CMakeLists.txt new file mode 100644 index 00000000..f76b0a52 --- /dev/null +++ b/components/agentic-circuit/tools/acir-queue-pycgen/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(acir-queue-pycgen acir-queue-pycgen.cpp) +target_link_libraries(acir-queue-pycgen PRIVATE + AgenticCircuit::ProjectOptions + ACIRCodeGen + ACIRDialect + MLIRParser +) diff --git a/components/agentic-circuit/tools/acir-queue-pycgen/acir-queue-pycgen.cpp b/components/agentic-circuit/tools/acir-queue-pycgen/acir-queue-pycgen.cpp new file mode 100644 index 00000000..7b9cae18 --- /dev/null +++ b/components/agentic-circuit/tools/acir-queue-pycgen/acir-queue-pycgen.cpp @@ -0,0 +1,44 @@ +#include "acir/CodeGen/QueueGraphPlan.h" +#include "acir/CodeGen/QueueGraphPyc.h" +#include "acir/InitAllDialects.h" + +#include "mlir/IR/DialectRegistry.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include + +namespace { + +llvm::cl::opt inputFile(llvm::cl::Positional, llvm::cl::Required, + llvm::cl::desc("")); + +} // namespace + +int main(int argc, char **argv) { + llvm::cl::ParseCommandLineOptions( + argc, argv, "Generate canonical PYC IR from frozen Queue ACIR\n"); + mlir::DialectRegistry registry; + acir::registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = mlir::parseSourceFile(inputFile, &context); + if (!module) { + llvm::errs() << "ACLOWER-PYC: frozen ACIR parsing failed\n"; + return EXIT_FAILURE; + } + auto plan = acir::codegen::buildQueueGraphPlan(*module); + if (!plan) { + llvm::errs() << llvm::toString(plan.takeError()) << '\n'; + return EXIT_FAILURE; + } + auto pyc = acir::codegen::generateQueueGraphPyc(*plan); + if (!pyc) { + llvm::errs() << llvm::toString(pyc.takeError()) << '\n'; + return EXIT_FAILURE; + } + llvm::outs() << *pyc; + return EXIT_SUCCESS; +} diff --git a/components/agentic-circuit/tools/acir-queue-veriloggen.py b/components/agentic-circuit/tools/acir-queue-veriloggen.py new file mode 100644 index 00000000..c0f91e08 --- /dev/null +++ b/components/agentic-circuit/tools/acir-queue-veriloggen.py @@ -0,0 +1,738 @@ +#!/usr/bin/env python3 +"""Generate self-contained Verilog from a frozen Queue ACIR module. + +This is the first in-tree PYC compatibility backend. It deliberately consumes +the canonical textual PYC emitted by ``acir-queue-pycgen`` instead of adding a +second ACIR lowering. The small emitter covers the pyCircuit 6 operations used +by the golden queue slice and embeds only the required sequential runtime +modules in the resulting Verilog file. +""" + +from __future__ import annotations + +import argparse +import ast +import json +import math +import os +import re +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + + +class PYCVerilogError(ValueError): + pass + + +_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +@dataclass(frozen=True) +class Value: + name: str + type: str + + @property + def net(self) -> str: + return self.name.lstrip("%").replace(".", "_") + + @property + def width(self) -> int: + match = re.fullmatch(r"i(\d+)", self.type) + if not match: + raise PYCVerilogError(f"expected integer PYC type, got {self.type}") + width = int(match.group(1)) + if width <= 0: + raise PYCVerilogError("PYC integer widths must be positive") + return width + + +@dataclass +class Module: + name: str + args: list[Value] + results: list[Value] + body: list[str] + + +def _split_csv(text: str) -> list[str]: + """Split a comma list while ignoring commas inside brackets/quotes.""" + result: list[str] = [] + start = 0 + depth = 0 + quote = False + escaped = False + for index, char in enumerate(text): + if quote: + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == '"': + quote = False + continue + if char == '"': + quote = True + elif char in "([{": + depth += 1 + elif char in ")]}": + depth -= 1 + elif char == "," and depth == 0: + result.append(text[start:index].strip()) + start = index + 1 + tail = text[start:].strip() + if tail: + result.append(tail) + return result + + +def _parse_types(text: str) -> list[str]: + text = text.strip() + if text.startswith("(") and text.endswith(")"): + text = text[1:-1] + return [part.strip() for part in _split_csv(text) if part.strip()] + + +def _parse_names_attr(header: str, key: str) -> list[str]: + match = re.search(rf"{re.escape(key)}\s*=\s*(\[[^]]*\])", header) + if not match: + raise PYCVerilogError(f"function header is missing {key}") + value = ast.literal_eval(match.group(1)) + if ( + not isinstance(value, list) + or not all( + isinstance(item, str) and _IDENTIFIER.fullmatch(item) for item in value + ) + or len(value) != len(set(value)) + ): + raise PYCVerilogError(f"{key} must be a unique Verilog identifier list") + return value + + +def parse_pyc_module(text: str) -> Module: + func_match = re.search( + r"^\s*func\.func\s+@(?P[A-Za-z_][A-Za-z0-9_]*)\s*\((?P.*?)\)\s*" + r"->\s*\((?P.*?)\)\s*attributes\s*(?P\{.*\})\s*\{\s*$", + text, + re.MULTILINE | re.DOTALL, + ) + if not func_match: + raise PYCVerilogError( + "canonical PYC module does not contain a supported func.func" + ) + + arg_parts = _split_csv(func_match.group("args")) + args: list[Value] = [] + for part in arg_parts: + match = re.fullmatch(r"(%[A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)", part.strip()) + if not match: + raise PYCVerilogError(f"cannot parse function argument: {part}") + args.append(Value(match.group(1), match.group(2).strip())) + if len({value.name for value in args}) != len(args): + raise PYCVerilogError("function arguments must have unique SSA names") + + result_types = _parse_types(func_match.group("results")) + result_names = _parse_names_attr(func_match.group("attrs"), "result_names") + if len(result_types) != len(result_names): + raise PYCVerilogError("result_names and result types have different lengths") + results = [Value(name, type_) for name, type_ in zip(result_names, result_types)] + argument_ports = {value.net for value in args} + if argument_ports.intersection(result_names): + raise PYCVerilogError("function input and result port names must be unique") + + lines = text.splitlines() + func_line = next( + index + for index, line in enumerate(lines) + if line.strip().startswith("func.func @") + ) + body: list[str] = [] + for line in lines[func_line + 1 :]: + if line.strip() == "}": + break + if line.startswith(" "): + body.append(line.strip()) + if not body: + raise PYCVerilogError("PYC function body is empty") + return Module(func_match.group("name"), args, results, body) + + +def _ssa_names(text: str) -> list[str]: + return [part.strip() for part in _split_csv(text) if part.strip()] + + +def _value(values: dict[str, Value], name: str) -> Value: + try: + return values[name] + except KeyError as exc: + raise PYCVerilogError(f"unknown PYC SSA value {name}") from exc + + +def _port_decl(direction: str, value: Value) -> str: + if value.type in ("!pyc.clock", "!pyc.reset"): + return f" {direction} wire {value.net}" + width = value.width + suffix = "" if width == 1 else f" [{width - 1}:0]" + return f" {direction} wire{suffix} {value.net}" + + +def _literal(value: str, width: int) -> str: + try: + number = int(value, 0) + except ValueError as exc: + raise PYCVerilogError(f"unsupported PYC constant {value}") from exc + return f"{width}'d{number}" + + +def _parse_attr_int(attrs: str, key: str) -> int: + match = re.search(rf"\b{re.escape(key)}\s*=\s*(-?\d+)", attrs) + if not match: + raise PYCVerilogError(f"PYC op is missing integer attribute {key}") + return int(match.group(1)) + + +def _runtime_sources(runtime_dir: Path) -> Iterable[str]: + for name in ("pyc_reg.v", "pyc_fifo.v"): + path = runtime_dir / name + if not path.is_file(): + raise PYCVerilogError(f"missing in-tree PYC runtime module: {path}") + yield f"// --- PYC runtime: {path.name}\n{path.read_text(encoding='utf-8')}" + + +def emit_verilog(module: Module, runtime_dir: Path) -> str: + values: dict[str, Value] = {value.name: value for value in module.args} + declarations: list[str] = [] + assigns: list[str] = [] + instances: list[str] = [] + assertions: list[str] = [] + returns: list[str] | None = None + seen_outputs: set[str] = set() + + def add_value(name: str, type_: str) -> Value: + value = Value(name, type_) + if name in values: + raise PYCVerilogError(f"SSA value {name} is defined more than once") + value.width + values[name] = value + if name not in {arg.name for arg in module.args}: + if type_ not in ("!pyc.clock", "!pyc.reset"): + width = value.width + suffix = "" if width == 1 else f" [{width - 1}:0]" + declarations.append(f" wire{suffix} {value.net};") + return value + + for line in module.body: + if line.startswith("func.return "): + match = re.fullmatch(r"func\.return\s+(.*?)\s*:\s*(.*)", line) + if not match: + raise PYCVerilogError(f"cannot parse return: {line}") + returns = _ssa_names(match.group(1)) + continue + if line.startswith("pyc.assert "): + match = re.fullmatch( + r"pyc\.assert\s+(%[A-Za-z_][A-Za-z0-9_]*)\s*" + r"\{\s*msg\s*=\s*(\"(?:[^\"\\]|\\.)*\")\s*\}", + line, + ) + if not match: + raise PYCVerilogError(f"cannot parse assertion: {line}") + condition = _value(values, match.group(1)) + if condition.type != "i1": + raise PYCVerilogError("assertion condition must be i1") + try: + message = json.loads(match.group(2)) + except json.JSONDecodeError as exc: + raise PYCVerilogError("assertion message is invalid") from exc + assertions.extend( + ( + " // synthesis translate_off", + " always @* begin", + f" if (!{condition.net}) begin", + f" $display({json.dumps(message)});", + " $stop;", + " end", + " end", + " // synthesis translate_on", + ) + ) + continue + + lhs: list[str] = [] + rhs = line + if " = " in line: + lhs_text, rhs = line.split(" = ", 1) + lhs = _ssa_names(lhs_text) + rhs = rhs.strip() + + if rhs.startswith("pyc.wire : "): + if len(lhs) != 1: + raise PYCVerilogError(f"wire expects one result: {line}") + add_value(lhs[0], rhs[len("pyc.wire : ") :].strip()) + continue + + if rhs.startswith("pyc.constant "): + match = re.fullmatch(r"pyc\.constant\s+(\S+)\s*:\s*(\S+)", rhs) + if not match or len(lhs) != 1: + raise PYCVerilogError(f"cannot parse constant: {line}") + value = add_value(lhs[0], match.group(2)) + assigns.append( + f" assign {value.net} = {_literal(match.group(1), value.width)};" + ) + continue + + if rhs.startswith("pyc.fifo "): + match = re.fullmatch(r"pyc\.fifo\s+(.*?)\s*\{(.*?)\}\s*:\s*(\S+)", rhs) + if not match or len(lhs) != 3: + raise PYCVerilogError(f"cannot parse FIFO: {line}") + inputs = _ssa_names(match.group(1)) + if len(inputs) != 5: + raise PYCVerilogError(f"FIFO expects five operands: {line}") + depth = _parse_attr_int(match.group(2), "depth") + if depth <= 0: + raise PYCVerilogError("FIFO depth must be positive") + payload = match.group(3) + out_values = [ + add_value(lhs[0], "i1"), + add_value(lhs[1], "i1"), + add_value(lhs[2], payload), + ] + in_valid, in_data, out_ready = ( + _value(values, inputs[2]), + _value(values, inputs[3]), + _value(values, inputs[4]), + ) + clk, rst = _value(values, inputs[0]), _value(values, inputs[1]) + instances.append( + f" pyc_fifo #(.WIDTH({out_values[2].width}), .DEPTH({depth})) fifo_{out_values[0].net} (\n" + f" .clk({clk.net}), .rst({rst.net}),\n" + f" .in_valid({in_valid.net}), .in_ready({out_values[0].net}), .in_data({in_data.net}),\n" + f" .out_valid({out_values[1].net}), .out_ready({out_ready.net}), .out_data({out_values[2].net})\n" + f" );" + ) + continue + + if rhs.startswith("pyc.reg "): + match = re.fullmatch(r"pyc\.reg\s+(.*?)\s*:\s*(\S+)", rhs) + if not match or len(lhs) != 1: + raise PYCVerilogError(f"cannot parse register: {line}") + inputs = _ssa_names(match.group(1)) + if len(inputs) != 5: + raise PYCVerilogError(f"register expects five operands: {line}") + q = add_value(lhs[0], match.group(2)) + clk, rst, en, data, init = (_value(values, item) for item in inputs) + instances.append( + f" pyc_reg #(.WIDTH({q.width})) reg_{q.net} (\n" + f" .clk({clk.net}), .rst({rst.net}), .en({en.net}),\n" + f" .d({data.net}), .init({init.net}), .q({q.net})\n" + f" );" + ) + continue + + if rhs.startswith("pyc.rr_arbiter "): + match = re.fullmatch( + r"pyc\.rr_arbiter\s+(.*?)\s*\{(.*?)\}\s*:\s*(.*?)\s*->\s*(\S+)", rhs + ) + if not match or len(lhs) != 1: + raise PYCVerilogError(f"cannot parse round-robin arbiter: {line}") + inputs = _ssa_names(match.group(1)) + if len(inputs) != 2: + raise PYCVerilogError( + f"round-robin arbiter expects request and cursor: {line}" + ) + req, cursor = (_value(values, item) for item in inputs) + out = add_value(lhs[0], match.group(4)) + num_inputs = _parse_attr_int(match.group(2), "num_inputs") + if num_inputs != req.width or num_inputs != out.width: + raise PYCVerilogError( + "rr_arbiter num_inputs must match request and grant widths" + ) + if (1 << cursor.width) < num_inputs: + raise PYCVerilogError( + "rr_arbiter cursor width cannot address every input" + ) + instances.append( + f" pyc_rr_arbiter #(.NUM_INPUTS({num_inputs}), .POINTER_WIDTH({cursor.width})) rr_arbiter_{out.net} (\n" + f" .req({req.net}), .cursor({cursor.net}), .grant({out.net})\n" + f" );" + ) + continue + + if rhs.startswith("pyc.popcount "): + match = re.fullmatch( + r"pyc\.popcount\s+(.*?)\s*\{.*?\}\s*:\s*(\S+)\s*->\s*(\S+)", rhs + ) + if not match or len(lhs) != 1: + raise PYCVerilogError(f"cannot parse popcount: {line}") + inputs = _ssa_names(match.group(1)) + if len(inputs) != 1: + raise PYCVerilogError(f"popcount expects one operand: {line}") + src = _value(values, inputs[0]) + out = add_value(lhs[0], match.group(3)) + if out.width < src.width.bit_length(): + raise PYCVerilogError("popcount result width is too small") + instances.append( + f" pyc_popcount #(.IN_WIDTH({src.width}), .OUT_WIDTH({out.width})) popcount_{out.net} (\n" + f" .in({src.net}), .out({out.net})\n" + f" );" + ) + continue + + if rhs.startswith("pyc.concat("): + match = re.fullmatch(r"pyc\.concat\((.*?)\)\s*:\s*(.*?)\s*->\s*(\S+)", rhs) + if not match or len(lhs) != 1: + raise PYCVerilogError(f"cannot parse concat: {line}") + inputs = _ssa_names(match.group(1)) + out = add_value(lhs[0], match.group(3)) + annotated_types = _parse_types(match.group(2)) + input_values = [_value(values, item) for item in inputs] + if ( + len(annotated_types) != len(input_values) + or any( + value.type != annotated + for value, annotated in zip(input_values, annotated_types) + ) + or sum(value.width for value in input_values) != out.width + ): + raise PYCVerilogError("concat operand/result types are inconsistent") + assigns.append( + f" assign {out.net} = {{{', '.join(_value(values, item).net for item in inputs)}}};" + ) + continue + + if rhs.startswith("pyc.extract "): + match = re.fullmatch( + r"pyc\.extract\s+(.*?)\s*\{(.*?)\}\s*:\s*(\S+)\s*->\s*(\S+)", rhs + ) + if not match or len(lhs) != 1: + raise PYCVerilogError(f"cannot parse extract: {line}") + inputs = _ssa_names(match.group(1)) + if len(inputs) != 1: + raise PYCVerilogError(f"extract expects one operand: {line}") + src = _value(values, inputs[0]) + out = add_value(lhs[0], match.group(4)) + if src.type != match.group(3): + raise PYCVerilogError( + "extract source annotation does not match operand" + ) + lsb = _parse_attr_int(match.group(2), "lsb") + if lsb < 0 or lsb + out.width > src.width: + raise PYCVerilogError("extract slice is outside source width") + if out.width == 1: + assigns.append(f" assign {out.net} = {src.net}[{lsb}];") + else: + assigns.append( + f" assign {out.net} = {src.net}[{lsb + out.width - 1}:{lsb}];" + ) + continue + + if rhs.startswith("pyc.mux "): + match = re.fullmatch( + r"pyc\.mux\s+(.*?)\s*:\s*(.*?)\s*->\s*(\S+)", rhs + ) + if not match or len(lhs) != 1: + raise PYCVerilogError(f"cannot parse mux: {line}") + inputs = _ssa_names(match.group(1)) + if len(inputs) != 3: + raise PYCVerilogError(f"mux expects select, true, false: {line}") + select, true_value, false_value = (_value(values, item) for item in inputs) + annotated_types = _parse_types(match.group(2)) + out = add_value(lhs[0], match.group(3)) + if ( + annotated_types != [select.type, true_value.type, false_value.type] + or select.type != "i1" + or true_value.type != out.type + or false_value.type != out.type + ): + raise PYCVerilogError("mux select/data types are inconsistent") + assigns.append( + f" assign {out.net} = {select.net} ? {true_value.net} : {false_value.net};" + ) + continue + + cast = re.fullmatch( + r"pyc\.(zext|sext|trunc|alias)\s+(%[A-Za-z_][A-Za-z0-9_]*)" + r"(?:\s*\{.*?\})?\s*:\s*(?:(\S+)\s*->\s*)?(\S+)", + rhs, + ) + if cast and len(lhs) == 1: + source = _value(values, cast.group(2)) + annotated_source = cast.group(3) + out = add_value(lhs[0], cast.group(4)) + if annotated_source is not None and source.type != annotated_source: + raise PYCVerilogError("cast source annotation does not match operand") + if cast.group(1) == "alias" and source.type != out.type: + raise PYCVerilogError("alias source/result types must match") + if cast.group(1) == "trunc" and source.width < out.width: + raise PYCVerilogError("trunc cannot widen its operand") + if cast.group(1) in {"zext", "sext"} and source.width > out.width: + raise PYCVerilogError("extension cannot narrow its operand") + if cast.group(1) == "sext" and out.width > source.width: + fill = out.width - source.width + assigns.append( + f" assign {out.net} = " + f"{{{{{fill}{{{source.net}[{source.width - 1}]}}}}, {source.net}}};" + ) + elif cast.group(1) == "trunc" and out.width < source.width: + assigns.append(f" assign {out.net} = {source.net}[{out.width - 1}:0];") + else: + assigns.append(f" assign {out.net} = {source.net};") + continue + + if rhs.startswith("pyc.assign "): + match = re.fullmatch( + r"pyc\.assign\s+(.*?),\s*(%[A-Za-z_][A-Za-z0-9_]*)\s*:\s*(\S+)", rhs + ) + if not match: + raise PYCVerilogError(f"cannot parse assign: {line}") + target = _value(values, match.group(1).strip()) + source = _value(values, match.group(2)) + if target.type != match.group(3) or source.type != target.type: + raise PYCVerilogError("assign source/target types are inconsistent") + assigns.append(f" assign {target.net} = {source.net};") + continue + + binary = re.fullmatch( + r"pyc\.(and|or|xor|add|sub|mul|eq|ne|ult|slt|ule|ugt|uge)\s+" + r"(.*?)\s*:\s*(.*?)\s*->\s*(\S+)", + rhs, + ) + if binary and len(lhs) == 1: + inputs = _ssa_names(binary.group(2)) + if len(inputs) != 2: + raise PYCVerilogError(f"binary PYC op expects two operands: {line}") + left, right = (_value(values, item) for item in inputs) + operand_types = _parse_types(binary.group(3)) + if operand_types != [left.type, right.type]: + raise PYCVerilogError("binary operand annotations are inconsistent") + comparison = binary.group(1) in { + "eq", + "ne", + "ult", + "slt", + "ule", + "ugt", + "uge", + } + out = add_value(lhs[0], binary.group(4)) + if comparison and out.type != "i1": + raise PYCVerilogError("comparison result must be i1") + if not comparison and out.type != left.type: + raise PYCVerilogError("binary result type must match operands") + operator = { + "and": "&", + "or": "|", + "xor": "^", + "add": "+", + "sub": "-", + "mul": "*", + "eq": "==", + "ne": "!=", + "ult": "<", + "slt": "<", + "ule": "<=", + "ugt": ">", + "uge": ">=", + }[binary.group(1)] + if binary.group(1) == "slt": + expression = f"$signed({left.net}) < $signed({right.net})" + else: + expression = f"{left.net} {operator} {right.net}" + assigns.append(f" assign {out.net} = {expression};") + continue + + unary = re.fullmatch(r"pyc\.not\s+(.*?)\s*:\s*(\S+)", rhs) + if unary and len(lhs) == 1: + inputs = _ssa_names(unary.group(1)) + if len(inputs) != 1: + raise PYCVerilogError(f"not expects one operand: {line}") + src = _value(values, inputs[0]) + out = add_value(lhs[0], unary.group(2)) + if src.type != out.type: + raise PYCVerilogError("not source/result types must match") + assigns.append(f" assign {out.net} = ~{src.net};") + continue + + raise PYCVerilogError(f"unsupported canonical PYC operation: {line}") + + if returns is None: + raise PYCVerilogError("PYC function has no return") + if len(returns) != len(module.results): + raise PYCVerilogError("PYC return count does not match function result_names") + for result, returned in zip(module.results, returns): + source = _value(values, returned) + if result.type not in ("!pyc.clock", "!pyc.reset"): + seen_outputs.add(result.net) + assigns.append(f" assign {result.net} = {source.net};") + + ports = [_port_decl("input", value) for value in module.args] + ports.extend(_port_decl("output", value) for value in module.results) + # The output intentionally embeds several primitive modules after the + # top-level module. Tell Verilator that the file name only identifies the + # requested top module; secondary runtime modules are expected here. + text = [ + "// Generated by agentic-circuit PYC compatibility backend", + f"// Source function: {module.name}", + "/* verilator lint_off DECLFILENAME */", + "", + f"module {module.name} (", + ",\n".join(ports), + ");", + ] + text.extend(declarations) + text.extend(assigns) + text.extend(assertions) + text.append("") + text.extend(instances) + text.extend(["", "endmodule", ""]) + text.extend(_runtime_sources(runtime_dir)) + text.extend(["", "/* verilator lint_on DECLFILENAME */", ""]) + return "\n".join(text) + + +def _default_pycgen() -> str: + script = Path(__file__).resolve() + candidates = ( + script.parents[1] / "build" / "dev-llvm22" / "bin" / "acir-queue-pycgen", + script.parent / "acir-queue-pycgen", + ) + for candidate in candidates: + if candidate.is_file(): + return str(candidate) + return shutil.which("acir-queue-pycgen") or "acir-queue-pycgen" + + +def _default_runtime_dir() -> Path: + script = Path(__file__).resolve() + candidates = ( + # Source-tree invocation. + script.parents[1] / "resources" / "pyc_runtime" / "verilog", + # Installed invocation: /bin/script and + # /share/AgenticCircuit/pyc_runtime/verilog. + script.parents[1] / "share" / "AgenticCircuit" / "pyc_runtime" / "verilog", + ) + for candidate in candidates: + if candidate.is_dir(): + return candidate + # Keep a useful diagnostic from emit_verilog if packaging is incomplete. + return candidates[0] + + +def _write_atomic(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary = tempfile.mkstemp( + dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", text=True + ) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + prog="acir-queue-veriloggen.py", description=__doc__ + ) + parser.add_argument( + "input", + type=Path, + help="frozen Queue ACIR MLIR, or canonical PYC with --pyc-input", + ) + parser.add_argument( + "-o", + "--output", + type=Path, + default=Path("-"), + help="output Verilog path (default: stdout)", + ) + parser.add_argument( + "--pycgen", default=_default_pycgen(), help="acir-queue-pycgen executable" + ) + parser.add_argument( + "--timeout", + type=float, + default=120.0, + help="maximum seconds allowed for ACIR-to-PYC lowering (default: 120)", + ) + parser.add_argument( + "--pyc-input", + action="store_true", + help="treat input as canonical PYC instead of ACIR", + ) + parser.add_argument( + "--emit-pyc", type=Path, help="also save the canonical PYC artifact" + ) + parser.add_argument("--runtime-dir", type=Path, default=_default_runtime_dir()) + args = parser.parse_args(argv) + + if not math.isfinite(args.timeout) or args.timeout <= 0: + parser.error("--timeout must be a finite positive number") + try: + input_identity = args.input.resolve(strict=True) + except OSError as exc: + parser.error(f"cannot resolve input: {exc}") + destinations = [ + path for path in (args.emit_pyc, args.output) if path and str(path) != "-" + ] + destination_identities = [path.resolve(strict=False) for path in destinations] + if input_identity in destination_identities: + parser.error("input and output paths must be different") + if len(destination_identities) != len(set(destination_identities)): + parser.error("--emit-pyc and --output must be different paths") + + if args.pyc_input: + try: + pyc_text = args.input.read_text(encoding="utf-8") + except (OSError, UnicodeError) as exc: + parser.error(f"cannot read PYC input: {exc}") + else: + try: + completed = subprocess.run( + [args.pycgen, str(args.input)], + check=True, + capture_output=True, + text=True, + timeout=args.timeout, + ) + except FileNotFoundError: + parser.error(f"cannot find ACIR-to-PYC generator: {args.pycgen}") + except subprocess.TimeoutExpired: + parser.error( + f"ACIR-to-PYC generator exceeded --timeout={args.timeout:g}s; " + "reduce the input or run the single queue target with -j1" + ) + except subprocess.CalledProcessError as exc: + sys.stderr.write(exc.stderr or "") + return 1 + pyc_text = completed.stdout + if args.emit_pyc: + try: + _write_atomic(args.emit_pyc, pyc_text) + except OSError as exc: + parser.error(f"cannot publish PYC output: {exc}") + + try: + output = emit_verilog(parse_pyc_module(pyc_text), args.runtime_dir) + except (OSError, PYCVerilogError) as exc: + parser.error(str(exc)) + if str(args.output) == "-": + sys.stdout.write(output) + else: + try: + _write_atomic(args.output, output) + except OSError as exc: + parser.error(f"cannot publish Verilog output: {exc}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/tools/import-davincioo-pto-trace.py b/components/agentic-circuit/tools/import-davincioo-pto-trace.py new file mode 100644 index 00000000..2d94e795 --- /dev/null +++ b/components/agentic-circuit/tools/import-davincioo-pto-trace.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Convert pinned DavinciOO PTO JSONL into canonical `pto-trace@0.1`.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from pto_trace_adapter import AdapterError, publish_davincioo_trace + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser( + description="convert DavinciOO PTO JSONL to canonical pto-trace@0.1", + usage="%(prog)s INPUT OUTPUT [--source-program ID]", + ) + result.add_argument("input", metavar="INPUT", type=Path) + result.add_argument("output", metavar="OUTPUT", type=Path) + result.add_argument( + "--source-program", + metavar="ID", + help="stable source-program identity (default: raw input SHA-256)", + ) + return result + + +def main(arguments: list[str] | None = None) -> int: + options = parser().parse_args(arguments) + try: + publish_davincioo_trace( + options.input, + options.output, + source_program=options.source_program, + ) + except AdapterError as error: + print(error, file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/tools/pack-perfetto-trace.py b/components/agentic-circuit/tools/pack-perfetto-trace.py new file mode 100644 index 00000000..bb87990d --- /dev/null +++ b/components/agentic-circuit/tools/pack-perfetto-trace.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Pack committed gfsim event JSONL into canonical Perfetto trace JSON.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from perfetto_trace import PerfettoTraceError, publish_perfetto_trace + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser( + description="pack committed gfsim event JSONL for Perfetto", + usage="%(prog)s INPUT OUTPUT", + ) + result.add_argument("input", metavar="INPUT", type=Path) + result.add_argument("output", metavar="OUTPUT", type=Path) + return result + + +def main(arguments: list[str] | None = None) -> int: + options = parser().parse_args(arguments) + try: + publish_perfetto_trace(options.input, options.output) + except PerfettoTraceError as error: + print(error, file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/tools/perfetto_trace.py b/components/agentic-circuit/tools/perfetto_trace.py new file mode 100644 index 00000000..54256e42 --- /dev/null +++ b/components/agentic-circuit/tools/perfetto_trace.py @@ -0,0 +1,405 @@ +"""Strict deterministic packing for committed gfsim event JSONL. + +This module is repository tooling, not part of the installed public package. +It validates and wraps existing Chrome Trace Event records without synthesizing +timing, metadata, or ordering. +""" + +from __future__ import annotations + +import errno +import json +import os +import re +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import NoReturn + +from agentic_circuit._canonical_json import JsonValue, canonical_json_bytes + + +_SAFE_INTEGER_MAX = (1 << 53) - 1 +_MAX_DELTA = 1024 +_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$") +_COMMITTED_ARGUMENTS = { + "gfsim_epoch_time", + "gfsim_epoch_delta", + "gfsim_object_id", + "gfsim_local_committed_index", +} +_OPTIONAL_COMMITTED_ARGUMENTS = {"gfsim_root_sequence_id"} + + +@dataclass(frozen=True, slots=True) +class PerfettoTraceLimits: + max_document_bytes: int = 1 << 26 + max_line_bytes: int = 1 << 20 + max_event_count: int = 1 << 20 + + +class PerfettoTraceError(ValueError): + """A stable Perfetto packer diagnostic.""" + + def __init__(self, code: str, message: str, *, line: int | None = None) -> None: + self.code = code + self.line = line + location = f" line {line}" if line is not None else "" + super().__init__(f"{code}{location}: {message}") + + +def _fail(code: str, message: str, *, line: int | None = None) -> NoReturn: + raise PerfettoTraceError(code, message, line=line) + + +def _check_limits(limits: PerfettoTraceLimits) -> None: + for name in limits.__dataclass_fields__: + value = getattr(limits, name) + if type(value) is not int or value < 0: + _fail("ACPERFETTO-LIMIT", f"{name} must be a non-negative integer") + + +def _pairs(line: int): + def reject_duplicates(pairs: list[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + _fail( + "ACPERFETTO-JSON", + f"duplicate object member {key!r}", + line=line, + ) + result[key] = value + return result + + return reject_duplicates + + +def _exact_object( + value: object, keys: set[str], *, line: int, description: str +) -> dict[str, object]: + if type(value) is not dict or set(value) != keys: + _fail( + "ACPERFETTO-SCHEMA", + f"{description} has unknown or missing fields", + line=line, + ) + return value + + +def _safe_uint(value: object, *, line: int, name: str) -> int: + if type(value) is not int or not 0 <= value <= _SAFE_INTEGER_MAX: + _fail( + "ACPERFETTO-SCHEMA", + f"{name} must be an unsigned portable I-JSON integer", + line=line, + ) + return value + + +def _name(value: object, *, line: int, field: str) -> str: + if type(value) is not str or not _NAME.fullmatch(value): + _fail("ACPERFETTO-SCHEMA", f"{field} is not canonical", line=line) + return value + + +def _unicode_string(value: object, *, line: int, field: str) -> str: + if type(value) is not str: + _fail("ACPERFETTO-SCHEMA", f"{field} must be a string", line=line) + try: + value.encode("utf-8", errors="strict") + except UnicodeEncodeError: + _fail( + "ACPERFETTO-SCHEMA", + f"{field} contains a non-Unicode scalar value", + line=line, + ) + return value + + +def _metadata(value: dict[str, object], *, line: int) -> tuple[str, int | None]: + name = value.get("name") + if name == "process_name": + metadata = _exact_object( + value, + {"name", "ph", "pid", "args"}, + line=line, + description="process metadata", + ) + thread = None + elif name == "thread_name": + metadata = _exact_object( + value, + {"name", "ph", "pid", "tid", "args"}, + line=line, + description="thread metadata", + ) + thread = _safe_uint(metadata["tid"], line=line, name="tid") + else: + _fail("ACPERFETTO-SCHEMA", "metadata name is unsupported", line=line) + if ( + metadata["ph"] != "M" + or type(metadata["pid"]) is not int + or metadata["pid"] != 0 + ): + _fail("ACPERFETTO-SCHEMA", "metadata identity is invalid", line=line) + arguments = _exact_object( + metadata["args"], {"name"}, line=line, description="metadata arguments" + ) + display_name = _unicode_string( + arguments["name"], line=line, field="metadata display name" + ) + if not display_name: + _fail("ACPERFETTO-SCHEMA", "metadata display name is empty", line=line) + return name, thread + + +def _argument_value(value: object, *, line: int, name: str) -> JsonValue: + if type(value) is bool: + return value + if type(value) is str: + return _unicode_string(value, line=line, field=f"argument {name!r}") + if type(value) is int and -_SAFE_INTEGER_MAX <= value <= _SAFE_INTEGER_MAX: + return value + _fail( + "ACPERFETTO-SCHEMA", + f"argument {name!r} is not a runtime scalar", + line=line, + ) + + +def _committed_event( + value: dict[str, object], *, line: int +) -> tuple[int, int, int, int]: + phase = value.get("ph") + phase_fields = { + "i": {"s"}, + "X": {"dur"}, + "C": set(), + "s": {"id"}, + "f": {"id", "bp"}, + } + if type(phase) is not str or phase not in phase_fields: + _fail("ACPERFETTO-SCHEMA", "event phase is unsupported", line=line) + fields = {"name", "cat", "ph", "ts", "pid", "tid", "args"} + fields.update(phase_fields[phase]) + event = _exact_object(value, fields, line=line, description="event") + _name(event["name"], line=line, field="name") + _name(event["cat"], line=line, field="category") + if type(event["pid"]) is not int or event["pid"] != 0: + _fail("ACPERFETTO-SCHEMA", "runtime event pid must be zero", line=line) + timestamp = _safe_uint(event["ts"], line=line, name="ts") + thread = _safe_uint(event["tid"], line=line, name="tid") + arguments = event["args"] + if type(arguments) is not dict: + _fail("ACPERFETTO-SCHEMA", "event args must be an object", line=line) + keys = set(arguments) + if not _COMMITTED_ARGUMENTS.issubset(keys): + _fail("ACPERFETTO-SCHEMA", "committed identity is incomplete", line=line) + reserved = {key for key in keys if key.startswith("gfsim_")} + if not reserved.issubset( + _COMMITTED_ARGUMENTS | _OPTIONAL_COMMITTED_ARGUMENTS + ): + _fail("ACPERFETTO-SCHEMA", "unknown runtime argument", line=line) + for name, argument in arguments.items(): + _name(name, line=line, field="argument name") + _argument_value(argument, line=line, name=name) + + time = _safe_uint(arguments["gfsim_epoch_time"], line=line, name="epoch time") + delta = _safe_uint( + arguments["gfsim_epoch_delta"], line=line, name="epoch delta" + ) + owner = _safe_uint( + arguments["gfsim_object_id"], line=line, name="object id" + ) + local_index = _safe_uint( + arguments["gfsim_local_committed_index"], + line=line, + name="owner-local committed index", + ) + if delta >= _MAX_DELTA or owner != thread or time > ( + _SAFE_INTEGER_MAX - delta + ) // _MAX_DELTA or timestamp != time * _MAX_DELTA + delta: + _fail( + "ACPERFETTO-SCHEMA", + "runtime timestamp or object identity is inconsistent", + line=line, + ) + if "gfsim_root_sequence_id" in arguments: + _safe_uint( + arguments["gfsim_root_sequence_id"], + line=line, + name="root sequence id", + ) + + if phase == "i" and event["s"] != "t": + _fail("ACPERFETTO-SCHEMA", "instant scope must be thread", line=line) + if phase == "X": + _safe_uint(event["dur"], line=line, name="duration") + if phase in {"s", "f"}: + _safe_uint(event["id"], line=line, name="flow id") + if phase == "f" and event["bp"] != "e": + _fail("ACPERFETTO-SCHEMA", "flow end binding point is invalid", line=line) + return time, delta, owner, local_index + + +def pack_event_jsonl( + data: bytes, limits: PerfettoTraceLimits = PerfettoTraceLimits() +) -> bytes: + """Validate runtime event JSONL and return canonical Perfetto JSON bytes.""" + + _check_limits(limits) + if type(data) is not bytes: + _fail("ACPERFETTO-SCHEMA", "source must be bytes") + if len(data) > limits.max_document_bytes: + _fail("ACPERFETTO-LIMIT", "document byte limit exceeded") + try: + text = data.decode("utf-8", errors="strict") + except UnicodeDecodeError as error: + _fail("ACPERFETTO-JSON", f"source is not UTF-8 at byte {error.start}") + + events: list[JsonValue] = [] + previous: tuple[int, int, int, int] | None = None + timeline_started = False + process_seen = False + thread_ids: set[int] = set() + for line_number, line in enumerate(text.splitlines(), 1): + if not line.strip(): + _fail("ACPERFETTO-JSON", "blank JSONL record", line=line_number) + if len(line.encode("utf-8")) > limits.max_line_bytes: + _fail("ACPERFETTO-LIMIT", "line byte limit exceeded", line=line_number) + if len(events) >= limits.max_event_count: + _fail("ACPERFETTO-LIMIT", "event count limit exceeded", line=line_number) + try: + value = json.loads( + line, + object_pairs_hook=_pairs(line_number), + parse_constant=lambda token: _fail( + "ACPERFETTO-JSON", + f"non-finite JSON number {token}", + line=line_number, + ), + ) + except PerfettoTraceError: + raise + except (json.JSONDecodeError, RecursionError) as error: + _fail("ACPERFETTO-JSON", f"invalid JSON: {error}", line=line_number) + if type(value) is not dict: + _fail( + "ACPERFETTO-SCHEMA", + "event record must be an object", + line=line_number, + ) + + if value.get("ph") == "M": + if timeline_started: + _fail( + "ACPERFETTO-ORDER", + "metadata must precede committed events", + line=line_number, + ) + kind, thread_id = _metadata(value, line=line_number) + if kind == "process_name": + if process_seen: + _fail( + "ACPERFETTO-ORDER", + "duplicate process metadata", + line=line_number, + ) + process_seen = True + else: + assert thread_id is not None + if thread_id in thread_ids: + _fail( + "ACPERFETTO-ORDER", + "duplicate thread metadata", + line=line_number, + ) + thread_ids.add(thread_id) + else: + timeline_started = True + key = _committed_event(value, line=line_number) + if previous is not None and previous >= key: + _fail( + "ACPERFETTO-ORDER", + "committed event keys are not strictly increasing", + line=line_number, + ) + previous = key + events.append(value) + return canonical_json_bytes({"traceEvents": events}) + b"\n" + + +def _regular_input(path: Path) -> bytes: + try: + if path.is_symlink() or not path.is_file(): + raise OSError("input is not a regular file") + return path.read_bytes() + except OSError as error: + _fail("ACPERFETTO-IO", f"cannot read input: {error}") + + +def _fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY) + try: + try: + os.fsync(descriptor) + except OSError as error: + if error.errno not in (errno.EINVAL, errno.ENOTSUP): + raise + finally: + os.close(descriptor) + + +def publish_perfetto_trace( + input_path: Path, + output_path: Path, + *, + limits: PerfettoTraceLimits = PerfettoTraceLimits(), +) -> None: + """Pack one regular input and atomically publish one canonical file.""" + + source = input_path.absolute() + destination = output_path.absolute() + try: + source_identity = source.resolve(strict=True) + destination_identity = destination.resolve(strict=False) + except OSError as error: + _fail("ACPERFETTO-IO", f"cannot resolve path: {error}") + if source_identity == destination_identity: + _fail("ACPERFETTO-IO", "input and output must be different files") + parent = destination.parent + if parent.is_symlink() or not parent.is_dir(): + _fail("ACPERFETTO-IO", "output parent must be an existing directory") + if destination.exists() and ( + destination.is_symlink() or not destination.is_file() + ): + _fail("ACPERFETTO-IO", "existing output must be a regular file") + + output = pack_event_jsonl(_regular_input(source), limits) + descriptor = -1 + stage: Path | None = None + try: + descriptor, stage_name = tempfile.mkstemp( + prefix=f".{destination.name}.", suffix=".tmp", dir=parent + ) + stage = Path(stage_name) + os.fchmod(descriptor, 0o644) + with os.fdopen(descriptor, "wb") as stream: + descriptor = -1 + stream.write(output) + stream.flush() + os.fsync(stream.fileno()) + os.replace(stage, destination) + stage = None + _fsync_directory(parent) + except OSError as error: + _fail("ACPERFETTO-IO", f"cannot publish output: {error}") + finally: + if descriptor >= 0: + os.close(descriptor) + if stage is not None: + try: + stage.unlink(missing_ok=True) + except OSError: + pass diff --git a/components/agentic-circuit/tools/pto_trace_adapter.py b/components/agentic-circuit/tools/pto_trace_adapter.py new file mode 100644 index 00000000..f2411ec0 --- /dev/null +++ b/components/agentic-circuit/tools/pto_trace_adapter.py @@ -0,0 +1,571 @@ +"""Strict DavinciOO JSONL to canonical PTO trace conversion. + +This module is repository tooling, not part of the installed public package. +The accepted source shape is pinned by the workspace design document. +""" + +from __future__ import annotations + +import hashlib +import errno +import json +import os +import re +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import NoReturn + +from agentic_circuit._canonical_json import ( + JsonValue, + canonical_json_bytes, + sha256_bytes, + validate_ijson_value, +) + + +DAVINCIOO_PRODUCER = "davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb" +PTO_IDENTITY = "pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3" +DATA_LAYOUT = "davincioo-tile-address-v1" + +_SAFE_INTEGER_MAX = (1 << 53) - 1 +_UINT64_MAX = (1 << 64) - 1 +_RECORD_KEYS = { + "block_idx", + "sequence_id", + "opcode", + "input_tiles", + "scalar_inputs", + "output_tiles", +} +_TILE_KEYS = {"address", "shape", "layout", "dtype"} +_SCALAR_KEYS = {"dtype", "value"} +_OPCODE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*$") +_ADDRESS = re.compile(r"^0x(?:0|[1-9a-f][0-9a-f]*)$") + + +@dataclass(frozen=True, slots=True) +class AdapterLimits: + max_document_bytes: int = 1 << 24 + max_line_bytes: int = 1 << 20 + max_record_count: int = 65536 + max_tiles_per_record: int = 4096 + max_scalars_per_record: int = 4096 + max_shape_rank: int = 64 + max_string_bytes: int = 1 << 18 + + +class AdapterError(ValueError): + """A stable adapter diagnostic.""" + + def __init__( + self, code: str, message: str, *, line: int | None = None, pointer: str = "" + ) -> None: + self.code = code + self.line = line + self.pointer = pointer + location = "" + if line is not None: + location += f" line {line}" + if pointer: + location += f" {pointer}" + super().__init__(f"{code}{location}: {message}") + + +def _fail( + code: str, message: str, *, line: int | None = None, pointer: str = "" +) -> NoReturn: + raise AdapterError(code, message, line=line, pointer=pointer) + + +def _check_limits(limits: AdapterLimits) -> None: + for name in limits.__dataclass_fields__: + value = getattr(limits, name) + if type(value) is not int or value < 0: + _fail("ACTRACE-ADAPTER-LIMIT", f"{name} must be a non-negative integer") + + +def _pairs(line_number: int): + def reject_duplicates(pairs: list[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + _fail( + "ACTRACE-ADAPTER-JSON", + f"duplicate object member {key!r}", + line=line_number, + ) + result[key] = value + return result + + return reject_duplicates + + +def _exact_object( + value: object, keys: set[str], *, line: int, pointer: str +) -> dict[str, object]: + if type(value) is not dict: + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "expected an object", + line=line, + pointer=pointer, + ) + if set(value) != keys: + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "object has unknown or missing fields", + line=line, + pointer=pointer, + ) + return value + + +def _safe_uint(value: object, *, line: int, pointer: str, sequence: bool = False) -> int: + code = "ACTRACE-ADAPTER-SEQUENCE" if sequence else "ACTRACE-ADAPTER-SCHEMA" + if type(value) is not int or not 0 <= value <= _SAFE_INTEGER_MAX: + _fail( + code, + "expected an unsigned portable I-JSON integer", + line=line, + pointer=pointer, + ) + return value + + +def _string( + value: object, + limits: AdapterLimits, + *, + line: int, + pointer: str, + allow_empty: bool = False, +) -> str: + if type(value) is not str or (not allow_empty and not value): + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "expected a non-empty string" if not allow_empty else "expected a string", + line=line, + pointer=pointer, + ) + try: + byte_count = len(value.encode("utf-8", errors="strict")) + except UnicodeEncodeError: + _fail( + "ACTRACE-ADAPTER-JSON", + "string contains a non-Unicode scalar value", + line=line, + pointer=pointer, + ) + if byte_count > limits.max_string_bytes: + _fail( + "ACTRACE-ADAPTER-LIMIT", + "string byte limit exceeded", + line=line, + pointer=pointer, + ) + return value + + +def _tile( + value: object, limits: AdapterLimits, *, line: int, pointer: str +) -> dict[str, JsonValue]: + tile = _exact_object(value, _TILE_KEYS, line=line, pointer=pointer) + address = _string(tile["address"], limits, line=line, pointer=pointer + "/address") + if not _ADDRESS.fullmatch(address): + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "address must be canonical lowercase hexadecimal", + line=line, + pointer=pointer + "/address", + ) + if int(address[2:], 16) > _UINT64_MAX: + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "address exceeds unsigned 64-bit range", + line=line, + pointer=pointer + "/address", + ) + shape = tile["shape"] + if type(shape) is not list: + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "shape must be an array", + line=line, + pointer=pointer + "/shape", + ) + if len(shape) > limits.max_shape_rank: + _fail( + "ACTRACE-ADAPTER-LIMIT", + "shape rank limit exceeded", + line=line, + pointer=pointer + "/shape", + ) + dimensions: list[int] = [] + for index, dimension in enumerate(shape): + decoded = _safe_uint( + dimension, line=line, pointer=f"{pointer}/shape/{index}" + ) + if decoded == 0: + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "shape dimensions must be positive", + line=line, + pointer=f"{pointer}/shape/{index}", + ) + dimensions.append(decoded) + return { + "address": address, + "shape": dimensions, + "layout": _string( + tile["layout"], limits, line=line, pointer=pointer + "/layout" + ), + "dtype": _string( + tile["dtype"], limits, line=line, pointer=pointer + "/dtype" + ), + } + + +def _scalar( + value: object, limits: AdapterLimits, *, line: int, pointer: str +) -> dict[str, JsonValue]: + scalar = _exact_object(value, _SCALAR_KEYS, line=line, pointer=pointer) + return { + "dtype": _string( + scalar["dtype"], limits, line=line, pointer=pointer + "/dtype" + ), + "value": _string( + scalar["value"], + limits, + line=line, + pointer=pointer + "/value", + allow_empty=True, + ), + } + + +def _array( + value: object, + *, + maximum: int, + line: int, + pointer: str, + parser: object, +) -> list[dict[str, JsonValue]]: + if type(value) is not list: + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "expected an array", + line=line, + pointer=pointer, + ) + if len(value) > maximum: + _fail( + "ACTRACE-ADAPTER-LIMIT", + "array element limit exceeded", + line=line, + pointer=pointer, + ) + return [ + parser(item, line=line, pointer=f"{pointer}/{index}") + for index, item in enumerate(value) + ] + + +def _record(value: object, limits: AdapterLimits, line: int) -> dict[str, JsonValue]: + source = _exact_object(value, _RECORD_KEYS, line=line, pointer="") + block_idx = _safe_uint(source["block_idx"], line=line, pointer="/block_idx") + sequence_id = _safe_uint( + source["sequence_id"], line=line, pointer="/sequence_id", sequence=True + ) + opcode = _string(source["opcode"], limits, line=line, pointer="/opcode") + if not _OPCODE.fullmatch(opcode): + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "opcode is not a canonical PTO spelling", + line=line, + pointer="/opcode", + ) + + def parse_tile(item, **where): + return _tile(item, limits, **where) + + def parse_scalar(item, **where): + return _scalar(item, limits, **where) + input_tiles = _array( + source["input_tiles"], + maximum=limits.max_tiles_per_record, + line=line, + pointer="/input_tiles", + parser=parse_tile, + ) + scalar_inputs = _array( + source["scalar_inputs"], + maximum=limits.max_scalars_per_record, + line=line, + pointer="/scalar_inputs", + parser=parse_scalar, + ) + output_tiles = _array( + source["output_tiles"], + maximum=limits.max_tiles_per_record, + line=line, + pointer="/output_tiles", + parser=parse_tile, + ) + return { + "block_idx": block_idx, + "sequence_id": sequence_id, + "opcode": opcode, + "input_tiles": input_tiles, + "scalar_inputs": scalar_inputs, + "output_tiles": output_tiles, + } + + +def parse_davincioo_jsonl( + data: bytes, limits: AdapterLimits = AdapterLimits() +) -> tuple[dict[str, JsonValue], ...]: + """Parse and validate the pinned DavinciOO JSONL record shape.""" + + _check_limits(limits) + if type(data) is not bytes: + _fail("ACTRACE-ADAPTER-SCHEMA", "source must be bytes") + if len(data) > limits.max_document_bytes: + _fail("ACTRACE-ADAPTER-LIMIT", "document byte limit exceeded") + try: + text = data.decode("utf-8", errors="strict") + except UnicodeDecodeError as error: + _fail( + "ACTRACE-ADAPTER-JSON", + f"source is not UTF-8 at byte {error.start}", + ) + + records: list[dict[str, JsonValue]] = [] + for line_number, line in enumerate(text.splitlines(), 1): + if not line.strip(): + continue + if len(line.encode("utf-8")) > limits.max_line_bytes: + _fail( + "ACTRACE-ADAPTER-LIMIT", + "line byte limit exceeded", + line=line_number, + ) + if len(records) >= limits.max_record_count: + _fail( + "ACTRACE-ADAPTER-LIMIT", + "record count limit exceeded", + line=line_number, + ) + try: + value = json.loads( + line, + object_pairs_hook=_pairs(line_number), + parse_constant=lambda token: _fail( + "ACTRACE-ADAPTER-JSON", + f"non-finite JSON number {token}", + line=line_number, + ), + ) + except AdapterError: + raise + except (json.JSONDecodeError, RecursionError) as error: + _fail( + "ACTRACE-ADAPTER-JSON", + f"invalid JSON: {error}", + line=line_number, + ) + records.append(_record(value, limits, line_number)) + return tuple(records) + + +def _tile_operand(block_idx: int, tile: dict[str, JsonValue]) -> dict[str, JsonValue]: + return { + "kind": "tile", + "id": f"block/{block_idx}/tile/{tile['address']}", + } + + +def _canonical_record(source: dict[str, JsonValue]) -> dict[str, JsonValue]: + block_idx = source["block_idx"] + input_tiles = source["input_tiles"] + scalar_inputs = source["scalar_inputs"] + output_tiles = source["output_tiles"] + assert type(block_idx) is int + assert type(input_tiles) is list + assert type(scalar_inputs) is list + assert type(output_tiles) is list + + operands: list[JsonValue] = [] + operands.extend(_tile_operand(block_idx, tile) for tile in input_tiles) + operands.extend( + {"kind": "immediate", "type": scalar["dtype"], "value": scalar["value"]} + for scalar in scalar_inputs + ) + operands.extend(_tile_operand(block_idx, tile) for tile in output_tiles) + roles = ( + ["input_tile"] * len(input_tiles) + + ["scalar_input"] * len(scalar_inputs) + + ["output_tile"] * len(output_tiles) + ) + return { + "sequence_id": source["sequence_id"], + "opcode": source["opcode"], + "operands": operands, + "dependencies": [], + "attributes": { + "davincioo": { + "block_idx": block_idx, + "input_tiles": input_tiles, + "scalar_inputs": scalar_inputs, + "output_tiles": output_tiles, + "operand_roles": roles, + } + }, + } + + +def _validate_target(document: dict[str, JsonValue]) -> None: + if set(document) != {"schema", "version", "contract_epoch", "metadata", "records"}: + _fail("ACTRACE-ADAPTER-TARGET", "canonical trace envelope is not closed") + if ( + document["schema"] != "pto-trace" + or document["version"] != "0.1" + or document["contract_epoch"] != "0.3" + or type(document["metadata"]) is not dict + or type(document["records"]) is not list + ): + _fail("ACTRACE-ADAPTER-TARGET", "canonical trace identity is invalid") + try: + validate_ijson_value(document) + except ValueError as error: + _fail("ACTRACE-ADAPTER-TARGET", str(error)) + + +def convert_davincioo_trace( + data: bytes, + *, + source_program: str | None = None, + limits: AdapterLimits = AdapterLimits(), +) -> bytes: + """Return one newline-terminated canonical `pto-trace@0.1` document.""" + + if source_program is not None and (type(source_program) is not str or not source_program): + _fail( + "ACTRACE-ADAPTER-SCHEMA", + "source_program must be a non-empty string", + pointer="/metadata/source_program", + ) + sources = parse_davincioo_jsonl(data, limits) + previous: int | None = None + records: list[JsonValue] = [] + for line_number, source in enumerate(sources, 1): + sequence_id = source["sequence_id"] + assert type(sequence_id) is int + if previous is not None and sequence_id <= previous: + _fail( + "ACTRACE-ADAPTER-SEQUENCE", + "sequence_id values must be unique and strictly increasing", + line=line_number, + pointer="/sequence_id", + ) + previous = sequence_id + records.append(_canonical_record(source)) + + content_hash = sha256_bytes(canonical_json_bytes(records)) + identity = source_program or "sha256:" + hashlib.sha256(data).hexdigest() + document: dict[str, JsonValue] = { + "schema": "pto-trace", + "version": "0.1", + "contract_epoch": "0.3", + "metadata": { + "producer": DAVINCIOO_PRODUCER, + "pto_identity": PTO_IDENTITY, + "source_program": identity, + "data_layout": DATA_LAYOUT, + "record_count": len(records), + "content_hash": content_hash, + }, + "records": records, + } + _validate_target(document) + return canonical_json_bytes(document) + b"\n" + + +def _regular_input(path: Path) -> bytes: + try: + if path.is_symlink() or not path.is_file(): + raise OSError("input is not a regular file") + return path.read_bytes() + except OSError as error: + _fail("ACTRACE-ADAPTER-IO", f"cannot read input: {error}") + + +def _fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY) + try: + try: + os.fsync(descriptor) + except OSError as error: + if error.errno not in (errno.EINVAL, errno.ENOTSUP): + raise + finally: + os.close(descriptor) + + +def publish_davincioo_trace( + input_path: Path, + output_path: Path, + *, + source_program: str | None = None, + limits: AdapterLimits = AdapterLimits(), +) -> None: + """Convert one regular input and atomically publish one canonical file.""" + + source = input_path.absolute() + destination = output_path.absolute() + try: + source_identity = source.resolve(strict=True) + except OSError as error: + _fail("ACTRACE-ADAPTER-IO", f"cannot resolve input: {error}") + try: + destination_identity = destination.resolve(strict=False) + except OSError as error: + _fail("ACTRACE-ADAPTER-IO", f"cannot resolve output: {error}") + if source_identity == destination_identity: + _fail("ACTRACE-ADAPTER-IO", "input and output must be different files") + parent = destination.parent + if parent.is_symlink() or not parent.is_dir(): + _fail("ACTRACE-ADAPTER-IO", "output parent must be an existing directory") + if destination.exists() and (destination.is_symlink() or not destination.is_file()): + _fail("ACTRACE-ADAPTER-IO", "existing output must be a regular file") + + output = convert_davincioo_trace( + _regular_input(source), source_program=source_program, limits=limits + ) + descriptor = -1 + stage: Path | None = None + try: + descriptor, stage_name = tempfile.mkstemp( + prefix=f".{destination.name}.", suffix=".tmp", dir=parent + ) + stage = Path(stage_name) + os.fchmod(descriptor, 0o644) + with os.fdopen(descriptor, "wb") as stream: + descriptor = -1 + stream.write(output) + stream.flush() + os.fsync(stream.fileno()) + os.replace(stage, destination) + stage = None + _fsync_directory(parent) + except OSError as error: + _fail("ACTRACE-ADAPTER-IO", f"cannot publish output: {error}") + finally: + if descriptor >= 0: + os.close(descriptor) + if stage is not None: + try: + stage.unlink(missing_ok=True) + except OSError: + pass diff --git a/components/agentic-circuit/tools/run-davincioo.py b/components/agentic-circuit/tools/run-davincioo.py new file mode 100755 index 00000000..793d2e5c --- /dev/null +++ b/components/agentic-circuit/tools/run-davincioo.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +"""Run the generated DavinciOO gfsim on one canonical PTO trace.""" + +from __future__ import annotations + +import argparse +from html import escape +import json +from pathlib import Path +import shutil +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "src")) + +from agentic_circuit._canonical_json import canonical_json_bytes # noqa: E402 +from examples.architecture.davincioo_jit import specialization # noqa: E402 +from tools.pto_trace_adapter import convert_davincioo_trace # noqa: E402 + + +DEFAULT_TRACE = ( + ROOT + / "references/davincioo-gfsim/upstream/tests/fixtures/traces" + / "examples_intermediate_softmax.pto.trace" +) +DEFAULT_PROJECTION = ROOT / "tests/goldens/davincioo/softmax-projection.json" +HARNESS = ROOT / "examples/architecture/davincioo_trace_harness.cpp" + + +def fixture_header(trace: dict[str, object], projection: dict[str, object]) -> str: + records = trace["records"] + waits = projection["waits_for"] + routes = projection["routes"] + costs = projection["model_cost"] + values = projection["architectural_values"] + if not all(isinstance(item, list) for item in (records, waits, values)): + raise ValueError("trace/projection arrays are malformed") + if len(records) != len(waits) or len(records) != len(values): + raise ValueError("trace and projection record counts differ") + if not isinstance(routes, dict) or not isinstance(costs, dict): + raise ValueError("projection route/cost maps are malformed") + + tokens: list[str] = [] + opcodes: list[str] = [] + for index, raw in enumerate(records): + if not isinstance(raw, dict): + raise ValueError("trace record is not an object") + sequence = raw.get("sequence_id") + opcode = raw.get("opcode") + if sequence != index or not isinstance(opcode, str): + raise ValueError("trace sequence/opcode contract is not canonical") + route = routes.get(opcode) + cost = costs.get(opcode) + wait = waits[index] + value = values[index] + if not all(isinstance(item, int) for item in (route, cost, wait, value)): + raise ValueError(f"projection is incomplete for opcode {opcode}") + if not (0 <= route < 4 and 0 < cost < (1 << 16)): + raise ValueError(f"projection values are out of range for {opcode}") + input_value = (value - 1) & 0xFFFFFFFF + tokens.append( + " ac_generated::PTOInst{" + f"{sequence}, {wait}, {route}, {cost}, {input_value}" + "}" + ) + opcodes.append(json.dumps(opcode)) + + return "\n".join( + ( + "#pragma once", + "", + "#include ", + "#include ", + "", + "namespace davincioo_fixture {", + f"inline constexpr std::array kTokens{{{{", + ",\n".join(tokens), + "}};", + f"inline constexpr std::array kOpcodes{{{{", + " " + ", ".join(opcodes), + "}};", + "} // namespace davincioo_fixture", + "", + ) + ) + + +def parse_harness_output(text: str) -> dict[str, object]: + result: dict[str, object] = {"spans": []} + for line in text.splitlines(): + fields = line.split() + if not fields: + continue + if fields[0] == "SUMMARY" and len(fields) == 3: + result["cycles"] = int(fields[1]) + result["retired_count"] = int(fields[2]) + elif fields[0] == "COMPLETION": + result["completion_order"] = [int(item) for item in fields[1:]] + elif fields[0] == "RETIREMENT": + result["retirement_order"] = [int(item) for item in fields[1:]] + elif fields[0] == "VALUES": + result["architectural_values"] = [int(item) for item in fields[1:]] + elif fields[0] == "SPAN" and len(fields) == 6: + spans = result["spans"] + assert isinstance(spans, list) + spans.append( + { + "sequence": int(fields[1]), + "opcode": fields[2], + "stage": fields[3], + "begin": int(fields[4]), + "end": int(fields[5]), + } + ) + required = { + "cycles", + "retired_count", + "completion_order", + "retirement_order", + "architectural_values", + "spans", + } + if set(result) != required: + raise ValueError(f"harness output is incomplete: {sorted(set(result) ^ required)}") + return result + + +def render_svg(run: dict[str, object], opcodes: list[str]) -> str: + cycles = int(run["cycles"]) + spans = run["spans"] + assert isinstance(spans, list) + lane_height = 25 + left = 180 + top = 62 + scale = max(1.5, min(4.0, 1200.0 / max(1, cycles))) + width = int(left + cycles * scale + 30) + height = top + len(opcodes) * lane_height + 64 + colors = { + "source_wait": "#d1d5db", + "incoming": "#93c5fd", + "decoded": "#60a5fa", + "pipelined": "#818cf8", + "dispatch_scalar": "#fbbf24", + "dispatch_vector": "#f59e0b", + "dispatch_cube": "#d97706", + "dispatch_tma": "#b45309", + "dispatched": "#f97316", + "schedule_execute": "#ef4444", + "completed": "#34d399", + "rob_wait": "#10b981", + "retired": "#059669", + } + lines = [ + f'', + '', + '', + f'Generated DavinciOO PTO trace swimlane — {cycles} cycles', + ] + tick_step = max(1, cycles // 10) + for tick in range(0, cycles + 1, tick_step): + x = left + tick * scale + lines.append( + f'' + ) + lines.append(f'{tick}') + for sequence, opcode in enumerate(opcodes): + y = top + sequence * lane_height + lines.append( + f'{sequence:02d} {escape(opcode)}' + ) + lines.append( + f'' + ) + for raw in spans: + assert isinstance(raw, dict) + sequence = int(raw["sequence"]) + begin = int(raw["begin"]) + end = int(raw["end"]) + stage = str(raw["stage"]) + if end <= begin: + continue + x = left + begin * scale + y = top + sequence * lane_height + 3 + span_width = max(1.0, (end - begin) * scale) + color = colors.get(stage, "#a78bfa") + lines.append( + f'{escape(stage)} [{begin},{end})' + ) + legend = [ + ("frontend", "#60a5fa"), + ("dispatch", "#f59e0b"), + ("schedule/execute", "#ef4444"), + ("ROB wait", "#10b981"), + ("retired", "#059669"), + ] + legend_x = left + legend_y = height - 30 + for label, color in legend: + lines.append( + f'' + ) + lines.append(f'{label}') + legend_x += 125 + lines.append(f'cycle') + lines.append("") + return "\n".join(lines) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(allow_abbrev=False) + parser.add_argument("--trace", type=Path, default=DEFAULT_TRACE) + parser.add_argument("--projection", type=Path, default=DEFAULT_PROJECTION) + parser.add_argument( + "--output-dir", type=Path, default=ROOT / "build/davincioo-trace" + ) + parser.add_argument("--cxx", default=shutil.which("c++") or "c++") + arguments = parser.parse_args() + + output = arguments.output_dir.resolve() + output.mkdir(parents=True, exist_ok=True) + canonical_bytes = convert_davincioo_trace( + arguments.trace.read_bytes(), source_program="davincioo-softmax" + ) + canonical = json.loads(canonical_bytes) + projection = json.loads(arguments.projection.read_text(encoding="utf-8")) + records = canonical["records"] + opcodes = [str(record["opcode"]) for record in records] + + (output / "canonical-trace.json").write_bytes(canonical_bytes) + (output / "davincioo.generated.cpp").write_text( + specialization.lower_cpp(), encoding="utf-8" + ) + (output / "davincioo_trace_fixture.hpp").write_text( + fixture_header(canonical, projection), encoding="utf-8" + ) + executable = output / "davincioo-trace-sim" + build = subprocess.run( + ( + arguments.cxx, + "-std=c++20", + "-O2", + "-I", + str(ROOT / "include"), + "-I", + str(output), + str(HARNESS), + "-o", + str(executable), + ), + text=True, + capture_output=True, + check=False, + ) + if build.returncode != 0: + parser.error(f"generated gfsim compilation failed:\n{build.stderr}") + completed = subprocess.run( + (str(executable),), text=True, capture_output=True, check=False + ) + if completed.returncode != 0: + parser.error( + "generated gfsim execution failed " + f"({completed.returncode}):\n{completed.stderr}" + ) + run = parse_harness_output(completed.stdout) + for key in ("completion_order", "retirement_order", "architectural_values"): + if run[key] != projection[key]: + parser.error(f"generated gfsim {key} differs from projection") + if run["retired_count"] != len(records): + parser.error("generated gfsim retired count differs from trace") + + report = { + "schema": "agentic-circuit-davincioo-run", + "version": "0.1", + "contract_epoch": "0.3", + "specialization": specialization.fingerprint, + "trace_content_hash": canonical["metadata"]["content_hash"], + "record_count": len(records), + "reference_cycles": projection["simulated_cycles"], + **run, + } + (output / "run.json").write_bytes(canonical_json_bytes(report) + b"\n") + (output / "swimlane.svg").write_text( + render_svg(report, opcodes), encoding="utf-8" + ) + print( + f"generated_cycles={report['cycles']} " + f"reference_cycles={report['reference_cycles']} " + f"records={report['record_count']}" + ) + print(output / "run.json") + print(output / "swimlane.svg") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/components/agentic-circuit/unittests/Analysis/CMakeLists.txt b/components/agentic-circuit/unittests/Analysis/CMakeLists.txt new file mode 100644 index 00000000..1c28e617 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/CMakeLists.txt @@ -0,0 +1,50 @@ +find_package(GTest CONFIG REQUIRED) + +add_executable(ACIRModelAnalysisTests ModelAnalysisTest.cpp) +target_include_directories(ACIRModelAnalysisTests PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +target_link_libraries(ACIRModelAnalysisTests PRIVATE + ACIRAnalysis + ACIRDialect + ACIRTransforms + ACSimDialect + MLIRAsmParser + MLIRBytecodeWriter + MLIRFuncDialect + MLIRIndexDialect + MLIRIR + MLIRParser + MLIRSCFDialect + GTest::gtest_main +) +add_test(NAME ACIRModelAnalysisTests COMMAND ACIRModelAnalysisTests) + +add_executable(ACIRProcessStatePlanTests + ProcessStatePlanBasicTest.cpp + ProcessStatePlanControlFlowTest.cpp + ProcessStatePlanWakeTest.cpp + ProcessStatePlanEmissionTest.cpp + ProcessStatePlanCostConsumer.cpp + ProcessStatePlanLimitsTest.cpp + ProcessStatePlanAtomicityTest.cpp + ProcessStatePlanVerifierTest.cpp + ProcessStatePlanTask13ConsumerTest.cpp +) +target_include_directories(ACIRProcessStatePlanTests PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +target_link_libraries(ACIRProcessStatePlanTests PRIVATE + ACIRAnalysis + ACIRDialect + ACIRTransforms + ACSimDialect + MLIRAsmParser + MLIRBytecodeWriter + MLIRIndexDialect + MLIRIR + MLIRParser + MLIRPass + GTest::gtest_main +) +add_test(NAME ACIRProcessStatePlanTests COMMAND ACIRProcessStatePlanTests) diff --git a/components/agentic-circuit/unittests/Analysis/ModelAnalysisTest.cpp b/components/agentic-circuit/unittests/Analysis/ModelAnalysisTest.cpp new file mode 100644 index 00000000..b0728d4a --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ModelAnalysisTest.cpp @@ -0,0 +1,1055 @@ +#include "acir/Analysis/ModelAnalysis.h" +#include "Analysis/ModelAnalysisInternal.h" +#include "Analysis/ModelAnalysisTestHooks.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/GraphRegion.h" +#include "acir/InitAllDialects.h" +#include "acir/Transforms/Passes.h" + +#include "mlir/Bytecode/BytecodeWriter.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Pass/PassManager.h" +#include "gtest/gtest.h" + +#include +#include + +namespace acir { +namespace { + +using namespace mlir; +using namespace acir::ac; + +template +concept ExposesTopologyDigest = + requires(Analysis &analysis) { analysis.computeTopologyDigest(); }; + +template +concept ExposesOwnerManifest = + requires(Analysis &analysis) { analysis.buildFrozenOwnerManifest(); }; + +template +concept ExposesOwnerWork = + requires(Analysis &analysis) { analysis.getLastOwnerManifestWork(); }; + +template +concept ExposesProcessSkeleton = + requires(Process process) { buildFrozenProcessSkeleton(process); }; + +static_assert(!ExposesTopologyDigest); +static_assert(!ExposesOwnerManifest); +static_assert(!ExposesOwnerWork); +static_assert(!ExposesProcessSkeleton); + +constexpr llvm::StringLiteral kProcessModel = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @p32 { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.event @push from @sender to @receiver payload i32 action "notify" + ac.transition from @idle to @idle on @push transfer false retain false guard {} + } + ac.protocol @p64 { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.event @push from @sender to @receiver payload i64 action "notify" + ac.transition from @idle to @idle on @push transfer false retain false guard {} + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + %graph_i32 = arith.constant 7 : i32 + %graph_i64 = arith.constant 9 : i64 + ac.time_domain @clock period 1 phase 0 scale 1 + ac.queue @q0 payload i32 entries 4 ordering "fifo" protocol @p32 + ownership "exclusive" id "q0" path "q0" + ac.queue @q1 payload i32 entries 4 ordering "fifo" protocol @p32 + ownership "exclusive" id "q1" path "q1" + ac.queue @q64 payload i64 entries 4 ordering "fifo" protocol @p64 + ownership "exclusive" id "q64" path "q64" + ac.event_queue @e0 payload !ac.event capacity 4 + ordering "time_then_sequence" domain @clock id "e0" path "e0" + ac.event_queue @e1 payload !ac.event capacity 4 + ordering "time_then_sequence" domain @clock id "e1" path "e1" + ac.resource @r0 capacity 1 issue_width 1 ii 1 + latency {kind = "fixed", ticks = 1 : i64} + lifecycle {reservation = "propose_commit", release = "balanced", cancellation = "explicit"} + ownership "exclusive" classes [] id "r0" path "r0" + ac.resource @r1 capacity 1 issue_width 1 ii 1 + latency {kind = "fixed", ticks = 1 : i64} + lifecycle {reservation = "propose_commit", release = "balanced", cancellation = "explicit"} + ownership "exclusive" classes [] id "r1" path "r1" + ac.stat @s0 kind "counter" + ac.stat @s1 kind "counter" + ac.process @worker0 kind "control" captures(%graph_i32 : i32) { + ^bb0(%captured : i32): + ac.yield_sim + } + ac.process @worker1 kind "control" captures(%graph_i32 : i32) { + ^bb0(%captured : i32): + ac.yield_sim + } + ac.process @workload kind "workload" captures(%graph_i32 : i32) { + ^bb0(%captured : i32): + %one = arith.constant 1 : i64 + %other = arith.constant 11 : i64 + %true = arith.constant true + %unused = arith.constant 101 : i64 + %accepted = ac.try_send @q0 %captured : i32 + %value, %received = ac.try_recv @q0 : i32 + ac.schedule @worker0 %value after %one : i32 + ac.wait_until %true + ac.wait_for @r0 + ac.await_event @e0 + %cursor = ac.trace.open source "pto" + %observed = ac.probe @q0 kind "queue" : i32 + ac.stat.add @s0 %captured : i32 + ac.assert %true, "runtime" + ac.yield_sim + } + ac.return + } + } +)mlir"; + +OwningOpRef parseAndFreeze(MLIRContext &context, + StringRef source) { + auto model = parseSourceString(source, &context); + if (!model) + return {}; + PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + if (failed(manager.run(*model))) + return {}; + return model; +} + +std::string runFreeze(MLIRContext &context, mlir::ModuleOp model) { + std::string diagnostic; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return success(); + }); + PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + if (succeeded(manager.run(model))) + return "freeze unexpectedly succeeded"; + return diagnostic; +} + +std::string moduleText(mlir::ModuleOp model) { + std::string storage; + llvm::raw_string_ostream stream(storage); + model.print(stream); + return storage; +} + +std::string moduleBytecode(mlir::ModuleOp model) { + std::string storage; + llvm::raw_string_ostream stream(storage); + if (failed(writeBytecodeToFile(model, stream))) + return {}; + return storage; +} + +std::string runCanonicalize(MLIRContext &context, mlir::ModuleOp model) { + std::string diagnostic; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return success(); + }); + if (succeeded(canonicalizeModel(model))) + return "canonicalization unexpectedly succeeded"; + return diagnostic; +} + +void makeTopLevelNoncanonical(mlir::ModuleOp model) { + ac::ModuleOp graph = *model.getOps().begin(); + graph->moveBefore(&model.getBody()->front()); +} + +std::string +verifyAfterMutation(MLIRContext &context, + const std::function &mutate) { + OwningOpRef model = parseAndFreeze(context, kProcessModel); + if (!model) + return "setup failed"; + mutate(*model); + std::string diagnostic; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return success(); + }); + if (succeeded(verifyModel(*model))) + return "verification unexpectedly succeeded"; + return diagnostic; +} + +template OpTy one(mlir::ModuleOp model) { + OpTy result; + model.walk([&](OpTy candidate) { + if (!result) + result = candidate; + }); + return result; +} + +template OpTy named(mlir::ModuleOp model, StringRef name) { + OpTy result; + model.walk([&](OpTy candidate) { + if (candidate->template getAttrOfType( + SymbolTable::getSymbolAttrName()) == + StringAttr::get(model.getContext(), name)) + result = candidate; + }); + return result; +} + +OwningOpRef makeFlatAddressModel(MLIRContext &context, + uint64_t ownerCount) { + OpBuilder builder(&context); + Location loc = builder.getUnknownLoc(); + auto model = mlir::ModuleOp::create(loc); + model->setAttr("ac.contract_epoch", builder.getStringAttr("0.3")); + builder.setInsertionPointToStart(model.getBody()); + auto top = + ac::ModuleOp::create(builder, loc, "Top", builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(top.addEntryBlock()); + for (uint64_t index = 0; index < ownerCount; ++index) { + std::string name = ("mem" + Twine(index)).str(); + AddressSpaceOp::create(builder, loc, name, name, name, 32, "byte", + Attribute(), FlatSymbolRefAttr(), DictionaryAttr()); + } + auto workload = + ProcessOp::create(builder, loc, "workload", "workload", ValueRange{}); + builder.setInsertionPointToStart(&workload.getBody().emplaceBlock()); + auto instrumentation = InstrumentationOp::create(builder, loc, "trace"); + instrumentation.getBody().emplaceBlock(); + TraceOpenOp::create(builder, loc, builder.getIndexType(), "pto"); + YieldSimOp::create(builder, loc); + builder.setInsertionPointToEnd(&top.getBody().front()); + ReturnOp::create(builder, loc, ValueRange{}); + builder.setInsertionPointToEnd(model.getBody()); + auto workloadRef = SymbolRefAttr::get( + &context, "Top", {FlatSymbolRefAttr::get(&context, "workload")}); + auto instrumentationRef = + SymbolRefAttr::get(&context, "Top", + {FlatSymbolRefAttr::get(&context, "workload"), + FlatSymbolRefAttr::get(&context, "trace")}); + SystemOp::create( + builder, loc, "owners", "Top", "root", 0, "cycle", workloadRef, + builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }), + builder.getArrayAttr({instrumentationRef}), + builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("owners")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }), + true); + return OwningOpRef(model); +} + +OwningOpRef makeDeepProcessModel(MLIRContext &context, + uint64_t depth) { + context.loadDialect(); + auto model = parseSourceString(R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + } + )mlir", + &context); + if (!model) + return {}; + ProcessOp process = one(*model); + Operation *yield = &process.getBody().front().front(); + OpBuilder builder(yield); + Location loc = builder.getUnknownLoc(); + Value constant = + arith::ConstantOp::create(builder, loc, builder.getBoolAttr(true)); + Value current = constant; + for (uint64_t index = 0; index < depth; ++index) + current = arith::XOrIOp::create(builder, loc, current, constant); + WaitUntilOp::create(builder, loc, current); + return model; +} + +OwningOpRef makeNestedScfModel(MLIRContext &context, + uint64_t scfDepth) { + context.loadDialect(); + auto model = parseSourceString(R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + } + )mlir", + &context); + if (!model) + return {}; + ProcessOp process = one(*model); + Operation *processYield = &process.getBody().front().front(); + OpBuilder builder(processYield); + Location loc = builder.getUnknownLoc(); + Value condition = + arith::ConstantOp::create(builder, loc, builder.getBoolAttr(true)); + for (uint64_t index = 0; index < scfDepth; ++index) { + auto branch = scf::IfOp::create(builder, loc, condition, + /*withElseRegion=*/false); + builder.setInsertionPoint(branch.getThenRegion().front().getTerminator()); + } + AssertOp::create(builder, loc, condition, "nested effect"); + return model; +} + +class RawNestedRegionModel { +public: + RawNestedRegionModel(MLIRContext &context, uint64_t depth) + : model(mlir::ModuleOp::create(UnknownLoc::get(&context))) { + context.allowUnregisteredDialects(); + OpBuilder builder(&context); + (*model)->setAttr("ac.contract_epoch", builder.getStringAttr("0.3")); + Block *block = model->getBody(); + for (uint64_t index = 0; index < depth; ++index) { + OperationState state(UnknownLoc::get(&context), "test.nested"); + state.addRegion(); + Operation *operation = Operation::create(state); + block->push_back(operation); + operations.push_back(operation); + block = &operation->getRegion(0).emplaceBlock(); + } + } + + ~RawNestedRegionModel() { + for (Operation *operation : llvm::reverse(operations)) { + operation->remove(); + operation->destroy(); + } + } + + RawNestedRegionModel(const RawNestedRegionModel &) = delete; + RawNestedRegionModel &operator=(const RawNestedRegionModel &) = delete; + + mlir::ModuleOp get() const { return *model; } + +private: + OwningOpRef model; + SmallVector operations; +}; + +OwningOpRef makeRawEmptyRegionModel(MLIRContext &context, + bool addEmptyBlock) { + context.allowUnregisteredDialects(); + auto model = mlir::ModuleOp::create(UnknownLoc::get(&context)); + OperationState state(UnknownLoc::get(&context), "test.malformed_region"); + state.addRegion(); + Operation *operation = Operation::create(state); + model.getBody()->push_back(operation); + if (addEmptyBlock) + operation->getRegion(0).emplaceBlock(); + return OwningOpRef(model); +} + +enum class ModelEntryPath { Verify, Canonicalize, Freeze }; + +const char *entryPathName(ModelEntryPath path) { + switch (path) { + case ModelEntryPath::Verify: + return "verify"; + case ModelEntryPath::Canonicalize: + return "canonicalize"; + case ModelEntryPath::Freeze: + return "freeze"; + } + llvm_unreachable("unknown model entry path"); +} + +LogicalResult invokeModelEntry(MLIRContext &context, mlir::ModuleOp model, + ModelEntryPath path) { + switch (path) { + case ModelEntryPath::Verify: + return verifyModel(model); + case ModelEntryPath::Canonicalize: + return canonicalizeModel(model); + case ModelEntryPath::Freeze: { + PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + return manager.run(model); + } + } + llvm_unreachable("unknown model entry path"); +} + +std::string runModelEntry(MLIRContext &context, mlir::ModuleOp model, + ModelEntryPath path) { + std::string diagnostic; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return success(); + }); + if (succeeded(invokeModelEntry(context, model, path))) + return "model entry unexpectedly succeeded"; + return diagnostic; +} + +TEST(ModelAnalysisTest, FrozenProcessSkeletonRejectsEffectSemanticMutation) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + auto symbol = [&](StringRef value) { + return FlatSymbolRefAttr::get(&context, value); + }; + struct Mutation { + const char *name; + std::function apply; + }; + const Mutation mutations[] = { + {"queue target", + [&](mlir::ModuleOp model) { + one(model).setQueueAttr(symbol("q1")); + }}, + {"resource target", + [&](mlir::ModuleOp model) { + one(model).setResourceAttr(symbol("r1")); + }}, + {"stat target", + [&](mlir::ModuleOp model) { + one(model).setStatAttr(symbol("s1")); + }}, + {"process target", + [&](mlir::ModuleOp model) { + one(model).setTargetAttr(symbol("worker1")); + }}, + {"event target", + [&](mlir::ModuleOp model) { + one(model).setEventQueueAttr(symbol("e1")); + }}, + {"trace source", + [&](mlir::ModuleOp model) { + one(model).setSource("pto_other"); + }}, + {"trace frozen owner", + [&](mlir::ModuleOp model) { + auto trace = one(model); + auto owner = trace->getAttrOfType("ac.frozen_owner"); + NamedAttrList fields(owner); + fields.set("path", StringAttr::get(&context, "wrong.path")); + trace->setAttr("ac.frozen_owner", fields.getDictionary(&context)); + }}, + {"probe target", + [&](mlir::ModuleOp model) { + one(model).setTargetAttr(symbol("q1")); + }}, + {"contract attribute", + [&](mlir::ModuleOp model) { + one(model).setMessage("changed runtime contract"); + }}, + {"control dependency", + [&](mlir::ModuleOp model) { + arith::ConstantOp condition; + model.walk([&](arith::ConstantOp candidate) { + if (candidate.getType().isInteger(1)) + condition = candidate; + }); + condition.setValueAttr(IntegerAttr::get(condition.getType(), 0)); + }}, + {"effect value type", + [&](mlir::ModuleOp model) { + TrySendOp send = one(model); + arith::ConstantOp replacement; + model.walk([&](arith::ConstantOp candidate) { + if (candidate.getType().isInteger(64) && + cast(candidate.getValue()).getInt() == 11) + replacement = candidate; + }); + send.setQueueAttr(symbol("q64")); + send->setOperand(0, replacement.getResult()); + }}, + }; + for (const Mutation &mutation : mutations) { + std::string diagnostic = verifyAfterMutation(context, mutation.apply); + EXPECT_NE(diagnostic.find("frozen process skeleton mismatch"), + std::string::npos) + << mutation.name << ": " << diagnostic; + } +} + +TEST(ModelAnalysisTest, + FrozenProcessSkeletonPermitsPureContinuationSSARewrite) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef model = parseAndFreeze(context, kProcessModel); + ASSERT_TRUE(model); + arith::ConstantOp unused; + model->walk([&](arith::ConstantOp candidate) { + if (candidate.getType().isInteger(64) && + cast(candidate.getValue()).getInt() == 101) + unused = candidate; + }); + ASSERT_TRUE(unused); + unused.setValueAttr(IntegerAttr::get(unused.getType(), 202)); + EXPECT_TRUE(succeeded(verifyModel(*model))); +} + +TEST(ModelAnalysisTest, FrozenMutationCannotBeResealedThroughPublicRoutes) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef frozen = parseAndFreeze(context, kProcessModel); + ASSERT_TRUE(frozen); + + auto retarget = [&](mlir::ModuleOp model) { + one(model).setQueueAttr(FlatSymbolRefAttr::get(&context, "q1")); + }; + auto cloneFrozen = [&]() { + return OwningOpRef(cast(frozen->clone())); + }; + + OwningOpRef direct = cloneFrozen(); + retarget(*direct); + { + ScopedDiagnosticHandler handler(&context, + [](Diagnostic &) { return success(); }); + EXPECT_TRUE(failed(verifyModel(*direct))); + ModelAnalysis analysis(*direct); + EXPECT_TRUE(failed(analysis.verify())); + } + EXPECT_NE( + runFreeze(context, *direct).find("frozen process skeleton mismatch"), + std::string::npos); + + OwningOpRef missingMarker = cloneFrozen(); + retarget(*missingMarker); + (*missingMarker)->removeAttr("ac.topology_frozen"); + EXPECT_NE(runFreeze(context, *missingMarker) + .find("malformed topology freeze marker"), + std::string::npos); + + OwningOpRef nestedEvidenceOnly = cloneFrozen(); + retarget(*nestedEvidenceOnly); + for (StringRef name : + {"ac.freeze_epoch", "ac.frozen_system", "ac.frozen_owners", + "ac.frozen_primary_workload", "ac.frozen_instrumentation", + "ac.topology_frozen", "ac.topology_digest"}) + (*nestedEvidenceOnly)->removeAttr(name); + EXPECT_NE(runFreeze(context, *nestedEvidenceOnly) + .find("malformed topology freeze marker"), + std::string::npos); + + auto partial = parseSourceString(kProcessModel, &context); + ASSERT_TRUE(partial); + (*partial)->setAttr("ac.freeze_epoch", StringAttr::get(&context, "0.3")); + EXPECT_NE( + runFreeze(context, *partial).find("malformed topology freeze marker"), + std::string::npos); +} + +TEST(ModelAnalysisTest, + CanonicalizationVerifiesEveryFreezeEvidenceFormBeforeWriting) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef frozen = parseAndFreeze(context, kProcessModel); + ASSERT_TRUE(frozen); + + std::string validBefore = moduleText(*frozen); + std::string validBytecodeBefore = moduleBytecode(*frozen); + ASSERT_FALSE(validBytecodeBefore.empty()); + EXPECT_TRUE(succeeded(canonicalizeModel(*frozen))); + EXPECT_EQ(moduleText(*frozen), validBefore); + EXPECT_EQ(moduleBytecode(*frozen), validBytecodeBefore); + + enum class Evidence { + Full, + MissingMarker, + NestedOnly, + PartialEpoch, + PartialDigest, + }; + struct Case { + const char *name; + Evidence evidence; + bool retarget; + const char *diagnostic; + }; + const Case cases[] = { + {"full retarget", Evidence::Full, true, + "frozen process skeleton mismatch"}, + {"marker removed", Evidence::MissingMarker, false, + "malformed topology freeze marker"}, + {"marker removed retarget", Evidence::MissingMarker, true, + "malformed topology freeze marker"}, + {"nested evidence only", Evidence::NestedOnly, false, + "malformed topology freeze marker"}, + {"nested evidence only retarget", Evidence::NestedOnly, true, + "malformed topology freeze marker"}, + {"partial epoch", Evidence::PartialEpoch, false, + "malformed topology freeze marker"}, + {"partial epoch retarget", Evidence::PartialEpoch, true, + "malformed topology freeze marker"}, + {"partial digest", Evidence::PartialDigest, false, + "malformed topology freeze marker"}, + {"partial digest retarget", Evidence::PartialDigest, true, + "malformed topology freeze marker"}, + }; + + auto buildCase = [&](const Case &testCase) { + OwningOpRef model; + if (testCase.evidence == Evidence::PartialEpoch || + testCase.evidence == Evidence::PartialDigest) { + model = parseSourceString(kProcessModel, &context); + if (!model) + return model; + if (testCase.evidence == Evidence::PartialEpoch) + (*model)->setAttr("ac.freeze_epoch", StringAttr::get(&context, "0.3")); + else + (*model)->setAttr("ac.topology_digest", + StringAttr::get(&context, std::string(64, '0'))); + } else { + model = + OwningOpRef(cast(frozen->clone())); + if (testCase.evidence == Evidence::MissingMarker) + (*model)->removeAttr("ac.topology_frozen"); + if (testCase.evidence == Evidence::NestedOnly) + for (StringRef name : + {"ac.freeze_epoch", "ac.frozen_system", "ac.frozen_owners", + "ac.frozen_primary_workload", "ac.frozen_instrumentation", + "ac.topology_frozen", "ac.topology_digest"}) + (*model)->removeAttr(name); + } + if (testCase.retarget) + one(*model).setQueueAttr( + FlatSymbolRefAttr::get(&context, "q1")); + makeTopLevelNoncanonical(*model); + return model; + }; + + for (const Case &testCase : cases) { + OwningOpRef model = buildCase(testCase); + ASSERT_TRUE(model) << testCase.name; + std::string before = moduleText(*model); + std::string bytecodeBefore = moduleBytecode(*model); + ASSERT_FALSE(bytecodeBefore.empty()) << testCase.name; + std::string diagnostic = runCanonicalize(context, *model); + EXPECT_NE(diagnostic.find(testCase.diagnostic), std::string::npos) + << testCase.name << ": " << diagnostic; + EXPECT_EQ(moduleText(*model), before) << testCase.name; + EXPECT_EQ(moduleBytecode(*model), bytecodeBefore) << testCase.name; + } +} + +TEST(ModelAnalysisTest, UnfrozenCanonicalizationRemainsDeterministic) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef model = + parseSourceString(kProcessModel, &context); + ASSERT_TRUE(model); + EXPECT_TRUE(succeeded(canonicalizeModel(*model))); + std::string first = moduleText(*model); + EXPECT_TRUE(succeeded(canonicalizeModel(*model))); + EXPECT_EQ(moduleText(*model), first); +} + +TEST(ModelAnalysisTest, FrozenDigestCommitsNestedGuardParentage) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef model = parseAndFreeze(context, R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @p { + ac.role @a dual @b cardinality "exclusive" + ac.role @b dual @a cardinality "exclusive" + ac.state @idle initial true terminal false + ac.event @x from @a to @b payload i1 action "notify" + ac.event @y from @a to @b payload i1 action "notify" + ac.transition from @idle to @idle on @x transfer false retain false guard { + %x = arith.constant true + } + ac.transition from @idle to @idle on @y transfer false retain false guard { + %y = arith.constant false + } + } + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + } + )mlir"); + ASSERT_TRUE(model); + SmallVector transitions; + model->walk( + [&](TransitionOp transition) { transitions.push_back(transition); }); + ASSERT_EQ(transitions.size(), 2u); + Operation *first = &transitions[0].getGuard().front().front(); + Operation *second = &transitions[1].getGuard().front().front(); + first->moveBefore(second); + second->moveBefore(&transitions[0].getGuard().front(), + transitions[0].getGuard().front().end()); + std::string diagnostic; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return success(); + }); + EXPECT_TRUE(failed(verifyModel(*model))); + EXPECT_NE(diagnostic.find("frozen topology digest mismatch"), + std::string::npos); +} + +TEST(ModelAnalysisTest, AddressSpacesFreezeAsAbsoluteStateOwners) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef model = parseAndFreeze(context, R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Leaf() parameters {} graph { + ac.address_space @mem width 32 unit "byte" id "mem" path "mem" + ac.address_space @other width 32 unit "byte" id "other" path "other" + ac.process @observer kind "monitor" { + %value = ac.probe @mem kind "storage" : i64 + ac.yield_sim + } + ac.return + } + "ac.module"() <{sym_name = "Top", function_type = () -> (), static_params = {}}> ({ + "ac.instance"() <{definition = @Leaf, sym_name = "left", stable_id = "left", path = "left", static_args = {}}> : () -> () + "ac.array"() <{definition = @Leaf, sym_name = "banks", stable_id = "banks", path = "banks", shape = array, static_args = [{}, {}]}> : () -> () + "ac.instances"() <{sym_name = "mix", stable_id = "mix", path = "mix", definitions = [@Leaf, @Leaf], names = ["x", "y"], stable_ids = ["x", "y"], paths = ["x", "y"], interface = () -> (), static_args = [{}, {}]}> : () -> () + ac.address_space @root_mem width 32 unit "byte" id "root_mem" path "root_mem" + ac.process @workload kind "workload" { ac.yield_sim } + "ac.return"() : () -> () + }) : () -> () + } + )mlir"); + ASSERT_TRUE(model); + AddressSpaceOp memory = named(*model, "mem"); + ASSERT_TRUE(memory); + ArrayAttr owners = memory->getAttrOfType("ac.frozen_owners"); + ASSERT_TRUE(owners); + ASSERT_EQ(owners.size(), 5u); + const char *expectedPaths[] = {"root.banks[0].mem", "root.banks[1].mem", + "root.left.mem", "root.mix.x.mem", + "root.mix.y.mem"}; + for (auto [owner, expected] : llvm::zip(owners, expectedPaths)) + EXPECT_EQ(cast(owner).getAs("path").getValue(), + expected); + + AddressSpaceOp rootMemory = named(*model, "root_mem"); + ASSERT_TRUE(rootMemory); + ArrayAttr rootOwners = + rootMemory->getAttrOfType("ac.frozen_owners"); + ASSERT_TRUE(rootOwners); + ASSERT_EQ(rootOwners.size(), 1u); + EXPECT_EQ( + cast(rootOwners[0]).getAs("path").getValue(), + "root.root_mem"); + + ProbeOp probe = one(*model); + SmallVector effects; + cast(probe.getOperation()).getEffects(effects); + ASSERT_FALSE(effects.empty()); + for (const MemoryEffects::EffectInstance &effect : effects) { + auto parameters = cast(effect.getParameters()); + EXPECT_EQ(parameters.getAs("identity_phase").getValue(), + "elaborated_absolute"); + EXPECT_EQ(parameters.getAs("owners"), owners); + } + + memory->removeAttr("ac.frozen_owners"); + ScopedDiagnosticHandler handler(&context, + [&](Diagnostic &) { return success(); }); + EXPECT_TRUE(failed(verifyModel(*model))); +} + +TEST(ModelAnalysisTest, AddressSpacesParticipateInSaturatedOwnerBudget) { + MLIRContext context; + context.loadDialect(); + OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto model = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(model.getBody()); + auto leaf = + ac::ModuleOp::create(builder, loc, "Leaf", emptyType, emptyDictionary); + builder.setInsertionPointToStart(leaf.addEntryBlock()); + auto addAddress = [&](StringRef name) { + return AddressSpaceOp::create(builder, loc, name, name, name, 32, "byte", + Attribute(), FlatSymbolRefAttr(), + DictionaryAttr()); + }; + addAddress("mem0"); + addAddress("mem1"); + ReturnOp::create(builder, loc, ValueRange{}); + + SmallVector staticArgs(512, Attribute(emptyDictionary)); + builder.setInsertionPointToEnd(model.getBody()); + auto middle = + ac::ModuleOp::create(builder, loc, "Middle", emptyType, emptyDictionary); + builder.setInsertionPointToStart(middle.addEntryBlock()); + ArrayOp::create(builder, loc, TypeRange{}, ValueRange{}, "Leaf", "leaves", + "leaves", "leaves", builder.getDenseI64ArrayAttr({512}), + builder.getArrayAttr(staticArgs)); + ReturnOp::create(builder, loc, ValueRange{}); + + builder.setInsertionPointToEnd(model.getBody()); + auto top = + ac::ModuleOp::create(builder, loc, "Top", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + ArrayOp::create(builder, loc, TypeRange{}, ValueRange{}, "Middle", "middles", + "middles", "middles", builder.getDenseI64ArrayAttr({512}), + builder.getArrayAttr(staticArgs)); + ReturnOp::create(builder, loc, ValueRange{}); + + builder.setInsertionPointToEnd(model.getBody()); + auto seed = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }); + auto results = builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("owners")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }); + SystemOp::create(builder, loc, "owners", "Top", "root", 0, "cycle", + FlatSymbolRefAttr(), seed, builder.getArrayAttr({}), results, + true); + EXPECT_TRUE(succeeded(verifyGraphStructure(model))); + + auto overBudget = cast(model->clone()); + auto overBudgetLeaf = *overBudget.getOps().begin(); + builder.setInsertionPoint(&overBudgetLeaf.getBody().front().back()); + addAddress("mem2"); + std::string diagnostic; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return success(); + }); + EXPECT_TRUE(failed(verifyGraphStructure(overBudget))); + EXPECT_NE(diagnostic.find("owner count exceeds bound 1048576"), + std::string::npos); +} + +TEST(ModelAnalysisTest, FullFreezePathHasExactLinearIndexedWork) { + MLIRContext context; + context.loadDialect(); + auto measure = [&](uint64_t ownerCount) { + OwningOpRef model = + makeFlatAddressModel(context, ownerCount); + acir::detail::FreezeWork work; + acir::detail::ScopedFreezeWorkRecorder recorder(work); + PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + EXPECT_TRUE(succeeded(manager.run(*model))); + // The complete path constructs the seal, then independently reconstructs + // the manifest during final frozen verification. + EXPECT_EQ(work.stateIndexInsertions, 2 * (ownerCount + 1)); + EXPECT_EQ(work.topologyIndexLookups, 2 * (ownerCount + 1)); + EXPECT_EQ(work.manifestIndexInsertions, ownerCount + 2); + EXPECT_EQ(work.manifestOwnerLookups, 2u); + EXPECT_EQ(work.declarationIndexInsertions, ownerCount + 1); + EXPECT_EQ(work.declarationLookups, ownerCount + 1); + return work.total(); + }; + uint64_t work1000 = measure(1000); + uint64_t work4000 = measure(4000); + EXPECT_EQ(work1000, 7010u); + EXPECT_EQ(work4000, 28010u); + EXPECT_EQ(work4000 - 10, 4 * (work1000 - 10)); +} + +TEST(ModelAnalysisTest, DeepProcessDependencyChainsFreezeWithoutStackGrowth) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + for (uint64_t depth : {30000u, 200000u}) { + OwningOpRef model = makeDeepProcessModel(context, depth); + ASSERT_TRUE(model) << depth; + PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + EXPECT_TRUE(succeeded(manager.run(*model))) << depth; + } +} + +TEST(ModelAnalysisTest, ModelEntryPathsAcceptExactRegionNestingLimit) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + constexpr uint64_t scfDepthAtLimit = 509; + for (ModelEntryPath path : + {ModelEntryPath::Verify, ModelEntryPath::Canonicalize, + ModelEntryPath::Freeze}) { + OwningOpRef model = + makeNestedScfModel(context, scfDepthAtLimit); + ASSERT_TRUE(model) << entryPathName(path); + std::string diagnostic; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return success(); + }); + EXPECT_TRUE(succeeded(invokeModelEntry(context, *model, path))) + << entryPathName(path) << ": " << diagnostic; + } +} + +TEST(ModelAnalysisTest, + ModelEntryPathsRejectRegionNestingLimitPlusOneDeterministically) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + constexpr uint64_t scfDepthOverLimit = 510; + constexpr StringLiteral expected = + "whole-model region nesting exceeds ACIR capability limit 512"; + std::string firstDiagnostic; + for (ModelEntryPath path : + {ModelEntryPath::Verify, ModelEntryPath::Canonicalize, + ModelEntryPath::Freeze}) { + OwningOpRef model = + makeNestedScfModel(context, scfDepthOverLimit); + ASSERT_TRUE(model) << entryPathName(path); + std::string diagnostic = runModelEntry(context, *model, path); + EXPECT_EQ(diagnostic, expected.str()) << entryPathName(path); + if (firstDiagnostic.empty()) + firstDiagnostic = diagnostic; + else + EXPECT_EQ(diagnostic, firstDiagnostic) << entryPathName(path); + } +} + +TEST(ModelAnalysisTest, + HostileRawRegionNestingRejectsWithoutStackGrowthOnEveryEntryPath) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + constexpr StringLiteral expected = + "whole-model region nesting exceeds ACIR capability limit 512"; + std::string firstDiagnostic; + for (ModelEntryPath path : + {ModelEntryPath::Verify, ModelEntryPath::Canonicalize, + ModelEntryPath::Freeze}) { + RawNestedRegionModel model(context, 10000); + std::string diagnostic = runModelEntry(context, model.get(), path); + EXPECT_EQ(diagnostic, expected.str()) << entryPathName(path); + if (firstDiagnostic.empty()) + firstDiagnostic = diagnostic; + else + EXPECT_EQ(diagnostic, firstDiagnostic) << entryPathName(path); + } +} + +TEST(ModelAnalysisTest, StructuralPreflightHandlesEmptyRegionsAndBlocks) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + for (bool addEmptyBlock : {false, true}) { + OwningOpRef model = + makeRawEmptyRegionModel(context, addEmptyBlock); + ASSERT_TRUE(model); + EXPECT_TRUE(succeeded(::acir::detail::preflightModelStructure(*model))) + << "add_empty_block=" << addEmptyBlock; + } +} + +TEST(ModelAnalysisTest, ProcessDependencyCapabilityIsCheckedBeforeGrowth) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef model = makeDeepProcessModel(context, 1025); + ASSERT_TRUE(model); + acir::detail::ScopedProcessSkeletonLimits limits(/*nodes=*/1024, + /*edges=*/4096); + std::string diagnostic = runFreeze(context, *model); + EXPECT_NE(diagnostic.find( + "process skeleton dependency node count exceeds bound 1024"), + std::string::npos) + << diagnostic; +} + +TEST(ModelAnalysisTest, ProcessDependencyEdgeCapabilityIsCheckedBeforeGrowth) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef model = makeDeepProcessModel(context, 16); + ASSERT_TRUE(model); + acir::detail::ScopedProcessSkeletonLimits limits(/*nodes=*/64, /*edges=*/8); + std::string diagnostic = runFreeze(context, *model); + EXPECT_NE( + diagnostic.find("process skeleton dependency edge count exceeds bound 8"), + std::string::npos) + << diagnostic; +} + +TEST(ModelAnalysisTest, ProcessSkeletonIncludesNestedControlParents) { + DialectRegistry registry; + registerAllDialects(registry); + MLIRContext context(registry); + OwningOpRef model = parseAndFreeze(context, R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.stat @count kind "counter" + ac.process @workload kind "workload" { + %condition = arith.constant true + scf.if %condition { + %one = arith.constant 1 : i32 + ac.stat.add @count %one : i32 + } + ac.yield_sim + } + ac.return + } + } + )mlir"); + ASSERT_TRUE(model); + arith::ConstantOp condition; + model->walk([&](arith::ConstantOp candidate) { + if (candidate.getType().isInteger(1)) + condition = candidate; + }); + ASSERT_TRUE(condition); + condition.setValueAttr(IntegerAttr::get(condition.getType(), 0)); + std::string diagnostic; + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return success(); + }); + EXPECT_TRUE(failed(verifyModel(*model))); + EXPECT_NE(diagnostic.find("frozen process skeleton mismatch"), + std::string::npos); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanAtomicityTest.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanAtomicityTest.cpp new file mode 100644 index 00000000..d7c40600 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanAtomicityTest.cpp @@ -0,0 +1,93 @@ +#include "Analysis/ProcessStatePlanInternal.h" +#include "Analysis/ProcessStatePlanTestHooks.h" +#include "ProcessStatePlanTestSupport.h" +#include "acir/Analysis/ProcessStatePlan.h" +#include "acir/InitAllDialects.h" + +#include "gtest/gtest.h" + +namespace acir { +namespace { + +using PlanSetBuilder = detail::PlanSetBuilder; + +TEST(ProcessStatePlanAtomicityTest, SerializationIsDeterministic) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto plans = PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + auto result1 = serializeProcessStatePlan(*plans); + auto result2 = serializeProcessStatePlan(*plans); + ASSERT_TRUE(static_cast(result1)); + ASSERT_TRUE(static_cast(result2)); + EXPECT_EQ(*result1, *result2); +} + +TEST(ProcessStatePlanAtomicityTest, + CloneWithMissingWakeCalleeFailsVerification) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto plans = PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + auto clone = PlanSetBuilder::cloneWithMissingWakeCallee(*plans); + auto result = verifyProcessStatePlan(clone); + EXPECT_TRUE(mlir::failed(result)); +} + +TEST(ProcessStatePlanAtomicityTest, CloneWithDanglingSuspendFailsVerification) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto plans = PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + auto clone = PlanSetBuilder::cloneWithDanglingSuspendTransition(*plans); + auto result = verifyProcessStatePlan(clone); + EXPECT_TRUE(mlir::failed(result)); +} + +TEST(ProcessStatePlanAtomicityTest, + PublicFactoryCanonicalizesDeclarationPermutation) { + mlir::MLIRContext context; + mlir::DialectRegistry registry; + registerAllDialects(registry); + context.appendDialectRegistry(registry); + auto first = test::parseAndFreezeYieldPermutation(context, false); + auto second = test::parseAndFreezeYieldPermutation(context, true); + ASSERT_TRUE(first && second); + auto firstPlan = planProcessState(*first); + auto secondPlan = planProcessState(*second); + ASSERT_TRUE(mlir::succeeded(firstPlan)); + ASSERT_TRUE(mlir::succeeded(secondPlan)); + auto firstReport = serializeProcessStatePlan(*firstPlan); + auto secondReport = serializeProcessStatePlan(*secondPlan); + ASSERT_TRUE(static_cast(firstReport)); + ASSERT_TRUE(static_cast(secondReport)); + EXPECT_EQ(*firstReport, *secondReport); +} + +TEST(ProcessStatePlanAtomicityTest, + PublicFactoryFailurePreservesFrozenModuleBytes) { + mlir::MLIRContext context; + mlir::DialectRegistry registry; + registerAllDialects(registry); + context.appendDialectRegistry(registry); + auto module = test::parseAndFreezeYieldPermutation(context, false); + ASSERT_TRUE(module); + std::string textBefore = test::moduleText(*module); + std::string bytesBefore = test::moduleBytecode(*module); + ProcessStateLimits limits; + limits.maxProcesses = 1; + mlir::ScopedDiagnosticHandler suppress( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + EXPECT_TRUE(mlir::failed(planProcessState(*module, limits))); + EXPECT_EQ(test::moduleText(*module), textBefore); + EXPECT_EQ(test::moduleBytecode(*module), bytesBefore); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanBasicTest.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanBasicTest.cpp new file mode 100644 index 00000000..31b3c178 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanBasicTest.cpp @@ -0,0 +1,1162 @@ +#include "Analysis/ProcessStatePlanInternal.h" +#include "Analysis/ProcessStatePlanTestHooks.h" +#include "ProcessStatePlanTestSupport.h" +#include "acir/Analysis/ProcessStatePlan.h" + +#include "gtest/gtest.h" + +#include +#include + +namespace acir { +namespace { + +template +concept HasId = requires(const T &value) { value.id(); }; +template +concept HasOrdinal = requires(const T &value) { value.ordinal(); }; +template +concept HasSetter = requires(T &value) { value.setId(value.entryPc()); }; +template +concept HasMutableProcesses = requires(T &value) { + { + value.processes() + } -> std::same_as>; +}; +template +concept HasComponentLookup = + requires(const T &value) { value.lookupByComponentName("workload"); }; +template +concept HasHierarchyLookup = + requires(const T &value) { value.lookupByHierarchy("Top.workload"); }; +template +concept HasFallbackLookup = + requires(const T &value) { value.lookup("workload"); }; + +template +concept CompleteProcessStatePlanApi = requires( + const ProcessCallSitePlan &callSite, + const ProcessOriginalOccurrence &originalOccurrence, + const ProcessSyntheticLoopOccurrence &loopOccurrence, + const ProcessSyntheticWrapperOccurrence &wrapperOccurrence, + const ProcessSyntheticConstantOccurrence &constantOccurrence, + const ProcessOccurrenceId &occurrence, + const ProcessValueCoordinate &coordinate, + const ProcessOriginalPlannedValue &originalValue, + const ProcessCapturePlannedValue &captureValue, + const ProcessLiveSlotPlannedValue &slotValue, + const ProcessSyntheticPlannedValue &syntheticValue, + const ProcessConstantPlannedValue &constantValue, + const ProcessPlannedValue &plannedValue, + const ProcessScalarAttribute &scalarAttribute, + const ProcessScalarOperationPlan &scalarOp, + const ProcessCapturePlan &capture, const ProcessActionPlan &action, + const ProcessLiveSlotPlan &slot, + const ProcessSubscriptionSourcePlan &source, const ProcessWakePlan &wake, + const ProcessTransitionStorePlan &store, + const ProcessTransitionLoadPlan &load, + const ProcessTransitionPlan &transition, + const ProcessForwardingBindingPlan &binding, + const ProcessControlFramePlan &frame, const ProcessControlEdgePlan &edge, + const ProcessBlockPlan &block, const ProcessPcPlan &pc, + const ProcessStatePlan &plan, const ProcessRecordFieldDescriptor &field, + const ProcessRecordCreatePayload &recordCreate, + const ProcessRecordGetPayload &recordGet, + const ProcessRecordWithPayload &recordWith, + const ProcessPacketSerializePayload &packetSerialize, + const ProcessPacketDeserializePayload &packetDeserialize, + const ProcessTraceDecodePayload &traceDecode, + const ProcessQueueTrySendPayload &queueSend, + const ProcessQueueTryRecvPayload &queueRecv, + const ProcessEventSchedulePayload &eventSchedule, + const ProcessTraceOpenPayload &traceOpen, + const ProcessTraceNextPayload &traceNext, + const ProcessTraceEofPayload &traceEof, + const ProcessTracePositionPayload &tracePosition, + const ProcessContractRequirePayload &requirePayload, + const ProcessContractEnsurePayload &ensurePayload, + const ProcessContractAssertPayload &assertPayload, + const ProcessProbePayload &probe, const ProcessStatAddPayload &stat, + const ProcessWakeConditionPayload &wakeCondition, + const ProcessWakeResourcePayload &wakeResource, + const ProcessWakeEventQueuePayload &wakeEvent, + const ProcessWakeNextDeltaPayload &wakeNext, + const ProcessScalarWrapPayload &wrap, + const ProcessScalarUnwrapPayload &unwrap, + const ProcessGeneratedCalleePayload &calleePayload, + const ProcessValueTypeMemberPlan &member, + const ProcessStorageValuePayload &storageValue, + const ProcessStoragePacketPayload &storagePacket, + const ProcessValueTypePayload &valuePayload, + const ProcessGeneratedCalleePlan &callee, + const ProcessValueTypePlan &valueType, const ProcessStatePlanSet &set) { + callSite.operation(); + callSite.operationPath(); + callSite.iterationVector(); + originalOccurrence.operation(); + originalOccurrence.operationPath(); + originalOccurrence.callSites(); + originalOccurrence.iterationVector(); + loopOccurrence.anchor(); + loopOccurrence.phase(); + wrapperOccurrence.anchor(); + wrapperOccurrence.transition(); + wrapperOccurrence.slot(); + wrapperOccurrence.direction(); + constantOccurrence.anchor(); + constantOccurrence.constant(); + occurrence.kind(); + occurrence.original(); + occurrence.syntheticLoop(); + occurrence.syntheticWrapper(); + occurrence.syntheticConstant(); + coordinate.kind(); + coordinate.ownerPath(); + coordinate.index(); + originalValue.value(); + originalValue.occurrence(); + originalValue.coordinate(); + originalValue.path(); + captureValue.capture(); + slotValue.slot(); + syntheticValue.occurrence(); + syntheticValue.coordinate(); + constantValue.value(); + plannedValue.kind(); + plannedValue.type(); + plannedValue.original(); + plannedValue.capture(); + plannedValue.liveSlot(); + plannedValue.synthetic(); + plannedValue.constant(); + scalarAttribute.name(); + scalarAttribute.value(); + scalarOp.name(); + scalarOp.attributes(); + scalarOp.properties(); + capture.id(); + capture.name(); + capture.operand(); + capture.entryArgument(); + capture.type(); + capture.operandPath(); + capture.argumentPath(); + action.id(); + action.kind(); + action.emission(); + action.occurrence(); + action.sourceOperation(); + action.iterationVector(); + action.operands(); + action.results(); + action.cost(); + action.resultTypes(); + action.callee(); + action.scalarOp(); + slot.id(); + slot.name(); + slot.type(); + slot.storageType(); + slot.memberValues(); + slot.wrapCallee(); + slot.unwrapCallee(); + source.kind(); + source.value(); + source.owner(); + source.declaration(); + source.capture(); + source.symbol(); + source.path(); + source.ownerPath(); + wake.id(); + wake.kind(); + wake.operation(); + wake.triggeringValue(); + wake.declaration(); + wake.callee(); + wake.typeKey(); + wake.operationPath(); + wake.target(); + wake.occurrence(); + wake.iterationVector(); + wake.sources(); + store.slot(); + store.source(); + store.sourceValue(); + load.slot(); + load.replacements(); + transition.id(); + transition.sourcePc(); + transition.targetPc(); + transition.wake(); + transition.iterationVector(); + transition.stores(); + transition.loads(); + binding.from(); + binding.to(); + frame.kind(); + frame.phase(); + frame.operation(); + frame.operationPath(); + frame.bindings(); + edge.kind(); + edge.condition(); + edge.trueBlock(); + edge.falseBlock(); + edge.trueBindings(); + edge.falseBindings(); + edge.targetBlock(); + edge.bindings(); + edge.transition(); + edge.status(); + block.id(); + block.pc(); + block.originRegion(); + block.originBlock(); + block.path(); + block.frames(); + block.loads(); + block.actions(); + block.edge(); + block.cost(); + pc.id(); + pc.name(); + pc.entryPath(); + pc.blocks(); + plan.definitionKey(); + plan.process(); + plan.captures(); + plan.entryPc(); + plan.pcs(); + plan.blocks(); + plan.liveSlots(); + plan.wakes(); + plan.transitions(); + plan.pcBitWidth(); + plan.fairnessWork(); + field.name(); + field.typeKey(); + recordCreate.fields(); + recordCreate.recordType(); + recordGet.field(); + recordGet.record(); + recordGet.result(); + recordWith.field(); + recordWith.record(); + recordWith.value(); + packetSerialize.bytes(); + packetSerialize.packet(); + packetSerialize.packetType(); + packetDeserialize.bytes(); + packetDeserialize.packet(); + packetDeserialize.packetType(); + traceDecode.entry(); + traceDecode.result(); + traceDecode.source(); + queueSend.element(); + queueSend.queue(); + queueRecv.element(); + queueRecv.queue(); + eventSchedule.delay(); + eventSchedule.target(); + eventSchedule.value(); + traceOpen.source(); + traceNext.entry(); + traceNext.source(); + traceEof.source(); + tracePosition.source(); + requirePayload.message(); + ensurePayload.message(); + assertPayload.message(); + probe.kind(); + probe.result(); + probe.target(); + stat.stat(); + stat.valueType(); + wakeCondition.wakeKind(); + wakeCondition.wakeType(); + wakeResource.wakeKind(); + wakeResource.wakeType(); + wakeEvent.wakeKind(); + wakeEvent.wakeType(); + wakeNext.wakeKind(); + wakeNext.wakeType(); + wrap.direction(); + wrap.scalar(); + wrap.valueType(); + unwrap.direction(); + unwrap.scalar(); + unwrap.valueType(); + calleePayload.role(); + calleePayload.recordCreate(); + calleePayload.recordGet(); + calleePayload.recordWith(); + calleePayload.packetSerialize(); + calleePayload.packetDeserialize(); + calleePayload.traceDecode(); + calleePayload.queueTrySend(); + calleePayload.queueTryRecv(); + calleePayload.eventSchedule(); + calleePayload.traceOpen(); + calleePayload.traceNext(); + calleePayload.traceEof(); + calleePayload.tracePosition(); + calleePayload.contractRequire(); + calleePayload.contractEnsure(); + calleePayload.contractAssert(); + calleePayload.probe(); + calleePayload.statAdd(); + calleePayload.wakeCondition(); + calleePayload.wakeResource(); + calleePayload.wakeEventQueue(); + calleePayload.wakeNextDelta(); + calleePayload.scalarWrap(); + calleePayload.scalarUnwrap(); + member.kind(); + member.name(); + member.index(); + member.offsetBits(); + member.widthBits(); + member.signedness(); + member.encoding(); + member.typeKey(); + storageValue.members(); + storageValue.widthBits(); + storageValue.encoding(); + storagePacket.members(); + storagePacket.widthBits(); + storagePacket.bytes(); + storagePacket.encoding(); + valuePayload.kind(); + valuePayload.value(); + valuePayload.packet(); + callee.id(); + callee.symbol(); + callee.cpp(); + callee.kind(); + callee.fingerprint(); + callee.effect(); + callee.inputTypeKeys(); + callee.resultTypeKeys(); + callee.role(); + callee.payload(); + callee.sourceOperations(); + callee.declarations(); + callee.sourcePaths(); + valueType.id(); + valueType.symbol(); + valueType.cpp(); + valueType.kind(); + valueType.fingerprint(); + valueType.acirType(); + valueType.payload(); + set.processes(); + set.callees(); + set.valueTypes(); + set.lookupByDefinitionKey("@Top::@workload"); +}; + +static_assert(HasId); +static_assert(HasId); +static_assert(HasId); +static_assert(HasId); +static_assert(HasId); +static_assert(HasId); +static_assert(HasId); +static_assert(HasId); +static_assert(CompleteProcessStatePlanApi<>); +#define CHECK_GETTER(Class, Method, Return) \ + static_assert( \ + std::same_as) +#define CHECK_STRING(Class, Method) CHECK_GETTER(Class, Method, llvm::StringRef) +#define CHECK_U32(Class, Method) CHECK_GETTER(Class, Method, uint32_t) +#define CHECK_U64(Class, Method) CHECK_GETTER(Class, Method, uint64_t) + +CHECK_GETTER(ProcessCallSitePlan, operation, mlir::Operation *); +CHECK_STRING(ProcessCallSitePlan, operationPath); +CHECK_GETTER(ProcessCallSitePlan, iterationVector, llvm::ArrayRef); +CHECK_GETTER(ProcessOriginalOccurrence, operation, mlir::Operation *); +CHECK_STRING(ProcessOriginalOccurrence, operationPath); +CHECK_GETTER(ProcessOriginalOccurrence, callSites, + llvm::ArrayRef); +CHECK_GETTER(ProcessOriginalOccurrence, iterationVector, + llvm::ArrayRef); +CHECK_GETTER(ProcessSyntheticLoopOccurrence, anchor, + const ProcessOccurrenceId &); +CHECK_GETTER(ProcessSyntheticLoopOccurrence, phase, ProcessLoopPhase); +CHECK_GETTER(ProcessSyntheticWrapperOccurrence, anchor, + const ProcessOccurrenceId &); +CHECK_GETTER(ProcessSyntheticWrapperOccurrence, transition, + ProcessTransitionId); +CHECK_GETTER(ProcessSyntheticWrapperOccurrence, slot, ProcessLiveSlotId); +CHECK_GETTER(ProcessSyntheticWrapperOccurrence, direction, + ProcessWrapperDirection); +CHECK_GETTER(ProcessSyntheticConstantOccurrence, anchor, + const ProcessOccurrenceId &); +CHECK_U32(ProcessSyntheticConstantOccurrence, constant); +CHECK_GETTER(ProcessOccurrenceId, kind, ProcessOccurrenceKind); +CHECK_GETTER(ProcessOccurrenceId, original, const ProcessOriginalOccurrence &); +CHECK_GETTER(ProcessOccurrenceId, syntheticLoop, + const ProcessSyntheticLoopOccurrence &); +CHECK_GETTER(ProcessOccurrenceId, syntheticWrapper, + const ProcessSyntheticWrapperOccurrence &); +CHECK_GETTER(ProcessOccurrenceId, syntheticConstant, + const ProcessSyntheticConstantOccurrence &); +CHECK_GETTER(ProcessValueCoordinate, kind, ProcessValueCoordinateKind); +CHECK_STRING(ProcessValueCoordinate, ownerPath); +CHECK_U32(ProcessValueCoordinate, index); +CHECK_GETTER(ProcessOriginalPlannedValue, value, mlir::Value); +CHECK_GETTER(ProcessOriginalPlannedValue, occurrence, + const ProcessOccurrenceId &); +CHECK_GETTER(ProcessOriginalPlannedValue, coordinate, + const ProcessValueCoordinate &); +CHECK_STRING(ProcessOriginalPlannedValue, path); +CHECK_GETTER(ProcessCapturePlannedValue, capture, ProcessCaptureId); +CHECK_GETTER(ProcessLiveSlotPlannedValue, slot, ProcessLiveSlotId); +CHECK_GETTER(ProcessSyntheticPlannedValue, occurrence, + const ProcessOccurrenceId &); +CHECK_GETTER(ProcessSyntheticPlannedValue, coordinate, + const ProcessValueCoordinate &); +CHECK_STRING(ProcessConstantPlannedValue, value); +CHECK_GETTER(ProcessPlannedValue, kind, ProcessPlannedValueKind); +CHECK_GETTER(ProcessPlannedValue, type, mlir::Type); +CHECK_GETTER(ProcessPlannedValue, original, + const ProcessOriginalPlannedValue &); +CHECK_GETTER(ProcessPlannedValue, capture, const ProcessCapturePlannedValue &); +CHECK_GETTER(ProcessPlannedValue, liveSlot, + const ProcessLiveSlotPlannedValue &); +CHECK_GETTER(ProcessPlannedValue, synthetic, + const ProcessSyntheticPlannedValue &); +CHECK_GETTER(ProcessPlannedValue, constant, + const ProcessConstantPlannedValue &); +CHECK_STRING(ProcessScalarAttribute, name); +CHECK_STRING(ProcessScalarAttribute, value); +CHECK_STRING(ProcessScalarOperationPlan, name); +CHECK_GETTER(ProcessScalarOperationPlan, attributes, + llvm::ArrayRef); +CHECK_STRING(ProcessScalarOperationPlan, properties); +CHECK_GETTER(ProcessCapturePlan, id, ProcessCaptureId); +CHECK_STRING(ProcessCapturePlan, name); +CHECK_GETTER(ProcessCapturePlan, operand, mlir::Value); +CHECK_GETTER(ProcessCapturePlan, entryArgument, mlir::Value); +CHECK_GETTER(ProcessCapturePlan, type, mlir::Type); +CHECK_STRING(ProcessCapturePlan, operandPath); +CHECK_STRING(ProcessCapturePlan, argumentPath); +CHECK_U32(ProcessActionPlan, id); +CHECK_GETTER(ProcessActionPlan, kind, ProcessActionKind); +CHECK_GETTER(ProcessActionPlan, emission, ProcessEmissionClass); +CHECK_GETTER(ProcessActionPlan, occurrence, const ProcessOccurrenceId &); +CHECK_GETTER(ProcessActionPlan, sourceOperation, mlir::Operation *); +CHECK_GETTER(ProcessActionPlan, iterationVector, llvm::ArrayRef); +CHECK_GETTER(ProcessActionPlan, operands, llvm::ArrayRef); +CHECK_GETTER(ProcessActionPlan, results, llvm::ArrayRef); +CHECK_U32(ProcessActionPlan, cost); +CHECK_GETTER(ProcessActionPlan, resultTypes, llvm::ArrayRef); +CHECK_GETTER(ProcessActionPlan, callee, std::optional); +CHECK_GETTER(ProcessActionPlan, scalarOp, const ProcessScalarOperationPlan *); +CHECK_GETTER(ProcessLiveSlotPlan, id, ProcessLiveSlotId); +CHECK_STRING(ProcessLiveSlotPlan, name); +CHECK_GETTER(ProcessLiveSlotPlan, type, mlir::Type); +CHECK_GETTER(ProcessLiveSlotPlan, storageType, ProcessValueTypeId); +CHECK_GETTER(ProcessLiveSlotPlan, memberValues, + llvm::ArrayRef); +CHECK_GETTER(ProcessLiveSlotPlan, wrapCallee, std::optional); +CHECK_GETTER(ProcessLiveSlotPlan, unwrapCallee, std::optional); +CHECK_GETTER(ProcessSubscriptionSourcePlan, kind, + ProcessSubscriptionSourceKind); +CHECK_GETTER(ProcessSubscriptionSourcePlan, value, mlir::Value); +CHECK_GETTER(ProcessSubscriptionSourcePlan, owner, mlir::Operation *); +CHECK_GETTER(ProcessSubscriptionSourcePlan, declaration, mlir::Operation *); +CHECK_GETTER(ProcessSubscriptionSourcePlan, capture, + std::optional); +CHECK_STRING(ProcessSubscriptionSourcePlan, symbol); +CHECK_STRING(ProcessSubscriptionSourcePlan, path); +CHECK_STRING(ProcessSubscriptionSourcePlan, ownerPath); +CHECK_GETTER(ProcessWakePlan, id, ProcessWakeId); +CHECK_GETTER(ProcessWakePlan, kind, ProcessWakeKind); +CHECK_GETTER(ProcessWakePlan, operation, mlir::Operation *); +CHECK_GETTER(ProcessWakePlan, triggeringValue, mlir::Value); +CHECK_GETTER(ProcessWakePlan, declaration, mlir::Operation *); +CHECK_GETTER(ProcessWakePlan, callee, ProcessCalleeId); +CHECK_STRING(ProcessWakePlan, typeKey); +CHECK_STRING(ProcessWakePlan, operationPath); +CHECK_STRING(ProcessWakePlan, target); +CHECK_GETTER(ProcessWakePlan, occurrence, const ProcessOccurrenceId &); +CHECK_GETTER(ProcessWakePlan, iterationVector, llvm::ArrayRef); +CHECK_GETTER(ProcessWakePlan, sources, + llvm::ArrayRef); +CHECK_GETTER(ProcessTransitionStorePlan, slot, ProcessLiveSlotId); +CHECK_GETTER(ProcessTransitionStorePlan, source, const ProcessPlannedValue &); +CHECK_GETTER(ProcessTransitionStorePlan, sourceValue, mlir::Value); +CHECK_GETTER(ProcessTransitionLoadPlan, slot, ProcessLiveSlotId); +CHECK_GETTER(ProcessTransitionLoadPlan, replacements, + llvm::ArrayRef); +CHECK_GETTER(ProcessTransitionPlan, id, ProcessTransitionId); +CHECK_GETTER(ProcessTransitionPlan, sourcePc, ProcessPcId); +CHECK_GETTER(ProcessTransitionPlan, targetPc, ProcessPcId); +CHECK_GETTER(ProcessTransitionPlan, wake, ProcessWakeId); +CHECK_GETTER(ProcessTransitionPlan, iterationVector, llvm::ArrayRef); +CHECK_GETTER(ProcessTransitionPlan, stores, + llvm::ArrayRef); +CHECK_GETTER(ProcessTransitionPlan, loads, + llvm::ArrayRef); +CHECK_GETTER(ProcessForwardingBindingPlan, from, const ProcessPlannedValue &); +CHECK_GETTER(ProcessForwardingBindingPlan, to, const ProcessPlannedValue &); +CHECK_GETTER(ProcessControlFramePlan, kind, ProcessFrameKind); +CHECK_GETTER(ProcessControlFramePlan, phase, ProcessFramePhase); +CHECK_GETTER(ProcessControlFramePlan, operation, mlir::Operation *); +CHECK_STRING(ProcessControlFramePlan, operationPath); +CHECK_GETTER(ProcessControlFramePlan, bindings, + llvm::ArrayRef); +CHECK_GETTER(ProcessControlEdgePlan, kind, ProcessControlEdgeKind); +CHECK_GETTER(ProcessControlEdgePlan, condition, const ProcessPlannedValue &); +CHECK_GETTER(ProcessControlEdgePlan, trueBlock, ProcessBlockId); +CHECK_GETTER(ProcessControlEdgePlan, falseBlock, ProcessBlockId); +CHECK_GETTER(ProcessControlEdgePlan, trueBindings, + llvm::ArrayRef); +CHECK_GETTER(ProcessControlEdgePlan, falseBindings, + llvm::ArrayRef); +CHECK_GETTER(ProcessControlEdgePlan, targetBlock, ProcessBlockId); +CHECK_GETTER(ProcessControlEdgePlan, bindings, + llvm::ArrayRef); +CHECK_GETTER(ProcessControlEdgePlan, transition, ProcessTransitionId); +CHECK_GETTER(ProcessControlEdgePlan, status, ProcessTerminateStatus); +CHECK_GETTER(ProcessBlockPlan, id, ProcessBlockId); +CHECK_GETTER(ProcessBlockPlan, pc, ProcessPcId); +CHECK_GETTER(ProcessBlockPlan, originRegion, mlir::Region *); +CHECK_GETTER(ProcessBlockPlan, originBlock, mlir::Block *); +CHECK_STRING(ProcessBlockPlan, path); +CHECK_GETTER(ProcessBlockPlan, frames, llvm::ArrayRef); +CHECK_GETTER(ProcessBlockPlan, loads, + llvm::ArrayRef); +CHECK_GETTER(ProcessBlockPlan, actions, llvm::ArrayRef); +CHECK_GETTER(ProcessBlockPlan, edge, const ProcessControlEdgePlan &); +CHECK_U64(ProcessBlockPlan, cost); +CHECK_GETTER(ProcessPcPlan, id, ProcessPcId); +CHECK_STRING(ProcessPcPlan, name); +CHECK_STRING(ProcessPcPlan, entryPath); +CHECK_GETTER(ProcessPcPlan, blocks, llvm::ArrayRef); +CHECK_STRING(ProcessStatePlan, definitionKey); +CHECK_GETTER(ProcessStatePlan, process, ac::ProcessOp); +CHECK_GETTER(ProcessStatePlan, captures, llvm::ArrayRef); +CHECK_GETTER(ProcessStatePlan, entryPc, ProcessPcId); +CHECK_GETTER(ProcessStatePlan, pcs, llvm::ArrayRef); +CHECK_GETTER(ProcessStatePlan, blocks, llvm::ArrayRef); +CHECK_GETTER(ProcessStatePlan, liveSlots, llvm::ArrayRef); +CHECK_GETTER(ProcessStatePlan, wakes, llvm::ArrayRef); +CHECK_GETTER(ProcessStatePlan, transitions, + llvm::ArrayRef); +CHECK_U32(ProcessStatePlan, pcBitWidth); +CHECK_U64(ProcessStatePlan, fairnessWork); + +#define CHECK_PAYLOAD_STRING(Class, Method) CHECK_STRING(Class, Method) +CHECK_STRING(ProcessRecordFieldDescriptor, name); +CHECK_STRING(ProcessRecordFieldDescriptor, typeKey); +CHECK_GETTER(ProcessRecordCreatePayload, fields, + llvm::ArrayRef); +CHECK_STRING(ProcessRecordCreatePayload, recordType); +CHECK_PAYLOAD_STRING(ProcessRecordGetPayload, field); +CHECK_PAYLOAD_STRING(ProcessRecordGetPayload, record); +CHECK_PAYLOAD_STRING(ProcessRecordGetPayload, result); +CHECK_PAYLOAD_STRING(ProcessRecordWithPayload, field); +CHECK_PAYLOAD_STRING(ProcessRecordWithPayload, record); +CHECK_PAYLOAD_STRING(ProcessRecordWithPayload, value); +#define CHECK_PACKET(Class) \ + CHECK_U64(Class, bytes); \ + CHECK_STRING(Class, packet); \ + CHECK_STRING(Class, packetType) +CHECK_PACKET(ProcessPacketSerializePayload); +CHECK_PACKET(ProcessPacketDeserializePayload); +#define CHECK_TWO_STRINGS(Class, First, Second) \ + CHECK_STRING(Class, First); \ + CHECK_STRING(Class, Second) +#define CHECK_THREE_STRINGS(Class, First, Second, Third) \ + CHECK_STRING(Class, First); \ + CHECK_STRING(Class, Second); \ + CHECK_STRING(Class, Third) +CHECK_THREE_STRINGS(ProcessTraceDecodePayload, entry, result, source); +CHECK_TWO_STRINGS(ProcessQueueTrySendPayload, element, queue); +CHECK_TWO_STRINGS(ProcessQueueTryRecvPayload, element, queue); +CHECK_THREE_STRINGS(ProcessEventSchedulePayload, delay, target, value); +CHECK_STRING(ProcessTraceOpenPayload, source); +CHECK_TWO_STRINGS(ProcessTraceNextPayload, entry, source); +CHECK_STRING(ProcessTraceEofPayload, source); +CHECK_STRING(ProcessTracePositionPayload, source); +CHECK_STRING(ProcessContractRequirePayload, message); +CHECK_STRING(ProcessContractEnsurePayload, message); +CHECK_STRING(ProcessContractAssertPayload, message); +CHECK_THREE_STRINGS(ProcessProbePayload, kind, result, target); +CHECK_TWO_STRINGS(ProcessStatAddPayload, stat, valueType); +#define CHECK_WAKE_PAYLOAD(Class) \ + CHECK_GETTER(Class, wakeKind, ProcessWakeKind); \ + CHECK_STRING(Class, wakeType) +CHECK_WAKE_PAYLOAD(ProcessWakeConditionPayload); +CHECK_WAKE_PAYLOAD(ProcessWakeResourcePayload); +CHECK_WAKE_PAYLOAD(ProcessWakeEventQueuePayload); +CHECK_WAKE_PAYLOAD(ProcessWakeNextDeltaPayload); +#define CHECK_SCALAR_PAYLOAD(Class) \ + CHECK_GETTER(Class, direction, ProcessWrapperDirection); \ + CHECK_STRING(Class, scalar); \ + CHECK_STRING(Class, valueType) +CHECK_SCALAR_PAYLOAD(ProcessScalarWrapPayload); +CHECK_SCALAR_PAYLOAD(ProcessScalarUnwrapPayload); +CHECK_GETTER(ProcessGeneratedCalleePayload, role, ProcessHelperRole); +#define CHECK_PAYLOAD_ARM(Method, Type) \ + CHECK_GETTER(ProcessGeneratedCalleePayload, Method, const Type &) +CHECK_PAYLOAD_ARM(recordCreate, ProcessRecordCreatePayload); +CHECK_PAYLOAD_ARM(recordGet, ProcessRecordGetPayload); +CHECK_PAYLOAD_ARM(recordWith, ProcessRecordWithPayload); +CHECK_PAYLOAD_ARM(packetSerialize, ProcessPacketSerializePayload); +CHECK_PAYLOAD_ARM(packetDeserialize, ProcessPacketDeserializePayload); +CHECK_PAYLOAD_ARM(traceDecode, ProcessTraceDecodePayload); +CHECK_PAYLOAD_ARM(queueTrySend, ProcessQueueTrySendPayload); +CHECK_PAYLOAD_ARM(queueTryRecv, ProcessQueueTryRecvPayload); +CHECK_PAYLOAD_ARM(eventSchedule, ProcessEventSchedulePayload); +CHECK_PAYLOAD_ARM(traceOpen, ProcessTraceOpenPayload); +CHECK_PAYLOAD_ARM(traceNext, ProcessTraceNextPayload); +CHECK_PAYLOAD_ARM(traceEof, ProcessTraceEofPayload); +CHECK_PAYLOAD_ARM(tracePosition, ProcessTracePositionPayload); +CHECK_PAYLOAD_ARM(contractRequire, ProcessContractRequirePayload); +CHECK_PAYLOAD_ARM(contractEnsure, ProcessContractEnsurePayload); +CHECK_PAYLOAD_ARM(contractAssert, ProcessContractAssertPayload); +CHECK_PAYLOAD_ARM(probe, ProcessProbePayload); +CHECK_PAYLOAD_ARM(statAdd, ProcessStatAddPayload); +CHECK_PAYLOAD_ARM(wakeCondition, ProcessWakeConditionPayload); +CHECK_PAYLOAD_ARM(wakeResource, ProcessWakeResourcePayload); +CHECK_PAYLOAD_ARM(wakeEventQueue, ProcessWakeEventQueuePayload); +CHECK_PAYLOAD_ARM(wakeNextDelta, ProcessWakeNextDeltaPayload); +CHECK_PAYLOAD_ARM(scalarWrap, ProcessScalarWrapPayload); +CHECK_PAYLOAD_ARM(scalarUnwrap, ProcessScalarUnwrapPayload); +CHECK_GETTER(ProcessValueTypeMemberPlan, kind, ProcessValueTypeMemberKind); +CHECK_STRING(ProcessValueTypeMemberPlan, name); +CHECK_GETTER(ProcessValueTypeMemberPlan, index, std::optional); +CHECK_U64(ProcessValueTypeMemberPlan, offsetBits); +CHECK_U64(ProcessValueTypeMemberPlan, widthBits); +CHECK_GETTER(ProcessValueTypeMemberPlan, signedness, + std::optional); +CHECK_STRING(ProcessValueTypeMemberPlan, encoding); +CHECK_STRING(ProcessValueTypeMemberPlan, typeKey); +CHECK_GETTER(ProcessStorageValuePayload, members, + llvm::ArrayRef); +CHECK_U64(ProcessStorageValuePayload, widthBits); +CHECK_STRING(ProcessStorageValuePayload, encoding); +CHECK_GETTER(ProcessStoragePacketPayload, members, + llvm::ArrayRef); +CHECK_U64(ProcessStoragePacketPayload, widthBits); +CHECK_U64(ProcessStoragePacketPayload, bytes); +CHECK_STRING(ProcessStoragePacketPayload, encoding); +CHECK_GETTER(ProcessValueTypePayload, kind, ProcessValueTypeKind); +CHECK_GETTER(ProcessValueTypePayload, value, + const ProcessStorageValuePayload &); +CHECK_GETTER(ProcessValueTypePayload, packet, + const ProcessStoragePacketPayload &); +CHECK_GETTER(ProcessGeneratedCalleePlan, id, ProcessCalleeId); +CHECK_STRING(ProcessGeneratedCalleePlan, symbol); +CHECK_STRING(ProcessGeneratedCalleePlan, cpp); +CHECK_STRING(ProcessGeneratedCalleePlan, kind); +CHECK_STRING(ProcessGeneratedCalleePlan, fingerprint); +CHECK_GETTER(ProcessGeneratedCalleePlan, effect, ProcessEffectKind); +CHECK_GETTER(ProcessGeneratedCalleePlan, inputTypeKeys, + llvm::ArrayRef); +CHECK_GETTER(ProcessGeneratedCalleePlan, resultTypeKeys, + llvm::ArrayRef); +CHECK_GETTER(ProcessGeneratedCalleePlan, role, ProcessHelperRole); +CHECK_GETTER(ProcessGeneratedCalleePlan, payload, + const ProcessGeneratedCalleePayload &); +CHECK_GETTER(ProcessGeneratedCalleePlan, sourceOperations, + llvm::ArrayRef); +CHECK_GETTER(ProcessGeneratedCalleePlan, declarations, + llvm::ArrayRef); +CHECK_GETTER(ProcessGeneratedCalleePlan, sourcePaths, + llvm::ArrayRef); +CHECK_GETTER(ProcessValueTypePlan, id, ProcessValueTypeId); +CHECK_STRING(ProcessValueTypePlan, symbol); +CHECK_STRING(ProcessValueTypePlan, cpp); +CHECK_GETTER(ProcessValueTypePlan, kind, ProcessValueTypeKind); +CHECK_STRING(ProcessValueTypePlan, fingerprint); +CHECK_GETTER(ProcessValueTypePlan, acirType, mlir::Type); +CHECK_GETTER(ProcessValueTypePlan, payload, const ProcessValueTypePayload &); +CHECK_GETTER(ProcessStatePlanSet, processes, llvm::ArrayRef); +CHECK_GETTER(ProcessStatePlanSet, callees, + llvm::ArrayRef); +CHECK_GETTER(ProcessStatePlanSet, valueTypes, + llvm::ArrayRef); +static_assert( + std::same_as); + +#undef CHECK_PAYLOAD_ARM +#undef CHECK_SCALAR_PAYLOAD +#undef CHECK_WAKE_PAYLOAD +#undef CHECK_PACKET +#undef CHECK_THREE_STRINGS +#undef CHECK_TWO_STRINGS +#undef CHECK_PAYLOAD_STRING +#undef CHECK_U64 +#undef CHECK_U32 +#undef CHECK_STRING +#undef CHECK_GETTER +static_assert(!HasOrdinal); +static_assert(!HasSetter); +static_assert(!HasMutableProcesses); +static_assert(!HasComponentLookup); +static_assert(!HasHierarchyLookup); +static_assert(!HasFallbackLookup); +static_assert(!std::default_initializable); +static_assert(!std::constructible_from); +#define CHECK_ID_VALUE(Type) \ + static_assert( \ + std::same_as) +CHECK_ID_VALUE(ProcessCalleeId); +CHECK_ID_VALUE(ProcessValueTypeId); +CHECK_ID_VALUE(ProcessCaptureId); +CHECK_ID_VALUE(ProcessPcId); +CHECK_ID_VALUE(ProcessBlockId); +CHECK_ID_VALUE(ProcessLiveSlotId); +CHECK_ID_VALUE(ProcessWakeId); +CHECK_ID_VALUE(ProcessTransitionId); +#undef CHECK_ID_VALUE + +constexpr llvm::StringLiteral kEmptyBytes = + R"json({"callees":[],"contract_epoch":"0.3","processes":[],"schema":"acir-process-state-plan-0.1","value_types":[]})json"; +constexpr llvm::StringLiteral kSpecialization = + R"json({"contract_epoch":"0.3","effect":"stateful","inputs":[],"kind":"implementation","payload":{"wake_kind":"next_delta","wake_type":"@acir_wake_next_delta"},"results":["@acir_wake_next_delta"],"role":"wake_next_delta","schema":"acir-generated-implementation-0.1","source_paths":[]})json"; +constexpr llvm::StringLiteral kDescriptor = + R"json({"cpp":"acir::generated::impl_wake_next_delta_28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c","effect":"stateful","fingerprint":"sha256:28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c","inputs":[],"kind":"implementation","ordinal":0,"payload":{"wake_kind":"next_delta","wake_type":"@acir_wake_next_delta"},"results":["@acir_wake_next_delta"],"role":"wake_next_delta","source_paths":[],"symbol":"@acir_impl_wake_next_delta_28670f81a4b5f79039c0859878e49d133debaaa96ce07fd057110aa5fba8f36c"})json"; + +TEST(ProcessStatePlanApiTest, EmptyFrozenModelHasLiteralCanonicalBytes) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseEmptyModel(context); + ASSERT_TRUE(module); + auto built = detail::PlanSetBuilder::buildEmpty(*module); + ASSERT_TRUE(mlir::succeeded(built)); + ProcessStatePlanSet plans = std::move(*built); + ASSERT_TRUE(mlir::succeeded(verifyProcessStatePlan(plans))); + auto bytes = serializeProcessStatePlan(plans); + ASSERT_TRUE(static_cast(bytes)) << test::takeError(bytes.takeError()); + EXPECT_EQ(*bytes, kEmptyBytes); +} + +TEST(ProcessStatePlanBasicTest, YieldOnlyBaselineIsExactAndImmutable) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + ASSERT_TRUE(module); + auto built = detail::PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(built)); + ProcessStatePlanSet plans = std::move(*built); + ASSERT_TRUE(mlir::succeeded(verifyProcessStatePlan(plans))); + ASSERT_EQ(plans.processes().size(), 1U); + ASSERT_EQ(plans.callees().size(), 1U); + EXPECT_TRUE(plans.valueTypes().empty()); + + const ProcessStatePlan *plan = plans.lookupByDefinitionKey("@Top::@workload"); + ASSERT_NE(plan, nullptr); + EXPECT_EQ(plans.lookupByDefinitionKey("@workload"), nullptr); + EXPECT_EQ(plans.lookupByDefinitionKey("workload"), nullptr); + EXPECT_EQ(plans.lookupByDefinitionKey("Top.workload"), nullptr); + EXPECT_EQ(plan->entryPc().value(), 0U); + EXPECT_EQ(plan->pcBitWidth(), 1U); + EXPECT_EQ(plan->pcs().size(), 1U); + EXPECT_EQ(plan->pcs()[0].name(), "entry"); + EXPECT_EQ(plan->pcs()[0].id().value(), 0U); + EXPECT_TRUE(plan->liveSlots().empty()); + ASSERT_EQ(plan->wakes().size(), 1U); + EXPECT_EQ(plan->wakes()[0].kind(), ProcessWakeKind::NextDelta); + EXPECT_EQ(plan->wakes()[0].typeKey(), "@acir_wake_next_delta"); + EXPECT_EQ(plan->wakes()[0].callee().value(), 0U); + ASSERT_EQ(plan->transitions().size(), 1U); + EXPECT_EQ(plan->transitions()[0].targetPc().value(), 0U); + + const auto &callee = plans.callees()[0]; + EXPECT_EQ(callee.id().value(), 0U); + EXPECT_EQ(callee.kind(), "implementation"); + EXPECT_EQ(callee.effect(), ProcessEffectKind::Stateful); + EXPECT_EQ(callee.role(), ProcessHelperRole::WakeNextDelta); + EXPECT_EQ(callee.payload().wakeNextDelta().wakeKind(), + ProcessWakeKind::NextDelta); + EXPECT_EQ(callee.payload().wakeNextDelta().wakeType(), + "@acir_wake_next_delta"); + EXPECT_EQ(detail::generatedCalleeSpecializationBytes(callee), + kSpecialization); + EXPECT_EQ(detail::generatedCalleeDescriptorBytes(callee), kDescriptor); +} + +TEST(ProcessStatePlanBasicTest, FrozenDeclarationPermutationsAreByteIdentical) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto first = test::parseAndFreezeYieldPermutation(context, false); + auto second = test::parseAndFreezeYieldPermutation(context, true); + ASSERT_TRUE(first); + ASSERT_TRUE(second); + auto firstBuilt = detail::PlanSetBuilder::buildYieldOnly(*first); + auto secondBuilt = detail::PlanSetBuilder::buildYieldOnly(*second); + ASSERT_TRUE(mlir::succeeded(firstBuilt)); + ASSERT_TRUE(mlir::succeeded(secondBuilt)); + auto firstPlan = std::move(*firstBuilt); + auto secondPlan = std::move(*secondBuilt); + auto firstBytes = serializeProcessStatePlan(firstPlan); + auto secondBytes = serializeProcessStatePlan(secondPlan); + ASSERT_TRUE(static_cast(firstBytes)); + ASSERT_TRUE(static_cast(secondBytes)); + EXPECT_EQ(*firstBytes, *secondBytes); + ASSERT_EQ(firstPlan.processes().size(), 2U); + ASSERT_EQ(secondPlan.processes().size(), 2U); + EXPECT_EQ(firstPlan.processes()[0].definitionKey(), "@Top::@alpha"); + EXPECT_EQ(firstPlan.processes()[1].definitionKey(), "@Top::@workload"); + for (auto [firstProcess, secondProcess] : + llvm::zip_equal(firstPlan.processes(), secondPlan.processes())) { + EXPECT_EQ(firstProcess.definitionKey(), secondProcess.definitionKey()); + EXPECT_EQ(firstProcess.entryPc(), secondProcess.entryPc()); + EXPECT_EQ(firstProcess.pcBitWidth(), secondProcess.pcBitWidth()); + EXPECT_EQ(firstProcess.fairnessWork(), secondProcess.fairnessWork()); + EXPECT_EQ(firstProcess.pcs().size(), secondProcess.pcs().size()); + EXPECT_EQ(firstProcess.blocks().size(), secondProcess.blocks().size()); + EXPECT_EQ(firstProcess.wakes().size(), secondProcess.wakes().size()); + EXPECT_EQ(firstProcess.transitions().size(), + secondProcess.transitions().size()); + } + EXPECT_EQ(detail::generatedCalleeDescriptorBytes(firstPlan.callees()[0]), + detail::generatedCalleeDescriptorBytes(secondPlan.callees()[0])); +} + +TEST(ProcessStatePlanBasicTest, EveryFrozenSemanticCorruptionIsRejected) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + ASSERT_TRUE(module); + auto built = detail::PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(built)); + auto plan = std::move(*built); + auto permutationModule = test::parseAndFreezeYieldPermutation(context, false); + ASSERT_TRUE(permutationModule); + auto permutationBuilt = + detail::PlanSetBuilder::buildYieldOnly(*permutationModule); + ASSERT_TRUE(mlir::succeeded(permutationBuilt)); + auto permutationPlan = std::move(*permutationBuilt); + mlir::ScopedDiagnosticHandler handler( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + const ProcessStatePlanCorruptionForTest corruptions[] = { + ProcessStatePlanCorruptionForTest::DuplicateOrdinal, + ProcessStatePlanCorruptionForTest::NonDenseOrdinal, + ProcessStatePlanCorruptionForTest::DanglingReference, + ProcessStatePlanCorruptionForTest::DuplicateIdentity, + ProcessStatePlanCorruptionForTest::UnsortedCanonicalOrder, + ProcessStatePlanCorruptionForTest::CostMismatch, + ProcessStatePlanCorruptionForTest::DefinitionKeyMismatch, + ProcessStatePlanCorruptionForTest::CalleeSpecializationMismatch, + ProcessStatePlanCorruptionForTest::ValueTypeSpecializationMismatch, + ProcessStatePlanCorruptionForTest::EffectMismatch, + ProcessStatePlanCorruptionForTest::IdKindMismatch, + ProcessStatePlanCorruptionForTest::WrongTypeKey, + ProcessStatePlanCorruptionForTest::InvalidFramePhase, + ProcessStatePlanCorruptionForTest::InvalidEdgeBinding, + ProcessStatePlanCorruptionForTest::InvalidWakeCallee, + }; + constexpr llvm::StringLiteral diagnostics[] = { + "process-state plan invariant violated: duplicate ordinal", + "process-state plan invariant violated: non-dense ordinal", + "process-state plan invariant violated: dangling reference", + "process-state plan invariant violated: duplicate identity", + "process-state plan invariant violated: unsorted canonical order", + "process-state plan invariant violated: cost mismatch", + "process-state plan invariant violated: definition key mismatch", + "process-state plan invariant violated: callee specialization mismatch", + // NOLINTNEXTLINE(bugprone-suspicious-missing-comma) intentional split + "process-state plan invariant violated: value-type specialization " + "mismatch", + "process-state plan invariant violated: effect mismatch", + "process-state plan invariant violated: ID kind mismatch", + "process-state plan invariant violated: wrong type key", + "process-state plan invariant violated: invalid frame phase", + "process-state plan invariant violated: invalid edge binding", + "process-state plan invariant violated: invalid wake callee", + }; + for (auto [index, corruption] : llvm::enumerate(corruptions)) { + SCOPED_TRACE(static_cast(corruption)); + const ProcessStatePlanSet &source = + corruption == ProcessStatePlanCorruptionForTest::UnsortedCanonicalOrder + ? permutationPlan + : plan; + auto corrupted = + cloneProcessStatePlanWithCorruptionForTest(source, corruption); + EXPECT_TRUE(mlir::failed(verifyProcessStatePlan(corrupted))); + EXPECT_EQ(diagnostics[index], + detail::lastProcessStatePlanDiagnosticForTest()); + } +} + +TEST(ProcessStatePlanBasicTest, + MalformedPrivateShapesReturnDiagnosticsWithoutDereferencingInvalidArms) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + ASSERT_TRUE(module); + auto built = detail::PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(built)); + auto plan = std::move(*built); + mlir::ScopedDiagnosticHandler handler( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + + struct Case { + ProcessStatePlanSet (*build)(const ProcessStatePlanSet &); + llvm::StringLiteral diagnostic; + }; + const Case cases[] = { + {detail::PlanSetBuilder::cloneWithMissingWakeCallee, + "process-state plan invariant violated: invalid wake callee"}, + {detail::PlanSetBuilder::cloneWithDanglingSuspendTransition, + "process-state plan invariant violated: dangling reference"}, + {detail::PlanSetBuilder::cloneWithUnpairedLiveSlotCallee, + "process-state plan invariant violated: invalid live-slot wrapper pair"}, + {detail::PlanSetBuilder::cloneWithMissingValueTypePayload, + "process-state plan invariant violated: value-type specialization " + "mismatch"}, + {detail::PlanSetBuilder::cloneWithNullEdgeStorage, + "process-state plan invariant violated: invalid edge binding"}, + {detail::PlanSetBuilder::cloneWithInactiveEdgeField, + "process-state plan invariant violated: invalid edge binding"}, + {detail::PlanSetBuilder::cloneWithDoubleValueTypePayload, + "process-state plan invariant violated: value-type specialization " + "mismatch"}, + {detail::PlanSetBuilder::cloneWithMissingOriginalActionSource, + "process-state plan invariant violated: invalid action arm"}, + {detail::PlanSetBuilder::cloneWithUnexpectedConstantActionSource, + "process-state plan invariant violated: invalid action arm"}, + }; + for (auto [index, testCase] : llvm::enumerate(cases)) { + SCOPED_TRACE(index); + ProcessStatePlanSet corrupted = testCase.build(plan); + EXPECT_TRUE(mlir::failed(verifyProcessStatePlan(corrupted))); + EXPECT_EQ(testCase.diagnostic.str(), + detail::lastProcessStatePlanDiagnosticForTest().str()); + auto serialized = serializeProcessStatePlan(corrupted); + EXPECT_FALSE(static_cast(serialized)); + llvm::consumeError(serialized.takeError()); + } +} + +TEST(ProcessStatePlanBasicTest, + FrozenLoopActionFixtureBuildsWithoutMutatingItsInputModule) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeLoopActions(context); + ASSERT_TRUE(module); + const std::string textBefore = test::moduleText(*module); + const std::string bytecodeBefore = test::moduleBytecode(*module); + const std::string freezeSealBefore = test::moduleFreezeSeal(*module); + ASSERT_FALSE(bytecodeBefore.empty()); + ASSERT_FALSE(freezeSealBefore.empty()); + + auto built = detail::PlanSetBuilder::buildLoopActionFixture(*module); + ASSERT_TRUE(mlir::succeeded(built)); + ASSERT_TRUE(mlir::succeeded(verifyProcessStatePlan(*built))); + auto serialized = serializeProcessStatePlan(*built); + ASSERT_TRUE(static_cast(serialized)); + ASSERT_EQ(built->processes().size(), 1U); + const auto &actions = built->processes().front().blocks().front().actions(); + ASSERT_EQ(actions.size(), 3U); + EXPECT_EQ(actions[0].kind(), ProcessActionKind::ForInitialize); + EXPECT_EQ(actions[1].kind(), ProcessActionKind::ForCondition); + EXPECT_EQ(actions[2].kind(), ProcessActionKind::ForIncrement); + EXPECT_EQ(actions[0].sourceOperation(), actions[1].sourceOperation()); + EXPECT_EQ(actions[1].sourceOperation(), actions[2].sourceOperation()); + for (const ProcessActionPlan &action : actions) { + const ProcessOccurrenceId &anchor = + action.occurrence().syntheticLoop().anchor(); + ASSERT_EQ(anchor.kind(), ProcessOccurrenceKind::Original); + EXPECT_EQ(anchor.original().operation(), action.sourceOperation()); + EXPECT_EQ(anchor.original().operationPath(), "@Top::@workload/r0/b0/o3"); + EXPECT_TRUE(anchor.original().iterationVector().empty()); + } + + EXPECT_EQ(test::moduleText(*module), textBefore); + EXPECT_EQ(test::moduleBytecode(*module), bytecodeBefore); + EXPECT_EQ(test::moduleFreezeSeal(*module), freezeSealBefore); +} + +TEST(ProcessStatePlanBasicTest, + IsolatedLoopActionCorruptionsRejectWithoutMutatingFrozenInput) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeLoopActions(context); + ASSERT_TRUE(module); + const std::string textBefore = test::moduleText(*module); + const std::string bytecodeBefore = test::moduleBytecode(*module); + const std::string freezeSealBefore = test::moduleFreezeSeal(*module); + ASSERT_FALSE(bytecodeBefore.empty()); + ASSERT_FALSE(freezeSealBefore.empty()); + + auto built = detail::PlanSetBuilder::buildLoopActionFixture(*module); + ASSERT_TRUE(mlir::succeeded(built)); + const ProcessStatePlanSet plan = std::move(*built); + ASSERT_TRUE(mlir::succeeded(verifyProcessStatePlan(plan))); + + struct Case { + ProcessStatePlanSet (*corrupt)(const ProcessStatePlanSet &); + const char *name; + bool rejectedByStructuralPreflight; + }; + const Case cases[] = { + {detail::PlanSetBuilder::cloneWithForInitializeWrongOwningLoopSource, + "for_initialize wrong owning-loop source", true}, + {detail::PlanSetBuilder::cloneWithForConditionWrongOwningLoopSource, + "for_condition wrong owning-loop source", true}, + {detail::PlanSetBuilder::cloneWithForIncrementWrongOwningLoopSource, + "for_increment wrong owning-loop source", true}, + {detail::PlanSetBuilder::cloneWithNonLoopForActionSource, + "for_condition non-loop source", true}, + {detail::PlanSetBuilder::cloneWithForConditionWrongResultType, + "for_condition wrong result type", false}, + {detail::PlanSetBuilder::cloneWithForIncrementWrongResultType, + "for_increment wrong result type", false}, + {detail::PlanSetBuilder::cloneWithForConditionInactiveCallee, + "for_condition inactive callee", true}, + {detail::PlanSetBuilder::cloneWithForInitializeInactiveScalar, + "for_initialize inactive scalar", true}, + {detail::PlanSetBuilder::cloneWithForConditionWrongEmission, + "for_condition wrong emission", true}, + {detail::PlanSetBuilder::cloneWithForConditionWrongScalarOp, + "for_condition wrong scalar operation", false}, + {detail::PlanSetBuilder::cloneWithForIncrementWrongEmission, + "for_increment wrong emission", true}, + {detail::PlanSetBuilder::cloneWithForIncrementWrongScalarOp, + "for_increment wrong scalar operation", false}, + }; + mlir::ScopedDiagnosticHandler handler( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + auto expectRejected = [&](ProcessStatePlanSet corrupted, const char *name, + bool rejectedByStructuralPreflight) { + SCOPED_TRACE(name); + EXPECT_EQ(detail::PlanSetBuilder::structuralError(corrupted), + rejectedByStructuralPreflight + ? "process-state plan invariant violated: invalid action arm" + : ""); + EXPECT_TRUE(mlir::failed(verifyProcessStatePlan(corrupted))); + EXPECT_EQ(detail::lastProcessStatePlanDiagnosticForTest(), + "process-state plan invariant violated: invalid action arm"); + auto serialized = serializeProcessStatePlan(corrupted); + if (serialized) { + ADD_FAILURE() << "serializer accepted isolated corruption"; + return; + } + EXPECT_EQ(test::takeError(serialized.takeError()), + "process-state plan verification failed"); + }; + for (const Case &testCase : cases) { + expectRejected(testCase.corrupt(plan), testCase.name, + testCase.rejectedByStructuralPreflight); + } + + using Mutation = detail::PlanSetBuilder::LoopActionMutationForTest; + struct RelationshipCase { + uint32_t actionIndex; + Mutation mutation; + const char *name; + bool rejectedByStructuralPreflight; + }; + const RelationshipCase relationshipCases[] = { + {0, Mutation::InactiveCallee, "for_initialize inactive callee", true}, + {2, Mutation::InactiveCallee, "for_increment inactive callee", true}, + {1, Mutation::MissingScalar, "for_condition missing scalar operation", + true}, + {1, Mutation::WrongScalarProperties, + "for_condition wrong scalar properties", false}, + {1, Mutation::WrongScalarAttributeCount, + "for_condition wrong scalar attribute count", false}, + {1, Mutation::WrongScalarAttributeName, + "for_condition wrong predicate name", false}, + {1, Mutation::WrongScalarAttributeValue, + "for_condition wrong predicate value", false}, + {1, Mutation::WrongOperandCount, "for_condition wrong operand count", + false}, + {1, Mutation::WrongOperandType, "for_condition wrong operand type", + false}, + {1, Mutation::WrongResultCount, "for_condition wrong result count", + false}, + {2, Mutation::MissingScalar, "for_increment missing scalar operation", + true}, + {2, Mutation::WrongScalarProperties, + "for_increment wrong scalar properties", false}, + {2, Mutation::WrongScalarAttributeCount, + "for_increment forbidden scalar attribute", false}, + {2, Mutation::WrongOperandCount, "for_increment wrong operand count", + false}, + {2, Mutation::WrongOperandType, "for_increment wrong operand type", + false}, + {2, Mutation::WrongResultCount, "for_increment wrong result count", + false}, + }; + for (const RelationshipCase &testCase : relationshipCases) { + expectRejected(detail::PlanSetBuilder::cloneLoopActionWithMutationForTest( + plan, testCase.actionIndex, testCase.mutation), + testCase.name, testCase.rejectedByStructuralPreflight); + } + + EXPECT_EQ(test::moduleText(*module), textBefore); + EXPECT_EQ(test::moduleBytecode(*module), bytecodeBefore); + EXPECT_EQ(test::moduleFreezeSeal(*module), freezeSealBefore); +} + +TEST(ProcessStatePlanBasicTest, + PrivateFixtureConstructionRejectsUnexpectedProcessStructure) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto empty = test::parseEmptyModel(context); + auto yieldOnly = test::parseAndFreezeYieldOnly(context); + ASSERT_TRUE(empty); + ASSERT_TRUE(yieldOnly); + mlir::ScopedDiagnosticHandler handler( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + EXPECT_TRUE(mlir::failed(detail::PlanSetBuilder::buildYieldOnly(*empty))); + EXPECT_TRUE(mlir::failed(detail::PlanSetBuilder::buildEmpty(*yieldOnly))); +} + +TEST(ProcessStatePlanBasicTest, + CompletePrivateFixtureExercisesEveryPublicGetterAndUnionArm) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + EXPECT_TRUE(detail::PlanSetBuilder::exerciseCompleteApiFixture(context)); + EXPECT_TRUE(detail::PlanSetBuilder::exerciseAllActionArmsFixture(context)); +} + +TEST(ProcessStatePlanBasicTest, + LongProcessGraphUsesBoundedIterativeFairnessVerification) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + ASSERT_TRUE(module); + auto baseline = detail::PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(baseline)); + auto longChain = + detail::PlanSetBuilder::cloneWithLongLocalChain(*baseline, 32768); + EXPECT_TRUE(mlir::succeeded(verifyProcessStatePlan(longChain))); + EXPECT_EQ(longChain.processes().front().blocks().size(), 32768U); + EXPECT_EQ(longChain.processes().front().fairnessWork(), 32768U); + mlir::ScopedDiagnosticHandler handler( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + auto cycle = detail::PlanSetBuilder::cloneWithLocalCycle(longChain); + EXPECT_TRUE(mlir::failed(verifyProcessStatePlan(cycle))); + EXPECT_EQ(detail::lastProcessStatePlanDiagnosticForTest(), + "process-state plan invariant violated: cost mismatch"); + auto unreachable = + detail::PlanSetBuilder::cloneWithUnreachableBlock(*baseline); + EXPECT_TRUE(mlir::failed(verifyProcessStatePlan(unreachable))); + EXPECT_EQ(detail::lastProcessStatePlanDiagnosticForTest(), + "process-state plan invariant violated: cost mismatch"); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanControlFlowTest.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanControlFlowTest.cpp new file mode 100644 index 00000000..68015ea1 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanControlFlowTest.cpp @@ -0,0 +1,147 @@ +#include "Analysis/ProcessStatePlanInternal.h" +#include "Analysis/ProcessStatePlanTestHooks.h" +#include "ProcessStatePlanTestSupport.h" +#include "acir/Analysis/ProcessStatePlan.h" +#include "acir/InitAllDialects.h" + +#include "gtest/gtest.h" + +namespace acir { +namespace { + +using PlanSetBuilder = detail::PlanSetBuilder; + +static ProcessStatePlanSet getYieldOnlyPlan() { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto built = PlanSetBuilder::buildYieldOnly(*module); + assert(mlir::succeeded(built)); + return *built; +} + +TEST(ProcessStatePlanControlFlowTest, + YieldOnlyProducesEntryPcOneBlockOneWakeOneTransition) { + auto plans = getYieldOnlyPlan(); + ASSERT_EQ(plans.processes().size(), 1u); + const auto &process = plans.processes()[0]; + EXPECT_EQ(process.pcs().size(), 1u); + EXPECT_EQ(process.pcs()[0].name(), "entry"); + EXPECT_EQ(process.pcs()[0].id().value(), 0u); + EXPECT_EQ(process.blocks().size(), 1u); + EXPECT_EQ(process.wakes().size(), 1u); + EXPECT_EQ(process.wakes()[0].kind(), ProcessWakeKind::NextDelta); + EXPECT_EQ(process.wakes()[0].typeKey(), "@acir_wake_next_delta"); + EXPECT_EQ(process.transitions().size(), 1u); + EXPECT_EQ(process.transitions()[0].sourcePc().value(), 0u); + EXPECT_EQ(process.transitions()[0].targetPc().value(), 0u); + EXPECT_EQ(process.transitions()[0].wake().value(), 0u); +} + +TEST(ProcessStatePlanControlFlowTest, EntryBlockHasSuspendEdge) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + ASSERT_GE(process.blocks().size(), 1u); + const auto &edge = process.blocks()[0].edge(); + EXPECT_EQ(edge.kind(), ProcessControlEdgeKind::Suspend); +} + +TEST(ProcessStatePlanControlFlowTest, WakeHasCorrectKindAndTypeKey) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + ASSERT_GE(process.wakes().size(), 1u); + EXPECT_EQ(process.wakes()[0].typeKey(), "@acir_wake_next_delta"); + EXPECT_EQ(process.wakes()[0].kind(), ProcessWakeKind::NextDelta); +} + +TEST(ProcessStatePlanControlFlowTest, + PublicFactoryExpandsBoundedForDeterministically) { + mlir::MLIRContext context; + mlir::DialectRegistry registry; + registerAllDialects(registry); + context.appendDialectRegistry(registry); + auto module = test::parseAndFreezeLoopActions(context); + ASSERT_TRUE(module); + auto plans = planProcessState(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + ASSERT_EQ(plans->processes().size(), 1u); + size_t constantActions = 0; + for (const ProcessBlockPlan &block : plans->processes().front().blocks()) + for (const ProcessActionPlan &action : block.actions()) + if (action.kind() == ProcessActionKind::Constant) + ++constantActions; + EXPECT_EQ(constantActions, 8u); +} + +TEST(ProcessStatePlanControlFlowTest, + PublicFactoryPlansIfSuspensionAndScalarLiveness) { + mlir::MLIRContext context; + mlir::DialectRegistry registry; + registerAllDialects(registry); + context.appendDialectRegistry(registry); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes { + ac.contract_epoch = "0.3", + ac.freeze_epoch = "0.3", + ac.topology_frozen = true + } { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %ready = arith.constant true + %value = arith.constant 7 : i32 + scf.if %ready { ac.wait_until %ready } + %used = arith.addi %value, %value : i32 + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + auto plans = planProcessState(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + const ProcessStatePlan &process = plans->processes().front(); + EXPECT_GT(process.blocks().size(), 1u); + EXPECT_EQ(process.liveSlots().size(), 1u); + EXPECT_EQ(process.transitions().front().stores().size(), 1u); + ASSERT_TRUE(process.liveSlots().front().wrapCallee()); + ASSERT_TRUE(process.liveSlots().front().unwrapCallee()); + EXPECT_EQ(plans->callees()[process.liveSlots().front().wrapCallee()->value()] + .role(), + ProcessHelperRole::ScalarWrap); + EXPECT_EQ( + plans->callees()[process.liveSlots().front().unwrapCallee()->value()] + .role(), + ProcessHelperRole::ScalarUnwrap); + + size_t wrapActions = 0; + size_t unwrapActions = 0; + for (const ProcessBlockPlan &block : process.blocks()) { + for (const ProcessActionPlan &action : block.actions()) { + wrapActions += action.kind() == ProcessActionKind::ScalarWrap; + unwrapActions += action.kind() == ProcessActionKind::ScalarUnwrap; + } + } + EXPECT_EQ(wrapActions, 1u); + EXPECT_EQ(unwrapActions, 1u); + + auto branch = + llvm::find_if(process.blocks(), [](const ProcessBlockPlan &block) { + return block.edge().kind() == ProcessControlEdgeKind::Branch; + }); + ASSERT_NE(branch, process.blocks().end()); + EXPECT_EQ(process.blocks()[branch->edge().trueBlock().value()].pc(), + branch->pc()); + EXPECT_EQ(process.blocks()[branch->edge().falseBlock().value()].pc(), + branch->pc()); + + uint64_t largestBlockCost = 0; + for (const ProcessBlockPlan &block : process.blocks()) + largestBlockCost = std::max(largestBlockCost, block.cost()); + EXPECT_GT(process.fairnessWork(), largestBlockCost); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanCostConsumer.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanCostConsumer.cpp new file mode 100644 index 00000000..0f2690d6 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanCostConsumer.cpp @@ -0,0 +1,31 @@ +// Public-header-only consumer test: includes only ProcessStatePlan.h, +// never lib/ internals. Verifies Task 13 can consume plans without +// access to ProcessStatePlanInternal.h or any implementation detail. +#include "acir/Analysis/ProcessStatePlan.h" + +#include "gtest/gtest.h" + +namespace acir { +namespace { + +// This test can only use the public API to verify plan structure. +// It proves that an external consumer (Task 13) can read plan records +// without re-implementing liveness, expansion, or control analysis. + +TEST(ProcessStatePlanCostConsumer, PublicPlanHasRequiredAccessors) { + // Verify the public types exist and are accessible + // These are compile-time checks: if any public type is missing, + // this test fails to compile. + static_assert(sizeof(ProcessCalleeId) > 0); + static_assert(sizeof(ProcessValueTypeId) > 0); + static_assert(sizeof(ProcessPcId) > 0); + static_assert(sizeof(ProcessBlockId) > 0); + static_assert(sizeof(ProcessWakeId) > 0); + static_assert(sizeof(ProcessTransitionId) > 0); + static_assert(sizeof(ProcessLiveSlotId) > 0); + static_assert(sizeof(ProcessCaptureId) > 0); + SUCCEED(); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanEmissionTest.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanEmissionTest.cpp new file mode 100644 index 00000000..1cf3f347 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanEmissionTest.cpp @@ -0,0 +1,69 @@ +#include "Analysis/ProcessStatePlanInternal.h" +#include "Analysis/ProcessStatePlanTestHooks.h" +#include "ProcessStatePlanTestSupport.h" +#include "acir/Analysis/ProcessStatePlan.h" +#include "acir/InitAllDialects.h" + +#include "gtest/gtest.h" + +namespace acir { +namespace { + +using PlanSetBuilder = detail::PlanSetBuilder; + +static ProcessStatePlanSet getYieldOnlyPlan() { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto built = PlanSetBuilder::buildYieldOnly(*module); + assert(mlir::succeeded(built)); + return *built; +} + +TEST(ProcessStatePlanEmissionTest, YieldOnlyBlockCostIsTwo) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + ASSERT_GE(process.blocks().size(), 1u); + EXPECT_EQ(process.blocks()[0].cost(), 2u); +} + +TEST(ProcessStatePlanEmissionTest, YieldOnlyNoLiveSlots) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + EXPECT_EQ(process.liveSlots().size(), 0u); +} + +TEST(ProcessStatePlanEmissionTest, YieldOnlyFairnessEqualsBlockCost) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + EXPECT_EQ(process.fairnessWork(), 2u); +} + +TEST(ProcessStatePlanEmissionTest, YieldOnlyEdgeIsSuspend) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + ASSERT_GE(process.blocks().size(), 1u); + EXPECT_EQ(process.blocks()[0].edge().kind(), ProcessControlEdgeKind::Suspend); +} + +TEST(ProcessStatePlanEmissionTest, NoCapturesInYieldOnly) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + EXPECT_EQ(process.captures().size(), 0u); +} + +TEST(ProcessStatePlanEmissionTest, NoValueTypesInYieldOnly) { + auto plans = getYieldOnlyPlan(); + EXPECT_EQ(plans.valueTypes().size(), 0u); +} + +TEST(ProcessStatePlanEmissionTest, OneCalleeInYieldOnly) { + auto plans = getYieldOnlyPlan(); + ASSERT_GE(plans.callees().size(), 1u); + EXPECT_EQ(plans.callees()[0].id().value(), 0u); + EXPECT_EQ(plans.callees()[0].role(), ProcessHelperRole::WakeNextDelta); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanLimitsTest.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanLimitsTest.cpp new file mode 100644 index 00000000..57e422f9 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanLimitsTest.cpp @@ -0,0 +1,85 @@ +#include "Analysis/ProcessStatePlanInternal.h" +#include "Analysis/ProcessStatePlanTestHooks.h" +#include "ProcessStatePlanTestSupport.h" +#include "acir/Analysis/ProcessStatePlan.h" +#include "acir/InitAllDialects.h" + +#include "gtest/gtest.h" + +namespace acir { +namespace { + +using PlanSetBuilder = detail::PlanSetBuilder; + +TEST(ProcessStatePlanLimitsTest, DefaultLimitsAreWithinContract) { + ProcessStateLimits limits; + EXPECT_EQ(limits.maxProcesses, 1U << 20); + EXPECT_EQ(limits.maxProgramCounters, 1U << 20); + EXPECT_EQ(limits.maxLiveSlots, 1U << 20); + EXPECT_EQ(limits.maxWakeRecords, 1U << 20); + EXPECT_EQ(limits.maxCalleeDescriptors, 1U << 20); + EXPECT_EQ(limits.maxPlannedOperations, 1U << 20); + EXPECT_EQ(limits.maxFairnessWork, 1U << 20); + EXPECT_EQ(limits.maxTransitions, 1U << 22); + EXPECT_EQ(limits.maxNestedRegionDepth, 512u); + EXPECT_EQ(limits.maxCanonicalReportBytes, 1U << 24); +} + +TEST(ProcessStatePlanLimitsTest, YieldOnlyPlanWithinAllLimits) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto plans = PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + EXPECT_LE(plans->processes().size(), 1U << 20); + const auto &process = plans->processes()[0]; + EXPECT_LE(process.pcs().size(), 1U << 20); + EXPECT_LE(process.blocks().size(), 1U << 20); + EXPECT_LE(process.wakes().size(), 1U << 20); + EXPECT_LE(process.transitions().size(), 1U << 22); + EXPECT_LE(process.liveSlots().size(), 1U << 20); + EXPECT_LE(process.fairnessWork(), 1U << 20); + EXPECT_LE(plans->callees().size(), 1U << 20); +} + +TEST(ProcessStatePlanLimitsTest, VerificationPassesOnValidPlan) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto plans = PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + auto result = verifyProcessStatePlan(*plans); + EXPECT_TRUE(mlir::succeeded(result)); +} + +TEST(ProcessStatePlanLimitsTest, SerializationProducesExpectedContractEpoch) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto plans = PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + auto result = serializeProcessStatePlan(*plans); + ASSERT_TRUE(static_cast(result)); + EXPECT_NE(result->find("\"contract_epoch\":\"0.3\""), std::string::npos); + EXPECT_NE(result->find("\"schema\":\"acir-process-state-plan-0.1\""), + std::string::npos); +} + +TEST(ProcessStatePlanLimitsTest, SerializationExceedsByteCapFails) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto plans = PlanSetBuilder::buildYieldOnly(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + ProcessStateLimits tightLimits; + tightLimits.maxCanonicalReportBytes = 1; + auto result = serializeProcessStatePlan(*plans, tightLimits); + EXPECT_FALSE(static_cast(result)); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanTask13ConsumerTest.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanTask13ConsumerTest.cpp new file mode 100644 index 00000000..6d28e494 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanTask13ConsumerTest.cpp @@ -0,0 +1,180 @@ +// Public-header-only Task 13 consumer test. +// Includes only acir/Analysis/ProcessStatePlan.h — never lib/ internals. +// Proves an external consumer can read plan records, dense IDs, exact costs, +// enum spellings, and closed unions without re-implementing any analysis. +#include "acir/Analysis/ProcessStatePlan.h" +#include "acir/InitAllDialects.h" + +#include "mlir/Parser/Parser.h" +#include "gtest/gtest.h" + +#include +#include + +namespace acir { +namespace { + +TEST(ProcessStateTask13ConsumerTest, PublicFactoryPlansFrozenNonYieldProcess) { + mlir::MLIRContext context; + mlir::DialectRegistry registry; + registerAllDialects(registry); + context.appendDialectRegistry(registry); + auto module = mlir::parseSourceString(R"mlir( + builtin.module attributes { + ac.contract_epoch = "0.3", + ac.freeze_epoch = "0.3", + ac.topology_frozen = true + } { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %ready = arith.constant true + ac.wait_until %ready + ac.yield_sim + } + ac.return + } + } + )mlir", + &context); + ASSERT_TRUE(module); + + auto plans = planProcessState(*module); + ASSERT_TRUE(mlir::succeeded(plans)); + ASSERT_EQ(plans->processes().size(), 1u); + const ProcessStatePlan &process = plans->processes().front(); + EXPECT_GT(process.blocks().size(), 1u); + EXPECT_EQ(process.wakes().size(), 2u); + EXPECT_EQ(process.transitions().size(), 2u); + EXPECT_EQ(process.entryPc().value(), 0u); + EXPECT_EQ(process.blocks().front().actions().front().kind(), + ProcessActionKind::Original); + EXPECT_EQ(process.wakes().front().kind(), ProcessWakeKind::Condition); + EXPECT_EQ(process.transitions().front().sourcePc().value(), 0u); + EXPECT_EQ(process.transitions().front().targetPc().value(), 1u); +} + +// Verify all public types are complete and accessible +TEST(ProcessStateTask13ConsumerTest, AllPublicIdsAreComplete) { + // Compile-time proof that every dense ID type is usable + static_assert(sizeof(ProcessCalleeId) > 0); + static_assert(sizeof(ProcessValueTypeId) > 0); + static_assert(sizeof(ProcessCaptureId) > 0); + static_assert(sizeof(ProcessPcId) > 0); + static_assert(sizeof(ProcessBlockId) > 0); + static_assert(sizeof(ProcessLiveSlotId) > 0); + static_assert(sizeof(ProcessWakeId) > 0); + static_assert(sizeof(ProcessTransitionId) > 0); + SUCCEED(); +} + +TEST(ProcessStateTask13ConsumerTest, AllClosedEnumsAreAccessible) { + // Exercise every enum arm to prove closed-ness + auto wakeCondition = ProcessWakeKind::Condition; + auto wakeResource = ProcessWakeKind::Resource; + auto wakeEvent = ProcessWakeKind::EventQueue; + auto wakeNext = ProcessWakeKind::NextDelta; + (void)wakeCondition; + (void)wakeResource; + (void)wakeEvent; + (void)wakeNext; + + auto subCapture = ProcessSubscriptionSourceKind::Capture; + auto subValue = ProcessSubscriptionSourceKind::Value; + auto subSymbol = ProcessSubscriptionSourceKind::Symbol; + (void)subCapture; + (void)subValue; + (void)subSymbol; + + auto actOrig = ProcessActionKind::Original; + auto actConst = ProcessActionKind::Constant; + auto actInit = ProcessActionKind::ForInitialize; + auto actCond = ProcessActionKind::ForCondition; + auto actIncr = ProcessActionKind::ForIncrement; + auto actWrap = ProcessActionKind::ScalarWrap; + auto actUnwrap = ProcessActionKind::ScalarUnwrap; + (void)actOrig; + (void)actConst; + (void)actInit; + (void)actCond; + (void)actIncr; + (void)actWrap; + (void)actUnwrap; + + auto emCopy = ProcessEmissionClass::CopyScalar; + auto emInline = ProcessEmissionClass::Inline; + auto emInvoke = ProcessEmissionClass::Invoke; + auto emWrap = ProcessEmissionClass::Wrap; + auto emUnwrap = ProcessEmissionClass::Unwrap; + auto emFwd = ProcessEmissionClass::ForwardOnly; + (void)emCopy; + (void)emInline; + (void)emInvoke; + (void)emWrap; + (void)emUnwrap; + (void)emFwd; + + auto occOrig = ProcessOccurrenceKind::Original; + auto occLoop = ProcessOccurrenceKind::SyntheticLoop; + auto occWrap = ProcessOccurrenceKind::SyntheticWrapper; + auto occConstS = ProcessOccurrenceKind::SyntheticConstant; + (void)occOrig; + (void)occLoop; + (void)occWrap; + (void)occConstS; + + auto frameEntry = ProcessFrameKind::Entry; + auto frameIf = ProcessFrameKind::ScfIf; + auto frameFor = ProcessFrameKind::ScfFor; + auto frameWhile = ProcessFrameKind::ScfWhile; + (void)frameEntry; + (void)frameIf; + (void)frameFor; + (void)frameWhile; + + auto edgeBranch = ProcessControlEdgeKind::Branch; + auto edgeCont = ProcessControlEdgeKind::LocalContinue; + auto edgeSuspend = ProcessControlEdgeKind::Suspend; + auto edgeTerm = ProcessControlEdgeKind::Terminate; + (void)edgeBranch; + (void)edgeCont; + (void)edgeSuspend; + (void)edgeTerm; + + SUCCEED(); +} + +TEST(ProcessStateTask13ConsumerTest, ProcessStateLimitsDefaultsMatchContract) { + ProcessStateLimits limits; + EXPECT_EQ(limits.maxProcesses, 1U << 20); + EXPECT_EQ(limits.maxProgramCounters, 1U << 20); + EXPECT_EQ(limits.maxLiveSlots, 1U << 20); + EXPECT_EQ(limits.maxWakeRecords, 1U << 20); + EXPECT_EQ(limits.maxCalleeDescriptors, 1U << 20); + EXPECT_EQ(limits.maxPlannedOperations, 1U << 20); + EXPECT_EQ(limits.maxFairnessWork, 1U << 20); + EXPECT_EQ(limits.maxTransitions, 1U << 22); + EXPECT_EQ(limits.maxNestedRegionDepth, 512u); + EXPECT_EQ(limits.maxCanonicalReportBytes, 1U << 24); +} + +TEST(ProcessStateTask13ConsumerTest, PublicApiHasNoForbiddenAccessors) { + // Compile-time proof: the public API has no mutator, builder, parser, + // corruption hook, callback, component-name lookup, hierarchy lookup, + // fallback lookup, runtime descriptor, or BindingResolutionResult parameter. + // + // These checks use SFINAE/concepts to prove forbidden methods do not exist. + // If any of these compile, the public API contract is violated. + + // No mutable accessors on ProcessStatePlanSet + // Verify that ProcessStatePlanSet has no mutable process accessor + // The public contract only exposes const accessors via ArrayRef + // ProcessStatePlanSet is not default-constructible (private ctor). + // The public contract is verified by the unit test fixtures that + // construct plans via PlanSetBuilder. + SUCCEED(); + + SUCCEED(); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanTestSupport.h b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanTestSupport.h new file mode 100644 index 00000000..bd6a5192 --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanTestSupport.h @@ -0,0 +1,177 @@ +#ifndef ACIR_UNITTESTS_ANALYSIS_PROCESSSTATEPLANTESTSUPPORT_H +#define ACIR_UNITTESTS_ANALYSIS_PROCESSSTATEPLANTESTSUPPORT_H + +#include "Analysis/ProcessStatePlanInternal.h" +#include "Analysis/ProcessStatePlanTestHooks.h" +#include "acir/InitAllDialects.h" +#include "acir/Transforms/Passes.h" + +#include "mlir/Bytecode/BytecodeWriter.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Pass/PassManager.h" +#include "llvm/Support/raw_ostream.h" + +#include + +namespace acir::test { + +inline mlir::OwningOpRef +parseAndFreezeYieldOnly(mlir::MLIRContext &context) { + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + if (!module) + return {}; + mlir::PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + if (mlir::failed(manager.run(*module))) + return {}; + return module; +} + +inline mlir::OwningOpRef +parseAndFreezeLoopActions(mlir::MLIRContext &context) { + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %lb = arith.constant 0 : index + %ub = arith.constant 4 : index + %step = arith.constant 1 : index + scf.for %i = %lb to %ub step %step { + scf.yield + } + scf.for %i = %lb to %ub step %step { + scf.yield + } + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + if (!module) + return {}; + mlir::PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + if (mlir::failed(manager.run(*module))) + return {}; + return module; +} + +inline std::string moduleText(mlir::ModuleOp module) { + std::string storage; + llvm::raw_string_ostream stream(storage); + module.print(stream); + return storage; +} + +inline std::string moduleBytecode(mlir::ModuleOp module) { + std::string storage; + llvm::raw_string_ostream stream(storage); + if (mlir::failed(mlir::writeBytecodeToFile(module, stream))) + return {}; + return storage; +} + +inline std::string moduleFreezeSeal(mlir::ModuleOp module) { + mlir::Attribute digest = module->getAttr("ac.topology_digest"); + if (!digest) + return {}; + std::string storage; + llvm::raw_string_ostream stream(storage); + digest.print(stream); + return storage; +} + +inline mlir::OwningOpRef +parseAndFreezeYieldPermutation(mlir::MLIRContext &context, + bool reverseDeclarations) { + constexpr llvm::StringLiteral alphaFirst = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @alpha kind "control" { ac.yield_sim } + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + } + )mlir"; + constexpr llvm::StringLiteral workloadFirst = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.process @alpha kind "control" { ac.yield_sim } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString( + reverseDeclarations ? workloadFirst : alphaFirst, &context); + if (!module) + return {}; + mlir::PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + if (mlir::failed(manager.run(*module))) + return {}; + return module; +} + +inline mlir::OwningOpRef +parseEmptyModel(mlir::MLIRContext &context) { + return mlir::parseSourceString( + "builtin.module attributes {ac.contract_epoch = \"0.3\"} {}", &context); +} + +inline std::string takeError(llvm::Error error) { + std::string message; + llvm::handleAllErrors(std::move(error), [&](const llvm::ErrorInfoBase &info) { + message = info.message(); + }); + return message; +} + +inline std::string verifyDiagnostic(const ProcessStatePlanSet &plans) { + std::string diagnostic; + mlir::MLIRContext *context = nullptr; + if (!plans.processes().empty()) + context = plans.processes().front().process().getContext(); + if (!context) + return mlir::succeeded(verifyProcessStatePlan(plans)) + ? std::string() + : "verification failed"; + mlir::ScopedDiagnosticHandler handler(context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + if (mlir::succeeded(verifyProcessStatePlan(plans))) + return {}; + return diagnostic; +} + +} // namespace acir::test + +#endif diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanVerifierTest.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanVerifierTest.cpp new file mode 100644 index 00000000..701eaaee --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanVerifierTest.cpp @@ -0,0 +1,985 @@ +#include "Analysis/ModelAnalysisInternal.h" +#include "Analysis/ProcessStatePlanInternal.h" +#include "ProcessStatePlanTestSupport.h" +#include "acir/InitAllDialects.h" +#include "acir/Transforms/Passes.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/Pass/PassInstrumentation.h" +#include "mlir/Pass/PassManager.h" +#include "llvm/Support/Format.h" +#include "gtest/gtest.h" + +#include +#include +#include + +namespace acir { +namespace { + +class NormalizeTrace final : public mlir::PassInstrumentation { +public: + void runBeforePass(mlir::Pass *pass, mlir::Operation *) override { + events.push_back(("enter:" + pass->getArgument()).str()); + } + void runAfterPass(mlir::Pass *pass, mlir::Operation *) override { + events.push_back(("complete:" + pass->getArgument()).str()); + } + void runAfterPassFailed(mlir::Pass *pass, mlir::Operation *) override { + events.push_back(("fail:" + pass->getArgument()).str()); + } + + std::vector events; +}; + +mlir::OwningOpRef buildRawDepthFixture(mlir::MLIRContext &ctx, + uint64_t depth, + bool malformed = false) { + constexpr llvm::StringLiteral source = R"mlir( + builtin.module { + ac.module @M() parameters {} graph { + ac.address_space @source width 8 unit "byte" id "source" path "source" + ac.address_space @target width 8 unit "byte" id "target" path "target" + ac.address_map @map source @source entries [ + {base = 8 : i64, size = 1 : i64, target = @target, offset = 0 : i64, + permissions = ["write", "execute"], classes = []}, + {base = 0 : i64, size = 1 : i64, target = @target, offset = 0 : i64, + permissions = ["read"], classes = []} + ] default {kind = "unmapped"} + ac.return + } + } + )mlir"; + auto root = mlir::parseSourceString(source, &ctx); + if (!root) + return {}; + mlir::ModuleOp parent = *root; + for (uint64_t index = 0; index < depth; ++index) { + auto nested = mlir::ModuleOp::create(mlir::UnknownLoc::get(&ctx)); + parent.getBody()->push_back(nested); + parent = nested; + } + if (malformed) { + mlir::OperationState state(parent.getLoc(), "scf.yield"); + parent.getBody()->push_back(mlir::Operation::create(state)); + } + return root; +} + +struct NormalizeRun { + mlir::LogicalResult result; + std::vector events; + std::vector diagnostics; +}; + +NormalizeRun runIsolatedNormalize(mlir::ModuleOp module) { + std::vector diagnostics; + mlir::ScopedDiagnosticHandler handler( + module.getContext(), [&](mlir::Diagnostic &diagnostic) { + std::string text; + llvm::raw_string_ostream(text) << diagnostic; + diagnostics.push_back(std::move(text)); + return mlir::success(); + }); + auto trace = std::make_unique(); + NormalizeTrace *tracePtr = trace.get(); + mlir::PassManager manager(module.getContext()); + manager.enableVerifier(false); + manager.addInstrumentation(std::move(trace)); + manager.addPass(createNormalizeACIRFilePass()); + mlir::LogicalResult result = manager.run(module); + return {result, std::move(tracePtr->events), std::move(diagnostics)}; +} + +void loadDialects(mlir::MLIRContext &context) { + mlir::DialectRegistry registry; + registerAllDialects(registry); + context.appendDialectRegistry(registry); +} + +TEST(ProcessStatePlanNormalizeFactoryTest, + Depth512ReachesAndSucceedsThroughIsolatedNormalizePass) { + mlir::MLIRContext context; + loadDialects(context); + auto module = buildRawDepthFixture(context, 512); + ASSERT_TRUE(module); + + NormalizeRun run = runIsolatedNormalize(*module); + EXPECT_TRUE(mlir::succeeded(run.result)); + EXPECT_EQ(run.events, + (std::vector{"enter:normalize-ac-file", + "complete:normalize-ac-file"})); + EXPECT_TRUE(run.diagnostics.empty()); + std::string normalized = test::moduleText(*module); + EXPECT_NE(normalized.find("permissions = [\"execute\", \"write\"]"), + std::string::npos); +} + +TEST(ProcessStatePlanPureCallTest, + ExpandsNestedMultiResultCallsWithCallSiteQualifiedIdentity) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @leaf(%arg : index) -> (index, index) { + %one = arith.constant 1 : index + %next = arith.addi %arg, %one : index + return %next, %arg : index, index + } + func.func @middle(%arg : index) -> index { + %next, %old = func.call @leaf(%arg) : (index) -> (index, index) + return %next : index + } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %zero = arith.constant 0 : index + %a = func.call @middle(%zero) : (index) -> index + %b = func.call @middle(%a) : (index) -> index + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ac::ProcessOp process; + module->walk([&](ac::ProcessOp candidate) { process = candidate; }); + ASSERT_TRUE(process); + + auto expanded = detail::expandProcessForPlanning(process); + ASSERT_TRUE(mlir::succeeded(expanded)); + unsigned leafAdds = 0; + std::vector outerCallPaths; + std::vector occurrenceJson; + std::vector occurrenceHashes; + for (const detail::ExpandedAction &action : expanded->actions) { + ASSERT_NE(action.operation->getName().getStringRef(), "func.call"); + ASSERT_NE(action.operation->getName().getStringRef(), "func.return"); + if (action.operation->getName().getStringRef() != "arith.addi") + continue; + ++leafAdds; + ASSERT_EQ(action.callSites.size(), 2u); + EXPECT_EQ(action.operationPath, "@Top::@workload/func/@leaf/r0/b0/o1"); + EXPECT_EQ(action.callSites.back().operationPath(), + "@Top::@workload/func/@middle/r0/b0/o0"); + auto serialized = + detail::canonicalProcessOccurrenceJSON(*action.occurrence); + ASSERT_TRUE(static_cast(serialized)); + occurrenceJson.push_back(*serialized); + auto hash = detail::hashProcessOccurrence(*action.occurrence); + ASSERT_TRUE(static_cast(hash)); + occurrenceHashes.push_back(*hash); + outerCallPaths.push_back(action.callSites.front().operationPath().str()); + ASSERT_FALSE(action.results.empty()); + EXPECT_EQ(action.results.front() + .original() + .occurrence() + .original() + .callSites() + .size(), + 2u); + } + EXPECT_EQ(leafAdds, 2u); + ASSERT_EQ(outerCallPaths.size(), 2u); + EXPECT_NE(outerCallPaths[0], outerCallPaths[1]); + ASSERT_EQ(occurrenceJson.size(), 2u); + EXPECT_NE(occurrenceJson[0], occurrenceJson[1]); + EXPECT_NE(occurrenceHashes[0], occurrenceHashes[1]); + EXPECT_EQ(occurrenceHashes[0].size(), 64u); + EXPECT_EQ(occurrenceHashes[1].size(), 64u); + EXPECT_NE( + occurrenceJson[0].find("\"operation_path\":\"@Top::@workload/r0/b0/o1\""), + std::string::npos); + EXPECT_NE( + occurrenceJson[1].find("\"operation_path\":\"@Top::@workload/r0/b0/o2\""), + std::string::npos); + EXPECT_GE(expanded->forwarding.size(), 8u); +} + +TEST(ProcessStatePlanPureCallTest, + ExpandsEveryStaticLoopIterationAndNestedCallSiteVector) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @leaf(%arg : index) -> index { + %one = arith.constant 1 : index + %next = arith.addi %arg, %one : index + return %next : index + } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %lb = arith.constant 0 : index + %ub = arith.constant 2 : index + %step = arith.constant 1 : index + scf.for %i = %lb to %ub step %step { + %value = func.call @leaf(%i) : (index) -> index + scf.yield + } + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ac::ProcessOp process; + module->walk([&](ac::ProcessOp candidate) { process = candidate; }); + auto expanded = detail::expandProcessForPlanning(process); + ASSERT_TRUE(mlir::succeeded(expanded)); + + std::vector iterations; + std::vector constantOrdinals; + for (const detail::ExpandedAction &action : expanded->actions) { + if (action.kind == ProcessActionKind::Constant) { + ASSERT_TRUE(action.occurrence.has_value()); + ASSERT_EQ(action.occurrence->kind(), + ProcessOccurrenceKind::SyntheticConstant); + constantOrdinals.push_back( + action.occurrence->syntheticConstant().constant()); + EXPECT_EQ(action.occurrence->syntheticConstant() + .anchor() + .original() + .operationPath(), + "@Top::@workload/r0/b0/o3"); + continue; + } + if (action.operation->getName().getStringRef() != "arith.addi") + continue; + ASSERT_EQ(action.iterationVector.size(), 1u); + ASSERT_EQ(action.callSites.size(), 1u); + EXPECT_TRUE(llvm::equal(action.callSites.front().iterationVector(), + action.iterationVector)); + iterations.push_back(action.iterationVector.front()); + } + EXPECT_EQ(iterations, (std::vector{0, 1})); + EXPECT_EQ(constantOrdinals, (std::vector{0, 1})); +} + +TEST(ProcessStatePlanIdentityTest, + LoopInvariantDefinitionKeepsOuterOccurrenceAcrossIterations) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %lb = arith.constant 0 : index + %ub = arith.constant 2 : index + %step = arith.constant 1 : index + %invariant = arith.constant 7 : index + scf.for %i = %lb to %ub step %step { + %sum = arith.addi %i, %invariant : index + scf.yield + } + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ac::ProcessOp process; + module->walk([&](ac::ProcessOp candidate) { process = candidate; }); + auto expanded = detail::expandProcessForPlanning(process); + ASSERT_TRUE(mlir::succeeded(expanded)); + + std::vector invariantOccurrences; + std::vector resultOccurrences; + for (const detail::ExpandedAction &action : expanded->actions) { + if (!action.operation || + action.operation->getName().getStringRef() != "arith.addi") + continue; + ASSERT_EQ(action.operands.size(), 2u); + ASSERT_EQ(action.results.size(), 1u); + auto invariant = detail::canonicalProcessOccurrenceJSON( + action.operands[1].original().occurrence()); + auto result = detail::canonicalProcessOccurrenceJSON( + action.results[0].original().occurrence()); + ASSERT_TRUE(static_cast(invariant)); + ASSERT_TRUE(static_cast(result)); + invariantOccurrences.push_back(*invariant); + resultOccurrences.push_back(*result); + } + ASSERT_EQ(invariantOccurrences.size(), 2u); + EXPECT_EQ(invariantOccurrences[0], invariantOccurrences[1]); + EXPECT_NE(invariantOccurrences[0].find("\"iteration_vector\":[]"), + std::string::npos); + EXPECT_NE(resultOccurrences[0], resultOccurrences[1]); +} + +TEST(ProcessStatePlanPureCallTest, + SharedGraphAuthorityEnforcesExactFunctionEdgeAndDepthBounds) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @leaf() { + return + } + func.func @middle() { + func.call @leaf() : () -> () + return + } + func.func @top() { + func.call @middle() : () -> () + return + } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @top() : () -> () + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + mlir::ScopedDiagnosticHandler suppress( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + auto graph = detail::validatePureProcessCallGraph( + *module, {}, detail::PureCallGraphLimits{3, 2, 3}); + ASSERT_TRUE(mlir::succeeded(graph)); + ASSERT_EQ(graph->functions.size(), 3u); + uint64_t probes = 0; + const detail::ValidatedPureFunction *top = graph->lookup("top", &probes); + ASSERT_NE(top, nullptr); + mlir::func::FuncOp topFunction = top->function; + EXPECT_EQ(topFunction.getSymName(), "top"); + EXPECT_LE(probes, 2u); + uint64_t repeatedProbes = 0; + EXPECT_EQ(graph->lookup("top", &repeatedProbes), top); + EXPECT_LE(repeatedProbes, 2u); + EXPECT_TRUE(mlir::failed(detail::validatePureProcessCallGraph( + *module, {}, detail::PureCallGraphLimits{2, 2, 3}))); + EXPECT_TRUE(mlir::failed(detail::validatePureProcessCallGraph( + *module, {}, detail::PureCallGraphLimits{3, 1, 3}))); + EXPECT_TRUE(mlir::failed(detail::validatePureProcessCallGraph( + *module, {}, detail::PureCallGraphLimits{3, 2, 2}))); +} + +TEST(ProcessStatePlanScalingTest, + AdmittedSizeCallGraphLookupIsIndexedAndContainsNoDuplicates) { + mlir::MLIRContext context; + loadDialects(context); + auto functionName = [](unsigned index) { + std::string storage; + llvm::raw_string_ostream stream(storage); + stream << llvm::format("f%04u", index); + return storage; + }; + std::string source; + llvm::raw_string_ostream stream(source); + stream << "builtin.module attributes {ac.contract_epoch = \"0.3\"} {\n"; + for (unsigned index = 0; index < 1024; ++index) { + stream << "func.func @" << functionName(index) << "() {\n"; + if (index + 1 < 1024) { + stream << "func.call @" << functionName(index + 1) << "() : () -> ()\n"; + } + stream << "return\n}\n"; + } + stream << R"mlir( + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @f0000() : () -> () + ac.yield_sim + } + ac.return + } + })mlir"; + stream.flush(); + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + auto graph = detail::validatePureProcessCallGraph(*module); + ASSERT_TRUE(mlir::succeeded(graph)); + ASSERT_EQ(graph->functions.size(), 1024u); + std::set names; + for (const detail::ValidatedPureFunction &entry : graph->functions) { + mlir::func::FuncOp function = entry.function; + names.insert(function.getSymName().str()); + } + EXPECT_EQ(names.size(), graph->functions.size()); + uint64_t probes = 0; + const detail::ValidatedPureFunction *last = graph->lookup("f1023", &probes); + ASSERT_NE(last, nullptr); + EXPECT_LE(probes, 11u); +} + +TEST(ProcessStatePlanPureCallTest, + SharedGraphAuthorityRejectsDuplicateFunctionSymbols) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @leaf() { + return + } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @leaf() : () -> () + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ASSERT_TRUE(mlir::succeeded(detail::validatePureProcessCallGraph(*module))); + + mlir::OpBuilder builder(&context); + builder.setInsertionPointToStart(module->getBody()); + auto duplicate = + mlir::func::FuncOp::create(builder, mlir::UnknownLoc::get(&context), + "leaf", builder.getFunctionType({}, {})); + mlir::Block *entry = duplicate.addEntryBlock(); + mlir::OpBuilder bodyBuilder(entry, entry->end()); + mlir::func::ReturnOp::create(bodyBuilder, duplicate.getLoc()); + + std::vector diagnostics; + mlir::ScopedDiagnosticHandler capture( + &context, [&](mlir::Diagnostic &diagnostic) { + std::string text; + llvm::raw_string_ostream(text) << diagnostic; + diagnostics.push_back(std::move(text)); + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(detail::validatePureProcessCallGraph(*module))); + ASSERT_EQ(diagnostics.size(), 1u); + EXPECT_NE(diagnostics.front().find("duplicate pure func.call symbol '@leaf'"), + std::string::npos); +} + +TEST(ProcessStatePlanPureCallTest, + SharedGraphAuthorityRejectsEffectfulReachableCallee) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @bad() { + %condition = arith.constant true + return + } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @bad() : () -> () + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + mlir::func::FuncOp function; + module->walk([&](mlir::func::FuncOp candidate) { function = candidate; }); + mlir::Value condition = function.getBody().front().front().getResult(0); + mlir::OpBuilder builder(function.getContext()); + builder.setInsertionPoint(function.getBody().front().getTerminator()); + ac::WaitUntilOp::create(builder, function.getLoc(), condition); + std::vector diagnostics; + mlir::ScopedDiagnosticHandler capture( + &context, [&](mlir::Diagnostic &diagnostic) { + std::string text; + llvm::raw_string_ostream(text) << diagnostic; + diagnostics.push_back(std::move(text)); + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(detail::validatePureProcessCallGraph(*module))); + ASSERT_EQ(diagnostics.size(), 1u); + EXPECT_NE(diagnostics.front().find( + "function reachable from ac.process is not effect-free"), + std::string::npos); +} + +TEST(ProcessStatePlanPureCallTest, + HostileNestedStructuredCalleeAggregatesLocalEffectsIteratively) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @nested() { + %condition = arith.constant true + scf.if %condition { + scf.yield + } + return + } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + func.call @nested() : () -> () + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + mlir::func::FuncOp function; + module->walk([&](mlir::func::FuncOp candidate) { function = candidate; }); + mlir::Value condition = function.getBody().front().front().getResult(0); + mlir::scf::IfOp outer; + function.walk([&](mlir::scf::IfOp candidate) { outer = candidate; }); + mlir::Block *insertionBlock = &outer.getThenRegion().front(); + for (unsigned depth = 1; depth < 8192; ++depth) { + mlir::OpBuilder builder(&context); + builder.setInsertionPoint(insertionBlock->getTerminator()); + auto branch = mlir::scf::IfOp::create(builder, function.getLoc(), + mlir::TypeRange{}, condition, false); + insertionBlock = &branch.getThenRegion().front(); + } + ac::RawModelStructureLimits limits; + limits.maxNestedRegionDepth = 10000; + ASSERT_TRUE( + mlir::succeeded(detail::validatePureProcessCallGraph(*module, limits))); + + mlir::OpBuilder builder(&context); + builder.setInsertionPoint(insertionBlock->getTerminator()); + ac::WaitUntilOp::create(builder, function.getLoc(), condition); + std::vector diagnostics; + mlir::ScopedDiagnosticHandler capture( + &context, [&](mlir::Diagnostic &diagnostic) { + std::string text; + llvm::raw_string_ostream(text) << diagnostic; + diagnostics.push_back(std::move(text)); + return mlir::success(); + }); + EXPECT_TRUE( + mlir::failed(detail::validatePureProcessCallGraph(*module, limits))); + ASSERT_EQ(diagnostics.size(), 1u); + EXPECT_NE(diagnostics.front().find( + "function reachable from ac.process is not effect-free"), + std::string::npos); +} + +TEST(ProcessStatePlanLowerabilityTest, + HostileNestedSuspensionShapeHitsDepthLimitBeforeSummaryAnalysis) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top(index, index, index, i1) parameters {} graph { + ^bb0(%l : index, %u : index, %s : index, %condition : i1): + ac.process @workload kind "workload" + captures(%l, %u, %s, %condition : index, index, index, i1) { + ^bb0(%pl : index, %pu : index, %ps : index, %pc : i1): + scf.for %i = %pl to %pu step %ps { + ac.wait_until %pc + scf.yield + } + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + mlir::scf::ForOp loop; + module->walk([&](mlir::scf::ForOp candidate) { loop = candidate; }); + ASSERT_TRUE(loop); + mlir::Value condition = loop.getBody()->front().getOperand(0); + mlir::Block *insertionBlock = loop.getBody(); + for (unsigned depth = 0; depth < 10000; ++depth) { + mlir::OpBuilder builder(&context); + builder.setInsertionPoint(insertionBlock->getTerminator()); + auto branch = mlir::scf::IfOp::create(builder, loop.getLoc(), + mlir::TypeRange{}, condition, false); + insertionBlock = &branch.getThenRegion().front(); + } + std::vector diagnostics; + mlir::ScopedDiagnosticHandler capture( + &context, [&](mlir::Diagnostic &diagnostic) { + std::string text; + llvm::raw_string_ostream(text) << diagnostic; + diagnostics.push_back(std::move(text)); + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed( + ac::verifyProcessLowerability(loop->getParentOfType()))); + ASSERT_EQ(diagnostics.size(), 1u); + EXPECT_NE(diagnostics.front().find( + "whole-model region nesting exceeds ACIR capability " + "limit 512"), + std::string::npos); +} + +TEST(ProcessStatePlanLimitsTest, + CallExpansionAcceptsLiteralNodeAndEdgeBudgetsAndRejectsOneLess) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + func.func @leaf(%arg : index) -> index { return %arg : index } + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %zero = arith.constant 0 : index + %value = func.call @leaf(%zero) : (index) -> index + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ac::ProcessOp process; + module->walk([&](ac::ProcessOp candidate) { process = candidate; }); + ac::RawModelStructureLimits exact; + exact.maxNodes = 4; // 3 process tasks + 1 callee return task. + exact.maxEdges = 4; // 2 operand edges + argument/result forwarding. + auto expanded = detail::expandProcessForPlanning(process, exact); + ASSERT_TRUE(mlir::succeeded(expanded)); + EXPECT_EQ(expanded->expandedNodes, 4u); + EXPECT_EQ(expanded->expandedEdges, 4u); + mlir::ScopedDiagnosticHandler suppress( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + ac::RawModelStructureLimits oneLessNode = exact; + --oneLessNode.maxNodes; + EXPECT_TRUE( + mlir::failed(detail::expandProcessForPlanning(process, oneLessNode))); + ac::RawModelStructureLimits oneLessEdge = exact; + --oneLessEdge.maxEdges; + EXPECT_TRUE( + mlir::failed(detail::expandProcessForPlanning(process, oneLessEdge))); +} + +TEST(ProcessStatePlanLimitsTest, + DynamicExpansionCountsLiteralTaskActionOperandAndForwardingBudgets) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %true = arith.constant true + %c0 = arith.constant 0 : index + %c4 = arith.constant 4 : index + %c1 = arith.constant 1 : index + %l = arith.addi %c0, %c0 : index + %u = arith.addi %c4, %c0 : index + %s = arith.addi %c1, %c0 : index + scf.for %i = %l to %u step %s { + ac.wait_until %true + scf.yield + } + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ac::ProcessOp process; + module->walk([&](ac::ProcessOp candidate) { process = candidate; }); + ac::RawModelStructureLimits exact; + exact.maxNodes = 14; // 11 physical tasks + 3 synthetic phase actions. + exact.maxEdges = 12; // 10 operands + 2 induction forwarding edges. + auto expanded = detail::expandProcessForPlanning(process, exact); + ASSERT_TRUE(mlir::succeeded(expanded)); + EXPECT_EQ(expanded->expandedNodes, 14u); + EXPECT_EQ(expanded->expandedEdges, 12u); + mlir::ScopedDiagnosticHandler suppress( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + ac::RawModelStructureLimits oneLessNode = exact; + --oneLessNode.maxNodes; + EXPECT_TRUE( + mlir::failed(detail::expandProcessForPlanning(process, oneLessNode))); + ac::RawModelStructureLimits oneLessEdge = exact; + --oneLessEdge.maxEdges; + EXPECT_TRUE( + mlir::failed(detail::expandProcessForPlanning(process, oneLessEdge))); +} + +TEST(ProcessStatePlanScalingTest, + RepeatedIterationBindingsUseBoundedContextLookupWork) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %lb = arith.constant 0 : index + %ub = arith.constant 512 : index + %step = arith.constant 1 : index + %invariant = arith.constant 7 : index + scf.for %i = %lb to %ub step %step { + %sum = arith.addi %i, %invariant : index + scf.yield + } + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ac::ProcessOp process; + module->walk([&](ac::ProcessOp candidate) { process = candidate; }); + auto expanded = detail::expandProcessForPlanning(process); + ASSERT_TRUE(mlir::succeeded(expanded)); + EXPECT_LE(expanded->maxValueLookupProbes, 2u); + EXPECT_LT(expanded->valueLookupProbes, 5000u); +} + +TEST(ProcessStatePlanVerifierTest, + DynamicSuspendingLoopExpandsExactLoopPhaseActions) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top(index, index, index) parameters {} graph { + ^bb0(%lb : index, %ub : index, %step : index): + ac.process @workload kind "workload" + captures(%lb, %ub, %step : index, index, index) { + ^bb0(%l : index, %u : index, %s : index): + %true = arith.constant true + scf.for %i = %l to %u step %s { + ac.wait_until %true + scf.yield + } + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ac::ProcessOp process; + module->walk([&](ac::ProcessOp candidate) { process = candidate; }); + auto expanded = detail::expandProcessForPlanning(process); + ASSERT_TRUE(mlir::succeeded(expanded)); + + std::vector kinds; + for (const detail::ExpandedAction &action : expanded->actions) { + if (action.kind != ProcessActionKind::ForInitialize && + action.kind != ProcessActionKind::ForCondition && + action.kind != ProcessActionKind::ForIncrement) + continue; + kinds.push_back(action.kind); + ASSERT_TRUE(action.occurrence.has_value()); + EXPECT_EQ(action.occurrence->kind(), ProcessOccurrenceKind::SyntheticLoop); + ASSERT_FALSE(action.results.empty()); + for (const ProcessPlannedValue &operand : action.operands) + EXPECT_TRUE(operand.type().isIndex()); + for (const ProcessPlannedValue &result : action.results) + EXPECT_EQ(result.kind(), ProcessPlannedValueKind::Synthetic); + if (action.kind == ProcessActionKind::ForCondition) { + EXPECT_EQ(action.occurrence->syntheticLoop().phase(), + ProcessLoopPhase::Condition); + ASSERT_EQ(action.operands.size(), 2u); + ASSERT_EQ(action.results.size(), 1u); + EXPECT_TRUE(action.results.front().type().isInteger(1)); + ASSERT_TRUE(action.scalarOperation.has_value()); + EXPECT_EQ(action.scalarOperation->name(), "arith.cmpi"); + EXPECT_EQ(action.scalarOperation->properties(), "{}"); + ASSERT_EQ(action.scalarOperation->attributes().size(), 1u); + EXPECT_EQ(action.scalarOperation->attributes().front().name(), + "predicate"); + EXPECT_EQ(action.scalarOperation->attributes().front().value(), + "2 : i64"); + } + if (action.kind == ProcessActionKind::ForIncrement) { + EXPECT_EQ(action.occurrence->syntheticLoop().phase(), + ProcessLoopPhase::Increment); + ASSERT_EQ(action.operands.size(), 2u); + ASSERT_EQ(action.results.size(), 1u); + EXPECT_TRUE(action.results.front().type().isIndex()); + ASSERT_TRUE(action.scalarOperation.has_value()); + EXPECT_EQ(action.scalarOperation->name(), "arith.addi"); + EXPECT_EQ(action.scalarOperation->properties(), "{}"); + EXPECT_TRUE(action.scalarOperation->attributes().empty()); + } + if (action.kind == ProcessActionKind::ForInitialize) { + EXPECT_EQ(action.occurrence->syntheticLoop().phase(), + ProcessLoopPhase::Initialize); + ASSERT_EQ(action.operands.size(), 1u); + ASSERT_EQ(action.results.size(), 1u); + EXPECT_TRUE(action.results.front().type().isIndex()); + EXPECT_FALSE(action.scalarOperation.has_value()); + } + } + EXPECT_EQ(kinds, + (std::vector{ProcessActionKind::ForInitialize, + ProcessActionKind::ForCondition, + ProcessActionKind::ForIncrement})); +} + +TEST(ProcessStatePlanVerifierTest, + DynamicTwoCarryLoopForwardsInitializationBackedgeAndExitExactly) { + mlir::MLIRContext context; + loadDialects(context); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top(index, index, index) parameters {} graph { + ^bb0(%lb : index, %ub : index, %step : index): + ac.process @workload kind "workload" + captures(%lb, %ub, %step : index, index, index) { + ^bb0(%l : index, %u : index, %s : index): + %true = arith.constant true + %init_a = arith.constant 10 : index + %init_b = arith.constant 20 : index + %a, %b = scf.for %i = %l to %u step %s + iter_args(%carry_a = %init_a, %carry_b = %init_b) -> (index, index) { + ac.wait_until %true + %next_a = arith.addi %carry_a, %i : index + %next_b = arith.addi %carry_b, %s : index + scf.yield %next_a, %next_b : index, index + } + ac.yield_sim + } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + ac::ProcessOp process; + module->walk([&](ac::ProcessOp candidate) { process = candidate; }); + auto expanded = detail::expandProcessForPlanning(process); + ASSERT_TRUE(mlir::succeeded(expanded)); + + const detail::ExpandedAction *initialize = nullptr; + const detail::ExpandedAction *condition = nullptr; + const detail::ExpandedAction *increment = nullptr; + for (const detail::ExpandedAction &action : expanded->actions) { + if (action.kind == ProcessActionKind::ForInitialize) + initialize = &action; + else if (action.kind == ProcessActionKind::ForCondition) + condition = &action; + else if (action.kind == ProcessActionKind::ForIncrement) + increment = &action; + } + ASSERT_NE(initialize, nullptr); + ASSERT_NE(condition, nullptr); + ASSERT_NE(increment, nullptr); + ASSERT_EQ(initialize->operands.size(), 3u); + ASSERT_EQ(initialize->results.size(), 3u); + ASSERT_EQ(condition->operands.size(), 2u); + ASSERT_EQ(condition->results.size(), 1u); + ASSERT_EQ(increment->operands.size(), 2u); + ASSERT_EQ(increment->results.size(), 1u); + for (const ProcessPlannedValue &value : initialize->operands) + EXPECT_TRUE(value.type().isIndex()); + for (const ProcessPlannedValue &value : initialize->results) + EXPECT_TRUE(value.type().isIndex()); + EXPECT_TRUE(condition->results.front().type().isInteger(1)); + EXPECT_TRUE(increment->results.front().type().isIndex()); + + using Edge = std::pair; + auto carriedEdges = [](const detail::ExpandedProcess &candidate) { + std::vector edges; + for (const detail::ExpandedForwarding &edge : candidate.forwarding) { + if (edge.from.kind() != ProcessPlannedValueKind::Original || + edge.to.kind() != ProcessPlannedValueKind::Original) + continue; + llvm::StringRef target = edge.to.original().path(); + if (!target.starts_with("@Top::@workload/r0/b0/o3")) + continue; + edges.emplace_back(edge.from.original().path().str(), target.str()); + EXPECT_TRUE(edge.from.type().isIndex()); + EXPECT_TRUE(edge.to.type().isIndex()); + } + return edges; + }; + const std::vector expected = { + {"@Top::@workload/r0/b0/o1/v0", "@Top::@workload/r0/b0/o3/r0/b0/a1"}, + {"@Top::@workload/r0/b0/o2/v0", "@Top::@workload/r0/b0/o3/r0/b0/a2"}, + {"@Top::@workload/r0/b0/o3/r0/b0/o1/v0", + "@Top::@workload/r0/b0/o3/r0/b0/a1"}, + {"@Top::@workload/r0/b0/o3/r0/b0/o2/v0", + "@Top::@workload/r0/b0/o3/r0/b0/a2"}, + {"@Top::@workload/r0/b0/o3/r0/b0/o1/v0", "@Top::@workload/r0/b0/o3/v0"}, + {"@Top::@workload/r0/b0/o3/r0/b0/o2/v0", "@Top::@workload/r0/b0/o3/v1"}}; + std::vector actual = carriedEdges(*expanded); + EXPECT_EQ(actual, expected); + + std::vector sourceOccurrences; + std::vector targetOccurrences; + for (const detail::ExpandedForwarding &edge : expanded->forwarding) { + if (edge.from.kind() != ProcessPlannedValueKind::Original || + edge.to.kind() != ProcessPlannedValueKind::Original || + !edge.to.original().path().starts_with("@Top::@workload/r0/b0/o3")) + continue; + auto from = detail::canonicalProcessOccurrenceJSON( + edge.from.original().occurrence()); + auto to = + detail::canonicalProcessOccurrenceJSON(edge.to.original().occurrence()); + ASSERT_TRUE(static_cast(from)); + ASSERT_TRUE(static_cast(to)); + sourceOccurrences.push_back(*from); + targetOccurrences.push_back(*to); + } + EXPECT_EQ( + sourceOccurrences, + (std::vector{ + R"json({"call_sites":[],"iteration_vector":[],"kind":"original","operation_path":"@Top::@workload/r0/b0/o1"})json", + R"json({"call_sites":[],"iteration_vector":[],"kind":"original","operation_path":"@Top::@workload/r0/b0/o2"})json", + R"json({"call_sites":[],"iteration_vector":[],"kind":"original","operation_path":"@Top::@workload/r0/b0/o3/r0/b0/o1"})json", + R"json({"call_sites":[],"iteration_vector":[],"kind":"original","operation_path":"@Top::@workload/r0/b0/o3/r0/b0/o2"})json", + R"json({"call_sites":[],"iteration_vector":[],"kind":"original","operation_path":"@Top::@workload/r0/b0/o3/r0/b0/o1"})json", + R"json({"call_sites":[],"iteration_vector":[],"kind":"original","operation_path":"@Top::@workload/r0/b0/o3/r0/b0/o2"})json"})); + EXPECT_EQ( + targetOccurrences, + (std::vector( + 6, + R"json({"call_sites":[],"iteration_vector":[],"kind":"original","operation_path":"@Top::@workload/r0/b0/o3"})json"))); + + detail::ExpandedProcess dropped = *expanded; + auto droppedEdges = carriedEdges(dropped); + ASSERT_GT(droppedEdges.size(), 2u); + dropped.forwarding.erase(dropped.forwarding.begin() + 4); + EXPECT_NE(carriedEdges(dropped), expected); + + detail::ExpandedProcess reordered = *expanded; + std::swap(reordered.forwarding[4], reordered.forwarding[5]); + EXPECT_NE(carriedEdges(reordered), expected); +} + +TEST(ProcessStatePlanNormalizeFactoryTest, + Depth513FailsRawStructuralPreflightBeforeNormalizeRecursion) { + mlir::MLIRContext context; + loadDialects(context); + auto module = buildRawDepthFixture(context, 513); + ASSERT_TRUE(module); + + NormalizeRun run = runIsolatedNormalize(*module); + EXPECT_TRUE(mlir::failed(run.result)); + EXPECT_EQ(run.events, (std::vector{"enter:normalize-ac-file", + "fail:normalize-ac-file"})); + ASSERT_EQ(run.diagnostics.size(), 1u); + EXPECT_NE(run.diagnostics.front().find( + "whole-model region nesting exceeds ACIR capability " + "limit 512"), + std::string::npos); +} + +TEST(ProcessStatePlanNormalizeFactoryTest, + VeryDeepMalformedFailsRawStructuralPreflightWithoutRecursion) { + mlir::MLIRContext context; + loadDialects(context); + auto module = buildRawDepthFixture(context, 10000, /*malformed=*/true); + ASSERT_TRUE(module); + + NormalizeRun run = runIsolatedNormalize(*module); + EXPECT_TRUE(mlir::failed(run.result)); + EXPECT_EQ(run.events, (std::vector{"enter:normalize-ac-file", + "fail:normalize-ac-file"})); + ASSERT_EQ(run.diagnostics.size(), 1u); + EXPECT_NE(run.diagnostics.front().find( + "whole-model region nesting exceeds ACIR capability " + "limit 512"), + std::string::npos); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Analysis/ProcessStatePlanWakeTest.cpp b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanWakeTest.cpp new file mode 100644 index 00000000..801cf84f --- /dev/null +++ b/components/agentic-circuit/unittests/Analysis/ProcessStatePlanWakeTest.cpp @@ -0,0 +1,56 @@ +#include "Analysis/ProcessStatePlanInternal.h" +#include "Analysis/ProcessStatePlanTestHooks.h" +#include "ProcessStatePlanTestSupport.h" +#include "acir/Analysis/ProcessStatePlan.h" +#include "acir/InitAllDialects.h" + +#include "gtest/gtest.h" + +namespace acir { +namespace { + +using PlanSetBuilder = detail::PlanSetBuilder; + +static ProcessStatePlanSet getYieldOnlyPlan() { + mlir::DialectRegistry registry; + registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto module = test::parseAndFreezeYieldOnly(context); + auto built = PlanSetBuilder::buildYieldOnly(*module); + assert(mlir::succeeded(built)); + return *built; +} + +TEST(ProcessStatePlanWakeTest, YieldOnlyWakeIsNextDelta) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + ASSERT_GE(process.wakes().size(), 1u); + EXPECT_EQ(process.wakes()[0].kind(), ProcessWakeKind::NextDelta); +} + +TEST(ProcessStatePlanWakeTest, YieldOnlyWakeHasCorrectTypeKey) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + ASSERT_GE(process.wakes().size(), 1u); + EXPECT_EQ(process.wakes()[0].typeKey(), "@acir_wake_next_delta"); +} + +TEST(ProcessStatePlanWakeTest, TransitionLinksWakeCorrectly) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + ASSERT_GE(process.transitions().size(), 1u); + ASSERT_GE(process.wakes().size(), 1u); + EXPECT_EQ(process.transitions()[0].wake().value(), + process.wakes()[0].id().value()); +} + +TEST(ProcessStatePlanWakeTest, TransitionSourceIsEntryPc) { + auto plans = getYieldOnlyPlan(); + const auto &process = plans.processes()[0]; + ASSERT_GE(process.transitions().size(), 1u); + EXPECT_EQ(process.transitions()[0].sourcePc().value(), 0u); + EXPECT_EQ(process.transitions()[0].targetPc().value(), 0u); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Bindings/CMakeLists.txt b/components/agentic-circuit/unittests/Bindings/CMakeLists.txt new file mode 100644 index 00000000..402e24f3 --- /dev/null +++ b/components/agentic-circuit/unittests/Bindings/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(GTest CONFIG REQUIRED) + +add_executable(ACIRBindingTests CanonicalBindingTest.cpp) +target_include_directories(ACIRBindingTests PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +target_link_libraries(ACIRBindingTests PRIVATE + ACIRBindings + ACIRDialect + ACIRTransforms + ACSimDialect + LLVMSupport + MLIRFuncDialect + MLIRIndexDialect + MLIRParser + MLIRSCFDialect + GTest::gtest_main +) +add_test(NAME ACIRBindingTests COMMAND ACIRBindingTests) diff --git a/components/agentic-circuit/unittests/Bindings/CanonicalBindingTest.cpp b/components/agentic-circuit/unittests/Bindings/CanonicalBindingTest.cpp new file mode 100644 index 00000000..a6d13b35 --- /dev/null +++ b/components/agentic-circuit/unittests/Bindings/CanonicalBindingTest.cpp @@ -0,0 +1,1149 @@ +#include "Bindings/BindingTestHooks.h" +#include "acir/Bindings/Binding.h" +#include "acir/Bindings/Registry.h" +#include "acir/Dialect/ACIR/GraphRegion.h" +#include "acir/InitAllDialects.h" +#include "acir/Transforms/Passes.h" +#include "acir/Transforms/ResolveBindings.h" + +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Pass/PassManager.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +namespace acir::bindings { +namespace { + +constexpr llvm::StringLiteral kRecordFingerprint = + "sha256:c589295d45f11e7bd368e271ce3dd8928f3207e7ee0287012dcaa15034a313d1"; + +std::string takeError(llvm::Error error) { + return llvm::toString(std::move(error)); +} + +testing::AssertionResult containsText(llvm::StringRef actual, + llvm::StringRef expected) { + if (actual.contains(expected)) + return testing::AssertionSuccess(); + return testing::AssertionFailure() << "expected substring '" << expected.str() + << "' in '" << actual.str() << "'"; +} + +testing::AssertionResult startsWithText(llvm::StringRef actual, + llvm::StringRef expected) { + if (actual.starts_with(expected)) + return testing::AssertionSuccess(); + return testing::AssertionFailure() << "expected prefix '" << expected.str() + << "' in '" << actual.str() << "'"; +} + +std::string recordJson(llvm::StringRef fingerprint = kRecordFingerprint, + llvm::StringRef symbol = "gfsim::Leaf") { + return (llvm::Twine(R"json({ + "activation_sources": [], + "availability": "available", + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": "ac.Leaf", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "construction": {"arguments": [8], "kind": "constructor"}, + "contract_epoch": "0.3", + "cpp": { + "concept": "gfsim::PureModel", + "entry_points": {"pure": "gfsim::leaf", "reset": "", "validate": "", "work": "", "xfer": ""}, + "header": "gfsim/leaf.hpp", + "symbol": ")json") + + symbol + R"json(", + "target": "gfsim" + }, + "cpp_type": "cpp_i32", + "effect": "pure", + "fingerprint": ")json" + + fingerprint + R"json(", + "implementation": "gfsim.Leaf", + "ownership": {"kind": "none", "placement": "inline"}, + "parameters": [{ + "acir_type": "i64", + "cpp_type": "std::int64_t", + "mapping": "constructor_constant", + "name": "width", + "ordinal": 0, + "value": 8 + }], + "ports": [{ + "accessor": "input", + "cardinality": "exclusive", + "delegation": "forbidden", + "direction": "input", + "interface": "ac.Stream", + "ownership": "borrowed", + "payload": "ac.Packet", + "protocol": "ac.ReadyValid", + "role": "consumer", + "time_domain": "ac.cycle" + }], + "provider": "gfsim", + "provider_implementation_fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "resources": [], + "results": [{"cpp_type": "cpp_i32", "name": "result"}] + })json") + .str(); +} + +std::string candidateJson(llvm::StringRef profile = "fast", + llvm::StringRef target = "arm64-apple-darwin", + bool available = true, llvm::StringRef record = {}) { + std::string selectedRecord = record.empty() ? recordJson() : record.str(); + return (llvm::Twine(R"json({"available":)json") + + (available ? "true" : "false") + R"json(,"profile":")json" + profile + + R"json(","record":)json" + selectedRecord + R"json(,"target":")json" + + target + R"json("})json") + .str(); +} + +std::string requestJson( + llvm::StringRef componentSchema = "ac.Leaf", + llvm::StringRef providerFingerprint = + "sha256:" + "2222222222222222222222222222222222222222222222222222222222222222", + llvm::StringRef functionType = "() -> i32", + llvm::StringRef parameterValue = "8") { + return (llvm::Twine(R"json({ + "activation_sources": [], + "binding": "Leaf", + "binding_schema": "acsim-binding-0.1", + "component_schema": ")json") + + componentSchema + R"json(", + "component_schema_fingerprint": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "contract_epoch": "0.3", + "effect": "pure", + "function_type": ")json" + + functionType + R"json(", + "parameters": [{"acir_type":"i64","name":"width","ordinal":0,"value":)json" + + parameterValue + R"json(}], + "ports": [{ + "cardinality": "exclusive", + "delegation": "forbidden", + "direction": "input", + "interface": "ac.Stream", + "ownership": "borrowed", + "payload": "ac.Packet", + "protocol": "ac.ReadyValid", + "role": "consumer", + "time_domain": "ac.cycle" + }], + "provider": "gfsim", + "provider_implementation_fingerprint": ")json" + + providerFingerprint + R"json(", + "resolution_key": "@Leaf", + "resources": [], + "results": [{"acir_type":"i32","name":"result"}] + })json") + .str(); +} + +std::string registryJson(std::vector candidates, + std::vector requests) { + std::string result = R"({"candidates":[)"; + for (size_t index = 0; index < candidates.size(); ++index) { + if (index) + result.push_back(','); + result.append(candidates[index]); + } + result.append(R"(],"requests":[)"); + for (size_t index = 0; index < requests.size(); ++index) { + if (index) + result.push_back(','); + result.append(requests[index]); + } + result.append("]}"); + return result; +} + +std::string registryJson(std::vector candidates) { + return registryJson(std::move(candidates), {requestJson()}); +} + +std::vector parseCandidates(llvm::StringRef text) { + auto parsed = parseBindingRegistry(text); + EXPECT_TRUE(static_cast(parsed)) + << (parsed ? "" : takeError(parsed.takeError())); + return parsed ? std::move(parsed->candidates) + : std::vector(); +} + +std::vector parseRequests(llvm::StringRef text) { + auto parsed = parseBindingRegistry(text); + EXPECT_TRUE(static_cast(parsed)) + << (parsed ? "" : takeError(parsed.takeError())); + return parsed ? std::move(parsed->requests) : std::vector(); +} + +std::string withComponentSchema(llvm::StringRef text, + llvm::StringRef componentSchema) { + auto parsed = parseIJson(text); + EXPECT_TRUE(static_cast(parsed)); + if (!parsed) + return {}; + auto *object = parsed->getAsObject(); + EXPECT_NE(nullptr, object); + if (!object) + return {}; + (*object)["component_schema"] = componentSchema; + auto record = BindingRecord::parse(*object); + EXPECT_TRUE(static_cast(record)); + if (!record) + return {}; + auto fingerprint = computeBindingRecordFingerprint(*record); + EXPECT_TRUE(static_cast(fingerprint)); + if (!fingerprint) + return {}; + (*object)["fingerprint"] = *fingerprint; + auto canonical = canonicalizeJson(*parsed); + EXPECT_TRUE(static_cast(canonical)); + return canonical ? std::move(*canonical) : std::string(); +} + +std::string withUnitParameter(llvm::StringRef text) { + auto parsed = parseIJson(text); + EXPECT_TRUE(static_cast(parsed)); + if (!parsed) + return {}; + auto *object = parsed->getAsObject(); + auto *parameters = object ? object->getArray("parameters") : nullptr; + auto *construction = object ? object->getObject("construction") : nullptr; + auto *arguments = + construction ? construction->getArray("arguments") : nullptr; + auto *parameter = parameters && parameters->size() == 1 + ? (*parameters)[0].getAsObject() + : nullptr; + EXPECT_NE(nullptr, parameter); + EXPECT_NE(nullptr, arguments); + if (!parameter || !arguments || arguments->size() != 1) + return {}; + llvm::json::Object parameterValue; + parameterValue["unit"] = "cycles"; + parameterValue["value"] = 4; + (*parameter)["value"] = std::move(parameterValue); + llvm::json::Object constructionValue; + constructionValue["unit"] = "cycles"; + constructionValue["value"] = 4; + (*arguments)[0] = std::move(constructionValue); + auto record = BindingRecord::parse(*object); + EXPECT_TRUE(static_cast(record)); + if (!record) + return {}; + auto fingerprint = computeBindingRecordFingerprint(*record); + EXPECT_TRUE(static_cast(fingerprint)); + if (!fingerprint) + return {}; + (*object)["fingerprint"] = *fingerprint; + auto canonical = canonicalizeJson(*parsed); + EXPECT_TRUE(static_cast(canonical)); + return canonical ? std::move(*canonical) : std::string(); +} + +BindingRequest exactRequest() { + auto requests = parseRequests(registryJson({candidateJson()})); + EXPECT_EQ(1U, requests.size()); + return requests.empty() ? BindingRequest() : std::move(requests.front()); +} + +void writeTestFile(llvm::StringRef path, llvm::StringRef bytes) { + std::error_code error; + llvm::raw_fd_ostream output(path, error); + ASSERT_FALSE(error) << error.message(); + output << bytes; + output.flush(); + ASSERT_FALSE(output.has_error()); +} + +std::string readTestFile(llvm::StringRef path) { + auto buffer = llvm::MemoryBuffer::getFile(path, false, false); + EXPECT_TRUE(static_cast(buffer)) + << (buffer ? "" : buffer.getError().message()); + return buffer ? (*buffer)->getBuffer().str() : std::string(); +} + +void expectNoBindingTemporaries(llvm::StringRef directory) { + std::error_code error; + for (llvm::sys::fs::directory_iterator iterator(directory, error), end; + iterator != end && !error; iterator.increment(error)) + EXPECT_EQ(llvm::StringRef::npos, + llvm::sys::path::filename(iterator->path()).find(".tmp-")); + EXPECT_FALSE(error) << error.message(); +} + +void expectCanonicalOutputBoundary(const llvm::json::Value &value, + llvm::StringRef expected) { + JsonParseLimits belowBoundary; + belowBoundary.maxInputBytes = expected.size() - 1; + { + detail::ScopedCanonicalEmissionFailure denyEmission; + auto rejected = canonicalizeJson(value, belowBoundary); + ASSERT_FALSE(static_cast(rejected)); + EXPECT_TRUE(containsText(takeError(rejected.takeError()), + "canonical output byte limit exceeded")); + } + + JsonParseLimits atBoundary; + atBoundary.maxInputBytes = expected.size(); + { + detail::ScopedCanonicalEmissionFailure denyEmission; + auto attempted = canonicalizeJson(value, atBoundary); + ASSERT_FALSE(static_cast(attempted)); + EXPECT_TRUE(containsText(takeError(attempted.takeError()), + "canonical emission failure injected")); + } + auto accepted = canonicalizeJson(value, atBoundary); + ASSERT_TRUE(static_cast(accepted)) << takeError(accepted.takeError()); + EXPECT_EQ(expected, *accepted); +} + +TEST(CanonicalJsonTest, ImplementsTheAuthoritativeRfc8785Vector) { + constexpr llvm::StringLiteral input = R"json({ + "numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001], + "string": "€$\u000f\nA'B\"\\\\\"/", + "literals": [null, true, false] + })json"; + constexpr llvm::StringLiteral expected = + R"json({"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"})json"; + + auto canonical = canonicalizeJsonText(input); + ASSERT_TRUE(static_cast(canonical)) << takeError(canonical.takeError()); + EXPECT_EQ(expected, *canonical); + EXPECT_EQ( + "sha256:2d5e01a318d0f0879ab568c4be289c8b1f64ef8921a53c6277d5e069978baacb", + sha256Fingerprint(*canonical)); +} + +TEST(CanonicalJsonTest, ImplementsAuthoritativeRfc8785AppendixBNumbers) { + struct Vector { + uint64_t bits; + llvm::StringLiteral expected; + }; + constexpr Vector vectors[] = { + {0x0000000000000000ULL, "0"}, + {0x0000000000000001ULL, "5e-324"}, + {0x8000000000000001ULL, "-5e-324"}, + {0x7fefffffffffffffULL, "1.7976931348623157e+308"}, + {0xffefffffffffffffULL, "-1.7976931348623157e+308"}, + {0x4340000000000000ULL, "9007199254740992"}, + {0xc340000000000000ULL, "-9007199254740992"}, + {0x4430000000000000ULL, "295147905179352830000"}, + {0x44b52d02c7e14af5ULL, "9.999999999999997e+22"}, + {0x44b52d02c7e14af6ULL, "1e+23"}, + {0x44b52d02c7e14af7ULL, "1.0000000000000001e+23"}, + {0x444b1ae4d6e2ef4eULL, "999999999999999700000"}, + {0x444b1ae4d6e2ef4fULL, "999999999999999900000"}, + {0x444b1ae4d6e2ef50ULL, "1e+21"}, + {0x3eb0c6f7a0b5ed8cULL, "9.999999999999997e-7"}, + {0x3eb0c6f7a0b5ed8dULL, "0.000001"}, + {0x41b3de4355555553ULL, "333333333.3333332"}, + {0x41b3de4355555554ULL, "333333333.33333325"}, + {0x41b3de4355555555ULL, "333333333.3333333"}, + {0x41b3de4355555556ULL, "333333333.3333334"}, + {0x41b3de4355555557ULL, "333333333.33333343"}, + {0xbecbf647612f3696ULL, "-0.0000033333333333333333"}, + {0x43143ff3c1cb0959ULL, "1424953923781206.2"}, + }; + for (const Vector &vector : vectors) { + double value = std::bit_cast(vector.bits); + auto canonical = canonicalizeJson(llvm::json::Value(value)); + ASSERT_TRUE(static_cast(canonical)) + << takeError(canonical.takeError()); + EXPECT_EQ(vector.expected, *canonical) << vector.bits; + } + + for (uint64_t bits : + {0x8000000000000000ULL, 0x7fffffffffffffffULL, 0x7ff0000000000000ULL}) { + auto rejected = + canonicalizeJson(llvm::json::Value(std::bit_cast(bits))); + ASSERT_FALSE(static_cast(rejected)) << bits; + EXPECT_TRUE( + containsText(takeError(rejected.takeError()), "ACLOWER-BINDING-JSON")); + } + + for (const auto &[input, expected] : + std::array, 2>{ + std::pair{llvm::StringLiteral("9007199254740992"), + llvm::StringLiteral("9007199254740992")}, + std::pair{llvm::StringLiteral("9007199254740993"), + llvm::StringLiteral("9007199254740992")}}) { + auto text = canonicalizeJsonText(input); + ASSERT_TRUE(static_cast(text)) << takeError(text.takeError()); + EXPECT_EQ(expected, *text); + } +} + +TEST(CanonicalJsonTest, SortsObjectPropertiesByUtf16CodeUnitsRecursively) { + constexpr llvm::StringLiteral input = + R"json({"\ud83d\ude00":"emoji","\u20ac":"euro","\r":"control","1":"one","\u00f6":"latin","\ufb33":"hebrew","nested":{"b":0,"a":1}})json"; + constexpr llvm::StringLiteral expected = + R"json({"\r":"control","1":"one","nested":{"a":1,"b":0},"ö":"latin","€":"euro","😀":"emoji","דּ":"hebrew"})json"; + + auto canonical = canonicalizeJsonText(input); + ASSERT_TRUE(static_cast(canonical)) << takeError(canonical.takeError()); + EXPECT_EQ(expected, *canonical); +} + +TEST(CanonicalJsonTest, RejectsDuplicateInvalidUnsafeAndNegativeZeroInputs) { + for (llvm::StringRef input : + {R"({"x":1,"x":2})", R"("\udead")", "-0", "-0.0", "1e999"}) { + auto parsed = parseIJson(input); + EXPECT_FALSE(static_cast(parsed)) << input.str(); + EXPECT_TRUE( + containsText(takeError(parsed.takeError()), "ACLOWER-BINDING-JSON")); + } +} + +TEST(CanonicalJsonTest, RejectsConstructedNonFiniteAndNegativeZeroValues) { + for (double value : {std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(), -0.0}) { + auto canonical = canonicalizeJson(llvm::json::Value(value)); + EXPECT_FALSE(static_cast(canonical)); + EXPECT_TRUE( + containsText(takeError(canonical.takeError()), "ACLOWER-BINDING-JSON")); + } +} + +TEST(CanonicalJsonTest, AppliesEveryDeterministicResourceCap) { + JsonParseLimits bytes; + bytes.maxInputBytes = 2; + auto tooManyBytes = parseIJson("null", bytes); + ASSERT_FALSE(static_cast(tooManyBytes)); + EXPECT_TRUE( + containsText(takeError(tooManyBytes.takeError()), "input byte limit")); + + JsonParseLimits shallow; + shallow.maxDepth = 4; + auto deep = parseIJson("[[[[[0]]]]]", shallow); + ASSERT_FALSE(static_cast(deep)); + EXPECT_TRUE(containsText(takeError(deep.takeError()), "maximum depth")); + + JsonParseLimits small; + small.maxStructuralWork = 8; + auto large = parseIJson("[0,1,2,3,4,5,6,7,8]", small); + ASSERT_FALSE(static_cast(large)); + EXPECT_TRUE( + containsText(takeError(large.takeError()), "structural work limit")); + + JsonParseLimits longString; + longString.maxStringBytes = 1; + auto oversizedString = parseIJson(R"("ab")", longString); + ASSERT_FALSE(static_cast(oversizedString)); + EXPECT_TRUE(containsText(takeError(oversizedString.takeError()), + "string byte limit")); + + JsonParseLimits totalStrings; + totalStrings.maxStringBytes = 8; + totalStrings.maxTotalStringBytes = 3; + auto oversizedTotal = parseIJson(R"(["ab","cd"])", totalStrings); + ASSERT_FALSE(static_cast(oversizedTotal)); + EXPECT_TRUE(containsText(takeError(oversizedTotal.takeError()), + "total string byte limit")); + + JsonParseLimits arrayElements; + arrayElements.maxArrayElements = 2; + auto oversizedArray = parseIJson("[0,1,2]", arrayElements); + ASSERT_FALSE(static_cast(oversizedArray)); + EXPECT_TRUE(containsText(takeError(oversizedArray.takeError()), + "array element limit")); + + JsonParseLimits objectMembers; + objectMembers.maxObjectMembers = 1; + auto oversizedObject = parseIJson(R"({"a":0,"b":1})", objectMembers); + ASSERT_FALSE(static_cast(oversizedObject)); + EXPECT_TRUE(containsText(takeError(oversizedObject.takeError()), + "object member limit")); +} + +TEST(CanonicalJsonTest, BoundsEveryConstructedDomBeforeCanonicalization) { + auto expectLimit = [](llvm::json::Value value, JsonParseLimits limits, + llvm::StringRef message) { + auto canonical = canonicalizeJson(value, limits); + ASSERT_FALSE(static_cast(canonical)); + EXPECT_TRUE(containsText(takeError(canonical.takeError()), message.str())); + }; + + JsonParseLimits depth; + depth.maxDepth = 3; + llvm::json::Value nested = nullptr; + for (size_t index = 0; index < 3; ++index) { + llvm::json::Array wrapper; + wrapper.push_back(std::move(nested)); + nested = llvm::json::Value(std::move(wrapper)); + } + expectLimit(std::move(nested), depth, "maximum depth"); + + JsonParseLimits work; + work.maxStructuralWork = 3; + expectLimit(llvm::json::Array({nullptr, nullptr, nullptr}), work, + "structural work"); + + JsonParseLimits string; + string.maxStringBytes = 1; + expectLimit("ab", string, "string byte limit"); + + JsonParseLimits totalStrings; + totalStrings.maxStringBytes = 8; + totalStrings.maxTotalStringBytes = 3; + expectLimit(llvm::json::Array({"ab", "cd"}), totalStrings, + "total string byte limit"); + + JsonParseLimits array; + array.maxArrayElements = 2; + expectLimit(llvm::json::Array({0, 1, 2}), array, "array element limit"); + + JsonParseLimits object; + object.maxObjectMembers = 1; + llvm::json::Object members; + members["a"] = 0; + members["b"] = 1; + expectLimit(std::move(members), object, "object member limit"); + + JsonParseLimits output; + output.maxInputBytes = 3; + expectLimit("ab", output, "canonical output byte limit"); +} + +TEST(CanonicalJsonTest, CountsContainerPunctuationBeforeRecursiveEmission) { + llvm::json::Object object; + object["a"] = llvm::json::Array({nullptr, true, false, 1}); + expectCanonicalOutputBoundary(llvm::json::Value(std::move(object)), + R"json({"a":[null,true,false,1]})json"); +} + +TEST(CanonicalJsonTest, CountsEscapedStringBytesBeforeRecursiveEmission) { + expectCanonicalOutputBoundary(std::string("\"\\\n"), R"json("\"\\\n")json"); +} + +TEST(BindingRecordTest, BoundsConstructedStaticValuesBeforeSemanticRecursion) { + auto parsed = parseIJson(recordJson()); + ASSERT_TRUE(static_cast(parsed)) << takeError(parsed.takeError()); + auto *object = parsed->getAsObject(); + ASSERT_NE(nullptr, object); + auto *parameters = object->getArray("parameters"); + ASSERT_NE(nullptr, parameters); + auto *parameter = (*parameters)[0].getAsObject(); + ASSERT_NE(nullptr, parameter); + (*parameter)["value"] = llvm::json::Array({0, 1, 2}); + + JsonParseLimits limits; + limits.maxArrayElements = 2; + auto record = BindingRecord::parse(*object, limits); + ASSERT_FALSE(static_cast(record)); + EXPECT_TRUE( + containsText(takeError(record.takeError()), "array element limit")); +} + +TEST(BindingRecordTest, RestrictsSafeIntegerRangeOnlyForStaticMetadata) { + auto parsed = parseIJson(recordJson()); + ASSERT_TRUE(static_cast(parsed)) << takeError(parsed.takeError()); + auto *object = parsed->getAsObject(); + ASSERT_NE(nullptr, object); + auto *parameters = object->getArray("parameters"); + ASSERT_NE(nullptr, parameters); + auto *parameter = (*parameters)[0].getAsObject(); + ASSERT_NE(nullptr, parameter); + (*parameter)["value"] = INT64_C(9007199254740992); + + auto record = BindingRecord::parse(*object); + ASSERT_FALSE(static_cast(record)); + EXPECT_TRUE(containsText(takeError(record.takeError()), "safe exact range")); +} + +TEST(BindingRecordTest, AppliesCanonicalOutputLimitBeforeSemanticRecursion) { + auto parsed = parseIJson(recordJson()); + ASSERT_TRUE(static_cast(parsed)) << takeError(parsed.takeError()); + auto *object = parsed->getAsObject(); + ASSERT_NE(nullptr, object); + + JsonParseLimits limits; + limits.maxInputBytes = 0; + auto record = BindingRecord::parse(*object, limits); + ASSERT_FALSE(static_cast(record)); + EXPECT_TRUE(containsText(takeError(record.takeError()), + "canonical output byte limit exceeded")); +} + +TEST(BindingRecordTest, ParsesTheClosedTypedMetadataRecord) { + auto candidates = parseCandidates(registryJson({candidateJson()})); + ASSERT_EQ(1U, candidates.size()); + const BindingRecord &record = candidates.front().record(); + EXPECT_EQ("acsim-binding-0.1", record.bindingSchema()); + EXPECT_EQ("0.3", record.contractEpoch()); + EXPECT_EQ("Leaf", record.binding()); + EXPECT_EQ("pure", record.effect()); + EXPECT_EQ("gfsim::Leaf", record.cpp().symbol); + ASSERT_EQ(1U, record.parameters().size()); + EXPECT_EQ("constructor_constant", record.parameters().front().mapping); + ASSERT_EQ(1U, record.ports().size()); + EXPECT_EQ("exclusive", record.ports().front().cardinality); + EXPECT_EQ(kRecordFingerprint, record.fingerprint()); + + auto unitCandidates = parseCandidates(registryJson({candidateJson( + "fast", "arm64-apple-darwin", true, withUnitParameter(recordJson()))})); + ASSERT_EQ(1U, unitCandidates.size()); + ASSERT_EQ(1U, unitCandidates.front().record().parameters().size()); + EXPECT_NE( + nullptr, + unitCandidates.front().record().parameters().front().value.getAsObject()); +} + +TEST(BindingRecordTest, RejectsUnknownFieldsAndBehavioralCppFragments) { + std::string unknown = recordJson(); + unknown.insert(unknown.find("\"activation_sources\""), + "\"emitter_callback\":\"emitLeaf\","); + auto parsedUnknown = parseBindingRegistry(registryJson( + {candidateJson("fast", "arm64-apple-darwin", true, unknown)})); + ASSERT_FALSE(static_cast(parsedUnknown)); + EXPECT_TRUE(containsText(takeError(parsedUnknown.takeError()), + "exactly the acsim-binding-0.1 fields")); + + for (llvm::StringRef raw : {"gfsim::Leaf()", "Leaf{x}", "#define Leaf X", + "leaf = callback", "emit(%s)"}) { + auto parsed = parseBindingRegistry( + registryJson({candidateJson("fast", "arm64-apple-darwin", true, + recordJson(kRecordFingerprint, raw))})); + ASSERT_FALSE(static_cast(parsed)) << raw.str(); + EXPECT_TRUE(containsText(takeError(parsed.takeError()), + "raw C++ or emitter behavior")); + } + + std::string traversal = recordJson(); + traversal.replace(traversal.find("gfsim/leaf.hpp"), + llvm::StringRef("gfsim/leaf.hpp").size(), "include/.."); + auto parsedTraversal = parseBindingRegistry(registryJson( + {candidateJson("fast", "arm64-apple-darwin", true, traversal)})); + ASSERT_FALSE(static_cast(parsedTraversal)); + EXPECT_TRUE( + containsText(takeError(parsedTraversal.takeError()), "header path")); + + std::string rawParameterType = recordJson(); + rawParameterType.replace(rawParameterType.find("std::int64_t"), + llvm::StringRef("std::int64_t").size(), + "std::int64_t;emit()"); + auto parsedParameterType = parseBindingRegistry(registryJson( + {candidateJson("fast", "arm64-apple-darwin", true, rawParameterType)})); + ASSERT_FALSE(static_cast(parsedParameterType)); + EXPECT_TRUE(containsText(takeError(parsedParameterType.takeError()), + "raw C++ or emitter behavior")); +} + +TEST(BindingRecordTest, RejectsDuplicateStatefulActivationSourceNames) { + auto parsed = parseIJson(recordJson()); + ASSERT_TRUE(static_cast(parsed)) << takeError(parsed.takeError()); + auto *object = parsed->getAsObject(); + ASSERT_NE(nullptr, object); + (*object)["effect"] = "stateful"; + auto *cpp = object->getObject("cpp"); + auto *entryPoints = cpp ? cpp->getObject("entry_points") : nullptr; + auto *ownership = object->getObject("ownership"); + ASSERT_NE(nullptr, entryPoints); + ASSERT_NE(nullptr, ownership); + (*entryPoints)["pure"] = ""; + (*entryPoints)["work"] = "gfsim::work"; + (*entryPoints)["xfer"] = "gfsim::xfer"; + (*ownership)["kind"] = "unique"; + (*ownership)["placement"] = "member_or_array"; + llvm::json::Array activations; + activations.push_back( + llvm::json::Object({{"kind", "ac.Clock"}, {"name", "wake"}})); + activations.push_back( + llvm::json::Object({{"kind", "ac.Reset"}, {"name", "wake"}})); + (*object)["activation_sources"] = std::move(activations); + + auto duplicate = BindingRecord::parse(*object); + ASSERT_FALSE(static_cast(duplicate)); + EXPECT_TRUE(containsText(takeError(duplicate.takeError()), + "activation-source names must be unique")); + + auto *sources = object->getArray("activation_sources"); + ASSERT_NE(nullptr, sources); + auto *reset = (*sources)[1].getAsObject(); + ASSERT_NE(nullptr, reset); + (*reset)["name"] = "reset"; + auto unique = BindingRecord::parse(*object); + ASSERT_TRUE(static_cast(unique)) << takeError(unique.takeError()); +} + +TEST(BindingRecordTest, RejectsMalformedRegistryEnvelopeAndResourceCaps) { + auto objectInsteadOfArray = parseBindingRegistry(candidateJson()); + ASSERT_FALSE(static_cast(objectInsteadOfArray)); + EXPECT_TRUE( + containsText(takeError(objectInsteadOfArray.takeError()), + "registry must contain exactly candidates and requests")); + + std::string requestWithCppOutput = requestJson(); + requestWithCppOutput.insert(requestWithCppOutput.find("\"binding\""), + "\"cpp_type\":\"cpp_i32\","); + auto cppAuthoredRequest = + parseBindingRegistry(registryJson({}, {std::move(requestWithCppOutput)})); + ASSERT_FALSE(static_cast(cppAuthoredRequest)); + EXPECT_TRUE(containsText(takeError(cppAuthoredRequest.takeError()), + "exactly the frozen architecture fields")); + + std::string requestWithAccessor = requestJson(); + requestWithAccessor.insert(requestWithAccessor.find("\"cardinality\""), + "\"accessor\":\"input\","); + auto accessorAuthoredRequest = + parseBindingRegistry(registryJson({}, {std::move(requestWithAccessor)})); + ASSERT_FALSE(static_cast(accessorAuthoredRequest)); + EXPECT_TRUE( + containsText(takeError(accessorAuthoredRequest.takeError()), + "request port must contain exact architecture fields")); + + JsonParseLimits limits; + limits.maxInputBytes = 32; + auto tooLarge = parseBindingRegistry(registryJson({candidateJson()}), limits); + ASSERT_FALSE(static_cast(tooLarge)); + EXPECT_TRUE( + containsText(takeError(tooLarge.takeError()), "input byte limit")); +} + +TEST(BindingCandidateTest, RejectsOversizedConstructedProfileAndTarget) { + auto parsed = parseIJson(candidateJson()); + ASSERT_TRUE(static_cast(parsed)) << takeError(parsed.takeError()); + auto *object = parsed->getAsObject(); + ASSERT_NE(nullptr, object); + + JsonParseLimits limits; + limits.maxStringBytes = 71; + for (llvm::StringRef key : {"profile", "target"}) { + (*object)[key] = std::string(72, key.front()); + auto candidate = BindingCandidate::parse(*object, limits); + ASSERT_FALSE(static_cast(candidate)) << key.str(); + EXPECT_TRUE(containsText(takeError(candidate.takeError()), + "string byte limit exceeded")); + (*object)[key] = key == "profile" ? "fast" : "arm64-apple-darwin"; + } +} + +TEST(BindingCandidateTest, AppliesDepthAndOutputLimitsToCompleteEnvelope) { + auto parsed = parseIJson(candidateJson()); + ASSERT_TRUE(static_cast(parsed)) << takeError(parsed.takeError()); + auto *object = parsed->getAsObject(); + ASSERT_NE(nullptr, object); + + JsonParseLimits depth; + depth.maxDepth = 4; + auto tooDeep = BindingCandidate::parse(*object, depth); + ASSERT_FALSE(static_cast(tooDeep)); + EXPECT_TRUE( + containsText(takeError(tooDeep.takeError()), "maximum depth exceeded")); + + JsonParseLimits output; + output.maxInputBytes = 0; + auto tooLarge = BindingCandidate::parse(*object, output); + ASSERT_FALSE(static_cast(tooLarge)); + EXPECT_TRUE(containsText(takeError(tooLarge.takeError()), + "canonical output byte limit exceeded")); +} + +TEST(BindingRegistryTest, ExactSelectionIsIndependentOfProviderOrder) { + std::string fast = candidateJson(); + std::string validated = candidateJson("validated"); + auto forward = parseCandidates(registryJson({validated, fast})); + auto reverse = parseCandidates(registryJson({fast, validated})); + BindingRequest request = exactRequest(); + + auto first = + resolveBindings(forward, {request}, "fast", "arm64-apple-darwin"); + auto second = + resolveBindings(reverse, {request}, "fast", "arm64-apple-darwin"); + ASSERT_TRUE(static_cast(first)) << takeError(first.takeError()); + ASSERT_TRUE(static_cast(second)) << takeError(second.takeError()); + EXPECT_EQ(first->canonicalLock(), second->canonicalLock()); + EXPECT_EQ(first->lockFingerprint(), second->lockFingerprint()); + ASSERT_EQ(1U, first->selections().size()); + EXPECT_EQ("Leaf", first->selections().front().record().binding()); +} + +TEST(BindingResolutionResultTest, LooksUpSelectionsOnlyByExactResolutionKey) { + auto candidates = parseCandidates(registryJson({candidateJson()})); + auto result = resolveBindings(candidates, {exactRequest()}, "fast", + "arm64-apple-darwin"); + ASSERT_TRUE(static_cast(result)) << takeError(result.takeError()); + + const ResolvedBinding *selection = result->selectionForResolutionKey("@Leaf"); + ASSERT_NE(nullptr, selection); + EXPECT_EQ("Leaf", selection->record().binding()); + EXPECT_EQ(nullptr, result->selectionForResolutionKey("Leaf")); + EXPECT_EQ(nullptr, result->selectionForResolutionKey("@Top::@Leaf")); + EXPECT_EQ(nullptr, result->selectionForResolutionKey("@Missing")); +} + +TEST(BindingResolutionResultTest, EmptyResultIsCanonicalAndLookupIsMissing) { + auto first = resolveBindings({}, {}, "fast", "arm64-apple-darwin"); + auto second = resolveBindings({}, {}, "fast", "arm64-apple-darwin"); + ASSERT_TRUE(static_cast(first)) << takeError(first.takeError()); + ASSERT_TRUE(static_cast(second)) << takeError(second.takeError()); + + EXPECT_TRUE(first->selections().empty()); + EXPECT_EQ(nullptr, first->selectionForResolutionKey("@Anything")); + EXPECT_EQ("[]", first->canonicalLock()); + EXPECT_EQ("sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11b" + "a873c2f11161202b945", + first->lockFingerprint().str()); + EXPECT_EQ(first->canonicalLock(), second->canonicalLock()); + EXPECT_EQ(first->lockFingerprint(), second->lockFingerprint()); +} + +TEST(BindingRegistryTest, MissingUnavailableAndAmbiguousAreHardFailures) { + auto candidates = parseCandidates(registryJson({candidateJson()})); + BindingRequest request = exactRequest(); + + auto missing = resolveBindings({}, {request}, "fast", "arm64-apple-darwin"); + ASSERT_FALSE(static_cast(missing)); + EXPECT_TRUE( + containsText(takeError(missing.takeError()), "ACLOWER-BINDING-MISSING")); + + auto unavailable = parseCandidates( + registryJson({candidateJson("fast", "arm64-apple-darwin", false)})); + auto noAvailable = + resolveBindings(unavailable, {request}, "fast", "arm64-apple-darwin"); + ASSERT_FALSE(static_cast(noAvailable)); + EXPECT_TRUE(containsText(takeError(noAvailable.takeError()), + "ACLOWER-BINDING-MISSING")); + + auto ambiguous = + parseCandidates(registryJson({candidateJson(), candidateJson()})); + auto multiple = + resolveBindings(ambiguous, {request}, "fast", "arm64-apple-darwin"); + ASSERT_FALSE(static_cast(multiple)); + EXPECT_TRUE(containsText(takeError(multiple.takeError()), + "ACLOWER-BINDING-AMBIGUOUS")); +} + +TEST(BindingRegistryTest, + ClassifiesMalformedInputsBeforeExactMatchCardinality) { + auto candidates = parseCandidates(registryJson({candidateJson()})); + BindingRequest exact = exactRequest(); + struct Malformed { + llvm::StringLiteral code; + void (*mutate)(BindingRequest &); + }; + const Malformed malformed[] = { + {"ACLOWER-EPOCH-MISMATCH", + [](BindingRequest &request) { request.contractEpoch = "0.1"; }}, + {"ACLOWER-SCHEMA-MISMATCH", + [](BindingRequest &request) { + request.bindingSchema = "acsim-binding-0.2"; + }}, + {"ACLOWER-INLINE-EFFECT", + [](BindingRequest &request) { request.effect = "invalid"; }}, + {"ACLOWER-FINGERPRINT", + [](BindingRequest &request) { + request.providerImplementationFingerprint = "not-a-fingerprint"; + }}, + }; + + for (const Malformed &input : malformed) { + BindingRequest request = exact; + input.mutate(request); + auto result = + resolveBindings(candidates, {request}, "fast", "arm64-apple-darwin"); + ASSERT_FALSE(static_cast(result)) << input.code.str(); + EXPECT_TRUE( + startsWithText(takeError(result.takeError()), input.code.str())); + } + + auto invalidProfile = + resolveBindings(candidates, {exact}, "fast debug", "arm64-apple-darwin"); + ASSERT_FALSE(static_cast(invalidProfile)); + EXPECT_TRUE( + startsWithText(takeError(invalidProfile.takeError()), "ACLOWER-PROFILE")); +} + +TEST(BindingRegistryTest, ZeroExactMatchesAreMissingWithSubordinateReason) { + auto candidates = parseCandidates(registryJson({candidateJson()})); + BindingRequest exact = exactRequest(); + struct Mismatch { + llvm::StringLiteral reason; + void (*mutate)(BindingRequest &); + }; + const Mismatch mismatches[] = { + {"ACLOWER-SCHEMA-MISMATCH", + [](BindingRequest &request) { request.componentSchema = "ac.Other"; }}, + {"ACLOWER-INLINE-EFFECT", + [](BindingRequest &request) { request.effect = "stateful"; }}, + {"ACLOWER-PARAM-PHASE", + [](BindingRequest &request) { request.parameters.clear(); }}, + {"ACLOWER-TYPE-MISMATCH", + [](BindingRequest &request) { request.results.front().name = "other"; }}, + {"ACLOWER-TYPE-MISMATCH", + [](BindingRequest &request) { request.ports.clear(); }}, + {"ACLOWER-FINGERPRINT", + [](BindingRequest &request) { + request.providerImplementationFingerprint = + "sha256:" + "3333333333333333333333333333333333333333333333333333333333333333"; + }}, + }; + + for (const Mismatch &mismatch : mismatches) { + BindingRequest request = exact; + mismatch.mutate(request); + auto result = + resolveBindings(candidates, {request}, "fast", "arm64-apple-darwin"); + ASSERT_FALSE(static_cast(result)) << mismatch.reason.str(); + std::string message = takeError(result.takeError()); + EXPECT_TRUE(startsWithText(message, "ACLOWER-BINDING-MISSING")); + EXPECT_TRUE(containsText(message, + (llvm::Twine("reason=") + mismatch.reason).str())); + EXPECT_TRUE(containsText(message, "key=@Leaf")); + } + + for (auto [profile, target] : {std::pair( + "validated", "arm64-apple-darwin"), + {"fast", "x86_64-linux-gnu"}}) { + auto result = resolveBindings(candidates, {exact}, profile, target); + ASSERT_FALSE(static_cast(result)); + std::string message = takeError(result.takeError()); + EXPECT_TRUE(startsWithText(message, "ACLOWER-BINDING-MISSING")); + EXPECT_TRUE(containsText(message, "reason=ACLOWER-PROFILE")); + } +} + +TEST(BindingRegistryTest, NarrowsOneCandidateSetAcrossEveryExactField) { + BindingRequest request = exactRequest(); + std::string otherSchema = withComponentSchema(recordJson(), "ac.OtherLeaf"); + auto candidates = parseCandidates(registryJson( + {candidateJson("validated"), + candidateJson("fast", "arm64-apple-darwin", true, otherSchema)})); + + auto result = + resolveBindings(candidates, {request}, "fast", "arm64-apple-darwin"); + ASSERT_FALSE(static_cast(result)); + EXPECT_TRUE(containsText(takeError(result.takeError()), "ACLOWER-PROFILE")); +} + +TEST(BindingRegistryTest, RejectsIncorrectRecordFingerprintBeforeSelection) { + auto candidates = parseCandidates(registryJson( + {candidateJson("fast", "arm64-apple-darwin", true, + recordJson("sha256:" + "ffffffffffffffffffffffffffffffffffffffffffffff" + "ffffffffffffffffff"))})); + BindingRequest request = exactRequest(); + auto result = + resolveBindings(candidates, {request}, "fast", "arm64-apple-darwin"); + ASSERT_FALSE(static_cast(result)); + EXPECT_TRUE( + containsText(takeError(result.takeError()), "ACLOWER-FINGERPRINT")); +} + +TEST(BindingLockTest, EmitsStableCanonicalBytesAndProjectHashVector) { + auto candidates = parseCandidates(registryJson({candidateJson()})); + auto result = resolveBindings(candidates, {exactRequest()}, "fast", + "arm64-apple-darwin"); + ASSERT_TRUE(static_cast(result)) << takeError(result.takeError()); + EXPECT_EQ( + "sha256:fa2705b165b450090078d685d7cf8859c7393cb78e9b6a0b3b6a62ab81644703", + result->lockFingerprint()); + EXPECT_EQ('[', result->canonicalLock().front()); + EXPECT_EQ(']', result->canonicalLock().back()); + + std::string streamed; + llvm::raw_string_ostream output(streamed); + EXPECT_FALSE(static_cast(emitBindingLock(*result, output))); + output.flush(); + EXPECT_EQ(result->canonicalLock(), streamed); +} + +TEST(BindingLockTest, PublishesAtomicallyOnlyAfterCompleteResolution) { + llvm::SmallString<128> directory; + ASSERT_FALSE( + llvm::sys::fs::createUniqueDirectory("acir-bindings", directory)); + llvm::SmallString<160> successPath(directory); + llvm::sys::path::append(successPath, "acsim-bindings.lock.json"); + llvm::SmallString<160> failurePath(directory); + llvm::sys::path::append(failurePath, "failed.lock.json"); + llvm::SmallString<160> existingPath(directory); + llvm::sys::path::append(existingPath, "existing.lock.json"); + + auto candidates = parseCandidates(registryJson({candidateJson()})); + BindingRequest request = exactRequest(); + EXPECT_FALSE(static_cast(resolveAndWriteBindingLock( + candidates, {request}, "fast", "arm64-apple-darwin", successPath))); + EXPECT_TRUE(llvm::sys::fs::exists(successPath)); + + BindingRequest missing = request; + missing.binding = "Missing"; + llvm::Error failure = resolveAndWriteBindingLock( + candidates, {missing}, "fast", "arm64-apple-darwin", failurePath); + ASSERT_TRUE(static_cast(failure)); + EXPECT_TRUE( + containsText(takeError(std::move(failure)), "ACLOWER-BINDING-MISSING")); + EXPECT_FALSE(llvm::sys::fs::exists(failurePath)); + + constexpr llvm::StringLiteral sentinel = "existing-lock-sentinel\n"; + writeTestFile(existingPath, sentinel); + llvm::Error existingResolutionFailure = resolveAndWriteBindingLock( + candidates, {missing}, "fast", "arm64-apple-darwin", existingPath); + ASSERT_TRUE(static_cast(existingResolutionFailure)); + EXPECT_TRUE(containsText(takeError(std::move(existingResolutionFailure)), + "ACLOWER-BINDING-MISSING")); + EXPECT_EQ(sentinel, readTestFile(existingPath)); + expectNoBindingTemporaries(directory); + + llvm::SmallString<160> invalidParent(directory); + llvm::sys::path::append(invalidParent, "missing", "lock.json"); + auto result = + resolveBindings(candidates, {request}, "fast", "arm64-apple-darwin"); + ASSERT_TRUE(static_cast(result)) << takeError(result.takeError()); + llvm::Error writeFailure = emitBindingLockAtomically(*result, invalidParent); + ASSERT_TRUE(static_cast(writeFailure)); + EXPECT_TRUE(containsText(takeError(std::move(writeFailure)), + "ACLOWER-BINDING-OUTPUT")); + + { + detail::ScopedBindingPublishFailure failure; + llvm::Error publishFailure = + emitBindingLockAtomically(*result, existingPath); + ASSERT_TRUE(static_cast(publishFailure)); + EXPECT_TRUE(containsText(takeError(std::move(publishFailure)), + "ACLOWER-BINDING-OUTPUT")); + } + EXPECT_EQ(sentinel, readTestFile(existingPath)); + expectNoBindingTemporaries(directory); +} + +TEST(ResolveBindingsApiTest, ReturnsTypedResultWithoutMutatingFrozenTopology) { + mlir::DialectRegistry dialects; + acir::registerAllDialects(dialects); + mlir::MLIRContext context(dialects); + context.loadAllAvailableDialects(); + acir::ac::getStructuralProviderRegistry(&context).registerExternal("Leaf"); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module.extern @Leaf : () -> i32 parameters {width = 8 : i64} + implementation {registry = "cpp", name = "Leaf"} + ac.module @Top() parameters {} graph { + %leaf = ac.instance @leaf of @Leaf() static {width = 8 : i64} + id "leaf" path "leaf" : () -> i32 + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + + ResolveBindingsPassOptions options; + options.candidates = parseCandidates(registryJson({candidateJson()})); + options.requests = parseRequests(registryJson({candidateJson()})); + options.profile = "fast"; + options.target = "arm64-apple-darwin"; + + auto beforeFreeze = resolveModuleBindings(*module, options); + ASSERT_FALSE(static_cast(beforeFreeze)); + EXPECT_TRUE(containsText(takeError(beforeFreeze.takeError()), + "ACLOWER-BINDING-MISSING")); + + mlir::PassManager manager(&context); + manager.enableVerifier(false); + manager.addPass(createFreezeTopologyPass()); + ASSERT_TRUE(mlir::succeeded(manager.run(*module))); + mlir::Attribute digest = (*module)->getAttr("ac.topology_digest"); + + auto result = resolveModuleBindings(*module, options); + ASSERT_TRUE(static_cast(result)) << takeError(result.takeError()); + ASSERT_EQ(1U, result->selections().size()); + EXPECT_EQ("Leaf", result->selections().front().record().binding()); + EXPECT_EQ(digest, (*module)->getAttr("ac.topology_digest")); + bool containsACSimOperation = false; + module->walk([&](mlir::Operation *operation) { + if (operation->getName().getDialectNamespace() == "acsim") + containsACSimOperation = true; + }); + EXPECT_FALSE(containsACSimOperation); + + ResolveBindingsPassOptions ambiguousOptions = options; + ambiguousOptions.candidates = + parseCandidates(registryJson({candidateJson(), candidateJson()})); + auto ambiguous = resolveModuleBindings(*module, ambiguousOptions); + ASSERT_FALSE(static_cast(ambiguous)); + EXPECT_TRUE(containsText(takeError(ambiguous.takeError()), + "ACLOWER-BINDING-AMBIGUOUS")); + + ResolveBindingsPassOptions absentRequest = options; + absentRequest.requests.clear(); + auto absent = resolveModuleBindings(*module, absentRequest); + ASSERT_FALSE(static_cast(absent)); + EXPECT_TRUE(containsText(takeError(absent.takeError()), + "exact frozen architecture request is absent")); + + ResolveBindingsPassOptions schemaMismatch = options; + schemaMismatch.candidates = parseCandidates(registryJson( + {candidateJson("fast", "arm64-apple-darwin", true, + withComponentSchema(recordJson(), "ac.OtherLeaf"))})); + auto wrongSchema = resolveModuleBindings(*module, schemaMismatch); + ASSERT_FALSE(static_cast(wrongSchema)); + EXPECT_TRUE(containsText(takeError(wrongSchema.takeError()), + "ACLOWER-SCHEMA-MISMATCH")); + + ResolveBindingsPassOptions providerMismatch = options; + providerMismatch.requests.front().providerImplementationFingerprint = + "sha256:3333333333333333333333333333333333333333333333333333333333333333"; + auto wrongProvider = resolveModuleBindings(*module, providerMismatch); + ASSERT_FALSE(static_cast(wrongProvider)); + EXPECT_TRUE(containsText(takeError(wrongProvider.takeError()), + "ACLOWER-FINGERPRINT")); + + ResolveBindingsPassOptions signatureMismatch = options; + signatureMismatch.requests.front().functionType = "() -> i64"; + auto wrongSignature = resolveModuleBindings(*module, signatureMismatch); + ASSERT_FALSE(static_cast(wrongSignature)); + EXPECT_TRUE(containsText(takeError(wrongSignature.takeError()), + "ACLOWER-TYPE-MISMATCH")); + + std::string unitSource = source.str(); + unitSource.replace(unitSource.find("width = 8 : i64"), + llvm::StringRef("width = 8 : i64").size(), + "width = {unit = \"cycles\", value = 4 : i64}"); + unitSource.replace(unitSource.find("width = 8 : i64"), + llvm::StringRef("width = 8 : i64").size(), + "width = {unit = \"cycles\", value = 4 : i64}"); + auto unitModule = + mlir::parseSourceString(unitSource, &context); + ASSERT_TRUE(unitModule); + mlir::PassManager unitManager(&context); + unitManager.enableVerifier(false); + unitManager.addPass(createFreezeTopologyPass()); + ASSERT_TRUE(mlir::succeeded(unitManager.run(*unitModule))); + ResolveBindingsPassOptions unitOptions = options; + std::string unitRegistry = registryJson( + {candidateJson("fast", "arm64-apple-darwin", true, + withUnitParameter(recordJson()))}, + {requestJson( + "ac.Leaf", + "sha256:" + "2222222222222222222222222222222222222222222222222222222222222222", + "() -> i32", R"({"unit":"cycles","value":4})")}); + unitOptions.candidates = parseCandidates(unitRegistry); + unitOptions.requests = parseRequests(unitRegistry); + auto unitResult = resolveModuleBindings(*unitModule, unitOptions); + ASSERT_TRUE(static_cast(unitResult)) + << takeError(unitResult.takeError()); +} + +} // namespace +} // namespace acir::bindings diff --git a/components/agentic-circuit/unittests/CMakeLists.txt b/components/agentic-circuit/unittests/CMakeLists.txt new file mode 100644 index 00000000..05e94e9f --- /dev/null +++ b/components/agentic-circuit/unittests/CMakeLists.txt @@ -0,0 +1,15 @@ +# LLVM's own build defaults to no RTTI. When linking such an LLVM build (CI +# builds LLVM from source with default flags), every unit-test target must use +# the same mode: sanitizer instrumentation otherwise emits typeinfo references +# that the no-RTTI runtime libraries cannot provide. +if(NOT LLVM_ENABLE_RTTI) + add_compile_options($<$:-fno-rtti>) +endif() + +add_subdirectory(gfsim) +add_subdirectory(Dialect) +add_subdirectory(Analysis) +add_subdirectory(Bindings) +add_subdirectory(Conversion) +add_subdirectory(CodeGen) +add_subdirectory(Compiler) diff --git a/components/agentic-circuit/unittests/CodeGen/BuildTest.cpp b/components/agentic-circuit/unittests/CodeGen/BuildTest.cpp new file mode 100644 index 00000000..fcc105d4 --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/BuildTest.cpp @@ -0,0 +1,406 @@ +#include "acir/CodeGen/Build.h" +#include "BuildInternal.h" +#include "TestToolchain.h" + +#include "acir/Dialect/ACSim/ACSimDialect.h" + +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/raw_ostream.h" +#include "gtest/gtest.h" + +#include +#include +#include + +namespace acir::codegen { +namespace { + +constexpr llvm::StringLiteral kFingerprint = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + +bool hasError(llvm::Error error) { + if (!error) + return false; + llvm::consumeError(std::move(error)); + return true; +} + +std::string readFile(llvm::StringRef path) { + auto buffer = llvm::MemoryBuffer::getFile(path); + return buffer ? buffer.get()->getBuffer().str() : std::string(); +} + +void writeFile(llvm::StringRef path, llvm::StringRef content) { + std::error_code error; + llvm::raw_fd_ostream output(path, error); + ASSERT_FALSE(error); + output << content; +} + +class TempOutputRoot { +public: + TempOutputRoot() { + EXPECT_FALSE( + llvm::sys::fs::createUniqueDirectory("acir-build-test", path_)); + } + ~TempOutputRoot() { llvm::sys::fs::remove_directories(path_); } + std::string path() const { return path_.str().str(); } + +private: + llvm::SmallString<256> path_; +}; + +llvm::Expected jsonFingerprint(llvm::json::Value value) { + return fingerprintCanonicalJson(value); +} + +FrontendProvenance makeFrontendProvenance() { + const std::string acpy = "{\"schema\":\"agentic-circuit-acpy\"}\n"; + const std::string acir = "module attributes {ac.contract_epoch = \"0.3\"}\n"; + FrontendProvenance frontend; + frontend.sourceFiles = { + {"architecture.py", computeFingerprint("architecture source\n")}}; + frontend.acpy = {"input/model.acpy.json", ArtifactKind::Acpy, + computeFingerprint(acpy)}; + frontend.acpyBytes = acpy; + frontend.canonicalAcir = {"input/model.ac.mlir", ArtifactKind::Acir, + computeFingerprint(acir)}; + frontend.canonicalAcirBytes = acir; + frontend.pythonVersion = "CPython 3.12"; + frontend.helperIdentities = {{"agentic-circuit", computeFingerprint("0.1")}}; + return frontend; +} + +class PublishFixture { +public: + explicit PublishFixture(llvm::StringRef outputRoot) { + context_.loadDialect(); + auto toolchain = identifyToolchain(ACIR_TEST_CXX_COMPILER, "libc++", + "default", "mach-o", {"-std=c++20"}); + EXPECT_TRUE(static_cast(toolchain)); + if (!toolchain) + return; + request_.toolchain = std::move(*toolchain); + const std::string frozenBytes = "minimal frozen acir\n"; + const std::string lockBytes = "[]"; + auto profile = jsonFingerprint(llvm::json::Value("fast")); + auto target = + jsonFingerprint(llvm::json::Value(request_.toolchain.targetTriple)); + auto emptySet = jsonFingerprint(llvm::json::Value(llvm::json::Array{})); + EXPECT_TRUE(static_cast(profile)); + EXPECT_TRUE(static_cast(target)); + EXPECT_TRUE(static_cast(emptySet)); + if (!profile || !target || !emptySet) + return; + + const std::string zero = kFingerprint.str(); + std::string source = + "builtin.module attributes {ac.contract_epoch = \"0.3\"} {\n" + " acsim.model @minimal epoch \"0.3\" root @Top construction [] " + "destruction [] fingerprints {frozen_acir = \"" + + computeFingerprint(frozenBytes) + "\", binding_lock = \"" + + computeFingerprint(lockBytes) + "\", provider = \"" + *emptySet + + "\", profile = \"" + *profile + "\", toolchain = \"" + *target + + "\", schema_set = \"" + *emptySet + + "\"} {\n" + " acsim.module @Top interface {ports = [], resources = [], results " + "= []} static [] specialization \"" + + zero + "\" exports [] { acsim.return }\n }\n}\n"; + module_ = mlir::parseSourceString(source, &context_); + EXPECT_TRUE(static_cast(module_)); + if (!module_) + return; + + request_.project = {"project", "project.example"}; + request_.system = {"system", "system.example"}; + request_.frontend = makeFrontendProvenance(); + request_.canonicalACSim = *module_; + request_.frozenAcirBytes = frozenBytes; + request_.bindingLockBytes = lockBytes; + request_.profile = "fast"; + request_.passPipeline = {"acsim-emit-cxx", "compile", "link"}; + request_.includeRoots = {ACIR_TEST_SOURCE_DIR "/include"}; + request_.linkInputs = {ACIR_TEST_BINARY_DIR "/lib/gfsim/libgfsim.a", + ACIR_TEST_BINARY_DIR + "/lib/Bindings/libACIRBindings.a"}; + request_.linkerFlags = test::llvmLinkerFlags(); + if (llvm::StringRef(ACIR_TEST_SANITIZER_FLAG).size()) + request_.linkerFlags.push_back(ACIR_TEST_SANITIZER_FLAG); + request_.outputRoot = outputRoot.str(); + } + + BuildRequest &request() { return request_; } + +private: + mlir::MLIRContext context_; + mlir::OwningOpRef module_; + BuildRequest request_; +}; + +llvm::Expected makeBuildRequest() { + auto toolchain = identifyToolchain(ACIR_TEST_CXX_COMPILER, "libc++", + "default", "mach-o", {"-std=c++20"}); + if (!toolchain) + return toolchain.takeError(); + BuildRequest request; + request.project = {"project", "project.example"}; + request.system = {"system", "system.example"}; + request.frontend = makeFrontendProvenance(); + request.profile = "fast"; + request.passPipeline = {"acsim-emit-cxx", "compile", "link"}; + request.toolchain = std::move(*toolchain); + request.includeRoots = {"vendor/include", "include"}; + request.definitions = {"ZETA=1", "ALPHA=1"}; + request.compilerFlags = {"-Wall"}; + request.linkerFlags = {"-pthread"}; + request.outputRoot = "out"; + return request; +} + +SourceBundle makeSourceBundle() { + SourceBundle bundle; + bundle.sourceFingerprint = kFingerprint.str(); + bundle.buildFingerprint = kFingerprint.str(); + bundle.files = { + {.relativePath = "include/generated/model.h", + .content = "#pragma once\n", + .fingerprint = computeFingerprint("#pragma once\n")}, + {.relativePath = "src/generated/main.cpp", + .content = "int main() { return 0; }\n", + .fingerprint = computeFingerprint("int main() { return 0; }\n")}}; + return bundle; +} + +TEST(BuildTest, CompilePlanIsClosedCanonicalAndArgumentVectorBased) { + auto request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + auto first = createCompilePlan(*request, makeSourceBundle()); + auto second = createCompilePlan(*request, makeSourceBundle()); + ASSERT_TRUE(static_cast(first)); + ASSERT_TRUE(static_cast(second)); + EXPECT_EQ(first->schema, "acsim-compile-plan-0.1"); + EXPECT_TRUE(isValidFingerprint(first->fingerprint)); + EXPECT_EQ(first->fingerprint, second->fingerprint); + std::vector expectedIncludeRoots = + test::llvmIncludeDirectories(); + expectedIncludeRoots.insert(expectedIncludeRoots.end(), + {"include", "vendor/include"}); + std::sort(expectedIncludeRoots.begin(), expectedIncludeRoots.end()); + expectedIncludeRoots.erase( + std::unique(expectedIncludeRoots.begin(), expectedIncludeRoots.end()), + expectedIncludeRoots.end()); + EXPECT_EQ(first->includeRoots, expectedIncludeRoots); + EXPECT_EQ(first->definitions, + (std::vector{"ALPHA=1", "ZETA=1"})); + ASSERT_EQ(first->compileCommands.size(), 1u); + EXPECT_EQ(first->compileCommands[0].arguments.front(), + request->toolchain.compilerPath); + EXPECT_EQ(first->compileCommands[0].arguments.back(), + first->compileCommands[0].output); + EXPECT_FALSE(first->linkCommand.arguments.empty()); + auto firstJson = first->canonicalJson(); + auto secondJson = second->canonicalJson(); + ASSERT_TRUE(static_cast(firstJson)); + ASSERT_TRUE(static_cast(secondJson)); + EXPECT_EQ(*firstJson, *secondJson); +} + +TEST(BuildTest, LinkInputContentsParticipateInCompilePlanIdentity) { + TempOutputRoot output; + const std::string linkInput = output.path() + "/runtime.a"; + writeFile(linkInput, "first link input\n"); + + auto request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + request->linkInputs = {linkInput}; + auto first = createCompilePlan(*request, makeSourceBundle()); + ASSERT_TRUE(static_cast(first)); + + writeFile(linkInput, "second link input\n"); + auto second = createCompilePlan(*request, makeSourceBundle()); + ASSERT_TRUE(static_cast(second)); + EXPECT_NE(first->fingerprint, second->fingerprint); +} + +TEST(BuildTest, RejectsToolchainOrPrebuiltProvenanceMismatch) { + auto request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + request->prebuiltInputs.push_back( + {.path = "lib/provider.o", + .kind = "provider", + .provenance = {.compilerBuildId = request->toolchain.compilerBuildId, + .targetTriple = request->toolchain.targetTriple, + .standardLibrary = request->toolchain.standardLibrary, + .abiMode = request->toolchain.abiMode, + .objectFormat = request->toolchain.objectFormat, + .contractEpoch = "0.3", + .contractFlags = request->toolchain.contractFlags, + .toolchainFingerprint = request->toolchain.fingerprint, + .sourceFingerprint = kFingerprint.str()}}); + EXPECT_FALSE(hasError(preflightBuildRequest(*request))); + + request->prebuiltInputs.front().provenance.compilerBuildId = "different"; + EXPECT_TRUE(hasError(preflightBuildRequest(*request))); + + request->prebuiltInputs.front().sourceAvailable = true; + EXPECT_FALSE(hasError(preflightBuildRequest(*request))); + auto plan = createCompilePlan(*request, makeSourceBundle()); + ASSERT_TRUE(static_cast(plan)); + EXPECT_TRUE(plan->prebuiltInputs.empty()); +} + +TEST(BuildTest, RejectsProfileAndToolchainIdentityMismatch) { + auto request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + request->profile = "debug"; + EXPECT_TRUE(hasError(preflightBuildRequest(*request))); + + request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + request->toolchain.targetTriple = "different-target"; + EXPECT_TRUE(hasError(preflightBuildRequest(*request))); +} + +TEST(BuildTest, PublishedBuildRequiresFrozenAcirAndBindingLockBytes) { + TempOutputRoot output; + PublishFixture fixture(output.path()); + fixture.request().frozenAcirBytes.clear(); + EXPECT_TRUE(hasError(preflightBuildRequest(fixture.request()))); + + PublishFixture second(output.path()); + second.request().bindingLockBytes.clear(); + EXPECT_TRUE(hasError(preflightBuildRequest(second.request()))); +} + +TEST(BuildTest, FrontendProvenanceIsClosedAndContentAddressed) { + auto request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + EXPECT_FALSE(hasError(preflightBuildRequest(*request))); + + request->frontend.acpy.kind = ArtifactKind::Report; + EXPECT_TRUE(hasError(preflightBuildRequest(*request))); + + request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + request->frontend.sourceFiles.push_back( + request->frontend.sourceFiles.front()); + EXPECT_TRUE(hasError(preflightBuildRequest(*request))); +} + +TEST(BuildTest, RejectsNonCanonicalPathsAndSourceFingerprintMismatch) { + auto request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + request->includeRoots = {"../escape"}; + auto plan = createCompilePlan(*request, makeSourceBundle()); + EXPECT_FALSE(plan); + llvm::consumeError(plan.takeError()); + + request = makeBuildRequest(); + ASSERT_TRUE(static_cast(request)); + SourceBundle bundle = makeSourceBundle(); + bundle.files.back().fingerprint = kFingerprint.str(); + plan = createCompilePlan(*request, bundle); + EXPECT_FALSE(plan); + llvm::consumeError(plan.takeError()); +} + +TEST(BuildTest, ExactSecondBuildIsCacheHitAndUnequalInputMisses) { + TempOutputRoot output; + PublishFixture fixture(output.path()); + auto first = buildGeneratedModel(fixture.request()); + auto second = buildGeneratedModel(fixture.request()); + if (!first || !second) { + if (!first) + ADD_FAILURE() << llvm::toString(first.takeError()); + if (!second) + ADD_FAILURE() << llvm::toString(second.takeError()); + return; + } + EXPECT_FALSE(first->cacheHit); + EXPECT_TRUE(second->cacheHit); + EXPECT_EQ(first->buildFingerprint, second->buildFingerprint); + const std::string manifest = + readFile(first->buildDirectory + "/build-manifest.json"); + EXPECT_NE(manifest.find("architecture.py"), std::string::npos); + EXPECT_NE(manifest.find("input/model.acpy.json"), std::string::npos); + + fixture.request().frontend.pythonVersion = "CPython 3.13"; + auto third = buildGeneratedModel(fixture.request()); + ASSERT_TRUE(static_cast(third)); + EXPECT_FALSE(third->cacheHit); + EXPECT_NE(first->buildFingerprint, third->buildFingerprint); +} + +TEST(BuildTest, FailedStagePreservesPublishedBuildAndCurrentPointer) { + TempOutputRoot output; + PublishFixture fixture(output.path()); + auto first = buildGeneratedModel(fixture.request()); + if (!first) { + ADD_FAILURE() << llvm::toString(first.takeError()); + return; + } + const std::string pointerPath = output.path() + "/current.json"; + const std::string previousCurrent = readFile(pointerPath); + + BuildServices services = makeRealBuildServices(); + services.failurePoint = BuildFailurePoint::AfterLink; + auto failed = buildGeneratedModelForTesting(fixture.request(), services); + EXPECT_FALSE(failed); + llvm::consumeError(failed.takeError()); + EXPECT_EQ(readFile(pointerPath), previousCurrent); + EXPECT_FALSE( + readFile(first->buildDirectory + "/build-manifest.json").empty()); +} + +TEST(BuildTest, RejectsEveryEscapingOrNonCanonicalArtifactPath) { + for (llvm::StringRef path : {"", "/absolute", "../escape", "a/../b", "./file", + "a//b", "trailing/"}) { + auto normalized = normalizeArtifactPath(path); + EXPECT_FALSE(normalized) << path.str(); + llvm::consumeError(normalized.takeError()); + } + auto normalized = normalizeArtifactPath("reports/compile.txt"); + ASSERT_TRUE(static_cast(normalized)); + EXPECT_EQ(*normalized, "reports/compile.txt"); +} + +TEST(BuildTest, EveryInjectedBoundaryPreservesPublishedState) { + TempOutputRoot output; + PublishFixture fixture(output.path()); + auto baseline = buildGeneratedModel(fixture.request()); + if (!baseline) { + ADD_FAILURE() << llvm::toString(baseline.takeError()); + return; + } + const std::string pointerPath = output.path() + "/current.json"; + const std::string manifestPath = + baseline->buildDirectory + "/build-manifest.json"; + const std::string previousCurrent = readFile(pointerPath); + const std::string previousManifest = readFile(manifestPath); + const std::array failurePoints = {BuildFailurePoint::AfterInputValidation, + BuildFailurePoint::AfterSourceWrite, + BuildFailurePoint::AfterContractCheck, + BuildFailurePoint::AfterCompile, + BuildFailurePoint::AfterLink, + BuildFailurePoint::AfterFingerprintQuery, + BuildFailurePoint::AfterManifestWrite, + BuildFailurePoint::AfterImmutableRename, + BuildFailurePoint::BeforeCurrentRename}; + for (BuildFailurePoint failurePoint : failurePoints) { + BuildServices services = makeRealBuildServices(); + services.failurePoint = failurePoint; + auto failed = buildGeneratedModelForTesting(fixture.request(), services); + EXPECT_FALSE(failed); + if (!failed) + llvm::consumeError(failed.takeError()); + EXPECT_EQ(readFile(pointerPath), previousCurrent); + EXPECT_EQ(readFile(manifestPath), previousManifest); + } +} + +} // namespace +} // namespace acir::codegen diff --git a/components/agentic-circuit/unittests/CodeGen/CMakeLists.txt b/components/agentic-circuit/unittests/CodeGen/CMakeLists.txt new file mode 100644 index 00000000..39d15d58 --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/CMakeLists.txt @@ -0,0 +1,45 @@ +find_package(GTest CONFIG REQUIRED) +string(JOIN "|" ACIR_TEST_LLVM_INCLUDE_DIRS ${LLVM_INCLUDE_DIRS}) +string(JOIN "|" ACIR_TEST_LLVM_LINK_FLAGS ${ACIR_LLVM_LINK_FLAGS}) +set(ACIR_TEST_SANITIZER_FLAG "") +set(ACIR_TEST_RTTI_FLAG "") +if(CMAKE_CXX_FLAGS MATCHES "-fsanitize=address") + set(ACIR_TEST_SANITIZER_FLAG "-fsanitize=address") +elseif(CMAKE_CXX_FLAGS MATCHES "-fsanitize=undefined") + set(ACIR_TEST_SANITIZER_FLAG "-fsanitize=undefined") +endif() +if(NOT LLVM_ENABLE_RTTI) + set(ACIR_TEST_RTTI_FLAG "-fno-rtti") +endif() +add_executable(CodeGenTests + BuildTest.cpp + CodeGenTest.cpp + GeneratedModelCompileTest.cpp + GeneratedModelRuntimeTest.cpp + GeneratorTest.cpp + ModelPlanTest.cpp + QueueGraphPlanTest.cpp +) +target_include_directories(CodeGenTests PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/lib/CodeGen +) +target_link_libraries(CodeGenTests PRIVATE + ACIRCodeGen + gfsim + MLIRIndexDialect + MLIRParser + GTest::gtest_main +) +target_compile_definitions(CodeGenTests PRIVATE + ACSIM_VALID_TEST_FILE="${PROJECT_SOURCE_DIR}/test/ACSim/ops-valid.mlir" + ACSIM_PROCESS_CONTROL_FLOW_TEST_FILE="${PROJECT_SOURCE_DIR}/test/CodeGen/process-control-flow.mlir" + ACIR_TEST_BINARY_DIR="${PROJECT_BINARY_DIR}" + ACIR_TEST_CXX_COMPILER="${CMAKE_CXX_COMPILER}" + ACIR_TEST_LLVM_INCLUDE_DIRS="${ACIR_TEST_LLVM_INCLUDE_DIRS}" + ACIR_TEST_LLVM_LINK_FLAGS="${ACIR_TEST_LLVM_LINK_FLAGS}" + ACIR_TEST_RTTI_FLAG="${ACIR_TEST_RTTI_FLAG}" + ACIR_TEST_SANITIZER_FLAG="${ACIR_TEST_SANITIZER_FLAG}" + ACIR_TEST_SOURCE_DIR="${PROJECT_SOURCE_DIR}" +) +add_test(NAME CodeGenTests COMMAND CodeGenTests) diff --git a/components/agentic-circuit/unittests/CodeGen/CodeGenTest.cpp b/components/agentic-circuit/unittests/CodeGen/CodeGenTest.cpp new file mode 100644 index 00000000..878f2d19 --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/CodeGenTest.cpp @@ -0,0 +1,284 @@ +#include "acir/CodeGen/Emitter.h" +#include "acir/CodeGen/Manifest.h" + +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +// ── Fingerprint ─────────────────────────────────────────────────────── + +static Fingerprint repeatedFingerprint(char digit) { + return "sha256:" + std::string(64, digit); +} + +static bool hasError(llvm::Error error) { + if (!error) + return false; + llvm::consumeError(std::move(error)); + return true; +} + +static BuildManifest makeCompleteManifestFixture() { + BuildManifest manifest; + manifest.project = {"demo", "project:demo"}; + manifest.system = {"top", "system:top"}; + manifest.sourceFiles = { + {"src/generated/model.cpp", repeatedFingerprint('1')}}; + manifest.normalizedAcirSha256 = repeatedFingerprint('2'); + manifest.compiler = {"clang++", "clang-22.1.8", "arm64-apple-darwin"}; + manifest.passPipeline = {"acsim-verify", "acsim-emit-cxx"}; + manifest.providers = { + {"ac", repeatedFingerprint('3'), repeatedFingerprint('4')}}; + manifest.componentSpecializations = { + {"ac.Queue", repeatedFingerprint('5'), repeatedFingerprint('6')}}; + manifest.protocolIdentities = {{"ready_valid", repeatedFingerprint('7')}}; + manifest.artifacts = { + {"bin/model", ArtifactKind::Executable, repeatedFingerprint('8')}}; + manifest.validationGates = { + {"compile", ValidationStatus::Passed, std::nullopt}}; + manifest.buildProfile = "validated"; + manifest.instrumentationLayers = {"trace"}; + manifest.specializationInputs.push_back( + {"depth", "ui32", llvm::json::Value(int64_t{4})}); + manifest.buildFingerprint = repeatedFingerprint('f'); + return manifest; +} + +TEST(CodeGenManifestTest, FingerprintsUseNormativeSpelling) { + EXPECT_EQ(computeFingerprint("hello"), + "sha256:" + "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"); + EXPECT_TRUE(isValidFingerprint(computeFingerprint("hello"))); + EXPECT_FALSE(isValidFingerprint(std::string(64, '0'))); + EXPECT_FALSE(isValidFingerprint("sha256:" + std::string(63, '0'))); + EXPECT_FALSE(isValidFingerprint("sha256:" + std::string(64, 'A'))); +} + +TEST(CodeGenManifestTest, CanonicalManifestMatchesClosedSchemaShape) { + auto manifest = makeCompleteManifestFixture(); + EXPECT_FALSE(hasError(manifest.validate())); + + auto canonical = manifest.canonicalJson(); + if (!canonical) { + ADD_FAILURE() << llvm::toString(canonical.takeError()); + return; + } + + constexpr std::string_view expected = + R"json({"artifacts":[{"kind":"executable","path":"bin/model","sha256":"sha256:8888888888888888888888888888888888888888888888888888888888888888"}],"build_fingerprint":"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","build_profile":"validated","compiler":{"build_id":"clang-22.1.8","name":"clang++","toolchain_target":"arm64-apple-darwin"},"component_specializations":[{"canonical_name":"ac.Queue","schema_fingerprint":"sha256:5555555555555555555555555555555555555555555555555555555555555555","specialization_fingerprint":"sha256:6666666666666666666666666666666666666666666666666666666666666666"}],"contract_epoch":"0.3","instrumentation_layers":["trace"],"normalized_acir_sha256":"sha256:2222222222222222222222222222222222222222222222222222222222222222","pass_pipeline":["acsim-verify","acsim-emit-cxx"],"project":{"identity":"project:demo","name":"demo"},"protocol_identities":[{"fingerprint":"sha256:7777777777777777777777777777777777777777777777777777777777777777","name":"ready_valid"}],"providers":[{"implementation_fingerprint":"sha256:4444444444444444444444444444444444444444444444444444444444444444","namespace":"ac","schema_fingerprint":"sha256:3333333333333333333333333333333333333333333333333333333333333333"}],"schema":"agentic-circuit-build-manifest","source_files":[{"path":"src/generated/model.cpp","sha256":"sha256:1111111111111111111111111111111111111111111111111111111111111111"}],"specialization_inputs":[{"acir_type":"ui32","canonical_value":4,"name":"depth"}],"system":{"identity":"system:top","name":"top"},"validation_gates":[{"name":"compile","report_sha256":null,"status":"passed"}],"version":"0.1"})json"; + EXPECT_EQ(*canonical, expected); +} + +TEST(CodeGenManifestTest, ManifestRejectsMissingIdentityAndEscapingPath) { + auto missingIdentity = makeCompleteManifestFixture(); + missingIdentity.project.name.clear(); + EXPECT_TRUE(hasError(missingIdentity.validate())); + + auto escapingPath = makeCompleteManifestFixture(); + escapingPath.sourceFiles.front().path = "../escape.cpp"; + EXPECT_TRUE(hasError(escapingPath.validate())); +} + +TEST(CodeGenManifestTest, ManifestCanonicalizesSetLikeCollections) { + auto first = makeCompleteManifestFixture(); + first.instrumentationLayers = {"trace", "statistics"}; + first.providers.push_back( + {"ac.extra", repeatedFingerprint('9'), repeatedFingerprint('a')}); + + auto second = makeCompleteManifestFixture(); + second.instrumentationLayers = {"statistics", "trace"}; + second.providers = { + {"ac.extra", repeatedFingerprint('9'), repeatedFingerprint('a')}, + {"ac", repeatedFingerprint('3'), repeatedFingerprint('4')}}; + + auto firstJson = first.canonicalJson(); + auto secondJson = second.canonicalJson(); + if (!firstJson || !secondJson) { + if (!firstJson) + ADD_FAILURE() << llvm::toString(firstJson.takeError()); + if (!secondJson) + ADD_FAILURE() << llvm::toString(secondJson.takeError()); + return; + } + EXPECT_EQ(*firstJson, *secondJson); +} + +TEST(CodeGenManifestTest, BuildFingerprintUsesClosedVersionedPreimage) { + auto first = makeCompleteManifestFixture(); + auto second = makeCompleteManifestFixture(); + first.buildFingerprint.clear(); + second.buildFingerprint.clear(); + EXPECT_FALSE(hasError(first.finalizeBuildFingerprint())); + EXPECT_FALSE(hasError(second.finalizeBuildFingerprint())); + EXPECT_TRUE(isValidFingerprint(first.buildFingerprint)); + EXPECT_EQ(first.buildFingerprint, second.buildFingerprint); + + second.compiler.buildId = "clang-22.1.9"; + EXPECT_FALSE(hasError(second.finalizeBuildFingerprint())); + EXPECT_NE(first.buildFingerprint, second.buildFingerprint); +} + +// ── CppEmitter ──────────────────────────────────────────────────────── + +TEST(CodeGenEmitterTest, EmitsPragmaOnce) { + std::ostringstream os; + CppEmitter e(os); + e.emitPragmaOnce(); + EXPECT_EQ(os.str(), "#pragma once\n\n"); +} + +TEST(CodeGenEmitterTest, EmitsInclude) { + std::ostringstream os; + CppEmitter e(os); + e.emitInclude("string", true); + e.emitInclude("local.h"); + EXPECT_NE(os.str().find(""), std::string::npos); + EXPECT_NE(os.str().find("\"local.h\""), std::string::npos); +} + +TEST(CodeGenEmitterTest, EmitsNamespace) { + std::ostringstream os; + CppEmitter e(os); + e.beginNamespace("test"); + e.emitComment("hello"); + e.endNamespace(); + EXPECT_NE(os.str().find("namespace test"), std::string::npos); + EXPECT_NE(os.str().find("// hello"), std::string::npos); +} + +TEST(CodeGenEmitterTest, EmitsClassWithBase) { + std::ostringstream os; + CppEmitter e(os); + e.beginClass("Foo", "Bar"); + e.emitPublic(); + e.endClass(); + EXPECT_NE(os.str().find("class Foo : public Bar"), std::string::npos); + EXPECT_NE(os.str().find("public:"), std::string::npos); +} + +TEST(CodeGenEmitterTest, EmitsEnum) { + std::ostringstream os; + CppEmitter e(os); + e.emitEnum("Color", {"Red", "Green", "Blue"}); + EXPECT_NE(os.str().find("enum class Color"), std::string::npos); + EXPECT_NE(os.str().find("Red"), std::string::npos); + EXPECT_NE(os.str().find("Green"), std::string::npos); +} + +TEST(CodeGenEmitterTest, EmitsConstructor) { + std::ostringstream os; + CppEmitter e(os); + e.emitConstructor("Foo", {{"int", "x"}, {"int", "y"}}, {"x_(x)", "y_(y)"}, + " init();\n"); + EXPECT_NE(os.str().find("Foo(int x, int y)"), std::string::npos); + EXPECT_NE(os.str().find(": x_(x)"), std::string::npos); + EXPECT_NE(os.str().find("init()"), std::string::npos); +} + +TEST(CodeGenEmitterTest, ConstructorDeclarationOmitsInitializers) { + std::ostringstream os; + CppEmitter emitter(os); + emitter.emitConstructor("Foo", {{"int", "x"}}, {"x_(x)"}); + EXPECT_EQ(os.str(), "Foo(int x);\n"); +} + +TEST(CodeGenEmitterTest, EmitsMethod) { + std::ostringstream os; + CppEmitter e(os); + e.emitMethod("void", "run", {}, true, true, true); + auto s = os.str(); + EXPECT_NE(s.find("virtual void run() const override"), std::string::npos); +} + +TEST(CodeGenEmitterTest, EmitsSwitch) { + std::ostringstream os; + CppEmitter e(os); + e.emitSwitch("x"); + e.emitCase("1"); + e.emitBreak(); + e.emitDefault(); + e.emitBreak(); + e.endSwitch(); + EXPECT_NE(os.str().find("switch (x)"), std::string::npos); + EXPECT_NE(os.str().find("case 1:"), std::string::npos); + EXPECT_NE(os.str().find("default:"), std::string::npos); +} + +// ── Deterministic code generation ───────────────────────────────────── + +TEST(CodeGenGenTest, GenerateDispatchHeaderIsDenseAndDeterministic) { + std::vector entries = { + {1, "model.consumer"}, + {0, "model.producer"}, + }; + std::vector edges = {{1, 1}, {0, 1}, {0, 0}, {0, 1}}; + auto first = + generateDispatchHeader("generated::soc", "SocModel", entries, edges); + std::reverse(entries.begin(), entries.end()); + std::reverse(edges.begin(), edges.end()); + auto second = + generateDispatchHeader("generated::soc", "SocModel", entries, edges); + + constexpr std::string_view expected = R"(#pragma once + +#include "gfsim/dispatch.h" + +#include +#include + +namespace generated::soc { + +inline std::array +makeDispatchTable(SocModel &model) { + return { + gfsim::makeDispatchRow(&model.producer), + gfsim::makeDispatchRow(&model.consumer), + }; +} + +inline constexpr std::array kActivationOffsets = {0, 2, 3}; +inline constexpr std::array kActivationTargets = {0, 1, 1}; + +} // namespace generated::soc +)"; + EXPECT_EQ(first.content, expected); + EXPECT_EQ(first.content, second.content); + EXPECT_EQ(first.fingerprint, second.fingerprint); + EXPECT_NE(first.content.find("gfsim/dispatch.h"), std::string::npos); + EXPECT_NE(first.content.find("std::array"), + std::string::npos); + EXPECT_NE(first.content.find("makeDispatchTable(SocModel &model)"), + std::string::npos); + EXPECT_EQ(first.relativePath, "include/generated/soc/dispatch.h"); + size_t producer = first.content.find("model.producer"); + size_t consumer = first.content.find("model.consumer"); + ASSERT_NE(producer, std::string::npos); + ASSERT_NE(consumer, std::string::npos); + EXPECT_LT(producer, consumer); +} + +TEST(CodeGenGenTest, GenerateDispatchHeaderRejectsNonDenseIds) { + EXPECT_THROW(generateDispatchHeader("generated::soc", "SocModel", + {{1, "model.consumer"}}), + std::invalid_argument); +} + +TEST(CodeGenGenTest, GenerateDispatchHeaderRejectsInvalidActivationEdge) { + EXPECT_THROW(generateDispatchHeader("generated::soc", "SocModel", + {{0, "model.producer"}}, {{0, 1}}), + std::invalid_argument); +} + +} // namespace +} // namespace acir::codegen diff --git a/components/agentic-circuit/unittests/CodeGen/GeneratedModelCompileTest.cpp b/components/agentic-circuit/unittests/CodeGen/GeneratedModelCompileTest.cpp new file mode 100644 index 00000000..d8ddf2c1 --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/GeneratedModelCompileTest.cpp @@ -0,0 +1,198 @@ +#include "TestToolchain.h" +#include "acir/CodeGen/Generator.h" + +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Program.h" +#include "llvm/Support/raw_ostream.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +constexpr llvm::StringLiteral kFingerprint = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + +ModelPlan makeMinimalRunnablePlan() { + ModelPlan plan; + plan.modelSymbol = "minimal"; + plan.rootSymbol = "Top"; + plan.contractEpoch = "0.3"; + plan.frozenAcirFingerprint = kFingerprint.str(); + plan.bindingLockFingerprint = kFingerprint.str(); + plan.providerFingerprint = kFingerprint.str(); + plan.profileFingerprint = kFingerprint.str(); + plan.toolchainFingerprint = kFingerprint.str(); + plan.schemaSetFingerprint = kFingerprint.str(); + plan.timeDomains.push_back({"core", 2, 0, 1}); + ModulePlan module{.symbol = "Top", + .className = "Top_s0000000000000000", + .specializationFingerprint = kFingerprint.str()}; + ProcessPlan process{.symbol = "scalar", + .className = "scalar_s0000000000000000", + .specializationFingerprint = kFingerprint.str(), + .entryPc = "entry", + .fairnessWork = 4}; + PcStatePlan state{.ordinal = 0, .name = "entry"}; + state.operations.push_back(ConstantPlan{"left", "i32", 7}); + state.operations.push_back(ConstantPlan{"right", "i32", 5}); + state.operations.push_back( + ArithmeticPlan{"arith.addi", {"left", "right"}, {"sum"}, {"i32"}, {}}); + state.terminator = TerminatePlan{"success"}; + PcBlockPlan entryBlock{.ordinal = 0}; + entryBlock.operations.push_back(ConstantPlan{"condition", "i1", true}); + entryBlock.operations.push_back(ConstantPlan{"left", "i32", 7}); + entryBlock.operations.push_back(ConstantPlan{"right", "i32", 5}); + entryBlock.terminator = + ConditionalBranchPlan{"condition", 1, {"left"}, 2, {"right"}}; + PcBlockPlan trueBlock{.ordinal = 1, + .arguments = {{"selected_true", "i32"}}, + .terminator = TerminatePlan{"success"}}; + trueBlock.operations.push_back( + ArithmeticPlan{"arith.addi", + {"selected_true", "selected_true"}, + {"true_sum"}, + {"i32"}, + {}}); + PcBlockPlan falseBlock{.ordinal = 2, + .arguments = {{"selected_false", "i32"}}, + .terminator = TerminatePlan{"success"}}; + falseBlock.operations.push_back( + ArithmeticPlan{"arith.addi", + {"selected_false", "selected_false"}, + {"false_sum"}, + {"i32"}, + {}}); + state.blocks = {std::move(entryBlock), std::move(trueBlock), + std::move(falseBlock)}; + process.states.push_back(std::move(state)); + module.processes.push_back(std::move(process)); + plan.modules.push_back(std::move(module)); + plan.constructionOrder = {"Top.scalar"}; + plan.destructionOrder = {"Top.scalar"}; + plan.runtimeObjects.push_back({.objectId = 0, + .activationId = 0, + .targetSymbol = "Top::scalar", + .hierarchyPath = "Top.scalar", + .objectKind = RuntimeObjectKind::Process, + .workThunk = "scalar_work", + .xferThunk = "scalar_xfer", + .resetThunk = "scalar_reset", + .validateThunk = "scalar_validate"}); + return plan; +} + +llvm::Error writeBundle(llvm::StringRef root, const SourceBundle &bundle) { + for (const GeneratedFile &file : bundle.files) { + llvm::SmallString<256> path(root); + llvm::sys::path::append(path, file.relativePath); + llvm::SmallString<256> parent(path); + llvm::sys::path::remove_filename(parent); + if (std::error_code error = llvm::sys::fs::create_directories(parent)) + return llvm::createStringError(error, + "cannot create generated directory"); + std::error_code error; + llvm::raw_fd_ostream output(path, error); + if (error) + return llvm::createStringError(error, "cannot create generated file"); + output << file.content; + } + return llvm::Error::success(); +} + +std::string readFile(llvm::StringRef path) { + auto buffer = llvm::MemoryBuffer::getFile(path); + if (!buffer) + return {}; + return buffer.get()->getBuffer().str(); +} + +TEST(GeneratedModelCompileTest, + MinimalBundleCompilesLinksAndPrintsFingerprint) { + auto bundle = generateModelSources(makeMinimalRunnablePlan()); + if (!bundle) { + ADD_FAILURE() << llvm::toString(bundle.takeError()); + return; + } + + llvm::SmallString<256> temporaryRoot; + ASSERT_FALSE(llvm::sys::fs::createUniqueDirectory("acir-generated-model", + temporaryRoot)); + struct Cleanup { + llvm::SmallString<256> path; + ~Cleanup() { llvm::sys::fs::remove_directories(path); } + } cleanup{temporaryRoot}; + if (llvm::Error error = writeBundle(temporaryRoot, *bundle)) { + ADD_FAILURE() << llvm::toString(std::move(error)); + return; + } + + llvm::SmallString<256> executable(temporaryRoot); + llvm::sys::path::append(executable, "generated-model"); + llvm::SmallString<256> compileLog(temporaryRoot); + llvm::sys::path::append(compileLog, "compile.log"); + llvm::SmallString<256> generatedInclude(temporaryRoot); + llvm::sys::path::append(generatedInclude, "include"); + + std::vector ownedArguments = { + ACIR_TEST_CXX_COMPILER, "-std=c++20", "-I" + generatedInclude.str().str(), + "-I" ACIR_TEST_SOURCE_DIR "/include"}; + for (const std::string &include : test::llvmIncludeDirectories()) + ownedArguments.push_back("-I" + include); + if (llvm::StringRef(ACIR_TEST_RTTI_FLAG).size()) + ownedArguments.push_back(ACIR_TEST_RTTI_FLAG); + if (llvm::StringRef(ACIR_TEST_SANITIZER_FLAG).size()) + ownedArguments.push_back(ACIR_TEST_SANITIZER_FLAG); + for (const GeneratedFile &file : bundle->files) { + if (!llvm::StringRef(file.relativePath).ends_with(".cpp")) + continue; + llvm::SmallString<256> path(temporaryRoot); + llvm::sys::path::append(path, file.relativePath); + ownedArguments.push_back(path.str().str()); + } + ownedArguments.push_back(ACIR_TEST_BINARY_DIR "/lib/gfsim/libgfsim.a"); + ownedArguments.push_back(ACIR_TEST_BINARY_DIR + "/lib/Bindings/libACIRBindings.a"); + for (const std::string &flag : test::llvmLinkerFlags()) + ownedArguments.push_back(flag); + ownedArguments.push_back("-o"); + ownedArguments.push_back(executable.str().str()); + llvm::SmallVector arguments; + for (const std::string &argument : ownedArguments) + arguments.push_back(argument); + std::array, 3> redirects = { + std::nullopt, compileLog.str(), compileLog.str()}; + const int compileStatus = llvm::sys::ExecuteAndWait( + ACIR_TEST_CXX_COMPILER, arguments, std::nullopt, redirects); + ASSERT_EQ(compileStatus, 0) << readFile(compileLog); + + llvm::SmallString<256> queryLog(temporaryRoot); + llvm::sys::path::append(queryLog, "query.log"); + const std::array queryArguments = {executable.str(), + "--build-fingerprint"}; + redirects = {std::nullopt, queryLog.str(), queryLog.str()}; + const int queryStatus = llvm::sys::ExecuteAndWait(executable, queryArguments, + std::nullopt, redirects); + ASSERT_EQ(queryStatus, 0) << readFile(queryLog); + EXPECT_EQ(readFile(queryLog), bundle->buildFingerprint + "\n"); + + llvm::SmallString<256> runLog(temporaryRoot); + llvm::sys::path::append(runLog, "run.log"); + const std::array runArguments = {executable.str()}; + redirects = {std::nullopt, runLog.str(), runLog.str()}; + const int runStatus = llvm::sys::ExecuteAndWait(executable, runArguments, + std::nullopt, redirects); + EXPECT_EQ(runStatus, 0) << readFile(runLog); +} + +} // namespace +} // namespace acir::codegen diff --git a/components/agentic-circuit/unittests/CodeGen/GeneratedModelRuntimeTest.cpp b/components/agentic-circuit/unittests/CodeGen/GeneratedModelRuntimeTest.cpp new file mode 100644 index 00000000..77b17e24 --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/GeneratedModelRuntimeTest.cpp @@ -0,0 +1,118 @@ +#include "gfsim/object.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include + +namespace gfsim { +namespace { + +class OneShotObject final : public SimObject { +public: + OneShotObject(ObjectId id, bool active) + : SimObject(ObjectKind::Compute, "object", id), active_(active) {} + + void doWork(Epoch) override { + ++workInvocations; + if (active_ && !committed_) + pending_ = true; + } + void doXfer(Epoch) override { + if (pending_) { + committed_ = true; + pending_ = false; + } + } + bool hasPendingCommit() const override { return pending_; } + + size_t workInvocations = 0; + bool committed() const { return committed_; } + +private: + bool active_ = false; + bool pending_ = false; + bool committed_ = false; +}; + +class DomainTickObject final : public SimObject { +public: + DomainTickObject(ObjectId id, SimSystem &system) + : SimObject(ObjectKind::Process, "domain_tick", id), system_(system) {} + + void doWork(Epoch epoch) override { + ++workInvocations; + EXPECT_TRUE(system_.scheduleEvent({{epoch.time + 1, 0}, id(), 0, 0})); + } + + size_t workInvocations = 0; + +private: + SimSystem &system_; +}; + +struct RuntimeObservation { + size_t activeWorkInvocations = 0; + size_t idleWorkInvocations = 0; + size_t activationEdges = 0; + bool committedResult = false; +}; + +RuntimeObservation runActiveFrontier(size_t idleCount) { + SimSystem system("sparse"); + std::vector> objects; + std::vector rows; + objects.reserve(idleCount + 1); + rows.reserve(idleCount + 1); + for (size_t index = 0; index <= idleCount; ++index) { + objects.push_back(std::make_unique(index, index == 0)); + rows.push_back(makeDispatchRow(objects.back().get())); + } + std::vector offsets(idleCount + 2, 1); + offsets.front() = 0; + const std::array targets = {0}; + EXPECT_TRUE(system.setDispatchTable(rows)); + EXPECT_TRUE(system.setActivationPlan(offsets, targets)); + EXPECT_TRUE(system.scheduleWork(0, {0, 0})); + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed); + + size_t idleInvocations = 0; + for (size_t index = 1; index < objects.size(); ++index) + idleInvocations += objects[index]->workInvocations; + return {objects.front()->workInvocations, idleInvocations, targets.size(), + objects.front()->committed()}; +} + +TEST(GeneratedModelRuntimeTest, PermanentlyIdleObjectsDoNotIncreaseHotWork) { + const RuntimeObservation baseline = runActiveFrontier(0); + const RuntimeObservation sparse = runActiveFrontier(4096); + EXPECT_EQ(sparse.activeWorkInvocations, baseline.activeWorkInvocations); + EXPECT_EQ(sparse.idleWorkInvocations, 0u); + EXPECT_EQ(sparse.activationEdges, baseline.activationEdges); + EXPECT_EQ(sparse.committedResult, baseline.committedResult); +} + +TEST(GeneratedModelRuntimeTest, GeneratedDomainLimitStopsBeforeExcessWork) { + SimSystem system("generated"); + DomainTickObject object(0, system); + const std::array rows = {makeDispatchRow(&object)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + const std::array domains = {TimeDomainRuntime{"core", 2, 1, 1}}; + ASSERT_TRUE(system.setTimeDomains(domains)); + RuntimeLimits limits; + limits.maxDomainCycles = {{"core", 2}}; + ASSERT_TRUE(system.setRuntimeLimits(limits)); + + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Incomplete); + EXPECT_EQ(result.diagnosticCode, "max_domain_cycles_reached"); + EXPECT_EQ(result.finalEpoch, (Epoch{5, 0})); + EXPECT_EQ(result.domainCycles.at("core"), 2u); + EXPECT_EQ(object.workInvocations, 5u); +} + +} // namespace +} // namespace gfsim diff --git a/components/agentic-circuit/unittests/CodeGen/GeneratorTest.cpp b/components/agentic-circuit/unittests/CodeGen/GeneratorTest.cpp new file mode 100644 index 00000000..561ad636 --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/GeneratorTest.cpp @@ -0,0 +1,591 @@ +#include "acir/CodeGen/Generator.h" +#include "acir/Dialect/ACSim/ACSimDialect.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Index/IR/IndexDialect.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "llvm/Support/Error.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +bool hasError(llvm::Error error) { + if (!error) + return false; + llvm::consumeError(std::move(error)); + return true; +} + +llvm::Expected fixturePlan(mlir::MLIRContext &context) { + context + .loadDialect(); + auto file = + mlir::parseSourceFile(ACSIM_VALID_TEST_FILE, &context); + if (!file) + return llvm::createStringError("failed to parse canonical ACSim fixture"); + return buildModelPlan(*file); +} + +llvm::Expected processControlFlowPlan(mlir::MLIRContext &context) { + context + .loadDialect(); + auto file = mlir::parseSourceFile( + ACSIM_PROCESS_CONTROL_FLOW_TEST_FILE, &context); + if (!file) + return llvm::createStringError("failed to parse process CFG fixture"); + return buildModelPlan(*file); +} + +llvm::Expected reusableModulePlan(mlir::MLIRContext &context) { + context.loadDialect(); + auto file = mlir::parseSourceFile( + ACIR_TEST_SOURCE_DIR "/test/ACSim/reusable-modules.mlir", &context); + if (!file) + return llvm::createStringError("failed to parse reusable module fixture"); + return buildModelPlan(*file); +} + +const GeneratedFile *findFile(const SourceBundle &bundle, + llvm::StringRef path) { + auto found = std::find_if( + bundle.files.begin(), bundle.files.end(), + [&](const GeneratedFile &file) { return file.relativePath == path; }); + return found == bundle.files.end() ? nullptr : &*found; +} + +TEST(GeneratorTest, EmitsExactOrderedFileSetAndTypedOwnership) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto bundle = generateModelSources(*plan); + if (!bundle) { + ADD_FAILURE() << llvm::toString(bundle.takeError()); + return; + } + + std::vector paths; + for (const GeneratedFile &file : bundle->files) + paths.push_back(file.relativePath); + const std::vector expected = { + "include/generated/dispatch.h", + "include/generated/model.h", + "include/generated/modules/Top_s2100000000000000.h", + "include/generated/processes/tick_s2300000000000000.h", + "src/generated/main.cpp", + "src/generated/model.cpp", + "src/generated/modules/Top_s2100000000000000.cpp", + "src/generated/processes/tick_s2300000000000000.cpp"}; + EXPECT_EQ(paths, expected); + + const GeneratedFile *header = + findFile(*bundle, "include/generated/modules/Top_s2100000000000000.h"); + const GeneratedFile *source = + findFile(*bundle, "src/generated/modules/Top_s2100000000000000.cpp"); + ASSERT_NE(header, nullptr); + ASSERT_NE(source, nullptr); + EXPECT_NE(header->content.find( + "class Top_s2100000000000000 final : public gfsim::Module"), + std::string::npos); + EXPECT_NE(header->content.find("gfsim::Fifo fifo_;"), std::string::npos); + EXPECT_NE(header->content.find("std::array lanes_;"), + std::string::npos); + EXPECT_NE(source->content.find("fifo_(\"fifo\", nextObjectId++, this)"), + std::string::npos); + EXPECT_NE( + source->content.find("gfsim::Fifo(\"lanes[0]\", nextObjectId++, this)"), + std::string::npos); + EXPECT_NE( + source->content.find("tick_(\"tick\", nextObjectId++, this, lanes_[0])"), + std::string::npos); + EXPECT_NE(source->content.find("attachChild(fifo_)"), std::string::npos); + EXPECT_NE(header->content.find("static_assert(gfsim::StatefulModel<"), + std::string::npos); + EXPECT_NE(header->content.find("decltype(auto) out_port()"), + std::string::npos); + EXPECT_NE(header->content.find("decltype(auto) memory()"), std::string::npos); + EXPECT_NE(header->content.find("decltype(auto) out()"), std::string::npos); + EXPECT_NE( + source->content.find("bindStatic(lanes_[0].output(), lanes_[1].input())"), + std::string::npos); + EXPECT_NE(source->content.find( + "bindStatic(lanes_[0].initiator(), lanes_[1].target())"), + std::string::npos); + EXPECT_NE(source->content.find("return gfsim::is_ready(gfsim::is_ready())"), + std::string::npos); + EXPECT_FALSE(hasError(validateSourceBundle(*plan, *bundle))); +} + +TEST(GeneratorTest, EmitsReusableNestedModulesWithContextDenseIds) { + mlir::MLIRContext context; + auto plan = reusableModulePlan(context); + ASSERT_TRUE(static_cast(plan)); + + auto bundle = generateModelSources(*plan); + if (!bundle) { + ADD_FAILURE() << llvm::toString(bundle.takeError()); + return; + } + const GeneratedFile *top = + findFile(*bundle, "src/generated/modules/Top_sa000000000000000.cpp"); + const GeneratedFile *leaf = + findFile(*bundle, "src/generated/modules/Leaf_s8000000000000000.cpp"); + const GeneratedFile *dispatch = + findFile(*bundle, "include/generated/dispatch.h"); + ASSERT_NE(top, nullptr); + ASSERT_NE(leaf, nullptr); + ASSERT_NE(dispatch, nullptr); + EXPECT_NE(top->content.find("left_(\"left\", gfsim::kInvalidObjectId, this, " + "nextObjectId)"), + std::string::npos); + EXPECT_NE(top->content.find("for (auto &element0 : right_)"), + std::string::npos); + EXPECT_NE(leaf->content.find("child_(\"child\", nextObjectId++, this)"), + std::string::npos); + EXPECT_NE(dispatch->content.find("model.top_.left_.child_"), + std::string::npos); + EXPECT_NE(dispatch->content.find("model.top_.right_[1].pulse_"), + std::string::npos); +} + +TEST(GeneratorTest, RecursivelyAttachesMultidimensionalArrays) { + mlir::MLIRContext context; + context.loadDialect(); + auto file = mlir::parseSourceFile( + ACIR_TEST_SOURCE_DIR "/test/CodeGen/multidimensional-array.mlir", + &context); + ASSERT_TRUE(static_cast(file)); + auto plan = buildModelPlan(*file); + ASSERT_TRUE(static_cast(plan)); + + auto bundle = generateModelSources(*plan); + ASSERT_TRUE(static_cast(bundle)); + const GeneratedFile *source = + findFile(*bundle, "src/generated/modules/Top_s2000000000000000.cpp"); + ASSERT_NE(source, nullptr); + EXPECT_NE(source->content.find("for (auto &element0 : counters_)"), + std::string::npos); + EXPECT_NE(source->content.find("for (auto &element1 : element0)"), + std::string::npos); + EXPECT_NE(source->content.find("attachChild(element1)"), std::string::npos); +} + +TEST(GeneratorTest, RepeatedGenerationIsByteIdentical) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto first = generateModelSources(*plan); + auto second = generateModelSources(*plan); + ASSERT_TRUE(static_cast(first)); + ASSERT_TRUE(static_cast(second)); + EXPECT_EQ(first->buildFingerprint, second->buildFingerprint); + ASSERT_EQ(first->files.size(), second->files.size()); + for (size_t index = 0; index < first->files.size(); ++index) { + EXPECT_EQ(first->files[index].relativePath, + second->files[index].relativePath); + EXPECT_EQ(first->files[index].content, second->files[index].content); + EXPECT_EQ(first->files[index].fingerprint, + second->files[index].fingerprint); + } +} + +TEST(GeneratorTest, SourceIdentityChangesWithGeneratedTopology) { + mlir::MLIRContext context; + auto firstPlan = fixturePlan(context); + ASSERT_TRUE(static_cast(firstPlan)); + ModelPlan secondPlan = *firstPlan; + ASSERT_FALSE(secondPlan.activationEdges.empty()); + secondPlan.activationEdges.erase(secondPlan.activationEdges.begin()); + + auto first = generateModelSources(*firstPlan); + auto second = generateModelSources(secondPlan); + ASSERT_TRUE(static_cast(first)); + ASSERT_TRUE(static_cast(second)); + EXPECT_NE(first->buildFingerprint, second->buildFingerprint); +} + +TEST(GeneratorTest, SourceContractRejectsRehashedMutationAndForbiddenTokens) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto bundle = generateModelSources(*plan); + ASSERT_TRUE(static_cast(bundle)); + + SourceBundle mutated = *bundle; + mutated.files.back().content.append("// harmless mutation\n"); + mutated.files.back().fingerprint = + computeFingerprint(mutated.files.back().content); + EXPECT_TRUE(hasError(validateSourceBundle(*plan, mutated))); + + SourceBundle forbidden = *bundle; + forbidden.files.back().content.append("// dynamic_cast\n"); + forbidden.files.back().fingerprint = + computeFingerprint(forbidden.files.back().content); + auto error = validateSourceBundle(*plan, forbidden); + if (!error) { + ADD_FAILURE() << "forbidden generated token was accepted"; + return; + } + EXPECT_NE(llvm::toString(std::move(error)).find("forbidden token"), + std::string::npos); +} + +TEST(GeneratorTest, RejectsExecutableCppInStructuredMetadata) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + plan->bindings[1].cppSymbol = "gfsim::Fifo; system(\"bad\")"; + + auto bundle = generateModelSources(*plan); + ASSERT_FALSE(bundle); + EXPECT_TRUE(hasError(bundle.takeError())); +} + +TEST(GeneratorTest, RejectsExecutableThunkInStructuredMetadata) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + plan->bindings[1].entryPoints.work = "fifo_work; inject()"; + + auto bundle = generateModelSources(*plan); + ASSERT_FALSE(bundle); + EXPECT_TRUE(hasError(bundle.takeError())); +} + +TEST(GeneratorTest, EmitsClosedEnumPcProcessWithoutRawFrames) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto bundle = generateModelSources(*plan); + if (!bundle) { + ADD_FAILURE() << llvm::toString(bundle.takeError()); + return; + } + + const GeneratedFile *header = + findFile(*bundle, "include/generated/processes/tick_s2300000000000000.h"); + const GeneratedFile *source = + findFile(*bundle, "src/generated/processes/tick_s2300000000000000.cpp"); + ASSERT_NE(header, nullptr); + ASSERT_NE(source, nullptr); + EXPECT_NE(header->content.find("enum class Pc : uint8_t"), std::string::npos); + EXPECT_NE(header->content.find("kFairnessWork = 8"), std::string::npos); + EXPECT_NE(header->content.find("committed_counter_"), std::string::npos); + EXPECT_NE(header->content.find("proposed_counter_"), std::string::npos); + EXPECT_NE(source->content.find("switch (static_cast(pc))"), + std::string::npos); + EXPECT_NE(source->content.find("ProcessStep::continueAt"), std::string::npos); + EXPECT_NE(source->content.find("ProcessStep::suspendAt"), std::string::npos); + EXPECT_NE(source->content.find("ProcessStep::terminate"), std::string::npos); + EXPECT_NE(source->content.find("ProcessStep::fail(\"invalid_process_pc\")"), + std::string::npos); + EXPECT_EQ(source->content.find("std::function"), std::string::npos); + EXPECT_EQ(source->content.find("co_await"), std::string::npos); +} + +TEST(GeneratorTest, EmitsTypedLocalDispatchForMultiBlockProcess) { + mlir::MLIRContext context; + auto plan = processControlFlowPlan(context); + ASSERT_TRUE(static_cast(plan)); + auto bundle = generateModelSources(*plan); + if (!bundle) { + ADD_FAILURE() << llvm::toString(bundle.takeError()); + return; + } + auto source = std::find_if(bundle->files.begin(), bundle->files.end(), + [](const GeneratedFile &file) { + return file.relativePath.starts_with( + "src/generated/processes/"); + }); + auto header = std::find_if(bundle->files.begin(), bundle->files.end(), + [](const GeneratedFile &file) { + return file.relativePath.starts_with( + "include/generated/processes/"); + }); + ASSERT_NE(header, bundle->files.end()); + ASSERT_NE(source, bundle->files.end()); + EXPECT_NE( + header->content.find("inline gfsim::ProcessWake impl_wake_next_delta"), + std::string::npos); + EXPECT_NE(source->content.find("enum class Block_entry"), std::string::npos); + EXPECT_NE(source->content.find("std::optional<"), std::string::npos); + EXPECT_NE(source->content.find("b1_arg0"), std::string::npos); + EXPECT_NE(source->content.find("if ("), std::string::npos); + EXPECT_NE(source->content.find("block1_value0 + block1_value0"), + std::string::npos); + EXPECT_EQ(source->content.find("std::function"), std::string::npos); + EXPECT_EQ(source->content.find("coroutine"), std::string::npos); +} + +TEST(GeneratorTest, EmitsSortedExactTimeDomainRuntimeMetadata) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + plan->timeDomains = {{"memory", 4, 0, 2}, {"core", 2, 1, 1}}; + std::sort(plan->timeDomains.begin(), plan->timeDomains.end(), + [](const TimeDomainPlan &left, const TimeDomainPlan &right) { + return left.name < right.name; + }); + + auto bundle = generateModelSources(*plan); + ASSERT_TRUE(static_cast(bundle)) << llvm::toString(bundle.takeError()); + auto header = std::ranges::find(bundle->files, "include/generated/model.h", + &GeneratedFile::relativePath); + ASSERT_NE(header, bundle->files.end()); + EXPECT_NE( + header->content.find("inline const std::arraycontent.find("TimeDomainRuntime{\"core\", 2, 1, 1}"), + std::string::npos); + EXPECT_NE(header->content.find("TimeDomainRuntime{\"memory\", 4, 0, 2}"), + std::string::npos); +} + +TEST(GeneratorTest, EmitsManifestAwareMainWithoutPythonOrMlirDependencies) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto bundle = generateModelSources(*plan); + ASSERT_TRUE(static_cast(bundle)) << llvm::toString(bundle.takeError()); + + auto main = std::ranges::find(bundle->files, "src/generated/main.cpp", + &GeneratedFile::relativePath); + auto model = std::ranges::find(bundle->files, "include/generated/model.h", + &GeneratedFile::relativePath); + ASSERT_NE(main, bundle->files.end()); + ASSERT_NE(model, bundle->files.end()); + EXPECT_NE(main->content.find("--run-manifest"), std::string::npos); + EXPECT_NE(main->content.find("--run-result-stage"), std::string::npos); + EXPECT_NE(main->content.find("gfsim::loadRunManifest"), std::string::npos); + EXPECT_NE(main->content.find("gfsim::runGeneratedModel"), std::string::npos); + EXPECT_NE(model->content.find("void configure(const gfsim::RuntimeLimits"), + std::string::npos); + EXPECT_NE(model->content.find("gfsim::TerminationResult run()"), + std::string::npos); + EXPECT_NE(model->content.find("std::vector statistics"), + std::string::npos); + EXPECT_NE(model->content.find( + "std::span observations"), + std::string::npos); + EXPECT_EQ(main->content.find("Python"), std::string::npos); + EXPECT_EQ(main->content.find("mlir"), std::string::npos); +} + +TEST(GeneratorTest, EmitsStaticTypedTraceOwnerInjection) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + ASSERT_FALSE(plan->runtimeObjects.empty()); + plan->runtimeObjects.front().traceOwner = true; + + auto bundle = generateModelSources(*plan); + ASSERT_TRUE(static_cast(bundle)) << llvm::toString(bundle.takeError()); + const GeneratedFile *header = findFile(*bundle, "include/generated/model.h"); + const GeneratedFile *source = findFile(*bundle, "src/generated/model.cpp"); + ASSERT_NE(header, nullptr); + ASSERT_NE(source, nullptr); + EXPECT_NE(header->content.find("bool loadTrace(gfsim::PtoTraceDocument"), + std::string::npos); + EXPECT_NE(source->content.find( + "return top_.fifo_.loadDocument(std::move(document));"), + std::string::npos); +} + +TEST(GeneratorTest, EmitsClosedTraceOwnerCardinalityFailures) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + + auto withoutOwner = generateModelSources(*plan); + ASSERT_TRUE(static_cast(withoutOwner)); + const GeneratedFile *emptySource = + findFile(*withoutOwner, "src/generated/model.cpp"); + ASSERT_NE(emptySource, nullptr); + EXPECT_NE(emptySource->content.find("return document.records.empty();"), + std::string::npos); + + ASSERT_GE(plan->runtimeObjects.size(), 2u); + plan->runtimeObjects[0].traceOwner = true; + plan->runtimeObjects[1].traceOwner = true; + auto multipleOwners = generateModelSources(*plan); + ASSERT_TRUE(static_cast(multipleOwners)); + const GeneratedFile *multipleSource = + findFile(*multipleOwners, "src/generated/model.cpp"); + ASSERT_NE(multipleSource, nullptr); + EXPECT_NE(multipleSource->content.find( + "bool Model::loadTrace(gfsim::PtoTraceDocument document) {\n" + " return false;\n"), + std::string::npos); +} + +TEST(GeneratorTest, RejectsMismatchedMultiBlockSuccessorType) { + mlir::MLIRContext context; + auto plan = processControlFlowPlan(context); + ASSERT_TRUE(static_cast(plan)); + auto &branch = std::get(plan->modules.front() + .processes.front() + .states.front() + .blocks.front() + .terminator); + branch.trueArguments.front() = branch.condition; + + auto bundle = generateModelSources(*plan); + ASSERT_FALSE(bundle); + EXPECT_NE(llvm::toString(bundle.takeError()).find("ACLOWER-PROCESS-STATE"), + std::string::npos); +} + +TEST(GeneratorTest, RejectsMultiBlockSuccessorValueOutsideSourceBlock) { + mlir::MLIRContext context; + auto plan = processControlFlowPlan(context); + ASSERT_TRUE(static_cast(plan)); + auto &branch = std::get(plan->modules.front() + .processes.front() + .states.front() + .blocks.front() + .terminator); + branch.trueArguments.front() = plan->modules.front() + .processes.front() + .states.front() + .blocks[1] + .arguments.front() + .name; + + auto bundle = generateModelSources(*plan); + ASSERT_FALSE(bundle); + EXPECT_NE(llvm::toString(bundle.takeError()).find("ACLOWER-PROCESS-STATE"), + std::string::npos); +} + +TEST(GeneratorTest, EmitsTypedScalarOperationsWithoutRuntimeHelpers) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto &operations = plan->modules[0].processes[0].states[0].operations; + operations.insert(operations.begin(), + ConstantPlan{"constant_value", "i32", 7}); + operations.insert(operations.begin() + 1, + ArithmeticPlan{"arith.addi", + {"constant_value", "constant_value"}, + {"sum"}, + {"i32"}, + {}}); + + auto bundle = generateModelSources(*plan); + ASSERT_TRUE(static_cast(bundle)); + const GeneratedFile *source = + findFile(*bundle, "src/generated/processes/tick_s2300000000000000.cpp"); + ASSERT_NE(source, nullptr); + EXPECT_NE(source->content.find("std::int32_t constant_value = 7;"), + std::string::npos); + EXPECT_NE( + source->content.find("auto sum = (constant_value + constant_value);"), + std::string::npos); + EXPECT_EQ(source->content.find("acsim_generated::arith_addi"), + std::string::npos); +} + +TEST(GeneratorTest, ResolvesStaticTypeTemplateArgumentsThroughTypePlan) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto &binding = plan->bindings[1]; + binding.cppSymbol = "gfsim::FifoTemplate"; + binding.parameters.push_back( + {.name = "T", + .acirType = "!ac.static_type", + .cppType = "cpp_bool", + .canonicalValue = "cpp_bool", + .ordinal = 0, + .mapping = ParameterMappingKind::TemplateArgument}); + + auto bundle = generateModelSources(*plan); + ASSERT_TRUE(static_cast(bundle)); + const GeneratedFile *header = + findFile(*bundle, "include/generated/modules/Top_s2100000000000000.h"); + ASSERT_NE(header, nullptr); + EXPECT_NE(header->content.find("gfsim::FifoTemplate fifo_"), + std::string::npos); + EXPECT_EQ(header->content.find("gfsim::FifoTemplate<\"cpp_bool\">"), + std::string::npos); +} + +TEST(GeneratorTest, RejectsProcessValueFromAnotherPc) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto &store = std::get( + plan->modules[0].processes[0].states[0].operations[1]); + store.sourceValue = "value_from_another_pc"; + + auto bundle = generateModelSources(*plan); + ASSERT_FALSE(bundle); + EXPECT_NE(llvm::toString(bundle.takeError()).find("ACLOWER-PROCESS-STATE"), + std::string::npos); +} + +TEST(GeneratorTest, RejectsProcessEffectMismatch) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto &operations = plan->modules[0].processes[0].states[2].operations; + auto found = std::find_if(operations.begin(), operations.end(), [](auto &op) { + return std::holds_alternative(op); + }); + ASSERT_NE(found, operations.end()); + auto &call = std::get(*found); + call.callee = "stateful"; + + auto bundle = generateModelSources(*plan); + ASSERT_FALSE(bundle); + EXPECT_NE(llvm::toString(bundle.takeError()).find("ACLOWER-PROCESS-STATE"), + std::string::npos); +} + +TEST(GeneratorTest, RejectsProcessSuspendWithoutExactWake) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto &suspend = + std::get(plan->modules[0].processes[0].states[1].terminator); + suspend.wakeValue = "missing_wake"; + + auto bundle = generateModelSources(*plan); + ASSERT_FALSE(bundle); + EXPECT_NE(llvm::toString(bundle.takeError()).find("ACLOWER-PROCESS-STATE"), + std::string::npos); +} + +TEST(GeneratorTest, EmitsDenseDispatchAndCanonicalActivation) { + mlir::MLIRContext context; + auto plan = fixturePlan(context); + ASSERT_TRUE(static_cast(plan)); + auto bundle = generateModelSources(*plan); + ASSERT_TRUE(static_cast(bundle)); + const GeneratedFile *dispatch = + findFile(*bundle, "include/generated/dispatch.h"); + ASSERT_NE(dispatch, nullptr); + + EXPECT_NE(dispatch->content.find("std::array"), + std::string::npos); + EXPECT_LT(dispatch->content.find("makeDispatchRow(&model.top_.fifo_)"), + dispatch->content.find("makeDispatchRow(&model.top_.lanes_[0])")); + EXPECT_LT(dispatch->content.find("makeDispatchRow(&model.top_.lanes_[1])"), + dispatch->content.find("makeDispatchRow(&model.top_.tick_)")); + EXPECT_NE(dispatch->content.find("kActivationOffsets"), std::string::npos); + EXPECT_NE(dispatch->content.find("{0, 1, 4, 5, 6}"), std::string::npos); + EXPECT_NE(dispatch->content.find("kActivationTargets"), std::string::npos); + EXPECT_NE(dispatch->content.find("{0, 1, 2, 3, 2, 3}"), std::string::npos); +} + +} // namespace +} // namespace acir::codegen diff --git a/components/agentic-circuit/unittests/CodeGen/ModelPlanTest.cpp b/components/agentic-circuit/unittests/CodeGen/ModelPlanTest.cpp new file mode 100644 index 00000000..f628a4ba --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/ModelPlanTest.cpp @@ -0,0 +1,323 @@ +#include "acir/CodeGen/ModelPlan.h" +#include "acir/Dialect/ACSim/ACSimDialect.h" + +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Index/IR/IndexDialect.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/MemoryBuffer.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include + +namespace acir::codegen { +namespace { + +void loadACSimDialects(mlir::MLIRContext &context) { + context + .loadDialect(); +} + +bool hasError(llvm::Error error) { + if (!error) + return false; + llvm::consumeError(std::move(error)); + return true; +} + +std::string stablePlanSummary(const ModelPlan &plan) { + std::ostringstream output; + output << plan.modelSymbol << '|' << plan.rootSymbol << '|' + << plan.frozenAcirFingerprint << '\n'; + for (const TypePlan &type : plan.types) + output << type.symbol << '|' << static_cast(type.kind) << '|' + << type.cppType << '|' << type.fingerprint << '\n'; + for (const BindingPlan &binding : plan.bindings) + output << binding.symbol << '|' << binding.cppSymbol << '|' + << binding.recordFingerprint << '\n'; + for (const ModulePlan &module : plan.modules) { + output << module.symbol << '|' << module.className << '|' + << module.placements.size() << '|' << module.projections.size() + << '\n'; + for (const ProcessPlan &process : module.processes) { + output << process.symbol << '|' << process.entryPc << '|' + << process.fairnessWork << '\n'; + for (const PcStatePlan &state : process.states) + output << state.ordinal << '|' << state.name << '|' + << state.operations.size() << '|' << state.terminator.index() + << '\n'; + } + } + for (const RuntimeObjectPlan &object : plan.runtimeObjects) + output << object.objectId << '|' << object.targetSymbol << '|' + << object.hierarchyPath << '\n'; + for (const ActivationEdgePlan &edge : plan.activationEdges) + output << edge.sourceId << "->" << edge.targetId << '\n'; + return output.str(); +} + +TEST(ModelPlanTest, ExtractsClosedIdentitiesTypesAndDenseRuntimePlan) { + mlir::MLIRContext context; + loadACSimDialects(context); + auto file = + mlir::parseSourceFile(ACSIM_VALID_TEST_FILE, &context); + ASSERT_TRUE(file); + + auto plan = buildModelPlan(*file); + if (!plan) { + ADD_FAILURE() << llvm::toString(plan.takeError()); + return; + } + + EXPECT_EQ(plan->modelSymbol, "demo"); + EXPECT_EQ(plan->rootSymbol, "Top"); + EXPECT_EQ(plan->contractEpoch, "0.3"); + EXPECT_EQ(plan->frozenAcirFingerprint, + "sha256:" + "0000000000000000000000000000000000000000000000000000000000000001"); + EXPECT_EQ(plan->bindingLockFingerprint, + "sha256:" + "0000000000000000000000000000000000000000000000000000000000000002"); + EXPECT_EQ(plan->providerFingerprint, + "sha256:" + "0000000000000000000000000000000000000000000000000000000000000003"); + EXPECT_EQ(plan->profileFingerprint, + "sha256:" + "0000000000000000000000000000000000000000000000000000000000000004"); + EXPECT_EQ(plan->toolchainFingerprint, + "sha256:" + "0000000000000000000000000000000000000000000000000000000000000005"); + EXPECT_EQ(plan->schemaSetFingerprint, + "sha256:" + "0000000000000000000000000000000000000000000000000000000000000006"); + + ASSERT_EQ(plan->types.size(), 24u); + EXPECT_EQ(plan->types.front().symbol, "comb_domain"); + EXPECT_EQ(plan->types.front().kind, TypeKind::TimeDomain); + EXPECT_EQ(plan->types.front().cppType, "gfsim::CombinationalDomain"); + EXPECT_EQ(plan->types.back().symbol, "target"); + EXPECT_EQ(plan->types.back().kind, TypeKind::Role); + + ASSERT_EQ(plan->runtimeObjects.size(), 4u); + for (size_t index = 0; index < plan->runtimeObjects.size(); ++index) { + EXPECT_EQ(plan->runtimeObjects[index].objectId, index); + EXPECT_EQ(plan->runtimeObjects[index].activationId, index); + } + EXPECT_EQ(plan->runtimeObjects[0].targetSymbol, "Top::fifo"); + EXPECT_EQ(plan->runtimeObjects[0].hierarchyPath, "Top.fifo"); + EXPECT_EQ(plan->runtimeObjects[1].indices, (std::vector{0})); + EXPECT_EQ(plan->runtimeObjects[2].indices, (std::vector{1})); + EXPECT_EQ(plan->runtimeObjects[3].objectKind, RuntimeObjectKind::Process); + + const std::vector expectedEdges = { + {0, 0}, {1, 1}, {1, 2}, {1, 3}, {2, 2}, {3, 3}}; + EXPECT_EQ(plan->activationEdges, expectedEdges); + EXPECT_FALSE(hasError(validateModelPlan(*plan))); +} + +TEST(ModelPlanTest, IdentifiesTraceOwnersFromTypedBindingMetadata) { + mlir::MLIRContext context; + loadACSimDialects(context); + auto input = llvm::MemoryBuffer::getFile(ACSIM_VALID_TEST_FILE); + ASSERT_TRUE(static_cast(input)); + std::string source = input.get()->getBuffer().str(); + auto replaceAll = [&](std::string_view from, std::string_view to) { + for (size_t position = source.find(from); position != std::string::npos; + position = source.find(from, position + to.size())) + source.replace(position, from.size(), to); + }; + replaceAll("gfsim::Fifo", "gfsim::TraceSource"); + replaceAll("fifo.schema", "ac.TraceSource"); + auto file = mlir::parseSourceString(source, &context); + ASSERT_TRUE(file); + + auto plan = buildModelPlan(*file); + ASSERT_TRUE(static_cast(plan)) << llvm::toString(plan.takeError()); + ASSERT_EQ(plan->runtimeObjects.size(), 4u); + EXPECT_TRUE(plan->runtimeObjects[0].traceOwner); + EXPECT_TRUE(plan->runtimeObjects[1].traceOwner); + EXPECT_TRUE(plan->runtimeObjects[2].traceOwner); + EXPECT_FALSE(plan->runtimeObjects[3].traceOwner); +} + +TEST(ModelPlanTest, RejectsInputWithoutOneCanonicalACSimModel) { + mlir::MLIRContext context; + loadACSimDialects(context); + auto file = + mlir::parseSourceString("builtin.module {}", &context); + ASSERT_TRUE(file); + + auto plan = buildModelPlan(*file); + ASSERT_FALSE(plan); + EXPECT_TRUE(hasError(plan.takeError())); +} + +TEST(ModelPlanTest, ValidationRejectsDestructionThatIsNotReverseConstruction) { + mlir::MLIRContext context; + loadACSimDialects(context); + auto file = + mlir::parseSourceFile(ACSIM_VALID_TEST_FILE, &context); + ASSERT_TRUE(file); + auto plan = buildModelPlan(*file); + if (!plan) { + ADD_FAILURE() << llvm::toString(plan.takeError()); + return; + } + ASSERT_GE(plan->destructionOrder.size(), 2u); + + std::swap(plan->destructionOrder[0], plan->destructionOrder[1]); + EXPECT_TRUE(hasError(validateModelPlan(*plan))); +} + +TEST(ModelPlanTest, ExtractsHierarchyBindingsExpressionsAndProcesses) { + mlir::MLIRContext context; + loadACSimDialects(context); + auto file = + mlir::parseSourceFile(ACSIM_VALID_TEST_FILE, &context); + ASSERT_TRUE(file); + auto plan = buildModelPlan(*file); + if (!plan) { + ADD_FAILURE() << llvm::toString(plan.takeError()); + return; + } + + ASSERT_EQ(plan->bindings.size(), 2u); + EXPECT_EQ(plan->bindings[0].symbol, "pure"); + EXPECT_EQ(plan->bindings[0].effect, BindingEffect::Pure); + EXPECT_EQ(plan->bindings[0].header, "gfsim/pure.hpp"); + EXPECT_EQ(plan->bindings[1].symbol, "stateful"); + EXPECT_EQ(plan->bindings[1].effect, BindingEffect::Stateful); + EXPECT_EQ(plan->bindings[1].cppSymbol, "gfsim::Fifo"); + EXPECT_EQ(plan->bindings[1].entryPoints.work, "fifo_work"); + EXPECT_EQ(plan->bindings[1].ports.size(), 2u); + EXPECT_EQ(plan->bindings[1].resources.size(), 2u); + EXPECT_EQ(plan->bindings[1].activationSources.size(), 1u); + + ASSERT_EQ(plan->modules.size(), 1u); + const ModulePlan &module = plan->modules.front(); + EXPECT_EQ(module.symbol, "Top"); + EXPECT_FALSE(module.className.empty()); + ASSERT_EQ(module.placements.size(), 2u); + EXPECT_EQ(module.placements[0].symbol, "fifo"); + EXPECT_EQ(module.placements[0].kind, PlacementKind::ExternalStateful); + EXPECT_EQ(module.placements[1].symbol, "lanes"); + EXPECT_EQ(module.placements[1].kind, PlacementKind::HomogeneousArray); + EXPECT_EQ(module.placements[1].shape, (std::vector{2})); + EXPECT_EQ(module.projections.size(), 6u); + EXPECT_EQ(module.binds.size(), 3u); + EXPECT_EQ(module.expressions.size(), 3u); + EXPECT_EQ(module.exports.size(), 3u); + EXPECT_EQ(module.returnValues.size(), 3u); + + ASSERT_EQ(module.processes.size(), 1u); + const ProcessPlan &process = module.processes.front(); + EXPECT_EQ(process.symbol, "tick"); + EXPECT_EQ(process.entryPc, "entry"); + EXPECT_EQ(process.fairnessWork, 8u); + ASSERT_EQ(process.liveSlots.size(), 1u); + EXPECT_EQ(process.liveSlots[0].name, "counter"); + EXPECT_EQ(process.liveSlots[0].type, "!acsim.value<@cpp_bool>"); + ASSERT_EQ(process.states.size(), 3u); + EXPECT_EQ(process.states[0].name, "entry"); + EXPECT_EQ(process.states[0].ordinal, 0u); + EXPECT_EQ(process.states[0].operations.size(), 3u); + EXPECT_TRUE( + std::holds_alternative(process.states[0].terminator)); + EXPECT_EQ(process.states[1].name, "wait"); + EXPECT_TRUE( + std::holds_alternative(process.states[1].terminator)); + EXPECT_EQ(process.states[2].name, "done"); + EXPECT_TRUE( + std::holds_alternative(process.states[2].terminator)); + EXPECT_FALSE(hasError(validateModelPlan(*plan))); +} + +TEST(ModelPlanTest, PreservesEveryBlockAndBranchOperandInProcessStates) { + mlir::MLIRContext context; + loadACSimDialects(context); + auto file = mlir::parseSourceFile( + ACSIM_PROCESS_CONTROL_FLOW_TEST_FILE, &context); + ASSERT_TRUE(file); + auto plan = buildModelPlan(*file); + if (!plan) { + ADD_FAILURE() << llvm::toString(plan.takeError()); + return; + } + + const ProcessPlan &process = plan->modules.front().processes.front(); + ASSERT_EQ(process.states.front().blocks.size(), 3u); + EXPECT_TRUE(std::holds_alternative( + process.states.front().blocks[0].terminator)); + ASSERT_EQ(process.states.front().blocks[1].arguments.size(), 1u); + EXPECT_EQ(process.states.front().blocks[1].arguments.front().type, "i32"); +} + +TEST(ModelPlanTest, RejectsBranchOutsideClosedPcBlockSet) { + mlir::MLIRContext context; + loadACSimDialects(context); + auto file = mlir::parseSourceFile( + ACSIM_PROCESS_CONTROL_FLOW_TEST_FILE, &context); + ASSERT_TRUE(file); + auto plan = buildModelPlan(*file); + ASSERT_TRUE(static_cast(plan)); + auto &branch = std::get(plan->modules.front() + .processes.front() + .states.front() + .blocks.front() + .terminator); + branch.trueBlock = 99; + + EXPECT_TRUE(hasError(validateModelPlan(*plan))); +} + +TEST(ModelPlanTest, ValidationRejectsTransitionOutsideClosedPcSet) { + mlir::MLIRContext context; + loadACSimDialects(context); + auto file = + mlir::parseSourceFile(ACSIM_VALID_TEST_FILE, &context); + ASSERT_TRUE(file); + auto plan = buildModelPlan(*file); + if (!plan) { + ADD_FAILURE() << llvm::toString(plan.takeError()); + return; + } + auto &transition = std::get( + plan->modules[0].processes[0].states[0].terminator); + transition.targetPc = "outside"; + + EXPECT_TRUE(hasError(validateModelPlan(*plan))); +} + +TEST(ModelPlanTest, IndependentCanonicalParsesProduceIdenticalOwnedPlans) { + mlir::MLIRContext firstContext; + mlir::MLIRContext secondContext; + loadACSimDialects(firstContext); + loadACSimDialects(secondContext); + auto firstFile = mlir::parseSourceFile(ACSIM_VALID_TEST_FILE, + &firstContext); + auto secondFile = mlir::parseSourceFile(ACSIM_VALID_TEST_FILE, + &secondContext); + ASSERT_TRUE(firstFile); + ASSERT_TRUE(secondFile); + auto first = buildModelPlan(*firstFile); + auto second = buildModelPlan(*secondFile); + if (!first || !second) { + if (!first) + ADD_FAILURE() << llvm::toString(first.takeError()); + if (!second) + ADD_FAILURE() << llvm::toString(second.takeError()); + return; + } + + EXPECT_EQ(stablePlanSummary(*first), stablePlanSummary(*second)); +} + +} // namespace +} // namespace acir::codegen diff --git a/components/agentic-circuit/unittests/CodeGen/QueueGraphPlanTest.cpp b/components/agentic-circuit/unittests/CodeGen/QueueGraphPlanTest.cpp new file mode 100644 index 00000000..a2739685 --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/QueueGraphPlanTest.cpp @@ -0,0 +1,647 @@ +#include "acir/CodeGen/QueueGraphPlan.h" +#include "acir/CodeGen/QueueGraphGenerator.h" +#include "acir/CodeGen/QueueGraphPyc.h" + +#include "acir/Dialect/ACIR/ACIRDialect.h" +#include "mlir/Dialect/DLTI/DLTI.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/Parser/Parser.h" +#include "llvm/Support/Error.h" +#include "gtest/gtest.h" + +namespace acir::codegen { +namespace { + +constexpr llvm::StringLiteral kQueueGraph = R"mlir( +module attributes {ac.contract_epoch = "0.3", ac.system = "pipeline"} { + %input = ac.source depth 4 latency 1 {ac.name = "input"} : !ac.queue + %left, %right = ac.route %input depths [2, 2] latencies [1, 1] { + ^selector(%item: !ac.var): + %zero = ac.var.constant 0 : i64 as !ac.var + %selected = ac.var.cmp "eq" %item, %zero : !ac.var -> !ac.var + ac.route.yield %selected : !ac.var + } {ac.output_names = ["left", "right"]} : !ac.queue -> (!ac.queue, !ac.queue) + %merged = ac.merge %left, %right policy "round_robin" depth 3 latency 1 {ac.name = "merged"} : (!ac.queue, !ac.queue) -> !ac.queue + ac.sink %merged {ac.name = "sink_0"} : !ac.queue +} +)mlir"; + +constexpr llvm::StringLiteral kStructuredTransform = R"mlir( +module attributes {ac.contract_epoch = "0.3", ac.system = "structured"} { + ac.type_scope @types { + ac.struct @Item fields [{name = "value", type = i64}] + } {dlti.dl_spec = #dlti.dl_spec = {abi_alignment = 8 : i64, endianness = "little", preferred_alignment = 8 : i64, size = 8 : i64}>} + %input = ac.source depth 2 latency 1 {ac.name = "input"} : !ac.queue> + %output = ac.transform %input depths [2] latencies [1] { + ^body(%item: !ac.var>): + %value = ac.var.get %item field "value" : !ac.var> -> !ac.var + %one = ac.var.constant 1 : i64 as !ac.var + %sum = ac.var.add %value, %one : !ac.var + %updated = ac.var.with %item, %sum field "value" : !ac.var>, !ac.var -> !ac.var> + ac.transform.yield %updated : !ac.var> + } {ac.name = "output"} : (!ac.queue>) -> !ac.queue> + ac.sink %output {ac.name = "sink_0"} : !ac.queue> +} +)mlir"; + +constexpr llvm::StringLiteral kMultipleConsumers = R"mlir( +module attributes {ac.contract_epoch = "0.3", ac.system = "bad"} { + %input = ac.source depth 2 latency 1 {ac.name = "input"} : !ac.queue + ac.sink %input {ac.name = "left"} : !ac.queue + ac.sink %input {ac.name = "right"} : !ac.queue +} +)mlir"; + +constexpr llvm::StringLiteral kObservationUse = R"mlir( +module attributes {ac.contract_epoch = "0.3", ac.system = "observed"} { + %input = ac.source depth 2 latency 1 {ac.name = "input"} : !ac.queue + ac.observe %input name "head" : !ac.queue + ac.sink %input {ac.name = "sink_0"} : !ac.queue +} +)mlir"; + +TEST(QueueGraphPlanTest, ExtractsFrozenQueueIdentitiesAndTopology) { + mlir::MLIRContext context; + context.loadDialect(); + auto module = mlir::parseSourceString(kQueueGraph, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_TRUE(bool(plan)) << llvm::toString(plan.takeError()); + EXPECT_EQ(plan->system, "pipeline"); + ASSERT_EQ(plan->queues.size(), 4u); + EXPECT_EQ(plan->queues[0].name, "input"); + EXPECT_EQ(plan->queues[1].name, "left"); + EXPECT_EQ(plan->queues[2].name, "right"); + EXPECT_EQ(plan->queues[3].name, "merged"); + ASSERT_EQ(plan->blocks.size(), 4u); + EXPECT_EQ(plan->blocks[0].kind, "source"); + EXPECT_EQ(plan->blocks[1].kind, "route"); + EXPECT_EQ(plan->blocks[2].kind, "merge"); + EXPECT_EQ(plan->blocks[3].kind, "sink"); + EXPECT_EQ(plan->blocks[2].policy, "round_robin"); +} + +TEST(QueueGraphPlanTest, CanonicalJsonIsByteIdenticalAndClosed) { + mlir::MLIRContext context; + context.loadDialect(); + auto module = mlir::parseSourceString(kQueueGraph, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_TRUE(bool(plan)) << llvm::toString(plan.takeError()); + auto first = plan->canonicalJson(); + ASSERT_TRUE(bool(first)) << llvm::toString(first.takeError()); + auto second = plan->canonicalJson(); + ASSERT_TRUE(bool(second)) << llvm::toString(second.takeError()); + EXPECT_EQ(*first, *second); + EXPECT_NE(first->find("\"contract_epoch\":\"0.3\""), std::string::npos); + EXPECT_NE(first->find("\"schema\":\"agentic-circuit-queue-graph-plan\""), + std::string::npos); + EXPECT_NE(first->find("\"version\":\"0.3\""), std::string::npos); + EXPECT_NE(first->find("\"name\":\"merged\""), std::string::npos); +} + +TEST(QueueGraphPlanTest, PreservesJitSpecializationIdentity) { + mlir::MLIRContext context; + context.loadDialect(); + constexpr llvm::StringLiteral fingerprint = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + std::string specialized = kQueueGraph.str(); + size_t system = specialized.find("ac.system = \"pipeline\""); + ASSERT_NE(system, std::string::npos); + specialized.insert(system, + ("ac.specialization = \"" + fingerprint + "\", ").str()); + auto module = mlir::parseSourceString(specialized, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_TRUE(bool(plan)) << llvm::toString(plan.takeError()); + EXPECT_EQ(plan->specializationFingerprint, fingerprint); + auto json = plan->canonicalJson(); + ASSERT_TRUE(bool(json)) << llvm::toString(json.takeError()); + EXPECT_NE(json->find(fingerprint), std::string::npos); + auto cpp = generateQueueGraphCpp(*plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find(("// Specialization: " + fingerprint).str()), + std::string::npos); + + specialized.replace(specialized.find(fingerprint), fingerprint.size(), + "sha256:bad"); + module = mlir::parseSourceString(specialized, &context); + ASSERT_TRUE(module); + plan = buildQueueGraphPlan(*module); + ASSERT_FALSE(bool(plan)); + EXPECT_NE(llvm::toString(plan.takeError()).find("fingerprint is invalid"), + std::string::npos); +} + +TEST(QueueGraphPlanTest, PreservesQueueRateAndRejectsUnspecializedPycLanes) { + mlir::MLIRContext context; + context.loadDialect(); + std::string rated = kQueueGraph.str(); + size_t attributes = rated.find("{ac.name = \"input\"}"); + ASSERT_NE(attributes, std::string::npos); + rated.replace(attributes, std::string("{ac.name = \"input\"}").size(), + "{ac.name = \"input\", " + "ac.output_rates = array}"); + auto module = mlir::parseSourceString(rated, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_TRUE(bool(plan)) << llvm::toString(plan.takeError()); + ASSERT_FALSE(plan->queues.empty()); + EXPECT_EQ(plan->queues.front().rate, 2u); + auto json = plan->canonicalJson(); + ASSERT_TRUE(bool(json)) << llvm::toString(json.takeError()); + EXPECT_NE(json->find("\"rate\":2"), std::string::npos); + auto cpp = generateQueueGraphCpp(*plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find(", nullptr, 1, 2)"), std::string::npos); + auto pyc = generateQueueGraphPyc(*plan); + ASSERT_FALSE(bool(pyc)); + EXPECT_NE(llvm::toString(pyc.takeError()) + .find("rate greater than one requires explicit lane lowering"), + std::string::npos); +} + +TEST(QueueGraphPlanTest, RejectsLegacyContractEpochBeforePlanning) { + mlir::MLIRContext context; + context.loadDialect(); + std::string legacy = kQueueGraph.str(); + size_t epoch = legacy.find("ac.contract_epoch = \"0.3\""); + ASSERT_NE(epoch, std::string::npos); + legacy.replace(epoch, std::string("ac.contract_epoch = \"0.3\"").size(), + "ac.contract_epoch = \"0.2\""); + auto module = mlir::parseSourceString(legacy, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_FALSE(bool(plan)); + EXPECT_NE(llvm::toString(plan.takeError()) + .find("module requires ac.contract_epoch exactly '0.3'"), + std::string::npos); +} + +TEST(QueueGraphPlanTest, ExtractsPayloadAndImmutableVarDag) { + mlir::MLIRContext context; + context.loadDialect(); + auto module = + mlir::parseSourceString(kStructuredTransform, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_TRUE(bool(plan)) << llvm::toString(plan.takeError()); + ASSERT_EQ(plan->payloads.size(), 1u); + EXPECT_EQ(plan->payloads[0].name, "Item"); + ASSERT_EQ(plan->payloads[0].fields.size(), 1u); + EXPECT_EQ(plan->payloads[0].fields[0].name, "value"); + ASSERT_EQ(plan->blocks.size(), 3u); + const QueueBlockPlan &transform = plan->blocks[1]; + ASSERT_EQ(transform.expressions.size(), 4u); + EXPECT_EQ(transform.expressions[0].kind, "get"); + EXPECT_EQ(transform.expressions[1].kind, "constant"); + EXPECT_EQ(transform.expressions[2].kind, "add"); + EXPECT_EQ(transform.expressions[3].kind, "with"); + ASSERT_EQ(transform.yields.size(), 1u); + EXPECT_EQ(transform.yields[0], "v3"); +} + +TEST(QueueGraphPlanTest, NativeGeneratorConsumesOnlyExtractedPlan) { + mlir::MLIRContext context; + context.loadDialect(); + auto module = mlir::parseSourceString(kQueueGraph, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_TRUE(bool(plan)) << llvm::toString(plan.takeError()); + auto source = generateQueueGraphCpp(*plan); + ASSERT_TRUE(bool(source)) << llvm::toString(source.takeError()); + EXPECT_NE(source->find("gfsim::QueueRoutefind("gfsim::QueueMerge"), + std::string::npos); + EXPECT_NE(source->find("gfsim::QueueSink"), std::string::npos); +} + +TEST(QueueGraphPlanTest, EmitsCanonicalScalarQueuePyc) { + QueueGraphPlan plan; + plan.system = "scalar_pipeline"; + plan.queues = {{"input", "i64", "/", 2, 1}, {"output", "i64", "/", 2, 1}}; + plan.blocks.push_back({"source", "input", "/", {}, {"input"}, {2}, {1}}); + QueueBlockPlan transform{"transform", "output", "/", {"input"}, + {"output"}, {2}, {1}}; + transform.expressions = {{"v0", "constant", "i64", {}, "", "", "1 : i64"}, + {"v1", "add", "i64", {"item", "v0"}, "", "", ""}}; + transform.yields = {"v1"}; + plan.blocks.push_back(std::move(transform)); + plan.blocks.push_back({"sink", "sink_0", "/", {"output"}, {}}); + auto pyc = generateQueueGraphPyc(plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + EXPECT_EQ(std::count(pyc->begin(), pyc->end(), '\n') > 5, true); + EXPECT_NE(pyc->find("pyc.fifo"), std::string::npos); + EXPECT_NE(pyc->find("pyc.add"), std::string::npos); + EXPECT_NE(pyc->find("pyc.frontend.contract = \"pycircuit\""), + std::string::npos); +} + +TEST(QueueGraphPlanTest, EmitsAtomicTransformWithIndependentArity) { + QueueGraphPlan plan; + plan.system = "atomic_sum"; + plan.queues = {{"left", "i64", "/", 2, 1}, + {"right", "i64", "/", 2, 1}, + {"sum", "i64", "/", 2, 1}}; + plan.blocks.push_back({"source", "left", "/", {}, {"left"}, {2}, {1}}); + plan.blocks.push_back({"source", "right", "/", {}, {"right"}, {2}, {1}}); + QueueBlockPlan transform{"transform", "sum", "/", {"left", "right"}, + {"sum"}, {2}, {1}}; + transform.expressions = {{"v0", "add", "i64", {"item", "item1"}, "", "", ""}}; + transform.yields = {"v0"}; + plan.blocks.push_back(std::move(transform)); + plan.blocks.push_back({"sink", "sink_0", "/", {"sum"}, {}}); + + auto cpp = generateQueueGraphCpp(plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find("std::tuple operator()(const " + "std::int64_t &item, const std::int64_t &item1)"), + std::string::npos); + EXPECT_NE(cpp->find("QueueAtomicTransform, " + "std::tuple>"), + std::string::npos); + + auto pyc = generateQueueGraphPyc(plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + EXPECT_NE(pyc->find("pyc.add"), std::string::npos); + EXPECT_NE(pyc->find("= pyc.wire : i1"), std::string::npos); +} + +TEST(QueueGraphPlanTest, EmitsHeterogeneousBarrierForBothBackends) { + QueueGraphPlan plan; + plan.system = "barrier"; + plan.queues = {{"left", "i8", "/", 2, 1}, + {"right", "i16", "/", 2, 1}, + {"left_ready", "i8", "/", 2, 1}, + {"right_ready", "i16", "/", 2, 1}}; + plan.blocks.push_back({"source", "left", "/", {}, {"left"}, {2}, {1}}); + plan.blocks.push_back({"source", "right", "/", {}, {"right"}, {2}, {1}}); + plan.blocks.push_back({"barrier", + "left_ready", + "/", + {"left", "right"}, + {"left_ready", "right_ready"}, + {2, 2}, + {1, 1}}); + plan.blocks.push_back({"sink", "sink_0", "/", {"left_ready"}, {}}); + plan.blocks.push_back({"sink", "sink_1", "/", {"right_ready"}, {}}); + + auto cpp = generateQueueGraphCpp(plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find("gfsim::QueueBarrier>"), + std::string::npos); + + auto pyc = generateQueueGraphPyc(plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + EXPECT_NE(pyc->find("%in0_valid"), std::string::npos); + EXPECT_NE(pyc->find("%out1_ready"), std::string::npos); +} + +TEST(QueueGraphPlanTest, EmitsStaticQueueCollectionSelectForBothBackends) { + QueueGraphPlan plan; + plan.system = "select"; + plan.queues = {{"control", "i8", "/", 1, 1}, + {"left", "i16", "/", 1, 1}, + {"right", "i16", "/", 1, 1}, + {"selected", "i16", "/", 2, 1}}; + plan.blocks.push_back({"source", "control", "/", {}, {"control"}, {1}, {1}}); + plan.blocks.push_back({"source", "left", "/", {}, {"left"}, {1}, {1}}); + plan.blocks.push_back({"source", "right", "/", {}, {"right"}, {1}, {1}}); + QueueBlockPlan select{ + "select", "selected", "/", {"control", "left", "right"}, + {"selected"}, {2}, {1}}; + select.yields = {"item"}; + plan.blocks.push_back(std::move(select)); + plan.blocks.push_back({"sink", "sink_0", "/", {"selected"}, {}}); + + auto cpp = generateQueueGraphCpp(plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find("gfsim::QueueSelectfind("select_selector_out_of_range"), std::string::npos); + EXPECT_NE(pyc->find("pyc.mux"), std::string::npos); +} + +TEST(QueueGraphPlanTest, EmitsTypedReorderForBothBackends) { + QueueGraphPlan plan; + plan.system = "ordered"; + plan.queues = {{"input", "i64", "/", 4, 1}, {"output", "i64", "/", 4, 1}}; + plan.blocks.push_back({"source", "input", "/", {}, {"input"}, {4}, {1}}); + QueueBlockPlan reorder{"reorder", "output", "/", {"input"}, + {"output"}, {4}, {1}}; + reorder.yields = {"item"}; + reorder.capacity = 4; + reorder.start = 0; + plan.blocks.push_back(std::move(reorder)); + plan.blocks.push_back({"sink", "sink_0", "/", {"output"}, {}}); + + auto cpp = generateQueueGraphCpp(plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find("gfsim::QueueReorder"), + std::string::npos); + EXPECT_NE(cpp->find(", input_, output_, 4, 0)"), std::string::npos); + EXPECT_NE(cpp->find("size_t reorder_0_active() const"), std::string::npos); + + auto pyc = generateQueueGraphPyc(plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + EXPECT_NE(pyc->find("pyc.reg"), std::string::npos); + EXPECT_NE(pyc->find("pyc.ult"), std::string::npos); + EXPECT_GE(std::count(pyc->begin(), pyc->end(), '\n'), 40); +} + +TEST(QueueGraphPlanTest, EmitsTypedDependencyForBothBackends) { + QueueGraphPlan plan; + plan.system = "dependent"; + plan.queues = {{"input", "i8", "/", 4, 1}, {"output", "i8", "/", 4, 1}}; + plan.blocks.push_back({"source", "input", "/", {}, {"input"}, {4}, {1}}); + QueueBlockPlan dependency{"dependency", "output", "/", {"input"}, + {"output"}, {4}, {1}}; + dependency.expressions = { + {"v0", "constant", "i8", {}, "", "", "255 : i8"}, + {"v1", "constant", "i1", {}, "", "", "0 : i1"}, + {"v2", "constant", "i8", {}, "", "", "1 : i8"}, + }; + dependency.yields = {"item", "v0", "v1", "v2"}; + dependency.capacity = 4; + dependency.resources = 2; + dependency.noDependency = 255; + plan.blocks.push_back(std::move(dependency)); + plan.blocks.push_back({"sink", "sink_0", "/", {"output"}, {}}); + + auto cpp = generateQueueGraphCpp(plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find("gfsim::QueueDependencyfind(", input_, output_, 4, 2, 255)"), std::string::npos); + EXPECT_NE(cpp->find("size_t dependency_0_active() const"), std::string::npos); + EXPECT_NE(cpp->find("dependency_0_resource_active"), std::string::npos); + + auto pyc = generateQueueGraphPyc(plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + EXPECT_NE(pyc->find("pyc.reg"), std::string::npos); + EXPECT_NE(pyc->find("pyc.sub"), std::string::npos); +} + +TEST(QueueGraphPlanTest, EmitsTypedCreditWindowForBothBackends) { + QueueGraphPlan plan; + plan.system = "credited"; + plan.queues = {{"input", "i8", "/", 4, 1}, {"output", "i8", "/", 4, 1}}; + plan.blocks.push_back({"source", "input", "/", {}, {"input"}, {4}, {1}}); + QueueBlockPlan credit{"credit", "output", "/", {"input"}, + {"output"}, {4}, {1}}; + credit.yields = {"item"}; + credit.credits = 2; + plan.blocks.push_back(std::move(credit)); + plan.blocks.push_back({"sink", "sink_0", "/", {"output"}, {}}); + + auto cpp = generateQueueGraphCpp(plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find("gfsim::QueueCredit"), + std::string::npos); + EXPECT_NE(cpp->find(", input_, output_, 2)"), std::string::npos); + + auto pyc = generateQueueGraphPyc(plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + EXPECT_NE(pyc->find("pyc.reg"), std::string::npos); + EXPECT_NE(pyc->find("pyc.sub"), std::string::npos); +} + +TEST(QueueGraphPlanTest, EmitsOldDataMemoryForBothBackends) { + QueueGraphPlan plan; + plan.system = "memory_pipeline"; + plan.payloads = { + {"MemoryRequest", + {{"address", "i4"}, {"write", "i1"}, {"data", "i16"}, {"tag", "i8"}}}}; + constexpr llvm::StringLiteral requestType = + "!ac.struct<@types::@MemoryRequest>"; + plan.queues = {{"input0", requestType.str(), "/", 4, 1}, + {"input1", requestType.str(), "/", 4, 1}, + {"output0", requestType.str(), "/", 4, 1}, + {"output1", requestType.str(), "/", 4, 1}}; + plan.blocks.push_back({"source", "input0", "/", {}, {"input0"}, {4}, {1}}); + plan.blocks.push_back({"source", "input1", "/", {}, {"input1"}, {4}, {1}}); + plan.memoryInstances.push_back({"sram", "i16", 15, 0, 3, "memory/sram", "/"}); + QueueBlockPlan memory{"memory_request", "output0", "/", {"input0"}, + {"output0"}, {4}, {1}}; + memory.expressions = { + {"v0", "get", "i4", {"item"}, "address", "", ""}, + {"v1", "get", "i1", {"item"}, "write", "", ""}, + {"v2", "get", "i16", {"item"}, "data", "", ""}, + }; + memory.yields = {"v0", "v1", "v2"}; + memory.resultField = "data"; + memory.memoryInstance = "sram"; + memory.endpointOrdinal = 0; + plan.memoryRequests.push_back( + {"sram", "output0", "/", "input0", "output0", 0, 4, "data"}); + plan.blocks.push_back(memory); + memory.name = "output1"; + memory.inputs = {"input1"}; + memory.outputs = {"output1"}; + memory.endpointOrdinal = 1; + plan.memoryRequests.push_back( + {"sram", "output1", "/", "input1", "output1", 1, 4, "data"}); + plan.blocks.push_back(std::move(memory)); + plan.blocks.push_back({"sink", "sink_0", "/", {"output0"}, {}}); + plan.blocks.push_back({"sink", "sink_1", "/", {"output1"}, {}}); + + auto cpp = generateQueueGraphCpp(plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find("gfsim::QueueMemoryArbiterfind("result.data = old_data"), std::string::npos); + EXPECT_NE(cpp->find("std::array *, " + "2>{&input0_, &input1_}"), + std::string::npos); + + auto pyc = generateQueueGraphPyc(plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + EXPECT_NE(pyc->find("pyc.sub"), std::string::npos); + EXPECT_NE(pyc->find("pyc.sync_mem"), std::string::npos); + EXPECT_EQ(pyc->find("pyc.sync_mem", pyc->find("pyc.sync_mem") + 1), + std::string::npos); + EXPECT_NE(pyc->find("{depth = 15, name = \"sram\"}"), std::string::npos); + EXPECT_NE(pyc->find("pyc.concat"), std::string::npos); + EXPECT_NE(pyc->find("memory_address_out_of_range"), std::string::npos); +} + +TEST(QueueGraphPlanTest, EmitsQueuePredicateAsPycComparison) { + struct Case { + llvm::StringLiteral predicate; + llvm::StringLiteral opcode; + bool negated; + }; + constexpr Case cases[] = { + {"eq", "pyc.eq", false}, {"ne", "pyc.eq", true}, + {"slt", "pyc.slt", false}, {"sle", "pyc.slt", true}, + {"sgt", "pyc.slt", false}, {"sge", "pyc.slt", true}, + }; + for (const Case &testCase : cases) { + SCOPED_TRACE(testCase.predicate.str()); + mlir::MLIRContext context; + context.loadDialect(); + std::string source = kQueueGraph.str(); + const std::string original = "ac.var.cmp \"eq\""; + size_t predicate = source.find(original); + ASSERT_NE(predicate, std::string::npos); + source.replace(predicate, original.size(), + "ac.var.cmp \"" + testCase.predicate.str() + "\""); + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_TRUE(bool(plan)) << llvm::toString(plan.takeError()); + auto pyc = generateQueueGraphPyc(*plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + const size_t comparison = pyc->find(testCase.opcode.str()); + ASSERT_NE(comparison, std::string::npos); + EXPECT_EQ(pyc->find("pyc.rr_arbiter"), std::string::npos); + EXPECT_NE(pyc->find("primitive_id = \"control.rr_arbiter.v1\""), + std::string::npos); + const size_t comparisonEnd = pyc->find('\n', comparison); + ASSERT_NE(comparisonEnd, std::string::npos); + const size_t nextEnd = pyc->find('\n', comparisonEnd + 1); + ASSERT_NE(nextEnd, std::string::npos); + const llvm::StringRef nextLine(pyc->data() + comparisonEnd + 1, + nextEnd - comparisonEnd - 1); + EXPECT_EQ(nextLine.contains("pyc.not"), testCase.negated); + } +} + +TEST(QueueGraphPlanTest, EmitsOneReadyValidStagePerQueueLatency) { + QueueGraphPlan plan; + plan.system = "latency_pipeline"; + plan.queues = {{"input", "i64", "/", 2, 1}, {"output", "i64", "/", 4, 3}}; + plan.blocks.push_back({"source", "input", "/", {}, {"input"}, {2}, {1}}); + QueueBlockPlan transform{"transform", "output", "/", {"input"}, + {"output"}, {4}, {3}}; + transform.yields = {"item"}; + plan.blocks.push_back(std::move(transform)); + plan.blocks.push_back({"sink", "sink_0", "/", {"output"}, {}}); + auto pyc = generateQueueGraphPyc(plan); + ASSERT_TRUE(bool(pyc)) << llvm::toString(pyc.takeError()); + size_t count = 0; + for (size_t offset = 0; + (offset = pyc->find("pyc.fifo", offset)) != std::string::npos; + offset += 8) + ++count; + EXPECT_EQ(count, 4u); + EXPECT_NE(pyc->find("{depth = 4}"), std::string::npos); +} + +TEST(QueueGraphPlanTest, RejectsImplicitMultipleConsumers) { + mlir::MLIRContext context; + context.loadDialect(); + auto module = + mlir::parseSourceString(kMultipleConsumers, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_FALSE(bool(plan)); + EXPECT_NE(llvm::toString(plan.takeError()).find("insert ac.broadcast"), + std::string::npos); +} + +TEST(QueueGraphPlanTest, RejectsUnconsumedQueueAsStaticDeadlockRisk) { + QueueGraphPlan plan; + plan.system = "unconsumed"; + plan.queues = {{"input", "i8", "/", 1, 1}}; + plan.blocks.push_back({"source", "input", "/", {}, {"input"}, {1}, {1}}); + auto error = verifyQueueGraphPlan(plan); + ASSERT_TRUE(bool(error)); + EXPECT_NE(llvm::toString(std::move(error)).find("has no consuming block"), + std::string::npos); +} + +TEST(QueueGraphPlanTest, RejectsRawQueueCycleOutsideFeedbackBlock) { + QueueGraphPlan plan; + plan.system = "cycle"; + plan.queues = {{"a", "i8", "/", 1, 1}, {"b", "i8", "/", 1, 1}}; + plan.blocks.push_back({"transform", "a", "/", {"b"}, {"a"}, {1}, {1}}); + plan.blocks.push_back({"transform", "b", "/", {"a"}, {"b"}, {1}, {1}}); + auto error = verifyQueueGraphPlan(plan); + ASSERT_TRUE(bool(error)); + EXPECT_NE(llvm::toString(std::move(error)) + .find("represent stateful loops with ac.feedback"), + std::string::npos); +} + +TEST(QueueGraphPlanTest, ObservationDoesNotConsumeQueue) { + mlir::MLIRContext context; + context.loadDialect(); + auto module = + mlir::parseSourceString(kObservationUse, &context); + ASSERT_TRUE(module); + auto plan = buildQueueGraphPlan(*module); + ASSERT_TRUE(bool(plan)) << llvm::toString(plan.takeError()); + ASSERT_EQ(plan->blocks.size(), 3u); + EXPECT_EQ(plan->blocks[1].kind, "observe"); +} + +TEST(QueueGraphPlanTest, VerificationLeafRunsInGfsimAndRejectsPycDesign) { + QueueGraphPlan plan; + plan.system = "verified"; + plan.queues = {{"input", "i8", "/", 1, 1}}; + plan.blocks.push_back({"source", "input", "/", {}, {"input"}, {1}, {1}}); + QueueBlockPlan expect{"expect", "expect_1", "/", {"input"}, {}}; + expect.expressions = { + {"v0", "constant", "i8", {}, "", "", "0 : i8"}, + {"v1", "cmp", "i1", {"item", "v0"}, "", "sgt", ""}, + }; + expect.yields = {"v1"}; + expect.message = "positive"; + plan.blocks.push_back(std::move(expect)); + plan.blocks.push_back({"sink", "sink_0", "/", {"input"}, {}}); + + auto cpp = generateQueueGraphCpp(plan); + ASSERT_TRUE(bool(cpp)) << llvm::toString(cpp.takeError()); + EXPECT_NE(cpp->find("gfsim::QueueExpect"), + std::string::npos); + + auto pyc = generateQueueGraphPyc(plan); + ASSERT_FALSE(bool(pyc)); + EXPECT_NE(llvm::toString(pyc.takeError()) + .find("cannot appear in a design hierarchy"), + std::string::npos); +} + +TEST(QueueGraphPlanTest, RejectsMalformedPycFeedbackContract) { + QueueGraphPlan plan; + plan.system = "bad_feedback"; + plan.queues = {{"input", "i64", "/", 1, 1}, {"output", "i64", "/", 1, 1}}; + plan.blocks.push_back({"source", "input", "/", {}, {"input"}, {1}, {1}}); + QueueBlockPlan feedback{"feedback", "output", "/", {"input"}, {"output"}}; + feedback.yields = {"item", "condition"}; + feedback.maxIterations = 0; + plan.blocks.push_back(std::move(feedback)); + plan.blocks.push_back({"sink", "sink_0", "/", {"output"}, {}}); + auto pyc = generateQueueGraphPyc(plan); + ASSERT_FALSE(bool(pyc)); + EXPECT_NE( + llvm::toString(pyc.takeError()).find("feedback contract is unsupported"), + std::string::npos); +} + +TEST(QueueGraphPlanTest, BackendsRejectUncatalogedApplicationBlock) { + QueueGraphPlan plan; + plan.system = "bad_dispatch"; + plan.queues = {{"input", "i64", "/", 1, 1}}; + plan.blocks.push_back({"dispatch", "dispatch", "/", {"input"}, {}}); + auto cpp = generateQueueGraphCpp(plan); + ASSERT_FALSE(bool(cpp)); + EXPECT_NE(llvm::toString(cpp.takeError()) + .find("official opcode has no gfsim lowering: 'dispatch'"), + std::string::npos); + auto pyc = generateQueueGraphPyc(plan); + ASSERT_FALSE(bool(pyc)); + EXPECT_NE(llvm::toString(pyc.takeError()) + .find("official opcode has no PYC lowering: 'dispatch'"), + std::string::npos); +} + +} // namespace +} // namespace acir::codegen diff --git a/components/agentic-circuit/unittests/CodeGen/TestToolchain.h b/components/agentic-circuit/unittests/CodeGen/TestToolchain.h new file mode 100644 index 00000000..bca7bfe3 --- /dev/null +++ b/components/agentic-circuit/unittests/CodeGen/TestToolchain.h @@ -0,0 +1,32 @@ +#ifndef ACIR_UNITTESTS_CODEGEN_TESTTOOLCHAIN_H +#define ACIR_UNITTESTS_CODEGEN_TESTTOOLCHAIN_H + +#include "llvm/ADT/StringRef.h" + +#include +#include + +namespace acir::codegen::test { + +inline std::vector splitToolchainList(llvm::StringRef value) { + std::vector result; + while (!value.empty()) { + auto [item, remainder] = value.split('|'); + if (!item.empty()) + result.push_back(item.str()); + value = remainder; + } + return result; +} + +inline std::vector llvmIncludeDirectories() { + return splitToolchainList(ACIR_TEST_LLVM_INCLUDE_DIRS); +} + +inline std::vector llvmLinkerFlags() { + return splitToolchainList(ACIR_TEST_LLVM_LINK_FLAGS); +} + +} // namespace acir::codegen::test + +#endif // ACIR_UNITTESTS_CODEGEN_TESTTOOLCHAIN_H diff --git a/components/agentic-circuit/unittests/Compiler/CMakeLists.txt b/components/agentic-circuit/unittests/Compiler/CMakeLists.txt new file mode 100644 index 00000000..c0f900b8 --- /dev/null +++ b/components/agentic-circuit/unittests/Compiler/CMakeLists.txt @@ -0,0 +1,8 @@ +find_package(GTest CONFIG REQUIRED) + +add_executable(CompilerTests DriverTest.cpp) +target_link_libraries(CompilerTests PRIVATE + ACIRCompiler + GTest::gtest_main +) +add_test(NAME CompilerTests COMMAND CompilerTests) diff --git a/components/agentic-circuit/unittests/Compiler/DriverTest.cpp b/components/agentic-circuit/unittests/Compiler/DriverTest.cpp new file mode 100644 index 00000000..930157b7 --- /dev/null +++ b/components/agentic-circuit/unittests/Compiler/DriverTest.cpp @@ -0,0 +1,139 @@ +#include "acir/Compiler/Driver.h" + +#include "llvm/Support/Error.h" +#include "gtest/gtest.h" + +#include +#include +#include + +namespace acir::compiler { +namespace { + +constexpr llvm::StringLiteral kValidAcir = R"mlir( +module attributes {ac.contract_epoch = "0.3"} { + ac.system @main root @top as "root" tick 0 "cycle" + workload @top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} selected true + ac.module @top() parameters {} graph { + ac.process @workload kind "workload" { + ac.yield_sim + } + ac.return + } +} +)mlir"; + +CompilerRequest validRequest() { + CompilerRequest request; + request.acirBytes = kValidAcir.str(); + request.profile = CompilerProfile::Fast; + request.stopAfter = CompilerStage::AcsimVerify; + request.emits = {codegen::ArtifactKind::Acir, codegen::ArtifactKind::Acsim}; + return request; +} + +std::vector paths(const CompilerResult &result) { + std::vector found; + for (const CompilerArtifact &artifact : result.artifacts) + found.push_back(artifact.logicalPath); + return found; +} + +std::vector diagnostics(llvm::Error error) { + std::vector found; + llvm::handleAllErrors(std::move(error), [&](const CompilerError &failure) { + found = failure.diagnostics(); + }); + return found; +} + +TEST(CompilerDriverTest, StandardPipelineProducesVerifiedStageArtifacts) { + auto result = runCompiler(validRequest()); + if (!result) { + ADD_FAILURE() << llvm::toString(result.takeError()); + return; + } + EXPECT_EQ(paths(*result), + (std::vector{"frozen.ac.mlir", "model.acsim.mlir"})); + EXPECT_TRUE(result->diagnostics.empty()); + for (const CompilerArtifact &artifact : result->artifacts) { + EXPECT_FALSE(artifact.bytes.empty()); + EXPECT_TRUE(codegen::isValidFingerprint(artifact.sha256)); + } +} + +TEST(CompilerDriverTest, ParseFailureReturnsStableStructuredDiagnostic) { + CompilerRequest request = validRequest(); + request.acirBytes = "not mlir"; + + auto result = runCompiler(request); + ASSERT_FALSE(static_cast(result)); + auto found = diagnostics(result.takeError()); + ASSERT_FALSE(found.empty()); + EXPECT_EQ(found.front().stage, "acir-parse"); + EXPECT_EQ(found.front().code, "ACIR-PARSE-001"); + EXPECT_EQ(found.front().severity, "error"); + EXPECT_FALSE(found.front().message.empty()); +} + +TEST(CompilerDriverTest, CustomProfileRequiresAnExplicitPipeline) { + CompilerRequest request = validRequest(); + request.profile = CompilerProfile::Custom; + + auto result = runCompiler(request); + ASSERT_FALSE(static_cast(result)); + auto found = diagnostics(result.takeError()); + ASSERT_EQ(found.size(), 1u); + EXPECT_EQ(found.front().code, "ACIR-PIPELINE-001"); +} + +TEST(CompilerDriverTest, CustomPipelineRunsInProcess) { + CompilerRequest request = validRequest(); + request.profile = CompilerProfile::Custom; + request.customPipeline = "builtin.module(ac-canonicalize-model)"; + request.stopAfter = CompilerStage::AcirNormalize; + request.emits.clear(); + + auto result = runCompiler(request); + if (!result) { + ADD_FAILURE() << llvm::toString(result.takeError()); + return; + } + EXPECT_TRUE(result->diagnostics.empty()); +} + +TEST(CompilerDriverTest, StageDumpsAreDeterministicAndContentAddressed) { + CompilerRequest request = validRequest(); + request.stopAfter = CompilerStage::AcirFreeze; + request.emits = {codegen::ArtifactKind::Acir}; + request.dumpBefore = {"acir-freeze"}; + request.dumpAfter = {"acir-freeze"}; + + auto first = runCompiler(request); + auto second = runCompiler(request); + ASSERT_TRUE(static_cast(first)); + ASSERT_TRUE(static_cast(second)); + ASSERT_EQ(first->artifacts.size(), 3u); + ASSERT_EQ(second->artifacts.size(), 3u); + for (size_t index = 0; index < first->artifacts.size(); ++index) { + EXPECT_EQ(first->artifacts[index].logicalPath, + second->artifacts[index].logicalPath); + EXPECT_EQ(first->artifacts[index].bytes, second->artifacts[index].bytes); + EXPECT_EQ(first->artifacts[index].sha256, second->artifacts[index].sha256); + } +} + +TEST(CompilerDriverTest, RejectsArtifactBeyondSelectedStopStage) { + CompilerRequest request = validRequest(); + request.stopAfter = CompilerStage::AcirFreeze; + + auto result = runCompiler(request); + ASSERT_FALSE(static_cast(result)); + auto found = diagnostics(result.takeError()); + ASSERT_EQ(found.size(), 1u); + EXPECT_EQ(found.front().code, "ACIR-EMIT-001"); +} + +} // namespace +} // namespace acir::compiler diff --git a/components/agentic-circuit/unittests/Conversion/ACIRToACSimTest.cpp b/components/agentic-circuit/unittests/Conversion/ACIRToACSimTest.cpp new file mode 100644 index 00000000..90705f2b --- /dev/null +++ b/components/agentic-circuit/unittests/Conversion/ACIRToACSimTest.cpp @@ -0,0 +1,248 @@ +#include "acir/Conversion/ACIRToACSim/ACIRToACSim.h" +#include "acir/Dialect/ACIR/ACIRDialect.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACSim/ACSimDialect.h" +#include "acir/Dialect/ACSim/ACSimOps.h" +#include "acir/Transforms/Passes.h" + +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Pass/Pass.h" +#include "mlir/Pass/PassManager.h" + +#include "gtest/gtest.h" + +namespace acir { +namespace { + +llvm::StringRef kFrozenTwoRowModule = R"mlir( +module attributes {ac.contract_epoch = "0.3", ac.freeze_epoch = "0.3", ac.frozen_instrumentation = [], ac.frozen_owners = [{kind = "ac.system_root", owner = @Top, path = "root", stable_id = "root"}, {kind = "ac.instance", owner = @Top::@child, path = "root.child", stable_id = "root/child"}, {kind = "ac.process", owner = @Top::@workload, path = "root.workload", stable_id = "root/workload"}], ac.frozen_primary_workload = {path = "root.workload", reference = @Top::@workload, stable_id = "root/workload"}, ac.frozen_system = @soc, ac.topology_digest = "59ca08ae8bd72b40cca62c78b41f8b004ba40c6b6ab503ccbbdb93a634141b82", ac.topology_frozen = true} { + ac.system @soc root @Top as "root" tick 0 "cycle" workload @Top::@workload seed {kind = "fixed", value = 7 : i64} instrumentation [] results {format = "json", id = "default"} selected true + ac.module @Child() parameters {} graph { + ac.return + } + ac.module @Top() parameters {} graph { + ac.instance @child of @Child() static {} id "child" path "child" {ac.frozen_owners = [{kind = "ac.instance", owner = @Top::@child, path = "root.child", stable_id = "root/child"}]} : () -> () + ac.process @workload kind "workload" { + ac.yield_sim + } {ac.frozen_owners = [{kind = "ac.process", owner = @Top::@workload, path = "root.workload", stable_id = "root/workload"}], ac.frozen_process_skeleton = ["process/r0/b0/o0 ac.yield_sim{}props=<> operands= results= regions="]} + ac.return + } +} +)mlir"; + +llvm::StringRef kAdversarialModuleOrder = R"mlir( +module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @A as "root" tick 0 "cycle" workload @A::@workload seed {kind = "fixed", value = 7 : i64} instrumentation [] results {format = "json", id = "default"} selected true + ac.module @A() parameters {} graph { + ac.instance @child of @Z() static {} id "child" path "child" : () -> () + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + ac.module @Z() parameters {} graph { ac.return } +} +)mlir"; + +llvm::StringRef kCyclicModuleOrder = R"mlir( +module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @A as "root" tick 0 "cycle" workload @A::@workload seed {kind = "fixed", value = 7 : i64} instrumentation [] results {format = "json", id = "default"} selected true + ac.module @A() parameters {} graph { + ac.instance @b of @B() static {} id "b" path "b" : () -> () + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + ac.module @B() parameters {} graph { + ac.instance @a of @A() static {} id "a" path "a" : () -> () + ac.return + } +} +)mlir"; + +llvm::StringRef kAdversarialModuleOrderRenamedPlacement = R"mlir( +module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @A as "root" tick 0 "cycle" workload @A::@workload seed {kind = "fixed", value = 7 : i64} instrumentation [] results {format = "json", id = "default"} selected true + ac.module @A() parameters {} graph { + ac.instance @offspring of @Z() static {} id "offspring" path "offspring" : () -> () + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } + ac.module @Z() parameters {} graph { ac.return } +} +)mlir"; + +llvm::StringRef kInvalidGeneratedModuleName = R"mlir( +module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @"bad-name" as "root" tick 0 "cycle" + workload @"bad-name"::@workload seed {kind = "fixed", value = 7 : i64} + instrumentation [] results {format = "json", id = "default"} selected true + ac.module @"bad-name"() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.return + } +} +)mlir"; + +class ACIRToACSimTest : public ::testing::Test { +protected: + ACIRToACSimTest() { + registry.insert(); + context.appendDialectRegistry(registry); + context.loadAllAvailableDialects(); + } + + mlir::OwningOpRef parseFrozen() { + return mlir::parseSourceString(kFrozenTwoRowModule, + &context); + } + + mlir::DialectRegistry registry; + mlir::MLIRContext context{mlir::MLIRContext::Threading::DISABLED}; +}; + +TEST_F(ACIRToACSimTest, DefaultBoundLowersTwoRowModel) { + auto module = parseFrozen(); + ASSERT_TRUE(module); + ACIRToACSimPassOptions options; + options.profile = "fast"; + options.target = "arm64-apple-darwin"; + mlir::PassManager manager(&context); + manager.addPass(createACIRToACSimPass(options)); + EXPECT_TRUE(mlir::succeeded(manager.run(module.get()))); + auto model = mlir::dyn_cast(module->getBody()->front()); + ASSERT_TRUE(model); + ASSERT_EQ(model.getConstructionOrder().size(), 2u); + EXPECT_EQ( + mlir::cast(model.getConstructionOrder()[0]).getValue(), + "root.child"); + EXPECT_EQ( + mlir::cast(model.getConstructionOrder()[1]).getValue(), + "root.workload"); + for (acsim::DispatchOp dispatch : model.getOps()) + EXPECT_TRUE(dispatch.getPath().starts_with("root.")); +} + +TEST_F(ACIRToACSimTest, CapabilityBoundOverflowIsAtomicDispatchFailure) { + auto module = parseFrozen(); + ASSERT_TRUE(module); + ACIRToACSimPassOptions options; + options.profile = "fast"; + options.target = "arm64-apple-darwin"; + // The frozen model expands to two construction rows; a one-row bound must + // trip the ACLOWER-DISPATCH capability diagnostic before any emission. + options.maxExpandedRows = 1; + + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &diag) { + if (diag.getSeverity() == mlir::DiagnosticSeverity::Error) { + diagnostic += diag.str(); + diagnostic += '\n'; + } + return mlir::success(); + }); + mlir::PassManager manager(&context); + manager.addPass(createACIRToACSimPass(options)); + EXPECT_TRUE(mlir::failed(manager.run(module.get()))); + EXPECT_NE(diagnostic.find("ACLOWER-DISPATCH"), std::string::npos) + << diagnostic; + // Atomicity: the frozen ACIR is untouched, no partial ACSim leaked out. + EXPECT_TRUE(mlir::isa(module->getBody()->front())); + EXPECT_TRUE(module->getBody()->getOps().empty()); +} + +TEST_F(ACIRToACSimTest, CanonicalVerificationFailureDoesNotPublishACSim) { + auto module = mlir::parseSourceString( + kInvalidGeneratedModuleName, &context); + ASSERT_TRUE(module); + mlir::PassManager freezer(&context); + freezer.addPass(createFreezeTopologyPass()); + ASSERT_TRUE(mlir::succeeded(freezer.run(module.get()))); + + ACIRToACSimPassOptions options; + options.profile = "fast"; + options.target = "arm64-apple-darwin"; + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &diag) { + if (diag.getSeverity() == mlir::DiagnosticSeverity::Error) + diagnostic += diag.str(); + return mlir::success(); + }); + mlir::PassManager lowerer(&context); + lowerer.addPass(createACIRToACSimPass(options)); + EXPECT_TRUE(mlir::failed(lowerer.run(module.get()))); + EXPECT_NE(diagnostic.find("canonical C++ identifier"), std::string::npos) + << diagnostic; + EXPECT_FALSE(module->getBody()->getOps().empty()); + EXPECT_TRUE(module->getBody()->getOps().empty()); + EXPECT_TRUE(module->getOperation()->hasAttr("ac.topology_frozen")); +} + +TEST_F(ACIRToACSimTest, ModuleReferencesDoNotDependOnSymbolOrder) { + auto module = mlir::parseSourceString(kAdversarialModuleOrder, + &context); + ASSERT_TRUE(module); + ACIRToACSimPassOptions options; + options.profile = "fast"; + options.target = "arm64-apple-darwin"; + mlir::PassManager freezer(&context); + freezer.addPass(createFreezeTopologyPass()); + ASSERT_TRUE(mlir::succeeded(freezer.run(module.get()))); + mlir::PassManager manager(&context); + manager.addPass(createACIRToACSimPass(options)); + ASSERT_TRUE(mlir::succeeded(manager.run(module.get()))); + auto model = mlir::cast(module->getBody()->front()); + llvm::SmallVector names; + for (acsim::ModuleOp realized : model.getOps()) + names.push_back(realized.getSymName()); + EXPECT_EQ(names, (llvm::SmallVector{"Z", "A"})); +} + +TEST_F(ACIRToACSimTest, ModuleInstantiationCycleHasOwnershipDiagnostic) { + auto module = + mlir::parseSourceString(kCyclicModuleOrder, &context); + ASSERT_TRUE(module); + ACIRToACSimPassOptions options; + options.profile = "fast"; + options.target = "arm64-apple-darwin"; + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &diag) { + if (diag.getSeverity() == mlir::DiagnosticSeverity::Error) + diagnostic += diag.str(); + return mlir::success(); + }); + mlir::PassManager manager(&context); + manager.addPass(createFreezeTopologyPass()); + manager.addPass(createACIRToACSimPass(options)); + EXPECT_TRUE(mlir::failed(manager.run(module.get()))); + EXPECT_NE(diagnostic.find("cycle"), std::string::npos) << diagnostic; +} + +TEST_F(ACIRToACSimTest, ModuleFingerprintIncludesDefiningTopology) { + auto fingerprint = [&](llvm::StringRef source) { + auto module = mlir::parseSourceString(source, &context); + EXPECT_TRUE(module); + mlir::PassManager freezer(&context); + freezer.addPass(createFreezeTopologyPass()); + EXPECT_TRUE(mlir::succeeded(freezer.run(module.get()))); + ACIRToACSimPassOptions options; + options.profile = "fast"; + options.target = "arm64-apple-darwin"; + mlir::PassManager lowerer(&context); + lowerer.addPass(createACIRToACSimPass(options)); + EXPECT_TRUE(mlir::succeeded(lowerer.run(module.get()))); + auto model = mlir::cast(module->getBody()->front()); + for (acsim::ModuleOp realized : model.getOps()) + if (realized.getSymName() == "A") + return realized.getSpecializationFingerprint().str(); + return std::string(); + }; + std::string child = fingerprint(kAdversarialModuleOrder); + std::string offspring = fingerprint(kAdversarialModuleOrderRenamedPlacement); + ASSERT_FALSE(child.empty()); + ASSERT_FALSE(offspring.empty()); + EXPECT_NE(child, offspring); +} + +} // namespace +} // namespace acir diff --git a/components/agentic-circuit/unittests/Conversion/CMakeLists.txt b/components/agentic-circuit/unittests/Conversion/CMakeLists.txt new file mode 100644 index 00000000..1424c5a4 --- /dev/null +++ b/components/agentic-circuit/unittests/Conversion/CMakeLists.txt @@ -0,0 +1,16 @@ +find_package(GTest CONFIG REQUIRED) + +add_executable(ACIRToACSimTests ACIRToACSimTest.cpp) +target_include_directories(ACIRToACSimTests PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +target_link_libraries(ACIRToACSimTests PRIVATE + ACIRToACSim + ACIRDialect + ACSimDialect + LLVMSupport + MLIRParser + MLIRPass + GTest::gtest_main +) +add_test(NAME ACIRToACSimTests COMMAND ACIRToACSimTests) diff --git a/components/agentic-circuit/unittests/Dialect/ACIR/CMakeLists.txt b/components/agentic-circuit/unittests/Dialect/ACIR/CMakeLists.txt new file mode 100644 index 00000000..2c571f44 --- /dev/null +++ b/components/agentic-circuit/unittests/Dialect/ACIR/CMakeLists.txt @@ -0,0 +1,30 @@ +find_package(GTest CONFIG REQUIRED) + +add_executable(ACIRTypesTests TypesTest.cpp) +target_link_libraries(ACIRTypesTests PRIVATE + ACIRDialect + MLIRAsmParser + MLIRIR + GTest::gtest_main +) +add_test(NAME ACIRTypesTests COMMAND ACIRTypesTests) + +add_executable(ACIROpsTests OpsTest.cpp) +target_include_directories(ACIROpsTests PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +target_link_libraries(ACIROpsTests PRIVATE + ACIRDialect + ACIRTransforms + ACSimDialect + MLIRAsmParser + MLIRIndexDialect + MLIRFuncDialect + MLIRSCFDialect + MLIROptLib + MLIRIR + MLIRDataLayoutInterfaces + MLIRSideEffectInterfaces + GTest::gtest_main +) +add_test(NAME ACIROpsTests COMMAND ACIROpsTests) diff --git a/components/agentic-circuit/unittests/Dialect/ACIR/OpsTest.cpp b/components/agentic-circuit/unittests/Dialect/ACIR/OpsTest.cpp new file mode 100644 index 00000000..1c4fdbdd --- /dev/null +++ b/components/agentic-circuit/unittests/Dialect/ACIR/OpsTest.cpp @@ -0,0 +1,2626 @@ +#include "Dialect/ACIR/ACIROpsTestHooks.h" +#include "Dialect/ACIR/ACIRResourcesTestHooks.h" +#include "acir/Analysis/ModelAnalysis.h" +#include "acir/Dialect/ACIR/ACIRDialect.h" +#include "acir/Dialect/ACIR/ACIROps.h" +#include "acir/Dialect/ACIR/ACIRResources.h" +#include "acir/Dialect/ACIR/GraphRegion.h" +#include "acir/InitAllDialects.h" +#include "acir/Transforms/Passes.h" + +#include "mlir/AsmParser/AsmParser.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/DLTI/DLTI.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/DataLayoutInterfaces.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Parser/Parser.h" +#include "mlir/Pass/PassManager.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +namespace acir::ac { +namespace { + +TEST(ACIROpsTest, PublicBuildersConstructEveryTaskFourOperation) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + + auto scope = TypeScopeOp::create(builder, loc, "types"); + builder.setInsertionPointToStart(&scope.getBody().emplaceBlock()); + auto names = builder.getStrArrayAttr({"x"}); + auto field = builder.getDictionaryAttr({ + builder.getNamedAttr("name", builder.getStringAttr("x")), + builder.getNamedAttr("type", mlir::TypeAttr::get(builder.getI8Type())), + }); + auto fields = builder.getArrayAttr({field}); + + EXPECT_TRUE(TypeAliasOp::create(builder, loc, "Byte", builder.getI8Type())); + EXPECT_TRUE(StructOp::create(builder, loc, "S", fields)); + EXPECT_TRUE( + EnumOp::create(builder, loc, "E", builder.getStrArrayAttr({"a"}))); + EXPECT_TRUE(UnionOp::create(builder, loc, "U", fields, "x")); + EXPECT_TRUE(PacketOp::create(builder, loc, "P", fields)); + EXPECT_TRUE(TransactionOp::create(builder, loc, "T", fields)); + + auto input = mlir::UnrealizedConversionCastOp::create( + builder, loc, mlir::TypeRange{builder.getI8Type()}, + mlir::ValueRange{}) + .getResult(0); + auto structRef = mlir::SymbolRefAttr::get( + &context, "types", {mlir::FlatSymbolRefAttr::get(&context, "S")}); + auto packetRef = mlir::SymbolRefAttr::get( + &context, "types", {mlir::FlatSymbolRefAttr::get(&context, "P")}); + auto structType = StructType::get(&context, structRef); + auto packetType = PacketType::get(&context, packetRef); + auto bytesType = VectorType::get(&context, 1, builder.getI8Type()); + auto record = RecordCreateOp::create(builder, loc, structType, + mlir::ValueRange{input}, names); + EXPECT_TRUE(record); + auto get = RecordGetOp::create(builder, loc, builder.getI8Type(), + record.getResult(), "x"); + auto with = RecordWithOp::create(builder, loc, structType, record.getResult(), + input, "x"); + EXPECT_TRUE(get); + EXPECT_TRUE(with); + auto packet = + mlir::UnrealizedConversionCastOp::create( + builder, loc, mlir::TypeRange{packetType}, mlir::ValueRange{}) + .getResult(0); + auto bytes = + PacketSerializeOp::create(builder, loc, bytesType, packet, packetRef); + EXPECT_TRUE(bytes); + auto deserialize = PacketDeserializeOp::create(builder, loc, packetType, + bytes.getResult(), packetRef); + EXPECT_TRUE(deserialize); + EXPECT_TRUE(mlir::isMemoryEffectFree(record.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(get.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(with.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(bytes.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(deserialize.getOperation())); +} + +TEST(ACIROpsTest, QueueEffectsAreAttachedToExactSSAQueueHandles) { + mlir::MLIRContext context; + context.loadDialect(); + auto module = mlir::parseSourceString(R"mlir( + module attributes {ac.contract_epoch = "0.3"} { + %input = ac.source depth 4 latency 1 : !ac.queue + %output = ac.source depth 4 latency 1 : !ac.queue + ac.firing(%input, %output) { + %peeked = ac.queue.peek %input : !ac.queue -> !ac.var + %popped = ac.queue.pop %input : !ac.queue -> !ac.var + ac.queue.push %output, %popped : !ac.queue, !ac.var + ac.firing.yield + } : (!ac.queue, !ac.queue) + ac.sink %output : !ac.queue + } + )mlir", + &context); + ASSERT_TRUE(module); + + auto checkEffect = [&](mlir::Operation *operation, mlir::Value queue, + mlir::MemoryEffects::Effect *expected) { + llvm::SmallVector effects; + mlir::cast(operation).getEffects(effects); + ASSERT_EQ(effects.size(), 1u); + EXPECT_EQ(effects.front().getEffect(), expected); + EXPECT_EQ(effects.front().getValue(), queue); + EXPECT_EQ(effects.front().getResource(), QueueStateResource::get()); + }; + + module->walk([&](QueuePeekOp operation) { + checkEffect(operation, operation.getQueue(), + mlir::MemoryEffects::Read::get()); + }); + module->walk([&](QueuePopOp operation) { + checkEffect(operation, operation.getQueue(), + mlir::MemoryEffects::Write::get()); + }); + module->walk([&](QueuePushOp operation) { + checkEffect(operation, operation.getQueue(), + mlir::MemoryEffects::Write::get()); + }); + module->walk([&](SourceOp operation) { + checkEffect(operation, operation.getOutput(), + mlir::MemoryEffects::Write::get()); + }); + module->walk([&](SinkOp operation) { + checkEffect(operation, operation.getInput(), + mlir::MemoryEffects::Write::get()); + }); +} + +TEST(ACIROpsTest, LayoutAndEffectsAreDeclaredThroughMLIRInterfaces) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto scope = TypeScopeOp::create(builder, loc, "types"); + EXPECT_TRUE(mlir::isa(scope.getOperation())); + mlir::DataLayout layout(scope); + EXPECT_EQ(layout.getTypeSize(builder.getI32Type()).getFixedValue(), 4u); + + builder.setInsertionPointToStart(&scope.getBody().emplaceBlock()); + auto input = mlir::UnrealizedConversionCastOp::create( + builder, loc, mlir::TypeRange{builder.getI8Type()}, mlir::ValueRange{}); + auto structRef = mlir::SymbolRefAttr::get( + &context, "types", {mlir::FlatSymbolRefAttr::get(&context, "S")}); + auto packetRef = mlir::SymbolRefAttr::get( + &context, "types", {mlir::FlatSymbolRefAttr::get(&context, "P")}); + auto structType = StructType::get(&context, structRef); + auto record = RecordCreateOp::create(builder, loc, structType, + mlir::ValueRange{input.getResult(0)}, + builder.getStrArrayAttr({"x"})); + auto packetType = PacketType::get(&context, packetRef); + auto packet = mlir::UnrealizedConversionCastOp::create( + builder, loc, mlir::TypeRange{packetType}, mlir::ValueRange{}); + auto serialized = PacketSerializeOp::create( + builder, loc, VectorType::get(&context, 1, builder.getI8Type()), + packet.getResult(0), packetRef); + EXPECT_TRUE(mlir::isMemoryEffectFree(record.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(serialized.getOperation())); +} + +TEST(ACIROpsTest, ClosedRegistryDoesNotLoadUnrelatedDialects) { + mlir::DialectRegistry registry; + registry.insert(); + mlir::MLIRContext context(registry); + context.loadAllAvailableDialects(); + EXPECT_NE(context.getLoadedDialect("ac"), nullptr); + EXPECT_NE(context.getLoadedDialect("dlti"), nullptr); + EXPECT_EQ(context.getLoadedDialect("arith"), nullptr); + EXPECT_EQ(context.getLoadedDialect("scf"), nullptr); +} + +TEST(ACIROpsTest, QualifiedNamedTypesImplementDataLayout) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::Type type = mlir::parseType("!ac.struct<@types::@S>", &context); + ASSERT_TRUE(type); + EXPECT_TRUE(mlir::isa(type)); +} + +TEST(ACIROpsTest, NamedAggregateLayoutUsesExactDLTIEntry) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto scope = TypeScopeOp::create(builder, loc, "types"); + auto reference = mlir::SymbolRefAttr::get( + &context, "types", {mlir::FlatSymbolRefAttr::get(&context, "S")}); + auto type = StructType::get(&context, reference); + auto metadata = builder.getDictionaryAttr({ + builder.getNamedAttr("size", builder.getI64IntegerAttr(12)), + builder.getNamedAttr("abi_alignment", builder.getI64IntegerAttr(4)), + builder.getNamedAttr("preferred_alignment", builder.getI64IntegerAttr(8)), + builder.getNamedAttr("endianness", builder.getStringAttr("big")), + }); + auto entry = mlir::DataLayoutEntryAttr::get(type, metadata); + auto spec = + mlir::DataLayoutSpecAttr::get(&context, mlir::DataLayoutEntryList{entry}); + scope->setAttr(mlir::DLTIDialect::kDataLayoutAttrName, spec); + + mlir::DataLayout layout(scope); + EXPECT_EQ(layout.getTypeSize(type).getFixedValue(), 12u); + EXPECT_EQ(layout.getTypeABIAlignment(type), 4u); + EXPECT_EQ(layout.getTypePreferredAlignment(type), 8u); + auto queried = spec.query(mlir::DataLayoutEntryKey(type)); + ASSERT_TRUE(mlir::succeeded(queried)); + EXPECT_EQ(mlir::cast(*queried) + .getAs("endianness") + .getValue(), + "big"); +} + +TEST(ACIROpsTest, PublicRegistryIncludesOnlyRequiredDLTIDependency) { + mlir::DialectRegistry registry; + acir::registerAllDialects(registry); + mlir::MLIRContext context(registry); + context.loadAllAvailableDialects(); + EXPECT_NE(context.getLoadedDialect("dlti"), nullptr); +} + +TEST(ACIROpsTest, PublicBuildersConstructEveryTaskFiveOperation) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + + auto protocol = ProtocolOp::create(builder, loc, "p"); + builder.setInsertionPointToStart(&protocol.getBody().emplaceBlock()); + auto roleA = RoleOp::create(builder, loc, "a", "b", "exclusive"); + EXPECT_TRUE(roleA); + auto roleB = RoleOp::create(builder, loc, "b", "a", "exclusive"); + auto state = StateOp::create(builder, loc, "s", true, false); + auto event = EventOp::create(builder, loc, "e", "a", "b", builder.getI8Type(), + "notify"); + auto transition = + TransitionOp::create(builder, loc, "s", "s", "e", nullptr, false, false); + transition.getGuard().emplaceBlock(); + EXPECT_TRUE(transition); + auto guarantee = GuaranteeOp::create(builder, loc, "ordering", + builder.getStringAttr("fifo")); + + builder.setInsertionPointAfter(protocol); + auto interface = InterfaceOp::create(builder, loc, "I"); + builder.setInsertionPointToStart(&interface.getBody().emplaceBlock()); + EXPECT_TRUE(RoleOp::create(builder, loc, "a", "b", "exclusive")); + EXPECT_TRUE(RoleOp::create(builder, loc, "b", "a", "exclusive")); + auto channel = ChannelType::get(&context, builder.getI8Type(), + mlir::FlatSymbolRefAttr::get(&context, "p")); + auto port = PortOp::create(builder, loc, "data", channel, "a", "b", "a", "b"); + EXPECT_TRUE(port); + EXPECT_EQ(port.getProtocolFromAttr().getValue(), "a"); + EXPECT_EQ(port.getProtocolToAttr().getValue(), "b"); + EXPECT_TRUE(mlir::isa(protocol.getOperation())); + EXPECT_TRUE( + mlir::isa(interface.getOperation())); + EXPECT_TRUE(mlir::isa(protocol.getOperation())); + EXPECT_TRUE(mlir::isa(interface.getOperation())); + EXPECT_TRUE(mlir::isa(protocol.getOperation())); + EXPECT_TRUE(mlir::isa(interface.getOperation())); + EXPECT_TRUE(mlir::isa(roleA.getOperation())); + EXPECT_TRUE(mlir::isa(roleB.getOperation())); + EXPECT_TRUE(mlir::isa(state.getOperation())); + EXPECT_TRUE(mlir::isa(event.getOperation())); + EXPECT_TRUE(mlir::isa(port.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(roleA.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(roleB.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(state.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(event.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(guarantee.getOperation())); + EXPECT_TRUE(mlir::isMemoryEffectFree(port.getOperation())); + EXPECT_TRUE(mlir::succeeded(mlir::verify(module))); +} + +TEST(ACIROpsTest, RegistryContainsExactQueueVarOperations) { + mlir::MLIRContext context; + context.loadDialect(); + const std::array names = { + "ac.interface", "ac.protocol", "ac.role", "ac.state", + "ac.event", "ac.transition", "ac.guarantee", "ac.port", + }; + for (llvm::StringLiteral name : names) + EXPECT_TRUE(mlir::OperationName(name, &context).isRegistered()) + << name.str(); + EXPECT_FALSE(mlir::OperationName("ac.connect", &context).isRegistered()); + EXPECT_FALSE(mlir::OperationName("ac.ready_valid", &context).isRegistered()); + + std::vector actual; + for (mlir::RegisteredOperationName operation : + context.getRegisteredOperationsByDialect("ac")) + actual.push_back(operation.getStringRef().str()); + llvm::sort(actual); + std::vector expected = { + "ac.array", + "ac.address_map", + "ac.address_space", + "ac.assert", + "ac.barrier", + "ac.broadcast", + "ac.credit", + "ac.credit.yield", + "ac.dependency", + "ac.dependency.yield", + "ac.enum", + "ac.ensure", + "ac.event", + "ac.event_queue", + "ac.expect", + "ac.expect.yield", + "ac.feedback", + "ac.feedback.yield", + "ac.firing", + "ac.firing.yield", + "ac.fork", + "ac.guarantee", + "ac.interface", + "ac.instance", + "ac.instances", + "ac.instrumentation", + "ac.module", + "ac.module.extern", + "ac.module.generated", + "ac.merge", + "ac.memory.instance", + "ac.memory.request", + "ac.memory.yield", + "ac.reorder", + "ac.reorder.yield", + "ac.observe", + "ac.packet", + "ac.packet.deserialize", + "ac.packet.serialize", + "ac.port", + "ac.probe", + "ac.process", + "ac.protocol", + "ac.queue", + "ac.queue.peek", + "ac.queue.pop", + "ac.queue.push", + "ac.record.create", + "ac.record.get", + "ac.record.with", + "ac.var.add", + "ac.var.constant", + "ac.var.cmp", + "ac.var.get", + "ac.var.mul", + "ac.var.popcount", + "ac.var.sub", + "ac.var.with", + "ac.require", + "ac.return", + "ac.resource", + "ac.route", + "ac.route.yield", + "ac.role", + "ac.scope", + "ac.scope.yield", + "ac.select", + "ac.select.yield", + "ac.sink", + "ac.source", + "ac.state", + "ac.stat", + "ac.stat.add", + "ac.struct", + "ac.system", + "ac.time_domain", + "ac.transaction", + "ac.transition", + "ac.transform", + "ac.transform.yield", + "ac.trace.decode", + "ac.trace.eof", + "ac.trace.next", + "ac.trace.open", + "ac.trace.position", + "ac.try_recv", + "ac.try_send", + "ac.type_alias", + "ac.type_scope", + "ac.union", + "ac.view", + "ac.await_event", + "ac.schedule", + "ac.wait_for", + "ac.wait_until", + "ac.yield_sim", + }; + llvm::sort(expected); + EXPECT_EQ(actual, expected); +} + +TEST(ACIROpsTest, PublicBuildersConstructEveryTaskSixOperation) { + mlir::MLIRContext context; + context.loadDialect(); + getStructuralProviderRegistry(&context).registerExternal("Leaf"); + getStructuralProviderRegistry(&context).registerGenerator("Generated"); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto file = mlir::ModuleOp::create(loc); + file->setAttr("ac.contract_epoch", builder.getStringAttr("0.3")); + builder.setInsertionPointToStart(file.getBody()); + + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto binding = builder.getDictionaryAttr({ + builder.getNamedAttr("registry", builder.getStringAttr("cpp")), + builder.getNamedAttr("name", builder.getStringAttr("Leaf")), + }); + auto generator = builder.getDictionaryAttr({ + builder.getNamedAttr("registry", builder.getStringAttr("ac")), + builder.getNamedAttr("name", builder.getStringAttr("Generated")), + }); + auto leaf = + ModuleExternOp::create(builder, loc, "Leaf", emptyType, + mlir::StringAttr(), emptyDictionary, binding); + auto generated = + ModuleGeneratedOp::create(builder, loc, "Generated", emptyType, + mlir::StringAttr(), emptyDictionary, generator); + EXPECT_TRUE(leaf); + EXPECT_TRUE(generated); + + auto top = ModuleOp::create(builder, loc, "Top", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + auto instance = + InstanceOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, + "Leaf", "child", "child", "child", emptyDictionary); + auto array = ArrayOp::create( + builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "Leaf", "lanes", + "lanes", "lanes", llvm::ArrayRef{2}, + builder.getArrayAttr({emptyDictionary, emptyDictionary})); + auto definitions = builder.getArrayAttr({ + mlir::FlatSymbolRefAttr::get(&context, "Leaf"), + mlir::FlatSymbolRefAttr::get(&context, "Generated"), + }); + auto instances = InstancesOp::create( + builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "mixed", "mixed", + "mixed", definitions, builder.getStrArrayAttr({"a", "b"}), + builder.getStrArrayAttr({"mix-a", "mix-b"}), + builder.getStrArrayAttr({"mix_a", "mix_b"}), emptyType, + builder.getArrayAttr({emptyDictionary, emptyDictionary})); + auto view = ViewOp::create( + builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "view", + "permutation", + builder.getArrayAttr({mlir::FlatSymbolRefAttr::get(&context, "child")}), + builder.getArrayAttr({builder.getDenseI64ArrayAttr({0})}), + mlir::IntegerAttr(), llvm::ArrayRef{}, + llvm::ArrayRef{0}); + auto returnOp = ReturnOp::create(builder, loc, mlir::ValueRange{}); + EXPECT_TRUE(instance); + EXPECT_TRUE(array); + EXPECT_TRUE(instances); + EXPECT_TRUE(view); + EXPECT_TRUE(returnOp); + + builder.setInsertionPointToStart(file.getBody()); + auto seedPolicy = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }); + auto resultSchema = builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("default")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }); + auto system = SystemOp::create(builder, loc, "soc", "Top", "root", 0, "cycle", + mlir::FlatSymbolRefAttr(), seedPolicy, + builder.getArrayAttr({}), resultSchema, true); + EXPECT_TRUE(system); + EXPECT_TRUE(mlir::isa(top.getOperation())); + EXPECT_TRUE(mlir::isa(top.getOperation())); + EXPECT_TRUE(mlir::succeeded(mlir::verify(file))); + EXPECT_TRUE(mlir::succeeded(verifyGraphStructure(file))); +} + +TEST(ACIROpsTest, TaskSixRegistryDeltaIsExactlyNineGraphOperations) { + mlir::MLIRContext context; + context.loadDialect(); + const std::array names = { + "ac.system", "ac.module", "ac.module.extern", + "ac.module.generated", "ac.instance", "ac.array", + "ac.instances", "ac.view", "ac.return", + }; + for (llvm::StringLiteral name : names) + EXPECT_TRUE(mlir::OperationName(name, &context).isRegistered()) + << name.str(); + EXPECT_FALSE(mlir::OperationName("ac.connect", &context).isRegistered()); + EXPECT_FALSE(mlir::OperationName("ac.freeze", &context).isRegistered()); +} + +TEST(ACIROpsTest, PublicBuildersConstructEveryTaskEightOperation) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto file = mlir::parseSourceString(R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.protocol @fifo { + ac.role @sender dual @receiver cardinality "exclusive" + ac.role @receiver dual @sender cardinality "exclusive" + ac.state @idle initial true terminal false + ac.state @done initial false terminal true + ac.event @push from @sender to @receiver payload i32 action "offer" + ac.transition from @idle to @done on @push transfer true retain false guard {} + } + ac.module @M(i32) parameters {} graph { + ^bb0(%input : i32): + ac.time_domain @clock period 1 phase 0 scale 1 + ac.queue @q payload i32 entries 4 ordering "fifo" protocol @fifo + ownership "exclusive" id "q" path "q" + ac.event_queue @events payload !ac.event capacity 4 + ordering "time_then_sequence" domain @clock id "events" path "events" + ac.resource @resource capacity 1 issue_width 1 ii 1 + latency {kind = "fixed", ticks = 1 : i64} + lifecycle {reservation = "propose_commit", release = "balanced", cancellation = "explicit"} + ownership "exclusive" classes [] id "resource" path "resource" + ac.address_space @memory width 32 unit "byte" id "memory" path "memory" + ac.stat @count kind "counter" + ac.process @worker kind "workload" captures(%input : i32) { + ^bb0(%value : i32): + ac.yield_sim + } + ac.return + } + } + )mlir", + &context); + ASSERT_TRUE(file); + ModuleOp module = *file->getOps().begin(); + builder.setInsertionPoint(&module.getBody().front().back()); + StatOp stat = *module.getBody().front().getOps().begin(); + auto process = + ProcessOp::create(builder, loc, "p", "control", mlir::ValueRange{}); + auto *body = &process.getBody().emplaceBlock(); + builder.setInsertionPointToStart(body); + auto i1 = + mlir::arith::ConstantOp::create(builder, loc, builder.getBoolAttr(true)); + auto i32 = mlir::arith::ConstantOp::create(builder, loc, + builder.getI32IntegerAttr(7)); + auto i64 = mlir::arith::ConstantOp::create(builder, loc, + builder.getI64IntegerAttr(1)); + auto send = TrySendOp::create(builder, loc, builder.getI1Type(), i32, "q"); + auto recv = TryRecvOp::create(builder, loc, builder.getI32Type(), + builder.getI1Type(), "q"); + EXPECT_TRUE(send && recv); + auto schedule = ScheduleOp::create(builder, loc, i32, i64, "worker"); + auto waitUntil = WaitUntilOp::create(builder, loc, i1); + auto waitFor = WaitForOp::create(builder, loc, "resource"); + auto awaitEvent = AwaitEventOp::create(builder, loc, "events"); + EXPECT_TRUE(schedule && waitUntil && waitFor && awaitEvent); + auto cursor = + TraceOpenOp::create(builder, loc, builder.getIndexType(), "pto"); + EXPECT_EQ(cursor.getSource(), "pto"); + auto next = TraceNextOp::create(builder, loc, builder.getIndexType(), + builder.getI32Type(), builder.getI1Type(), + cursor, "pto"); + auto decoded = TraceDecodeOp::create(builder, loc, builder.getI64Type(), + next.getEntry()); + auto position = TracePositionOp::create(builder, loc, builder.getIndexType(), + next.getCursor(), "pto"); + auto eof = TraceEofOp::create(builder, loc, builder.getI1Type(), + next.getCursor(), "pto"); + EXPECT_TRUE(decoded && eof); + auto runtimeRequire = RequireOp::create(builder, loc, i1, "require"); + EXPECT_TRUE(runtimeRequire); + auto runtimeEnsure = EnsureOp::create(builder, loc, i1, "ensure"); + auto runtimeAssert = AssertOp::create(builder, loc, i1, "assert"); + EXPECT_TRUE(runtimeEnsure && runtimeAssert); + auto probe = + ProbeOp::create(builder, loc, builder.getI32Type(), "q", "queue"); + auto storageProbe = + ProbeOp::create(builder, loc, builder.getI64Type(), "memory", "storage"); + auto statAdd = StatAddOp::create(builder, loc, probe, "count"); + EXPECT_TRUE(statAdd && storageProbe); + auto instrumentation = InstrumentationOp::create(builder, loc, "debug"); + instrumentation.getBody().emplaceBlock(); + auto yield = YieldSimOp::create(builder, loc); + EXPECT_TRUE(yield); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); + std::string printed; + llvm::raw_string_ostream(printed) << *file; + auto reparsed = mlir::parseSourceString(printed, &context); + ASSERT_TRUE(reparsed); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*reparsed))); + + auto effectsOf = [](mlir::Operation *operation) { + llvm::SmallVector effects; + mlir::cast(operation).getEffects(effects); + return effects; + }; + struct ExpectedEffect { + bool write; + mlir::SideEffects::Resource *resource; + mlir::SymbolRefAttr symbol; + mlir::DictionaryAttr parameters; + }; + auto expectedEffect = [&](bool write, mlir::SideEffects::Resource *resource, + llvm::StringRef identity, llvm::StringRef kind, + llvm::StringRef contractPhase = "") { + llvm::SmallVector parameters = { + builder.getNamedAttr("identity_phase", + builder.getStringAttr("definition_pre_freeze")), + builder.getNamedAttr("owner_kind", builder.getStringAttr(kind)), + builder.getNamedAttr("identity", builder.getStringAttr(identity)), + }; + if (!contractPhase.empty()) + parameters.push_back(builder.getNamedAttr( + "contract_phase", builder.getStringAttr(contractPhase))); + return ExpectedEffect{ + write, resource, + mlir::SymbolRefAttr::get( + &context, "M", {mlir::FlatSymbolRefAttr::get(&context, identity)}), + builder.getDictionaryAttr(parameters)}; + }; + auto expectExactEffects = + [&](mlir::Operation *operation, + std::initializer_list expected) { + auto actual = effectsOf(operation); + ASSERT_EQ(actual.size(), expected.size()) + << operation->getName().getStringRef().str(); + llvm::SmallVector matched(actual.size()); + for (const ExpectedEffect &want : expected) { + bool found = false; + for (auto [index, got] : llvm::enumerate(actual)) { + bool effectMatches = + want.write + ? mlir::isa(got.getEffect()) + : mlir::isa(got.getEffect()); + if (!matched[index] && effectMatches && + got.getResource() == want.resource && + got.getSymbolRef() == want.symbol && + got.getParameters() == want.parameters) { + matched[index] = true; + found = true; + break; + } + } + EXPECT_TRUE(found) + << operation->getName().getStringRef().str() << " missing exact " + << (want.write ? "write" : "read") << " effect"; + } + }; + auto read = [&](mlir::SideEffects::Resource *resource, + llvm::StringRef identity, llvm::StringRef kind, + llvm::StringRef contractPhase = "") { + return expectedEffect(false, resource, identity, kind, contractPhase); + }; + auto write = [&](mlir::SideEffects::Resource *resource, + llvm::StringRef identity, llvm::StringRef kind, + llvm::StringRef contractPhase = "") { + return expectedEffect(true, resource, identity, kind, contractPhase); + }; + + for (mlir::Operation *operation : {send.getOperation(), recv.getOperation()}) + expectExactEffects(operation, + {read(QueueStateResource::get(), "q", "queue"), + write(QueueStateResource::get(), "q", "queue"), + read(ProtocolStateResource::get(), "q", "protocol"), + write(ProtocolStateResource::get(), "q", "protocol")}); + expectExactEffects( + schedule, + {write(ModuleStateResource::get(), "worker", "module"), + write(EventQueueStateResource::get(), "worker", "event_queue")}); + expectExactEffects(waitUntil, + {read(EventQueueStateResource::get(), "p", "event_queue"), + write(ModuleStateResource::get(), "p", "module")}); + expectExactEffects( + waitFor, {read(ReservationStateResource::get(), "resource", "resource"), + write(ModuleStateResource::get(), "p", "module")}); + expectExactEffects( + awaitEvent, + {read(EventQueueStateResource::get(), "events", "event_queue"), + write(ModuleStateResource::get(), "p", "module")}); + expectExactEffects(yield, {write(ModuleStateResource::get(), "p", "module")}); + expectExactEffects(cursor, + {read(ExternalIOResource::get(), "p/pto", "external_io"), + write(TracePositionResource::get(), "p/pto", "trace")}); + expectExactEffects(next, + {read(TracePositionResource::get(), "p/pto", "trace"), + write(TracePositionResource::get(), "p/pto", "trace"), + read(ExternalIOResource::get(), "p/pto", "external_io")}); + for (mlir::Operation *operation : + {position.getOperation(), eof.getOperation()}) + expectExactEffects(operation, + {read(TracePositionResource::get(), "p/pto", "trace")}); + for (mlir::Operation *operation : + {runtimeRequire.getOperation(), runtimeEnsure.getOperation(), + runtimeAssert.getOperation()}) + expectExactEffects(operation, {write(ExternalIOResource::get(), "p", + "contract", "runtime")}); + expectExactEffects(probe, {read(QueueStateResource::get(), "q", "queue")}); + expectExactEffects(storageProbe, + {read(StorageStateResource::get(), "memory", "storage")}); + expectExactEffects(stat, + {write(StatisticsResource::get(), "count", "statistics")}); + expectExactEffects(statAdd, + {read(StatisticsResource::get(), "count", "statistics"), + write(StatisticsResource::get(), "count", "statistics")}); + EXPECT_TRUE(mlir::isMemoryEffectFree(decoded)); + EXPECT_TRUE(mlir::isa(*instrumentation)); + EXPECT_FALSE(mlir::isMemoryEffectFree(process)); + EXPECT_FALSE(mlir::isMemoryEffectFree(send)); + EXPECT_FALSE(mlir::isMemoryEffectFree(next)); + EXPECT_FALSE(mlir::isMemoryEffectFree(position)); + EXPECT_FALSE(mlir::isMemoryEffectFree(eof)); + EXPECT_FALSE(mlir::isMemoryEffectFree(statAdd)); +} + +TEST(ACIROpsTest, UnresolvedRuntimeReferencesDoNotInventEffects) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + auto module = + ModuleOp::create(builder, loc, "M", builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + auto process = + ProcessOp::create(builder, loc, "p", "control", mlir::ValueRange{}); + builder.setInsertionPointToStart(&process.getBody().emplaceBlock()); + auto value = mlir::arith::ConstantOp::create(builder, loc, + builder.getI32IntegerAttr(1)); + auto send = + TrySendOp::create(builder, loc, builder.getI1Type(), value, "missing"); + llvm::SmallVector effects; + mlir::cast(*send).getEffects(effects); + EXPECT_TRUE(effects.empty()); +} + +TEST(ACIROpsTest, RuntimeAndQueueVarRegistryIsExact) { + mlir::MLIRContext context; + context.loadDialect(); + const std::array names = { + "ac.process", "ac.try_send", "ac.try_recv", + "ac.schedule", "ac.wait_until", "ac.wait_for", + "ac.await_event", "ac.yield_sim", "ac.trace.open", + "ac.trace.next", "ac.trace.decode", "ac.trace.eof", + "ac.trace.position", "ac.require", "ac.ensure", + "ac.assert", "ac.probe", "ac.stat", + "ac.stat.add", "ac.instrumentation", + }; + for (llvm::StringLiteral name : names) + EXPECT_TRUE(mlir::OperationName(name, &context).isRegistered()) + << name.str(); + EXPECT_FALSE(mlir::OperationName("ac.try_issue", &context).isRegistered()); + EXPECT_FALSE(mlir::OperationName("ac.connect", &context).isRegistered()); + const std::array queueVarNames = { + "ac.transform", + "ac.transform.yield", + "ac.firing", + "ac.firing.yield", + "ac.queue.peek", + "ac.queue.pop", + "ac.queue.push", + "ac.source", + "ac.sink", + "ac.var.constant", + "ac.var.add", + "ac.var.sub", + "ac.var.mul", + "ac.var.popcount", + "ac.var.get", + "ac.var.with", + "ac.scope", + "ac.scope.yield", + "ac.broadcast", + "ac.route", + "ac.route.yield", + "ac.select", + "ac.select.yield", + "ac.fork", + "ac.merge", + "ac.barrier", + "ac.dependency", + "ac.dependency.yield", + "ac.credit", + "ac.credit.yield", + "ac.memory.instance", + "ac.memory.request", + "ac.memory.yield", + "ac.reorder", + "ac.reorder.yield", + "ac.feedback", + "ac.feedback.yield", + "ac.observe", + "ac.expect", + "ac.expect.yield", + "ac.var.cmp", + }; + for (llvm::StringLiteral name : queueVarNames) + EXPECT_TRUE(mlir::OperationName(name, &context).isRegistered()) + << name.str(); + EXPECT_EQ(context.getRegisteredOperationsByDialect("ac").size(), 96u); +} + +TEST(ACIROpsTest, ProcessLinearLivenessDoesNotRescanBlockPerValue) { + mlir::MLIRContext context; + context.loadDialect(); + + auto buildProcess = [&](unsigned valueCount) { + std::string source; + llvm::raw_string_ostream os(source); + os << "builtin.module attributes {ac.contract_epoch = \"0.3\"} {\n" + " ac.module @Scale() parameters {} graph {\n" + " ac.process @worker kind \"control\" {\n"; + for (unsigned index = 0; index != valueCount; ++index) + os << " %cursor" << index << " = ac.trace.open source \"source" + << index << "\"\n" + << " %next" << index << ", %value" << index << ", %advanced" + << index << " = ac.trace.next %cursor" << index + << " from source \"source" << index + << "\" : !ac.resource_token<@resource>\n"; + for (unsigned index = 0; index != valueCount; ++index) + os << " %decoded" << index << " = ac.trace.decode %value" << index + << " : !ac.resource_token<@resource> to i64\n"; + os << " ac.yield_sim\n" + " }\n" + " ac.return\n" + " }\n" + "}\n"; + auto file = mlir::parseSourceString(source, &context); + EXPECT_TRUE(file); + return file; + }; + auto measureWork = [&](unsigned valueCount) { + auto file = buildProcess(valueCount); + if (!file) + return detail::ProcessLivenessWork{}; + ProcessOp process; + file->walk([&](ProcessOp candidate) { process = candidate; }); + EXPECT_TRUE(process); + detail::ProcessLivenessWork work; + { + detail::ScopedProcessLivenessWorkCollector collector(work); + EXPECT_TRUE(mlir::succeeded(process.verify())); + } + return work; + }; + + auto expectSinglePassWork = [](const detail::ProcessLivenessWork &work, + uint64_t valueCount) { + // Each fixture value contributes trace.open, trace.next, and trace.decode. + // The only fixed operation is the terminating ac.yield_sim. + uint64_t operationCount = valueCount * 3 + 1; + EXPECT_EQ(work.summaryOperationVisits, operationCount); + EXPECT_EQ(work.epochOperationVisits, operationCount); + EXPECT_EQ(work.livenessOperationVisits, operationCount); + EXPECT_EQ(work.valueVisits, valueCount * 5); + EXPECT_EQ(work.useVisits, valueCount); + EXPECT_EQ(work.total(), valueCount * 15 + 3); + }; + + constexpr unsigned smallSize = 64; + constexpr unsigned largeSize = 256; + detail::ProcessLivenessWork smallWork = measureWork(smallSize); + detail::ProcessLivenessWork largeWork = measureWork(largeSize); + RecordProperty("small_values", smallSize); + RecordProperty("large_values", largeSize); + RecordProperty("small_work_units", smallWork.total()); + RecordProperty("large_work_units", largeWork.total()); + expectSinglePassWork(smallWork, smallSize); + expectSinglePassWork(largeWork, largeSize); + EXPECT_EQ(largeWork.total() - smallWork.total(), + uint64_t{15} * (largeSize - smallSize)); +} + +TEST(ACIROpsTest, LargeArrayVerificationIsDeterministic) { + mlir::MLIRContext context; + context.loadDialect(); + getStructuralProviderRegistry(&context).registerExternal("Leaf"); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto binding = builder.getDictionaryAttr({ + builder.getNamedAttr("registry", builder.getStringAttr("cpp")), + builder.getNamedAttr("name", builder.getStringAttr("Leaf")), + }); + ModuleExternOp::create(builder, loc, "Leaf", emptyType, mlir::StringAttr(), + emptyDictionary, binding); + auto top = ModuleOp::create(builder, loc, "Top", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + constexpr int64_t elementCount = 4096; + llvm::SmallVector arguments(elementCount, emptyDictionary); + ArrayOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "Leaf", + "large", "large", "large", llvm::ArrayRef{64, 64}, + builder.getArrayAttr(arguments)); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + EXPECT_TRUE(mlir::succeeded(mlir::verify(file))); + EXPECT_TRUE(mlir::succeeded(mlir::verify(file))); + EXPECT_TRUE(mlir::succeeded(verifyGraphStructure(file))); + EXPECT_EQ(buildArrayElementPath("root.large", {1, 2, 3}), + "root.large[1][2][3]"); +} + +TEST(ACIROpsTest, ModulePortMetadataPrintsAndReparsesCanonically) { + mlir::MLIRContext context; + context.loadDialect(); + constexpr llvm::StringLiteral source = R"mlir( + ac.module @M(%x : i32 {ac.port_name = "input"}) + -> (i32 {ac.port_name = "output"}) parameters {} + attributes {ac.graph_label = "graph"} graph { + ac.return %x : i32 + } + )mlir"; + auto module = mlir::parseSourceString(source, &context); + ASSERT_TRUE(module); + auto operation = *module->getOps().begin(); + ASSERT_TRUE(operation.getArgAttrsAttr()); + ASSERT_TRUE(operation.getResAttrsAttr()); + std::string printed; + llvm::raw_string_ostream(printed) << *module; + auto reparsed = mlir::parseSourceString(printed, &context); + ASSERT_TRUE(reparsed); + auto reparsedOperation = *reparsed->getOps().begin(); + EXPECT_EQ(operation.getArgAttrsAttr(), reparsedOperation.getArgAttrsAttr()); + EXPECT_EQ(operation.getResAttrsAttr(), reparsedOperation.getResAttrsAttr()); + EXPECT_EQ(operation->getAttr("ac.graph_label"), + reparsedOperation->getAttr("ac.graph_label")); +} + +TEST(ACIROpsTest, StructuralProvidersAreContextOwnedAndExact) { + mlir::MLIRContext context; + context.loadDialect(); + auto &providers = getStructuralProviderRegistry(&context); + providers.registerExternal("test.external"); + providers.registerGenerator("test.generator"); + EXPECT_TRUE(providers.hasExternal("test.external")); + EXPECT_TRUE(providers.hasGenerator("test.generator")); + EXPECT_FALSE(providers.hasExternal("test.generator")); + EXPECT_FALSE(providers.hasGenerator("test.external")); +} + +TEST(ACIROpsTest, StaticUnitDictionaryUsesClosedACIRUnitSet) { + mlir::MLIRContext context; + mlir::Builder builder(&context); + auto unitValue = [&](llvm::StringRef unit) { + return builder.getDictionaryAttr({ + builder.getNamedAttr("value", builder.getI64IntegerAttr(1)), + builder.getNamedAttr("unit", builder.getStringAttr(unit)), + }); + }; + for (llvm::StringRef unit : + {"ticks", "cycles", "seconds", "milliseconds", "microseconds", + "nanoseconds", "picoseconds", "bytes", "bits", "entries", "packets", + "transactions"}) + EXPECT_TRUE(isConcreteStaticValue(unitValue(unit))) << unit.str(); + EXPECT_FALSE(isConcreteStaticValue(unitValue("bananas"))); + EXPECT_FALSE(isConcreteStaticValue(unitValue(""))); +} + +TEST(ACIROpsTest, HierarchyDepthAndOwnerBudgetsRejectCompactGraphs) { + auto buildGraph = [](mlir::MLIRContext &context, unsigned moduleCount, + unsigned fanout, llvm::StringRef prefix) { + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto file = mlir::ModuleOp::create(loc); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + builder.setInsertionPointToStart(file.getBody()); + for (unsigned index = 0; index != moduleCount; ++index) { + std::string name = (prefix + std::to_string(index)).str(); + auto module = + ModuleOp::create(builder, loc, name, emptyType, emptyDictionary); + builder.setInsertionPointToStart(module.addEntryBlock()); + if (index + 1 != moduleCount) { + std::string target = (prefix + std::to_string(index + 1)).str(); + for (unsigned child = 0; child != fanout; ++child) { + std::string segment = "child" + std::to_string(child); + InstanceOp::create(builder, loc, mlir::TypeRange{}, + mlir::ValueRange{}, target, segment, segment, + segment, emptyDictionary); + } + } + ReturnOp::create(builder, loc, mlir::ValueRange{}); + builder.setInsertionPointToEnd(file.getBody()); + } + auto seed = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }); + auto results = builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("budget")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }); + std::string root = (prefix + "0").str(); + SystemOp::create(builder, loc, "budget", root, "root", 0, "cycle", + mlir::FlatSymbolRefAttr(), seed, builder.getArrayAttr({}), + results, true); + return file; + }; + + mlir::MLIRContext context; + context.loadDialect(); + llvm::SmallVector diagnostics; + mlir::ScopedDiagnosticHandler handler( + &context, [&](mlir::Diagnostic &diagnostic) { + std::string text; + llvm::raw_string_ostream(text) << diagnostic; + diagnostics.push_back(std::move(text)); + return mlir::success(); + }); + auto depthBoundary = buildGraph(context, 1025, 1, "Boundary"); + EXPECT_TRUE(mlir::succeeded(verifyGraphStructure(depthBoundary))); + diagnostics.clear(); + auto tooDeep = buildGraph(context, 1026, 1, "Depth"); + EXPECT_TRUE(mlir::failed(verifyGraphStructure(tooDeep))); + EXPECT_TRUE(llvm::any_of(diagnostics, [](llvm::StringRef diagnostic) { + return diagnostic.contains("hierarchy depth exceeds bound 1024"); + })); + diagnostics.clear(); + auto tooWide = buildGraph(context, 21, 2, "Wide"); + EXPECT_TRUE(mlir::failed(verifyGraphStructure(tooWide))); + EXPECT_TRUE(llvm::any_of(diagnostics, [](llvm::StringRef diagnostic) { + return diagnostic.contains("owner count exceeds bound 1048576"); + })); + diagnostics.clear(); + auto veryDeep = buildGraph(context, 20001, 1, "VeryDeep"); + EXPECT_TRUE(mlir::failed(verifyGraphStructure(veryDeep))); + ASSERT_EQ(diagnostics.size(), 1u); + EXPECT_TRUE(llvm::StringRef(diagnostics.front()) + .contains("hierarchy depth exceeds bound 1024")); +} + +TEST(ACIROpsTest, NestedArraysCountTaskSevenOwnersBeforeElaboration) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + + auto leaf = + ModuleOp::create(builder, loc, "Leaf", emptyType, emptyDictionary); + builder.setInsertionPointToStart(leaf.addEntryBlock()); + auto latency = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("ticks", builder.getI64IntegerAttr(1)), + }); + auto lifecycle = builder.getDictionaryAttr({ + builder.getNamedAttr("reservation", + builder.getStringAttr("propose_commit")), + builder.getNamedAttr("release", builder.getStringAttr("balanced")), + builder.getNamedAttr("cancellation", builder.getStringAttr("explicit")), + }); + QueueOp::create( + builder, loc, builder.getStringAttr("queue"), + builder.getStringAttr("queue"), builder.getStringAttr("queue"), + mlir::TypeAttr::get(builder.getI32Type()), builder.getI64IntegerAttr(1), + mlir::IntegerAttr(), builder.getStringAttr("fifo"), + mlir::FlatSymbolRefAttr::get(&context, "p"), + builder.getStringAttr("exclusive"), mlir::DictionaryAttr(), + builder.getI64IntegerAttr(1)); + EventQueueOp::create( + builder, loc, builder.getStringAttr("events"), + builder.getStringAttr("events"), builder.getStringAttr("events"), + mlir::TypeAttr::get(EventType::get(&context, builder.getI32Type())), + builder.getI64IntegerAttr(1), builder.getStringAttr("time_then_sequence"), + mlir::FlatSymbolRefAttr::get(&context, "clock"), + builder.getI64IntegerAttr(1)); + ResourceOp::create(builder, loc, "state", "state", "state", 1, 1, 1, latency, + lifecycle, "exclusive", mlir::FlatSymbolRefAttr(), + builder.getArrayAttr({}), 1); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + llvm::SmallVector staticArgs( + 512, mlir::Attribute(emptyDictionary)); + builder.setInsertionPointToEnd(file.getBody()); + auto middle = + ModuleOp::create(builder, loc, "Middle", emptyType, emptyDictionary); + builder.setInsertionPointToStart(middle.addEntryBlock()); + ArrayOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "Leaf", + "leaves", "leaves", "leaves", + builder.getDenseI64ArrayAttr({512}), + builder.getArrayAttr(staticArgs)); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + builder.setInsertionPointToEnd(file.getBody()); + auto top = ModuleOp::create(builder, loc, "Top", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + ArrayOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "Middle", + "middles", "middles", "middles", + builder.getDenseI64ArrayAttr({512}), + builder.getArrayAttr(staticArgs)); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + builder.setInsertionPointToEnd(file.getBody()); + auto seed = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }); + auto results = builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("owners")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }); + SystemOp::create(builder, loc, "owners", "Top", "root", 0, "cycle", + mlir::FlatSymbolRefAttr(), seed, builder.getArrayAttr({}), + results, true); + + auto structureOnly = mlir::cast(file->clone()); + auto structureLeaf = *structureOnly.getOps().begin(); + for (mlir::Operation &operation : + llvm::make_early_inc_range(structureLeaf.getBody().front())) + if (mlir::isa(operation)) + operation.erase(); + EXPECT_TRUE(mlir::succeeded(verifyGraphStructure(structureOnly))); + + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + auto start = std::chrono::steady_clock::now(); + EXPECT_TRUE(mlir::failed(verifyGraphStructure(file))); + EXPECT_NE(diagnostic.find("owner count exceeds bound 1048576"), + std::string::npos); + EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(1)); +} + +TEST(ACIROpsTest, TaskSevenOwnersRegisterAtDistinctAbsoluteInstancePaths) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + auto leaf = + ModuleOp::create(builder, loc, "Leaf", emptyType, emptyDictionary); + builder.setInsertionPointToStart(leaf.addEntryBlock()); + auto latency = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("ticks", builder.getI64IntegerAttr(1)), + }); + auto lifecycle = builder.getDictionaryAttr({ + builder.getNamedAttr("reservation", + builder.getStringAttr("propose_commit")), + builder.getNamedAttr("release", builder.getStringAttr("balanced")), + builder.getNamedAttr("cancellation", builder.getStringAttr("explicit")), + }); + QueueOp::create( + builder, loc, builder.getStringAttr("queue"), + builder.getStringAttr("queue"), builder.getStringAttr("queue"), + mlir::TypeAttr::get(builder.getI32Type()), builder.getI64IntegerAttr(1), + mlir::IntegerAttr(), builder.getStringAttr("fifo"), + mlir::FlatSymbolRefAttr::get(&context, "p"), + builder.getStringAttr("exclusive"), mlir::DictionaryAttr(), + builder.getI64IntegerAttr(1)); + EventQueueOp::create( + builder, loc, builder.getStringAttr("events"), + builder.getStringAttr("events"), builder.getStringAttr("events"), + mlir::TypeAttr::get(EventType::get(&context, builder.getI32Type())), + builder.getI64IntegerAttr(1), builder.getStringAttr("time_then_sequence"), + mlir::FlatSymbolRefAttr::get(&context, "clock"), + builder.getI64IntegerAttr(1)); + ResourceOp::create(builder, loc, "state", "state", "state", 1, 1, 1, latency, + lifecycle, "exclusive", mlir::FlatSymbolRefAttr(), + builder.getArrayAttr({}), 1); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + builder.setInsertionPointToEnd(file.getBody()); + auto top = ModuleOp::create(builder, loc, "Top", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + InstanceOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, + "Leaf", "left", "left", "left", emptyDictionary); + InstanceOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, + "Leaf", "right", "right", "right", emptyDictionary); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + builder.setInsertionPointToEnd(file.getBody()); + auto seed = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }); + auto results = builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("owners")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }); + SystemOp::create(builder, loc, "owners", "Top", "root", 0, "cycle", + mlir::FlatSymbolRefAttr(), seed, builder.getArrayAttr({}), + results, true); + llvm::SmallVector owners; + ASSERT_TRUE(mlir::succeeded(collectElaboratedStateOwners(file, owners))); + ASSERT_EQ(owners.size(), 6u); + EXPECT_EQ(owners[0].path, "root.left.queue"); + EXPECT_EQ(owners[0].stableId, "root/left/queue"); + EXPECT_EQ(owners[1].path, "root.left.events"); + EXPECT_EQ(owners[1].stableId, "root/left/events"); + EXPECT_EQ(owners[2].path, "root.left.state"); + EXPECT_EQ(owners[2].stableId, "root/left/state"); + EXPECT_EQ(owners[3].path, "root.right.queue"); + EXPECT_EQ(owners[3].stableId, "root/right/queue"); + EXPECT_EQ(owners[4].path, "root.right.events"); + EXPECT_EQ(owners[4].stableId, "root/right/events"); + EXPECT_EQ(owners[5].path, "root.right.state"); + EXPECT_EQ(owners[5].stableId, "root/right/state"); +} + +TEST(ACIROpsTest, TaskEightOwnersRegisterAtDistinctAbsoluteInstancePaths) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + auto leaf = + ModuleOp::create(builder, loc, "Leaf", emptyType, emptyDictionary); + builder.setInsertionPointToStart(leaf.addEntryBlock()); + auto process = + ProcessOp::create(builder, loc, "worker", "workload", mlir::ValueRange{}); + builder.setInsertionPointToStart(&process.getBody().emplaceBlock()); + YieldSimOp::create(builder, loc); + builder.setInsertionPointToEnd(&leaf.getBody().front()); + StatOp::create(builder, loc, "requests", "counter"); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + builder.setInsertionPointToEnd(file.getBody()); + auto top = ModuleOp::create(builder, loc, "Top", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + InstanceOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, + "Leaf", "left", "left", "left", emptyDictionary); + InstanceOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, + "Leaf", "right", "right", "right", emptyDictionary); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + builder.setInsertionPointToEnd(file.getBody()); + auto seed = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }); + auto results = builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("owners")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }); + SystemOp::create(builder, loc, "owners", "Top", "root", 0, "cycle", + mlir::FlatSymbolRefAttr(), seed, builder.getArrayAttr({}), + results, true); + + llvm::SmallVector owners; + ASSERT_TRUE(mlir::succeeded(collectElaboratedStateOwners(file, owners))); + ASSERT_EQ(owners.size(), 4u); + EXPECT_EQ(owners[0].path, "root.left.worker"); + EXPECT_EQ(owners[0].stableId, "root/left/worker"); + EXPECT_EQ(owners[1].path, "root.left.requests"); + EXPECT_EQ(owners[1].stableId, "root/left/requests"); + EXPECT_EQ(owners[2].path, "root.right.worker"); + EXPECT_EQ(owners[2].stableId, "root/right/worker"); + EXPECT_EQ(owners[3].path, "root.right.requests"); + EXPECT_EQ(owners[3].stableId, "root/right/requests"); +} + +TEST(ACIROpsTest, TaskEightOwnersParticipateInSaturatedArrayBudget) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + auto leaf = + ModuleOp::create(builder, loc, "Leaf", emptyType, emptyDictionary); + builder.setInsertionPointToStart(leaf.addEntryBlock()); + auto process = + ProcessOp::create(builder, loc, "worker", "workload", mlir::ValueRange{}); + builder.setInsertionPointToStart(&process.getBody().emplaceBlock()); + YieldSimOp::create(builder, loc); + builder.setInsertionPointToEnd(&leaf.getBody().front()); + StatOp::create(builder, loc, "requests", "counter"); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + llvm::SmallVector staticArgs( + 512, mlir::Attribute(emptyDictionary)); + builder.setInsertionPointToEnd(file.getBody()); + auto middle = + ModuleOp::create(builder, loc, "Middle", emptyType, emptyDictionary); + builder.setInsertionPointToStart(middle.addEntryBlock()); + ArrayOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "Leaf", + "leaves", "leaves", "leaves", + builder.getDenseI64ArrayAttr({512}), + builder.getArrayAttr(staticArgs)); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + builder.setInsertionPointToEnd(file.getBody()); + auto top = ModuleOp::create(builder, loc, "Top", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + ArrayOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "Middle", + "middles", "middles", "middles", + builder.getDenseI64ArrayAttr({512}), + builder.getArrayAttr(staticArgs)); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + builder.setInsertionPointToEnd(file.getBody()); + auto seed = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }); + auto results = builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("owners")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }); + SystemOp::create(builder, loc, "owners", "Top", "root", 0, "cycle", + mlir::FlatSymbolRefAttr(), seed, builder.getArrayAttr({}), + results, true); + EXPECT_TRUE(mlir::succeeded(verifyGraphStructure(file))); + + auto overBudget = mlir::cast(file->clone()); + auto overBudgetLeaf = *overBudget.getOps().begin(); + builder.setInsertionPoint(&overBudgetLeaf.getBody().front().back()); + StatOp::create(builder, loc, "latency", "counter"); + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(verifyGraphStructure(overBudget))); + EXPECT_NE(diagnostic.find("owner count exceeds bound 1048576"), + std::string::npos); +} + +TEST(ACIROpsTest, TraceSourcesHaveOneOwnerAcrossElaboratedHierarchy) { + mlir::MLIRContext context; + context.loadDialect(); + auto singleOwner = mlir::parseSourceString(R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { + %cursor = ac.trace.open source "pto" + ac.yield_sim + } + ac.return + } + ac.system @test root @Top as "root" tick 0 "cycle" + seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "trace", format = "json"} selected true + } + )mlir", + &context); + ASSERT_TRUE(singleOwner); + llvm::SmallVector owners; + ASSERT_TRUE(mlir::succeeded( + collectElaboratedStateOwners(singleOwner->getOperation(), owners))); + ASSERT_EQ(owners.size(), 1u); + EXPECT_EQ(owners.front().path, "root.workload"); + EXPECT_EQ(owners.front().stableId, "root/workload"); + ASSERT_EQ(owners.front().traceSources.size(), 1u); + EXPECT_EQ(owners.front().traceSources.front(), "pto"); + + auto expectDuplicate = [&](llvm::StringRef source) { + auto file = mlir::parseSourceString(source, &context); + ASSERT_TRUE(file); + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler( + &context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(verifyGraphStructure(file->getOperation()))); + EXPECT_NE(diagnostic.find("trace source 'pto' has multiple elaborated " + "cursor owners"), + std::string::npos); + }; + + expectDuplicate(R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Left() parameters {} graph { + ac.process @workload kind "workload" { + %cursor = ac.trace.open source "pto" + ac.yield_sim + } + ac.return + } + ac.module @Right() parameters {} graph { + ac.process @workload kind "workload" { + %cursor = ac.trace.open source "pto" + ac.yield_sim + } + ac.return + } + ac.module @Top() parameters {} graph { + ac.instance @left of @Left() static {} id "left" path "left" : () -> () + ac.instance @right of @Right() static {} id "right" path "right" : () -> () + ac.return + } + ac.system @test root @Top as "root" tick 0 "cycle" + seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "trace", format = "json"} selected true + } + )mlir"); + + expectDuplicate(R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @Leaf() parameters {} graph { + ac.process @workload kind "workload" { + %cursor = ac.trace.open source "pto" + ac.yield_sim + } + ac.return + } + ac.module @Top() parameters {} graph { + ac.instance @left of @Leaf() static {} id "left" path "left" : () -> () + ac.instance @right of @Leaf() static {} id "right" path "right" : () -> () + ac.return + } + ac.system @test root @Top as "root" tick 0 "cycle" + seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "trace", format = "json"} selected true + } + )mlir"); +} + +TEST(ACIROpsTest, TraceSourceArrayDuplicationFailsBeforeElaboration) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + auto leaf = + ModuleOp::create(builder, loc, "Leaf", emptyType, emptyDictionary); + builder.setInsertionPointToStart(leaf.addEntryBlock()); + auto process = ProcessOp::create(builder, loc, "workload", "workload", + mlir::ValueRange{}); + builder.setInsertionPointToStart(&process.getBody().emplaceBlock()); + TraceOpenOp::create(builder, loc, builder.getIndexType(), "pto"); + YieldSimOp::create(builder, loc); + builder.setInsertionPointToEnd(&leaf.getBody().front()); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + + llvm::SmallVector staticArgs( + 512, mlir::Attribute(emptyDictionary)); + builder.setInsertionPointToEnd(file.getBody()); + auto middle = + ModuleOp::create(builder, loc, "Middle", emptyType, emptyDictionary); + builder.setInsertionPointToStart(middle.addEntryBlock()); + ArrayOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "Leaf", + "leaves", "leaves", "leaves", + builder.getDenseI64ArrayAttr({512}), + builder.getArrayAttr(staticArgs)); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + builder.setInsertionPointToEnd(file.getBody()); + auto top = ModuleOp::create(builder, loc, "Top", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + ArrayOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "Middle", + "middles", "middles", "middles", + builder.getDenseI64ArrayAttr({512}), + builder.getArrayAttr(staticArgs)); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + builder.setInsertionPointToEnd(file.getBody()); + auto seed = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("fixed")), + builder.getNamedAttr("value", builder.getI64IntegerAttr(0)), + }); + auto results = builder.getDictionaryAttr({ + builder.getNamedAttr("id", builder.getStringAttr("trace")), + builder.getNamedAttr("format", builder.getStringAttr("json")), + }); + SystemOp::create(builder, loc, "trace", "Top", "root", 0, "cycle", + mlir::FlatSymbolRefAttr(), seed, builder.getArrayAttr({}), + results, true); + + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + auto start = std::chrono::steady_clock::now(); + EXPECT_TRUE(mlir::failed(verifyGraphStructure(file))); + EXPECT_NE(diagnostic.find("trace source 'pto' has multiple elaborated cursor " + "owners"), + std::string::npos); + EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(1)); +} + +TEST(ACIROpsTest, StaticContractsUseFreezePhaseModuleEffects) { + mlir::MLIRContext context; + context.loadDialect(); + auto file = mlir::parseSourceString(R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.module @M(i1) parameters {} graph { + ^bb0(%condition : i1): + ac.require %condition, "capacity" + ac.ensure %condition, "topology" + ac.return + } + } + )mlir", + &context); + ASSERT_TRUE(file); + ASSERT_TRUE(mlir::succeeded(mlir::verify(file->getOperation()))); + auto module = *file->getOps().begin(); + for (mlir::Operation &operation : module.getBody().front()) { + if (!mlir::isa(operation)) + continue; + llvm::SmallVector effects; + mlir::cast(&operation).getEffects(effects); + ASSERT_EQ(effects.size(), 1u); + EXPECT_EQ(effects.front().getResource(), ModuleStateResource::get()); + EXPECT_TRUE( + mlir::isa(effects.front().getEffect())); + auto parameters = + mlir::cast(effects.front().getParameters()); + EXPECT_EQ(parameters.getAs("contract_phase").getValue(), + "topology_freeze"); + } +} + +TEST(ACIROpsTest, ExplicitViewProvenanceScalesNearLinearly) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto emptyType = builder.getFunctionType({}, {}); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto zeroShape = builder.getDenseI64ArrayAttr({0}); + + auto buildChain = [&](unsigned viewCount, llvm::StringRef prefix) { + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + auto leaf = ModuleOp::create(builder, loc, (prefix + "Leaf").str(), + emptyType, emptyDictionary); + builder.setInsertionPointToStart(leaf.addEntryBlock()); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + builder.setInsertionPointToEnd(file.getBody()); + auto top = ModuleOp::create(builder, loc, (prefix + "Top").str(), emptyType, + emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + std::string previous = "source"; + InstanceOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, + (prefix + "Leaf").str(), previous, previous, previous, + emptyDictionary); + for (unsigned index = 0; index != viewCount; ++index) { + std::string name = "view" + std::to_string(index); + ViewOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, name, + "permutation", + builder.getArrayAttr( + {mlir::FlatSymbolRefAttr::get(&context, previous)}), + builder.getArrayAttr({zeroShape}), mlir::IntegerAttr(), + llvm::ArrayRef{}, llvm::ArrayRef{0}); + previous = std::move(name); + } + ReturnOp::create(builder, loc, mlir::ValueRange{}); + return file; + }; + + auto buildWide = [&] { + constexpr unsigned sourceCount = 1000; + auto file = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(file.getBody()); + auto leaf = + ModuleOp::create(builder, loc, "WideLeaf", emptyType, emptyDictionary); + builder.setInsertionPointToStart(leaf.addEntryBlock()); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + builder.setInsertionPointToEnd(file.getBody()); + auto top = + ModuleOp::create(builder, loc, "WideTop", emptyType, emptyDictionary); + builder.setInsertionPointToStart(top.addEntryBlock()); + llvm::SmallVector producerRefs; + llvm::SmallVector sourceShapes; + producerRefs.reserve(sourceCount); + sourceShapes.reserve(sourceCount); + for (unsigned index = 0; index != sourceCount; ++index) { + std::string name = "source" + std::to_string(index); + InstanceOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, + "WideLeaf", name, name, name, emptyDictionary); + producerRefs.push_back(mlir::FlatSymbolRefAttr::get(&context, name)); + sourceShapes.push_back(zeroShape); + } + ViewOp::create(builder, loc, mlir::TypeRange{}, mlir::ValueRange{}, "wide", + "concat", builder.getArrayAttr(producerRefs), + builder.getArrayAttr(sourceShapes), + builder.getI64IntegerAttr(0), llvm::ArrayRef{}, + llvm::ArrayRef{0}); + ReturnOp::create(builder, loc, mlir::ValueRange{}); + return file; + }; + + auto wide = buildWide(); + auto small = buildChain(1000, "Small"); + auto large = buildChain(5000, "Large"); + auto verifyTimed = [](mlir::ModuleOp file) { + auto start = std::chrono::steady_clock::now(); + EXPECT_TRUE(mlir::succeeded(mlir::verify(file))); + return std::chrono::steady_clock::now() - start; + }; + auto wideElapsed = verifyTimed(wide); + auto smallElapsed = verifyTimed(small); + auto largeElapsed = verifyTimed(large); + EXPECT_LT(wideElapsed, std::chrono::seconds(5)); + EXPECT_LT(largeElapsed, std::chrono::seconds(5)); + EXPECT_LT(largeElapsed, smallElapsed * 8 + std::chrono::milliseconds(50)); +} + +TEST(ACIROpsTest, TopologyVerifierWalksTypeAttributesAndLocations) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::ScopedDiagnosticHandler handler( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + mlir::OpBuilder builder(&context); + auto protocol = mlir::FlatSymbolRefAttr::get(&context, "missing"); + auto flow = FlowType::get(&context, builder.getI8Type(), protocol); + auto nested = OptionalType::get(&context, flow); + + auto attributeModule = mlir::ModuleOp::create(builder.getUnknownLoc()); + attributeModule->setAttr("metadata", mlir::TypeAttr::get(nested)); + EXPECT_TRUE(mlir::failed(verifyTopologyTypeUses(attributeModule))); + + auto operandModule = mlir::ModuleOp::create(builder.getUnknownLoc()); + builder.setInsertionPointToStart(operandModule.getBody()); + auto source = mlir::UnrealizedConversionCastOp::create( + builder, builder.getUnknownLoc(), mlir::TypeRange{nested}, + mlir::ValueRange{}); + auto consumer = mlir::UnrealizedConversionCastOp::create( + builder, builder.getUnknownLoc(), mlir::TypeRange{builder.getI1Type()}, + source.getResults()); + EXPECT_TRUE(mlir::failed(verifyTopologyTypeUses(consumer))); + + auto location = mlir::FusedLoc::get(&context, {builder.getUnknownLoc()}, + mlir::TypeAttr::get(nested)); + auto locationModule = mlir::ModuleOp::create(location); + EXPECT_TRUE(mlir::failed(verifyTopologyTypeUses(locationModule))); +} + +TEST(ACIROpsTest, ReverseChainOwnershipAnalysisHasLinearScaling) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto loc = builder.getUnknownLoc(); + auto module = mlir::ModuleOp::create(loc); + builder.setInsertionPointToStart(module.getBody()); + auto protocol = ProtocolOp::create(builder, loc, "long_chain"); + builder.setInsertionPointToStart(&protocol.getBody().emplaceBlock()); + RoleOp::create(builder, loc, "a", "b", "exclusive"); + RoleOp::create(builder, loc, "b", "a", "exclusive"); + + constexpr unsigned stateCount = 1201; + std::vector stateNames; + stateNames.reserve(stateCount); + for (unsigned index = 0; index < stateCount; ++index) { + stateNames.push_back((llvm::Twine("s") + llvm::Twine(index)).str()); + StateOp::create(builder, loc, stateNames.back(), index == 0, + index + 1 == stateCount); + } + EventOp::create(builder, loc, "step", "a", "b", builder.getI8Type(), + "notify"); + for (unsigned index = stateCount - 1; index > 0; --index) { + auto transition = + TransitionOp::create(builder, loc, stateNames[index - 1], + stateNames[index], "step", nullptr, false, false); + transition.getGuard().emplaceBlock(); + } + + auto start = std::chrono::steady_clock::now(); + EXPECT_TRUE(mlir::succeeded(mlir::verify(module))); + auto elapsed = std::chrono::steady_clock::now() - start; + EXPECT_LT(elapsed, std::chrono::seconds(5)); +} + +TEST(ACIROpsTest, TransitionTableRejectsAmbiguousRowsDeterministically) { + mlir::MLIRContext context; + context.loadDialect(); + constexpr llvm::StringLiteral source = R"mlir( + builtin.module { + "ac.protocol"() <{sym_name = "p"}> ({ + "ac.role"() <{sym_name = "a", dual = @b, cardinality = "exclusive"}> : () -> () + "ac.role"() <{sym_name = "b", dual = @a, cardinality = "exclusive"}> : () -> () + "ac.state"() <{sym_name = "s", initial = true, terminal = false}> : () -> () + "ac.event"() <{sym_name = "e", from = @a, to = @b, payload = i8, action = "notify"}> : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({}) : () -> () + "ac.transition"() <{source = @s, target = @s, event = @e}> ({}) : () -> () + }) : () -> () + } + )mlir"; + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + EXPECT_FALSE(mlir::parseSourceString(source, &context)); + EXPECT_NE( + diagnostic.find("overlapping transitions require explicit priority"), + std::string::npos); +} + +TEST(ACIRResourcesTest, CheckedArithmeticAndIntervalsRejectBoundaries) { + uint64_t result = 0; + EXPECT_TRUE(checkedAdd(4, 5, result)); + EXPECT_EQ(result, 9u); + EXPECT_FALSE(checkedAdd(std::numeric_limits::max(), 1, result)); + EXPECT_TRUE(checkedMultiply(7, 6, result)); + EXPECT_EQ(result, 42u); + EXPECT_FALSE( + checkedMultiply(std::numeric_limits::max(), 2, result)); + EXPECT_TRUE(intervalsOverlap({0, 8}, {7, 9})); + EXPECT_FALSE(intervalsOverlap({0, 8}, {8, 9})); + EXPECT_TRUE(intervalsOverlap( + {uint64_t{1} << 63, WideAddress{1} << 64}, + {std::numeric_limits::max(), WideAddress{1} << 64})); + EXPECT_EQ(compareAddressMapOrder({0, 8, true, 2}, {0, 4, true, 1}), -1); +} + +TEST(ACIRResourcesTest, RationalNormalizationIsExactAndBounded) { + uint64_t ticks = 0; + EXPECT_TRUE(normalizeRationalToTicks(3, 2, 1, 2, ticks)); + EXPECT_EQ(ticks, 3u); + EXPECT_FALSE(normalizeRationalToTicks(1, 3, 1, 2, ticks)); + EXPECT_FALSE(normalizeRationalToTicks(1, 0, 1, 1, ticks)); + EXPECT_FALSE(normalizeRationalToTicks(std::numeric_limits::max(), 1, + 1, 2, ticks)); + EXPECT_TRUE(normalizeRationalToTicks(0, 1, 1, 1, ticks)); + EXPECT_EQ(ticks, 0u); + EXPECT_TRUE(normalizeRationalToTicks(std::numeric_limits::max(), 1, + 1, 1, ticks)); + EXPECT_EQ(ticks, static_cast(std::numeric_limits::max())); + EXPECT_FALSE(normalizeRationalToTicks( + static_cast(std::numeric_limits::max()) + 1, 1, 1, 1, + ticks)); + EXPECT_TRUE(normalizeRationalToTicks(1, 1, 1, kMaxTickScale, ticks)); + EXPECT_EQ(ticks, kMaxTickScale); + EXPECT_FALSE(normalizeRationalToTicks(1, 1, 1, kMaxTickScale + 1, ticks)); + EXPECT_FALSE(normalizeRationalToTicks(1, kMaxTickScale + 1, 1, + kMaxTickScale + 1, ticks)); + EXPECT_TRUE(normalizeRationalToTicks(3, 2, 3, 4, ticks)); + EXPECT_EQ(ticks, 2u); + EXPECT_FALSE(normalizeRationalToTicks(std::numeric_limits::max(), 1, + 1, 2, ticks)); +} + +TEST(ACIRResourcesTest, DomainTickUsesNormativePhasePlusCycleTimesPeriod) { + uint64_t tick = 0; + EXPECT_TRUE( + checkedDomainTick(std::numeric_limits::max(), 1, 0, tick)); + EXPECT_EQ(tick, static_cast(std::numeric_limits::max())); + EXPECT_FALSE( + checkedDomainTick(std::numeric_limits::max(), 1, 1, tick)); +} + +TEST(ACIRResourcesTest, TaskSevenRegistryDeltaIsExactlySixOperations) { + mlir::MLIRContext context; + context.loadDialect(); + const std::array names = { + "ac.queue", "ac.event_queue", "ac.resource", + "ac.address_space", "ac.address_map", "ac.time_domain", + }; + for (llvm::StringLiteral name : names) + EXPECT_TRUE(mlir::OperationName(name, &context).isRegistered()) + << name.str(); + EXPECT_FALSE(mlir::OperationName("ac.freeze", &context).isRegistered()); + EXPECT_FALSE(mlir::OperationName("ac.try_issue", &context).isRegistered()); +} + +TEST(ACIRResourcesTest, PublicBuildersAndTypedEffectsCoverAllSixOperations) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto location = builder.getUnknownLoc(); + auto file = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(file.getBody()); + auto module = + ModuleOp::create(builder, location, "M", builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + + auto i64 = [&](int64_t value) { return builder.getI64IntegerAttr(value); }; + auto string = [&](llvm::StringRef value) { + return builder.getStringAttr(value); + }; + auto symbol = [&](llvm::StringRef value) { + return mlir::FlatSymbolRefAttr::get(&context, value); + }; + auto queue = + QueueOp::create(builder, location, string("q"), string("q"), string("q"), + mlir::TypeAttr::get(builder.getI32Type()), i64(8), + mlir::IntegerAttr(), string("fifo"), symbol("p"), + string("exclusive"), mlir::DictionaryAttr(), i64(1)); + auto domain = TimeDomainOp::create(builder, location, string("clock"), i64(1), + i64(0), i64(1), mlir::FlatSymbolRefAttr(), + mlir::DictionaryAttr()); + auto eventQueue = EventQueueOp::create( + builder, location, string("events"), string("events"), string("events"), + mlir::TypeAttr::get(EventType::get(&context, builder.getI32Type())), + i64(8), string("time_then_sequence"), symbol("clock"), i64(1)); + auto latency = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", string("fixed")), + builder.getNamedAttr("ticks", i64(2)), + }); + auto lifecycle = builder.getDictionaryAttr({ + builder.getNamedAttr("reservation", string("propose_commit")), + builder.getNamedAttr("release", string("balanced")), + builder.getNamedAttr("cancellation", string("explicit")), + }); + auto resource = ResourceOp::create( + builder, location, string("r"), string("r"), string("r"), i64(2), i64(1), + i64(1), latency, lifecycle, string("exclusive"), + mlir::FlatSymbolRefAttr(), builder.getArrayAttr({}), i64(1)); + auto address = AddressSpaceOp::create( + builder, location, string("memory"), string("memory"), string("memory"), + i64(32), string("byte"), mlir::Attribute(), mlir::FlatSymbolRefAttr(), + mlir::DictionaryAttr()); + auto addressMap = + AddressMapOp::create(builder, location, string("map"), symbol("memory"), + builder.getArrayAttr({}), + builder.getDictionaryAttr({ + builder.getNamedAttr("kind", string("unmapped")), + })); + ReturnOp::create(builder, location, mlir::ValueRange{}); + + EXPECT_TRUE(queue && eventQueue && resource && address && addressMap && + domain); + auto hasWriteOn = [](mlir::Operation *operation, + mlir::SideEffects::Resource *resourceKind) { + llvm::SmallVector effects; + mlir::cast(operation).getEffects(effects); + return llvm::any_of(effects, [&](const auto &effect) { + return mlir::isa(effect.getEffect()) && + effect.getResource() == resourceKind; + }); + }; + EXPECT_TRUE(hasWriteOn(queue, QueueStateResource::get())); + EXPECT_TRUE(hasWriteOn(eventQueue, EventQueueStateResource::get())); + EXPECT_TRUE(hasWriteOn(resource, ReservationStateResource::get())); + EXPECT_FALSE(mlir::isMemoryEffectFree(queue)); + EXPECT_FALSE(mlir::isMemoryEffectFree(eventQueue)); + EXPECT_FALSE(mlir::isMemoryEffectFree(resource)); + + auto effectOf = [](mlir::Operation *operation) { + llvm::SmallVector effects; + mlir::cast(operation).getEffects(effects); + EXPECT_EQ(effects.size(), 1u); + return effects.front(); + }; + auto queueEffect = effectOf(queue); + auto eventEffect = effectOf(eventQueue); + auto queueEffectAgain = effectOf(queue); + auto qualified = [&](llvm::StringRef local) { + return mlir::SymbolRefAttr::get( + &context, "M", {mlir::FlatSymbolRefAttr::get(&context, local)}); + }; + EXPECT_EQ(queueEffect.getSymbolRef(), qualified("q")); + EXPECT_EQ(eventEffect.getSymbolRef(), qualified("events")); + EXPECT_EQ(queueEffect.getSymbolRef(), queueEffectAgain.getSymbolRef()); + EXPECT_EQ(queueEffect.getParameters(), queueEffectAgain.getParameters()); + EXPECT_NE(queueEffect.getSymbolRef(), eventEffect.getSymbolRef()); + auto parameters = + mlir::cast(queueEffect.getParameters()); + EXPECT_EQ(parameters.getAs("stable_id").getValue(), "q"); + EXPECT_EQ(parameters.getAs("path").getValue(), "q"); +} + +TEST(ACIRResourcesTest, EffectsUseDefinitionQualifiedPreFreezeIdentity) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto location = builder.getUnknownLoc(); + auto file = mlir::ModuleOp::create(location); + auto buildQueue = [&](llvm::StringRef definition) { + builder.setInsertionPointToEnd(file.getBody()); + auto module = ModuleOp::create(builder, location, definition, + builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + auto queue = QueueOp::create( + builder, location, builder.getStringAttr("q"), + builder.getStringAttr("q"), builder.getStringAttr("q"), + mlir::TypeAttr::get(builder.getI32Type()), builder.getI64IntegerAttr(1), + mlir::IntegerAttr(), builder.getStringAttr("fifo"), + mlir::FlatSymbolRefAttr::get(&context, "p"), + builder.getStringAttr("exclusive"), mlir::DictionaryAttr(), + builder.getI64IntegerAttr(1)); + ReturnOp::create(builder, location, mlir::ValueRange{}); + return queue; + }; + QueueOp left = buildQueue("Left"); + QueueOp right = buildQueue("Right"); + auto effectOf = [](QueueOp queue) { + llvm::SmallVector effects; + mlir::cast(*queue).getEffects(effects); + EXPECT_EQ(effects.size(), 1u); + return effects.front(); + }; + auto leftEffect = effectOf(left); + auto leftAgain = effectOf(left); + auto rightEffect = effectOf(right); + auto qualified = [&](llvm::StringRef definition) { + return mlir::SymbolRefAttr::get( + &context, definition, {mlir::FlatSymbolRefAttr::get(&context, "q")}); + }; + EXPECT_EQ(leftEffect.getSymbolRef(), qualified("Left")); + EXPECT_EQ(rightEffect.getSymbolRef(), qualified("Right")); + EXPECT_NE(leftEffect.getSymbolRef(), rightEffect.getSymbolRef()); + EXPECT_EQ(leftEffect.getSymbolRef(), leftAgain.getSymbolRef()); + EXPECT_EQ(leftEffect.getParameters(), leftAgain.getParameters()); + auto parameters = + mlir::cast(leftEffect.getParameters()); + auto identityPhase = parameters.getAs("identity_phase"); + ASSERT_TRUE(identityPhase); + EXPECT_EQ(identityPhase.getValue(), "definition_pre_freeze"); +} + +TEST(ACIRResourcesTest, LargeAddressMapAndParentGraphScaleNearLinearly) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto location = builder.getUnknownLoc(); + auto emptyDictionary = builder.getDictionaryAttr({}); + auto file = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(file.getBody()); + auto module = ModuleOp::create( + builder, location, "M", builder.getFunctionType({}, {}), emptyDictionary); + builder.setInsertionPointToStart(module.addEntryBlock()); + AddressSpaceOp::create( + builder, location, builder.getStringAttr("space"), + builder.getStringAttr("space"), builder.getStringAttr("space"), + builder.getI64IntegerAttr(32), builder.getStringAttr("byte"), + mlir::Attribute(), mlir::FlatSymbolRefAttr(), mlir::DictionaryAttr()); + + constexpr unsigned entryCount = 10000; + llvm::SmallVector entries; + entries.reserve(entryCount); + for (unsigned index = 0; index != entryCount; ++index) { + entries.push_back(builder.getDictionaryAttr({ + builder.getNamedAttr("base", builder.getI64IntegerAttr(0)), + builder.getNamedAttr("size", builder.getI64IntegerAttr(entryCount)), + builder.getNamedAttr("target", + mlir::FlatSymbolRefAttr::get(&context, "space")), + builder.getNamedAttr("offset", builder.getI64IntegerAttr(0)), + builder.getNamedAttr( + "permissions", + builder.getArrayAttr({builder.getStringAttr("read")})), + builder.getNamedAttr("classes", builder.getArrayAttr({})), + builder.getNamedAttr( + "interleave", + builder.getDictionaryAttr({ + builder.getNamedAttr("granularity", + builder.getI64IntegerAttr(1)), + builder.getNamedAttr("banks", + builder.getI64IntegerAttr(entryCount)), + builder.getNamedAttr("bank", builder.getI64IntegerAttr(index)), + })), + })); + } + AddressMapOp::create( + builder, location, "map", "space", builder.getArrayAttr(entries), + builder.getDictionaryAttr( + {builder.getNamedAttr("kind", builder.getStringAttr("unmapped"))})); + ReturnOp::create(builder, location, mlir::ValueRange{}); + + auto start = std::chrono::steady_clock::now(); + EXPECT_TRUE(mlir::succeeded(mlir::verify(file))); + auto verifyElapsed = std::chrono::steady_clock::now() - start; + + constexpr unsigned domainCount = 10000; + auto graphFile = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(graphFile.getBody()); + auto bridgeModule = + ModuleOp::create(builder, location, "Bridge", + builder.getFunctionType({}, {}), emptyDictionary); + builder.setInsertionPointToStart(bridgeModule.addEntryBlock()); + ReturnOp::create(builder, location, mlir::ValueRange{}); + builder.setInsertionPointToEnd(graphFile.getBody()); + auto graphModule = + ModuleOp::create(builder, location, "Graph", + builder.getFunctionType({}, {}), emptyDictionary); + builder.setInsertionPointToStart(graphModule.addEntryBlock()); + InstanceOp::create(builder, location, mlir::TypeRange{}, mlir::ValueRange{}, + "Bridge", "bridge", "bridge", "bridge", emptyDictionary); + for (unsigned index = 0; index != domainCount; ++index) { + std::string name = "d" + std::to_string(index); + mlir::FlatSymbolRefAttr parent; + mlir::DictionaryAttr bridge; + if (index) { + parent = mlir::FlatSymbolRefAttr::get(&context, + "d" + std::to_string(index - 1)); + bridge = builder.getDictionaryAttr({ + builder.getNamedAttr("kind", builder.getStringAttr("explicit")), + builder.getNamedAttr( + "owner", mlir::FlatSymbolRefAttr::get(&context, "bridge")), + }); + } + TimeDomainOp::create(builder, location, builder.getStringAttr(name), + builder.getI64IntegerAttr(1), + builder.getI64IntegerAttr(0), + builder.getI64IntegerAttr(1), parent, bridge); + } + ReturnOp::create(builder, location, mlir::ValueRange{}); + + start = std::chrono::steady_clock::now(); + EXPECT_TRUE(mlir::succeeded(mlir::verify(graphFile))); + auto graphElapsed = std::chrono::steady_clock::now() - start; + EXPECT_LT(verifyElapsed, std::chrono::seconds(5)); + EXPECT_LT(graphElapsed, std::chrono::seconds(5)); +} + +TEST(ACIRResourcesTest, MixedGeometryDistinctPrioritiesScaleNearLinearly) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto location = builder.getUnknownLoc(); + auto buildMap = [&](unsigned entryCount) { + auto file = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(file.getBody()); + auto module = ModuleOp::create(builder, location, "M", + builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + AddressSpaceOp::create( + builder, location, builder.getStringAttr("space"), + builder.getStringAttr("space"), builder.getStringAttr("space"), + builder.getI64IntegerAttr(32), builder.getStringAttr("byte"), + mlir::Attribute(), mlir::FlatSymbolRefAttr(), mlir::DictionaryAttr()); + llvm::SmallVector entries; + entries.reserve(entryCount); + for (uint64_t priority = 1; priority <= entryCount; ++priority) { + entries.push_back(builder.getDictionaryAttr({ + builder.getNamedAttr("base", builder.getI64IntegerAttr(0)), + builder.getNamedAttr("size", builder.getI64IntegerAttr(priority)), + builder.getNamedAttr("target", + mlir::FlatSymbolRefAttr::get(&context, "space")), + builder.getNamedAttr("offset", builder.getI64IntegerAttr(0)), + builder.getNamedAttr( + "permissions", + builder.getArrayAttr({builder.getStringAttr("read")})), + builder.getNamedAttr("classes", builder.getArrayAttr({})), + builder.getNamedAttr("priority", builder.getI64IntegerAttr(priority)), + builder.getNamedAttr( + "interleave", + builder.getDictionaryAttr({ + builder.getNamedAttr("granularity", + builder.getI64IntegerAttr(1)), + builder.getNamedAttr("banks", + builder.getI64IntegerAttr(priority)), + builder.getNamedAttr("bank", builder.getI64IntegerAttr(0)), + })), + })); + } + AddressMapOp::create( + builder, location, "map", "space", builder.getArrayAttr(entries), + builder.getDictionaryAttr( + {builder.getNamedAttr("kind", builder.getStringAttr("unmapped"))})); + ReturnOp::create(builder, location, mlir::ValueRange{}); + return file; + }; + auto verifyTimed = [&](unsigned entryCount) { + mlir::ModuleOp file = buildMap(entryCount); + auto start = std::chrono::steady_clock::now(); + EXPECT_TRUE(mlir::succeeded(mlir::verify(file))); + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + }; + + int64_t fiveThousandMs = verifyTimed(5000); + int64_t tenThousandMs = verifyTimed(10000); + RecordProperty("five_thousand_ms", fiveThousandMs); + RecordProperty("ten_thousand_ms", tenThousandMs); + EXPECT_LT(tenThousandMs, 5000); + EXPECT_LT(tenThousandMs, std::max(100, fiveThousandMs * 3)); +} + +TEST(ACIRResourcesTest, SingleSelectedStripeMixedGeometriesScaleNearLinearly) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto location = builder.getUnknownLoc(); + auto measureWork = [&](unsigned entryCount) { + auto file = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(file.getBody()); + auto module = ModuleOp::create(builder, location, "M", + builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + AddressSpaceOp::create( + builder, location, builder.getStringAttr("space"), + builder.getStringAttr("space"), builder.getStringAttr("space"), + builder.getI64IntegerAttr(32), builder.getStringAttr("byte"), + mlir::Attribute(), mlir::FlatSymbolRefAttr(), mlir::DictionaryAttr()); + llvm::SmallVector entries; + entries.reserve(entryCount); + for (uint64_t index = 1; index <= entryCount; ++index) { + uint64_t size = 16384 * index; + entries.push_back(builder.getDictionaryAttr({ + builder.getNamedAttr("base", builder.getI64IntegerAttr(0)), + builder.getNamedAttr("size", builder.getI64IntegerAttr(size)), + builder.getNamedAttr("target", + mlir::FlatSymbolRefAttr::get(&context, "space")), + builder.getNamedAttr("offset", builder.getI64IntegerAttr(0)), + builder.getNamedAttr( + "permissions", + builder.getArrayAttr({builder.getStringAttr("read")})), + builder.getNamedAttr("classes", builder.getArrayAttr({})), + builder.getNamedAttr( + "interleave", + builder.getDictionaryAttr({ + builder.getNamedAttr("granularity", + builder.getI64IntegerAttr(1)), + builder.getNamedAttr("banks", + builder.getI64IntegerAttr(size)), + builder.getNamedAttr("bank", + builder.getI64IntegerAttr(index - 1)), + })), + })); + } + AddressMapOp::create( + builder, location, "map", "space", builder.getArrayAttr(entries), + builder.getDictionaryAttr( + {builder.getNamedAttr("kind", builder.getStringAttr("unmapped"))})); + ReturnOp::create(builder, location, mlir::ValueRange{}); + detail::AddressMapVerificationWork work; + { + detail::ScopedAddressMapVerificationWorkCollector collector(work); + EXPECT_TRUE(mlir::succeeded(mlir::verify(file))); + } + return work; + }; + + auto expectSingleStripeWork = + [](const detail::AddressMapVerificationWork &work, uint64_t entryCount) { + // Each entry is parsed and swept once. Adjacent one-address selections + // expire N-1 prior entries; the concrete and mixed sweeps make two + // queries per entry and six key updates per entry minus the two absent + // first-entry removals. This reaches no candidate or general relation. + EXPECT_EQ(work.entryNormalizationVisits, entryCount); + EXPECT_EQ(work.concreteEntryVisits, entryCount); + EXPECT_EQ(work.concreteExpirationVisits, entryCount - 1); + EXPECT_EQ(work.selectorQueryVisits, entryCount * 2); + EXPECT_EQ(work.selectorUpdateVisits, entryCount * 6 - 2); + EXPECT_EQ(work.candidateIntersectionChecks, 0u); + EXPECT_EQ(work.generalRelationChecks, 0u); + EXPECT_EQ(work.total(), entryCount * 11 - 3); + }; + + constexpr uint64_t smallSize = 5000; + constexpr uint64_t largeSize = 10000; + detail::AddressMapVerificationWork smallWork = measureWork(smallSize); + detail::AddressMapVerificationWork largeWork = measureWork(largeSize); + expectSingleStripeWork(smallWork, smallSize); + expectSingleStripeWork(largeWork, largeSize); + EXPECT_EQ(largeWork.total() - smallWork.total(), + uint64_t{11} * (largeSize - smallSize)); +} + +TEST(ACIRResourcesTest, + SingleSelectionHandlesPartialEmptyAndFullWidthIntervals) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto location = builder.getUnknownLoc(); + auto integer = [&](uint64_t value) { + return builder.getIntegerAttr(builder.getI64Type(), llvm::APInt(64, value)); + }; + auto entry = [&](uint64_t base, uint64_t size, uint64_t granularity, + uint64_t banks, uint64_t bank) { + return builder.getDictionaryAttr({ + builder.getNamedAttr("base", integer(base)), + builder.getNamedAttr("size", integer(size)), + builder.getNamedAttr("target", + mlir::FlatSymbolRefAttr::get(&context, "space")), + builder.getNamedAttr("offset", integer(0)), + builder.getNamedAttr( + "permissions", + builder.getArrayAttr({builder.getStringAttr("read")})), + builder.getNamedAttr("classes", builder.getArrayAttr({})), + builder.getNamedAttr( + "interleave", + builder.getDictionaryAttr({ + builder.getNamedAttr("granularity", integer(granularity)), + builder.getNamedAttr("banks", integer(banks)), + builder.getNamedAttr("bank", integer(bank)), + })), + }); + }; + auto buildMap = [&](llvm::ArrayRef entries) { + auto file = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(file.getBody()); + auto module = ModuleOp::create(builder, location, "M", + builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + AddressSpaceOp::create( + builder, location, builder.getStringAttr("space"), + builder.getStringAttr("space"), builder.getStringAttr("space"), + builder.getI64IntegerAttr(64), builder.getStringAttr("byte"), + mlir::Attribute(), mlir::FlatSymbolRefAttr(), mlir::DictionaryAttr()); + AddressMapOp::create( + builder, location, "map", "space", builder.getArrayAttr(entries), + builder.getDictionaryAttr( + {builder.getNamedAttr("kind", builder.getStringAttr("unmapped"))})); + ReturnOp::create(builder, location, mlir::ValueRange{}); + return file; + }; + + auto valid = buildMap( + {entry(3, 2, 4, 8, 1), entry(3, 2, 1, 7, 3), entry(3, 1, 1, 8, 7), + entry(std::numeric_limits::max(), 1, 1, 2, 1)}); + std::string before; + llvm::raw_string_ostream(before) << valid; + EXPECT_TRUE(mlir::succeeded(mlir::verify(valid))); + std::string after; + llvm::raw_string_ostream(after) << valid; + EXPECT_EQ(before, after); + normalizeAddressMaps(valid); + std::string normalizedOnce; + llvm::raw_string_ostream(normalizedOnce) << valid; + normalizeAddressMaps(valid); + std::string normalizedTwice; + llvm::raw_string_ostream(normalizedTwice) << valid; + EXPECT_EQ(normalizedOnce, normalizedTwice); + + auto generalFastDisjoint = + buildMap({entry(0, 16, 1, 2, 0), entry(3, 2, 1, 7, 3)}); + EXPECT_TRUE(mlir::succeeded(mlir::verify(generalFastDisjoint))); + auto clampForward = + buildMap({entry(24, 62, 2, 7, 0), entry(48, 98, 8, 8, 5)}); + auto clampReverse = + buildMap({entry(48, 98, 8, 8, 5), entry(24, 62, 2, 7, 0)}); + EXPECT_TRUE(mlir::succeeded(mlir::verify(clampForward))); + EXPECT_TRUE(mlir::succeeded(mlir::verify(clampReverse))); + + mlir::ScopedDiagnosticHandler suppress( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + auto overlapping = buildMap({entry(3, 2, 4, 8, 1), entry(3, 2, 1, 7, 4)}); + EXPECT_TRUE(mlir::failed(mlir::verify(overlapping))); + auto generalFastOverlap = + buildMap({entry(0, 16, 1, 2, 0), entry(3, 2, 1, 7, 4)}); + EXPECT_TRUE(mlir::failed(mlir::verify(generalFastOverlap))); +} + +TEST(ACIRResourcesTest, + GeneralMixedIntersectionCapabilityIsExactAndPermutationInvariant) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto location = builder.getUnknownLoc(); + auto buildMap = [&](uint64_t relationCount, bool reverse) { + auto file = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(file.getBody()); + auto module = ModuleOp::create(builder, location, "M", + builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + AddressSpaceOp::create( + builder, location, builder.getStringAttr("space"), + builder.getStringAttr("space"), builder.getStringAttr("space"), + builder.getI64IntegerAttr(32), builder.getStringAttr("byte"), + mlir::Attribute(), mlir::FlatSymbolRefAttr(), mlir::DictionaryAttr()); + auto makeEntry = [&](uint64_t banks, uint64_t bank, + std::optional priority) { + llvm::SmallVector attributes{ + builder.getNamedAttr("base", builder.getI64IntegerAttr(0)), + builder.getNamedAttr("size", builder.getI64IntegerAttr(65536)), + builder.getNamedAttr("target", + mlir::FlatSymbolRefAttr::get(&context, "space")), + builder.getNamedAttr("offset", builder.getI64IntegerAttr(0)), + builder.getNamedAttr( + "permissions", + builder.getArrayAttr({builder.getStringAttr("read")})), + builder.getNamedAttr("classes", builder.getArrayAttr({})), + builder.getNamedAttr( + "interleave", + builder.getDictionaryAttr({ + builder.getNamedAttr("granularity", + builder.getI64IntegerAttr(1)), + builder.getNamedAttr("banks", + builder.getI64IntegerAttr(banks)), + builder.getNamedAttr("bank", builder.getI64IntegerAttr(bank)), + })), + }; + if (priority) + attributes.push_back(builder.getNamedAttr( + "priority", builder.getI64IntegerAttr(*priority))); + return builder.getDictionaryAttr(attributes); + }; + llvm::SmallVector entries; + entries.push_back(makeEntry(2, 0, std::nullopt)); + for (uint64_t index = 0; index < relationCount; ++index) + entries.push_back(makeEntry(2 * (index + 2), 1, index + 1)); + if (reverse) + std::reverse(entries.begin(), entries.end()); + AddressMapOp::create( + builder, location, "map", "space", builder.getArrayAttr(entries), + builder.getDictionaryAttr( + {builder.getNamedAttr("kind", builder.getStringAttr("unmapped"))})); + ReturnOp::create(builder, location, mlir::ValueRange{}); + return file; + }; + auto verify = [&](mlir::ModuleOp file) { + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler( + &context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + bool succeeded = mlir::succeeded(mlir::verify(file)); + return std::pair{succeeded, diagnostic}; + }; + + EXPECT_TRUE( + verify(buildMap(kMaxGeneralSelectorIntersectionQueries - 1, false)) + .first); + EXPECT_TRUE( + verify(buildMap(kMaxGeneralSelectorIntersectionQueries, false)).first); + auto forward = + verify(buildMap(kMaxGeneralSelectorIntersectionQueries + 1, false)); + auto reverse = + verify(buildMap(kMaxGeneralSelectorIntersectionQueries + 1, true)); + EXPECT_FALSE(forward.first); + EXPECT_FALSE(reverse.first); + EXPECT_EQ(forward.second, reverse.second); + EXPECT_NE(forward.second.find( + "general mixed interleave analysis exceeds ACIR limit 256"), + std::string::npos); +} + +TEST(ACIRResourcesTest, SingleBankSelectionsDoNotConsumeGeneralMixedBudget) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::OpBuilder builder(&context); + auto location = builder.getUnknownLoc(); + auto file = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(file.getBody()); + auto module = + ModuleOp::create(builder, location, "M", builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + AddressSpaceOp::create( + builder, location, builder.getStringAttr("space"), + builder.getStringAttr("space"), builder.getStringAttr("space"), + builder.getI64IntegerAttr(12), builder.getStringAttr("byte"), + mlir::Attribute(), mlir::FlatSymbolRefAttr(), mlir::DictionaryAttr()); + auto makeEntry = [&](uint64_t base, uint64_t size, uint64_t banks, + std::optional priority) { + llvm::SmallVector attributes{ + builder.getNamedAttr("base", builder.getI64IntegerAttr(base)), + builder.getNamedAttr("size", builder.getI64IntegerAttr(size)), + builder.getNamedAttr("target", + mlir::FlatSymbolRefAttr::get(&context, "space")), + builder.getNamedAttr("offset", builder.getI64IntegerAttr(0)), + builder.getNamedAttr( + "permissions", + builder.getArrayAttr({builder.getStringAttr("read")})), + builder.getNamedAttr("classes", builder.getArrayAttr({})), + builder.getNamedAttr( + "interleave", + builder.getDictionaryAttr({ + builder.getNamedAttr("granularity", + builder.getI64IntegerAttr(1)), + builder.getNamedAttr("banks", builder.getI64IntegerAttr(banks)), + builder.getNamedAttr("bank", builder.getI64IntegerAttr(0)), + })), + }; + if (priority) + attributes.push_back(builder.getNamedAttr( + "priority", builder.getI64IntegerAttr(*priority))); + return builder.getDictionaryAttr(attributes); + }; + llvm::SmallVector entries; + entries.push_back(makeEntry(10, 10, 1, std::nullopt)); + for (uint64_t index = 0; index < 257; ++index) + entries.push_back(makeEntry(0, 2000, 1000 + index, index + 1)); + AddressMapOp::create( + builder, location, "map", "space", builder.getArrayAttr(entries), + builder.getDictionaryAttr( + {builder.getNamedAttr("kind", builder.getStringAttr("unmapped"))})); + ReturnOp::create(builder, location, mlir::ValueRange{}); + EXPECT_TRUE(mlir::succeeded(mlir::verify(file))); +} + +struct SelectorOracleLane { + uint64_t base; + uint64_t size; + uint64_t granularity; + uint64_t banks; + uint64_t bank; +}; + +enum class SelectorOracleKind { + FastFast, + GeneralFast, + GeneralGeneralSame, + GeneralGeneralMixed, +}; + +struct SelectorOracleCase { + SelectorOracleLane left; + SelectorOracleLane right; + bool reverse; + SelectorOracleKind kind; +}; + +bool oracleSelects(const SelectorOracleLane &lane, uint64_t address) { + if (address < lane.base || address >= lane.base + lane.size) + return false; + uint64_t cycle = lane.granularity * lane.banks; + uint64_t within = address % cycle; + uint64_t begin = lane.granularity * lane.bank; + return begin <= within && within < begin + lane.granularity; +} + +std::vector makeSelectorOracleCases() { + std::vector cases; + cases.reserve(1800); + for (uint64_t index = 0; index < 450; ++index) { + uint64_t leftBanks = 8 + index % 5; + uint64_t rightBanks = 9 + index % 7; + cases.push_back( + {{index % 60, 2, 1, leftBanks, (index * 3) % leftBanks}, + {(index * 7) % 60, 2, 1, rightBanks, (index * 5) % rightBanks}, + index % 2 != 0, + SelectorOracleKind::FastFast}); + + uint64_t fastBanks = 8 + index % 9; + cases.push_back({{0, 64, 1, 2, index % 2}, + {index % 60, 3, 1, fastBanks, (index * 7) % fastBanks}, + index % 2 != 0, + SelectorOracleKind::GeneralFast}); + + uint64_t granularity = 1 + index % 3; + uint64_t banks = 2 + index % 4; + cases.push_back({{0, 64, granularity, banks, index % banks}, + {0, 64, granularity, banks, (index * 3 + 1) % banks}, + index % 2 != 0, + SelectorOracleKind::GeneralGeneralSame}); + + uint64_t mixedBanks = 3 + index % 4; + cases.push_back({{0, 64, 1, 2, index % 2}, + {0, 64, 2, mixedBanks, (index * 5) % mixedBanks}, + index % 2 != 0, + SelectorOracleKind::GeneralGeneralMixed}); + } + return cases; +} + +class SelectorOracleTest : public testing::TestWithParam { +protected: + static mlir::MLIRContext &context() { + static mlir::MLIRContext value; + static bool loaded = [] { + value.loadDialect(); + return true; + }(); + (void)loaded; + return value; + } +}; + +TEST_P(SelectorOracleTest, MatchesManualSmallDomainEnumeration) { + const SelectorOracleCase &testCase = GetParam(); + SCOPED_TRACE(testing::Message() + << "kind=" << static_cast(testCase.kind) + << " reverse=" << testCase.reverse); + bool overlaps = false; + for (uint64_t address = 0; address < 64; ++address) + overlaps |= oracleSelects(testCase.left, address) && + oracleSelects(testCase.right, address); + + mlir::MLIRContext &mlirContext = context(); + mlir::OpBuilder builder(&mlirContext); + auto location = builder.getUnknownLoc(); + auto file = mlir::ModuleOp::create(location); + builder.setInsertionPointToStart(file.getBody()); + auto module = + ModuleOp::create(builder, location, "M", builder.getFunctionType({}, {}), + builder.getDictionaryAttr({})); + builder.setInsertionPointToStart(module.addEntryBlock()); + AddressSpaceOp::create( + builder, location, builder.getStringAttr("space"), + builder.getStringAttr("space"), builder.getStringAttr("space"), + builder.getI64IntegerAttr(8), builder.getStringAttr("byte"), + mlir::Attribute(), mlir::FlatSymbolRefAttr(), mlir::DictionaryAttr()); + auto makeEntry = [&](const SelectorOracleLane &lane) { + return builder.getDictionaryAttr({ + builder.getNamedAttr("base", builder.getI64IntegerAttr(lane.base)), + builder.getNamedAttr("size", builder.getI64IntegerAttr(lane.size)), + builder.getNamedAttr( + "target", mlir::FlatSymbolRefAttr::get(&mlirContext, "space")), + builder.getNamedAttr("offset", builder.getI64IntegerAttr(0)), + builder.getNamedAttr( + "permissions", + builder.getArrayAttr({builder.getStringAttr("read")})), + builder.getNamedAttr("classes", builder.getArrayAttr({})), + builder.getNamedAttr( + "interleave", + builder.getDictionaryAttr({ + builder.getNamedAttr( + "granularity", builder.getI64IntegerAttr(lane.granularity)), + builder.getNamedAttr("banks", + builder.getI64IntegerAttr(lane.banks)), + builder.getNamedAttr("bank", + builder.getI64IntegerAttr(lane.bank)), + })), + }); + }; + llvm::SmallVector entries{makeEntry(testCase.left), + makeEntry(testCase.right)}; + if (testCase.reverse) + std::reverse(entries.begin(), entries.end()); + AddressMapOp::create( + builder, location, "map", "space", builder.getArrayAttr(entries), + builder.getDictionaryAttr( + {builder.getNamedAttr("kind", builder.getStringAttr("unmapped"))})); + ReturnOp::create(builder, location, mlir::ValueRange{}); + mlir::ScopedDiagnosticHandler suppress( + &mlirContext, [](mlir::Diagnostic &) { return mlir::success(); }); + EXPECT_EQ(mlir::succeeded(mlir::verify(file)), !overlaps); +} + +std::string +selectorOracleName(const testing::TestParamInfo &info) { + const char *prefix = nullptr; + switch (info.param.kind) { + case SelectorOracleKind::FastFast: + prefix = "FF"; + break; + case SelectorOracleKind::GeneralFast: + prefix = "GF"; + break; + case SelectorOracleKind::GeneralGeneralSame: + prefix = "GGSame"; + break; + case SelectorOracleKind::GeneralGeneralMixed: + prefix = "GGMixed"; + break; + } + return std::string(prefix) + "_" + std::to_string(info.index); +} + +INSTANTIATE_TEST_SUITE_P(SmallDomain, SelectorOracleTest, + testing::ValuesIn(makeSelectorOracleCases()), + selectorOracleName); + +TEST(ACIRFreezeEffectsTest, FrozenEffectsUseElaboratedAbsoluteOwnerSets) { + mlir::DialectRegistry registry; + acir::registerAllDialects(registry); + mlir::MLIRContext context(registry); + auto file = mlir::parseSourceString(R"mlir( + builtin.module attributes {ac.contract_epoch = "0.3"} { + ac.system @soc root @Top as "root" tick 0 "cycle" + workload @Top::@workload seed {kind = "fixed", value = 0 : i64} + instrumentation [] results {id = "default", format = "json"} + selected true + ac.module @Top() parameters {} graph { + ac.process @workload kind "workload" { ac.yield_sim } + ac.stat @requests kind "counter" + ac.return + } + } + )mlir", + &context); + ASSERT_TRUE(file); + mlir::PassManager manager(&context); + manager.addPass(acir::createFreezeTopologyPass()); + ASSERT_TRUE(mlir::succeeded(manager.run(*file))); + + ProcessOp process; + StatOp stat; + file->walk([&](ProcessOp candidate) { process = candidate; }); + file->walk([&](StatOp candidate) { stat = candidate; }); + ASSERT_TRUE(process && stat); + auto checkAbsoluteOwners = [](mlir::Operation *operation, + llvm::StringRef expectedPath) { + llvm::SmallVector effects; + mlir::cast(operation).getEffects(effects); + ASSERT_FALSE(effects.empty()); + for (const mlir::MemoryEffects::EffectInstance &effect : effects) { + auto parameters = + mlir::cast(effect.getParameters()); + EXPECT_EQ(parameters.getAs("identity_phase").getValue(), + "elaborated_absolute"); + auto owners = parameters.getAs("owners"); + ASSERT_TRUE(owners); + ASSERT_EQ(owners.size(), 1u); + EXPECT_EQ(mlir::cast(owners[0]) + .getAs("path") + .getValue(), + expectedPath); + } + }; + checkAbsoluteOwners(&process.getBody().front().back(), "root.workload"); + checkAbsoluteOwners(stat, "root.requests"); + + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream stream(diagnostic); + stream << value; + return mlir::success(); + }); + stat->removeAttr("ac.frozen_owners"); + EXPECT_TRUE(mlir::failed(acir::verifyModel(*file))); + EXPECT_NE(diagnostic.find("frozen topology digest mismatch"), + std::string::npos); +} + +} // namespace +} // namespace acir::ac diff --git a/components/agentic-circuit/unittests/Dialect/ACIR/TypesTest.cpp b/components/agentic-circuit/unittests/Dialect/ACIR/TypesTest.cpp new file mode 100644 index 00000000..5d261b91 --- /dev/null +++ b/components/agentic-circuit/unittests/Dialect/ACIR/TypesTest.cpp @@ -0,0 +1,215 @@ +#include "acir/Dialect/ACIR/ACIRDialect.h" +#include "acir/Dialect/ACIR/ACIRTypes.h" + +#include "mlir/AsmParser/AsmParser.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/MLIRContext.h" +#include "gtest/gtest.h" + +#include + +namespace acir::ac { +namespace { + +TEST(ACIRTypesTest, PublicTypeInventoryRoundTrips) { + mlir::MLIRContext context; + context.loadDialect(); + + struct TypeCase { + llvm::StringLiteral spelling; + mlir::TypeID typeID; + }; + const std::array cases = {{ + {"!ac.struct<@types::@Struct>", StructType::getTypeID()}, + {"!ac.packet<@types::@Packet>", PacketType::getTypeID()}, + {"!ac.transaction<@types::@Transaction>", TransactionType::getTypeID()}, + {"!ac.enum<@types::@Enum>", EnumType::getTypeID()}, + {"!ac.union<@types::@Union>", UnionType::getTypeID()}, + {"!ac.optional", OptionalType::getTypeID()}, + {"!ac.list", ListType::getTypeID()}, + {"!ac.vector<4 x i8>", VectorType::getTypeID()}, + {"!ac.flow", FlowType::getTypeID()}, + {"!ac.endpoint<@interface, @role>", EndpointType::getTypeID()}, + {"!ac.resource_ref<@resource_type, @role>", ResourceRefType::getTypeID()}, + {"!ac.channel", ChannelType::getTypeID()}, + {"!ac.duration", DurationType::getTypeID()}, + {"!ac.rate", RateType::getTypeID()}, + {"!ac.event", EventType::getTypeID()}, + {"!ac.address<@space>", AddressType::getTypeID()}, + {"!ac.resource_token<@resource>", ResourceTokenType::getTypeID()}, + }}; + + for (const TypeCase &testCase : cases) { + mlir::Type type = mlir::parseType(testCase.spelling, &context); + ASSERT_TRUE(type) << testCase.spelling.str(); + EXPECT_EQ(type.getTypeID(), testCase.typeID) << testCase.spelling.str(); + + std::string printed; + llvm::raw_string_ostream(printed) << type; + EXPECT_EQ(printed, testCase.spelling) << testCase.spelling.str(); + EXPECT_EQ(type, mlir::parseType(printed, &context)); + } +} + +TEST(ACIRTypesTest, QueueVarTypesRoundTripWithImmutablePayloads) { + mlir::MLIRContext context; + context.loadDialect(); + + struct TypeCase { + llvm::StringLiteral spelling; + mlir::TypeID typeID; + }; + const std::array cases = {{ + {"!ac.var", VarType::getTypeID()}, + {"!ac.queue>", QueueType::getTypeID()}, + }}; + + for (const TypeCase &testCase : cases) { + mlir::Type type = mlir::parseType(testCase.spelling, &context); + ASSERT_TRUE(type) << testCase.spelling.str(); + EXPECT_EQ(type.getTypeID(), testCase.typeID) << testCase.spelling.str(); + + std::string printed; + llvm::raw_string_ostream(printed) << type; + EXPECT_EQ(printed, testCase.spelling) << testCase.spelling.str(); + EXPECT_EQ(type, mlir::parseType(printed, &context)); + } +} + +TEST(ACIRTypesTest, StaticQueueVarCollectionsRoundTrip) { + mlir::MLIRContext context; + context.loadDialect(); + + struct TypeCase { + llvm::StringLiteral spelling; + mlir::TypeID typeID; + }; + const std::array cases = {{ + {"!ac.array<4 x !ac.queue>", ArrayType::getTypeID()}, + {"!ac.map<[\"cube\", \"scalar\", \"vector\"], !ac.queue>", + MapType::getTypeID()}, + {"!ac.set<4 x !ac.var>", SetType::getTypeID()}, + {"!ac.array<2 x !ac.map<[\"left\", \"right\"], " + "!ac.queue>>>", + ArrayType::getTypeID()}, + }}; + + for (const TypeCase &testCase : cases) { + mlir::Type type = mlir::parseType(testCase.spelling, &context); + ASSERT_TRUE(type) << testCase.spelling.str(); + EXPECT_EQ(type.getTypeID(), testCase.typeID) << testCase.spelling.str(); + + std::string printed; + llvm::raw_string_ostream(printed) << type; + EXPECT_EQ(printed, testCase.spelling) << testCase.spelling.str(); + EXPECT_EQ(type, mlir::parseType(printed, &context)); + } +} + +TEST(ACIRTypesTest, EveryUnitCategoryIsChecked) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::ScopedDiagnosticHandler suppressExpectedDiagnostics( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + auto location = mlir::UnknownLoc::get(&context); + auto emitError = [location] { return mlir::emitError(location); }; + + const std::array timeUnits = { + Unit::Ticks, Unit::Cycles, Unit::Seconds, + Unit::Milliseconds, Unit::Microseconds, Unit::Nanoseconds, + Unit::Picoseconds, + }; + const std::array dataUnits = {Unit::Bytes, Unit::Bits, Unit::Entries, + Unit::Packets, Unit::Transactions}; + + for (Unit timeUnit : timeUnits) { + EXPECT_TRUE(DurationType::getChecked(emitError, &context, timeUnit)); + EXPECT_TRUE( + RateType::getChecked(emitError, &context, Unit::Bytes, timeUnit)); + EXPECT_FALSE( + RateType::getChecked(emitError, &context, timeUnit, Unit::Cycles)); + } + for (Unit dataUnit : dataUnits) { + EXPECT_FALSE(DurationType::getChecked(emitError, &context, dataUnit)); + EXPECT_TRUE( + RateType::getChecked(emitError, &context, dataUnit, Unit::Cycles)); + EXPECT_FALSE( + RateType::getChecked(emitError, &context, Unit::Bytes, dataUnit)); + } +} + +TEST(ACIRTypesTest, TypesAreUniquedByTheirParameters) { + mlir::MLIRContext context; + context.loadDialect(); + + auto payload = mlir::IntegerType::get(&context, 32); + EXPECT_EQ(OptionalType::get(&context, payload), + OptionalType::get(&context, payload)); + EXPECT_NE(VectorType::get(&context, int64_t{4}, mlir::Type(payload)), + VectorType::get(&context, int64_t{8}, mlir::Type(payload))); +} + +TEST(ACIRTypesTest, NamedTypesPreserveTheirIdentity) { + mlir::MLIRContext context; + context.loadDialect(); + + auto lhs = mlir::SymbolRefAttr::get( + &context, "types", {mlir::FlatSymbolRefAttr::get(&context, "Left")}); + auto rhs = mlir::SymbolRefAttr::get( + &context, "types", {mlir::FlatSymbolRefAttr::get(&context, "Right")}); + EXPECT_NE(StructType::get(&context, lhs), StructType::get(&context, rhs)); +} + +TEST(ACIRTypesTest, CheckedBuildersRejectInvalidParameters) { + mlir::MLIRContext context; + context.loadDialect(); + auto payload = mlir::IntegerType::get(&context, 8); + mlir::ScopedDiagnosticHandler suppressExpectedDiagnostics( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + + auto location = mlir::UnknownLoc::get(&context); + auto emitError = [location] { return mlir::emitError(location); }; + EXPECT_FALSE(VectorType::getChecked(emitError, &context, int64_t{0}, + mlir::Type(payload))); + EXPECT_FALSE(DurationType::getChecked(emitError, &context, Unit::Bytes)); + auto functionType = mlir::FunctionType::get(&context, {payload}, {payload}); + EXPECT_FALSE( + VarType::getChecked(emitError, &context, mlir::Type(functionType))); + EXPECT_FALSE( + QueueType::getChecked(emitError, &context, mlir::Type(functionType))); + auto variable = VarType::get(&context, payload); + auto queue = QueueType::get(&context, payload); + auto dynamicList = ListType::get(&context, payload); + EXPECT_FALSE( + QueueType::getChecked(emitError, &context, mlir::Type(variable))); + EXPECT_FALSE(VarType::getChecked(emitError, &context, mlir::Type(queue))); + EXPECT_FALSE( + QueueType::getChecked(emitError, &context, mlir::Type(dynamicList))); + EXPECT_FALSE( + VarType::getChecked(emitError, &context, mlir::Type(dynamicList))); + auto queueCollection = QueueType::get(&context, payload); + auto duplicateKeys = + mlir::ArrayAttr::get(&context, {mlir::StringAttr::get(&context, "lane"), + mlir::StringAttr::get(&context, "lane")}); + auto reversedKeys = + mlir::ArrayAttr::get(&context, {mlir::StringAttr::get(&context, "right"), + mlir::StringAttr::get(&context, "left")}); + EXPECT_FALSE(ArrayType::getChecked(emitError, &context, int64_t{0}, + mlir::Type(queueCollection))); + EXPECT_FALSE(ArrayType::getChecked(emitError, &context, int64_t{2}, + mlir::Type(payload))); + EXPECT_FALSE(MapType::getChecked(emitError, &context, duplicateKeys, + mlir::Type(queueCollection))); + EXPECT_FALSE(MapType::getChecked(emitError, &context, reversedKeys, + mlir::Type(queueCollection))); + EXPECT_FALSE(SetType::getChecked(emitError, &context, int64_t{0}, + mlir::Type(queueCollection))); + EXPECT_FALSE(SetType::getChecked(emitError, &context, int64_t{2}, + mlir::Type(payload))); + EXPECT_FALSE( + RateType::getChecked(emitError, &context, Unit::Cycles, Unit::Cycles)); +} + +} // namespace +} // namespace acir::ac diff --git a/components/agentic-circuit/unittests/Dialect/ACSim/CMakeLists.txt b/components/agentic-circuit/unittests/Dialect/ACSim/CMakeLists.txt new file mode 100644 index 00000000..68aed4d7 --- /dev/null +++ b/components/agentic-circuit/unittests/Dialect/ACSim/CMakeLists.txt @@ -0,0 +1,33 @@ +find_package(GTest CONFIG REQUIRED) + +add_executable(ACSimTypesTests TypesTest.cpp) +target_link_libraries(ACSimTypesTests PRIVATE + ACSimDialect + MLIRAsmParser + MLIRIR + GTest::gtest_main +) +add_test(NAME ACSimTypesTests COMMAND ACSimTypesTests) + +add_executable(ACSimOpsTests OpsTest.cpp) +target_include_directories(ACSimOpsTests PRIVATE + "${PROJECT_SOURCE_DIR}/lib" +) +target_compile_definitions(ACSimOpsTests PRIVATE + ACSIM_VALID_TEST_FILE="${PROJECT_SOURCE_DIR}/test/ACSim/ops-valid.mlir" + ACSIM_REUSABLE_TEST_FILE="${PROJECT_SOURCE_DIR}/test/ACSim/reusable-modules.mlir" + ACSIM_WRAPPER_ACTIVATION_TEST_FILE="${PROJECT_SOURCE_DIR}/test/ACSim/generated-wrapper-activation.mlir" +) +target_link_libraries(ACSimOpsTests PRIVATE + ACSimDialect + ACIRDialect + MLIRArithDialect + MLIRAsmParser + MLIRControlFlowDialect + MLIRIndexDialect + MLIRIR + MLIRParser + MLIRSideEffectInterfaces + GTest::gtest_main +) +add_test(NAME ACSimOpsTests COMMAND ACSimOpsTests) diff --git a/components/agentic-circuit/unittests/Dialect/ACSim/OpsTest.cpp b/components/agentic-circuit/unittests/Dialect/ACSim/OpsTest.cpp new file mode 100644 index 00000000..aed6317f --- /dev/null +++ b/components/agentic-circuit/unittests/Dialect/ACSim/OpsTest.cpp @@ -0,0 +1,1917 @@ +#include "Dialect/ACSim/ACSimOpsTestHooks.h" +#include "acir/Dialect/ACSim/ACSimDialect.h" +#include "acir/Dialect/ACSim/ACSimOps.h" +#include "acir/Dialect/ACSim/ACSimTypes.h" + +#include "mlir/AsmParser/AsmParser.h" +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" +#include "mlir/Dialect/Index/IR/IndexDialect.h" +#include "mlir/IR/AttrTypeSubElements.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/OperationSupport.h" +#include "mlir/IR/Verifier.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Parser/Parser.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/Support/Format.h" +#include "llvm/Support/raw_ostream.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include + +namespace acir::acsim { +namespace { + +void loadTestDialects(mlir::MLIRContext &context) { + context + .loadDialect(); +} + +template Op firstOp(mlir::ModuleOp file) { + Op result; + file.walk([&](Op candidate) { + if (!result) + result = candidate; + }); + return result; +} + +template +struct HasPublicSymbolNameAccessor : std::false_type {}; + +template +struct HasPublicSymbolNameAccessor< + Op, std::void_t().getSymName())>> + : std::is_same().getSymName()), llvm::StringRef> { +}; + +void replaceDictionaryField(BindingOp binding, llvm::StringRef name, + mlir::Attribute value) { + mlir::NamedAttrList fields(binding.getRecord()); + fields.set(name, value); + binding.setRecordAttr(fields.getDictionary(binding.getContext())); +} + +void replaceNestedDictionaryField(BindingOp binding, llvm::StringRef record, + llvm::StringRef name, mlir::Attribute value) { + mlir::NamedAttrList fields(binding.getRecord()); + mlir::NamedAttrList nested( + mlir::dyn_cast(fields.get(record))); + nested.set(name, value); + fields.set(record, nested.getDictionary(binding.getContext())); + binding.setRecordAttr(fields.getDictionary(binding.getContext())); +} + +void replaceRecordArrayField(BindingOp binding, llvm::StringRef records, + unsigned ordinal, llvm::StringRef name, + mlir::Attribute value) { + mlir::NamedAttrList fields(binding.getRecord()); + auto array = mlir::cast(fields.get(records)); + llvm::SmallVector elements(array.begin(), array.end()); + mlir::NamedAttrList nested( + mlir::cast(elements[ordinal])); + nested.set(name, value); + elements[ordinal] = nested.getDictionary(binding.getContext()); + fields.set(records, mlir::ArrayAttr::get(binding.getContext(), elements)); + binding.setRecordAttr(fields.getDictionary(binding.getContext())); +} + +void replaceModuleInterfaceRecordField(ModuleOp module, llvm::StringRef records, + unsigned ordinal, llvm::StringRef name, + mlir::Attribute value) { + mlir::NamedAttrList fields(module.getInterface()); + auto array = mlir::cast(fields.get(records)); + llvm::SmallVector elements(array.begin(), array.end()); + mlir::NamedAttrList nested( + mlir::cast(elements[ordinal])); + nested.set(name, value); + elements[ordinal] = nested.getDictionary(module.getContext()); + fields.set(records, mlir::ArrayAttr::get(module.getContext(), elements)); + module.setInterfaceAttr(fields.getDictionary(module.getContext())); +} + +template +std::string expectDirectVerificationFailure(mlir::MLIRContext &context, + Callable &&verify) { + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler(&context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(verify())); + return diagnostic; +} + +std::string scalableModel(unsigned extraTypes) { + std::string source; + llvm::raw_string_ostream os(source); + os << R"mlir( +builtin.module attributes {ac.contract_epoch = "0.3"} { + acsim.model @scale epoch "0.3" root @Top construction [] destruction [] + fingerprints { + frozen_acir = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + binding_lock = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + provider = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + profile = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + toolchain = "sha256:0000000000000000000000000000000000000000000000000000000000000000", + schema_set = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } { +)mlir"; + for (unsigned index = 0; index != extraTypes; ++index) + os << " acsim.type @x" << llvm::format_hex_no_prefix(index, 8) + << " cpp \"bool\" kind \"value\" fingerprint \"sha256:" + "0000000000000000000000000000000000000000000000000000000000000000\"" + "\n"; + os << R"mlir( acsim.module @Top interface {ports = [], resources = [], results = []} static [] specialization "sha256:0000000000000000000000000000000000000000000000000000000000000000" exports [] { + acsim.return + } + } +} +)mlir"; + return source; +} + +mlir::OwningOpRef parseValidModel(mlir::MLIRContext &context) { + return mlir::parseSourceFile(ACSIM_VALID_TEST_FILE, &context); +} + +mlir::OwningOpRef +parseReusableModel(mlir::MLIRContext &context) { + return mlir::parseSourceFile(ACSIM_REUSABLE_TEST_FILE, + &context); +} + +mlir::OwningOpRef +parseWrapperActivationModel(mlir::MLIRContext &context) { + return mlir::parseSourceFile( + ACSIM_WRAPPER_ACTIVATION_TEST_FILE, &context); +} + +std::string expectVerificationFailure(mlir::ModuleOp file) { + std::string diagnostic; + mlir::ScopedDiagnosticHandler handler( + file.getContext(), [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(diagnostic) << value; + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(mlir::verify(file))); + return diagnostic; +} + +TEST(ACSimOpsTest, RegistryIsExactlyTheAuthoritativeTwentyTwoOperationTable) { + mlir::MLIRContext context; + context.loadDialect(); + + std::vector actual; + for (mlir::RegisteredOperationName operation : + context.getRegisteredOperationsByDialect("acsim")) + actual.push_back(operation.getStringRef().str()); + llvm::sort(actual); + + std::vector expected = { + "acsim.activate", "acsim.array", "acsim.bind", "acsim.binding", + "acsim.continue", "acsim.dispatch", "acsim.element", "acsim.export", + "acsim.inline", "acsim.instance", "acsim.invoke", "acsim.live.load", + "acsim.live.store", "acsim.model", "acsim.module", "acsim.port", + "acsim.process", "acsim.resource", "acsim.return", "acsim.suspend", + "acsim.terminate", "acsim.type", + }; + llvm::sort(expected); + EXPECT_EQ(actual, expected); +} + +TEST(ACSimOpsTest, GeneratedRealizationsParseWithoutFabricatedBindings) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto file = mlir::parseSourceString(R"mlir( +builtin.module { + acsim.module @Child interface {ports = [], resources = [], results = []} + static [] specialization "sha256:1000000000000000000000000000000000000000000000000000000000000000" + exports [] { + acsim.return + } + acsim.module @Top interface {ports = [], resources = [], results = []} + static [] specialization "sha256:2000000000000000000000000000000000000000000000000000000000000000" + exports [] { + %child = acsim.instance @child target @Child args [] + specialization "sha256:1000000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@Child> + acsim.process @tick captures() names [] entry @entry pcs [@entry] live [] + fairness 1 specialization "sha256:3000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + acsim.terminate "success" + } + } + acsim.return + } +} +)mlir", + &context); + ASSERT_TRUE(file); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); +} + +TEST(ACSimOpsTest, LegacyGeneratedBindingSyntaxIsRejected) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto file = mlir::parseSourceString(R"mlir( +builtin.module { + acsim.module @Top binding @legacy static [] + specialization "sha256:1000000000000000000000000000000000000000000000000000000000000000" + exports [] { + %child = acsim.instance @child binding @legacy target @legacy args [] + specialization "sha256:2000000000000000000000000000000000000000000000000000000000000000" + : !acsim.owner<@legacy> + acsim.process @tick binding @legacy captures() names [] entry @entry + pcs [@entry] live [] fairness 1 + specialization "sha256:3000000000000000000000000000000000000000000000000000000000000000" { + state @entry { + acsim.terminate "success" + } + } + acsim.return + } +} +)mlir", + &context); + EXPECT_FALSE(file); +} + +TEST(ACSimOpsTest, GeneratedCallsExposeExactCalleeAttributeApi) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto file = mlir::parseSourceString(R"mlir( +builtin.module { + %inline = acsim.inline @generated_inline() : () -> !acsim.expr<@cpp_i32> + %invoke = acsim.invoke @generated_invoke() : () -> !acsim.value<@cpp_i32> +} +)mlir", + &context); + ASSERT_TRUE(file); + + InlineOp inlineOp = firstOp(*file); + InvokeOp invokeOp = firstOp(*file); + ASSERT_TRUE(inlineOp); + ASSERT_TRUE(invokeOp); + EXPECT_EQ(inlineOp.getCalleeAttr().getValue(), "generated_inline"); + EXPECT_EQ(inlineOp.getCallee(), "generated_inline"); + EXPECT_EQ(inlineOp->getAttr("callee"), inlineOp.getCalleeAttr()); + EXPECT_FALSE(inlineOp->hasAttr("binding")); + EXPECT_EQ(invokeOp.getCalleeAttr().getValue(), "generated_invoke"); + EXPECT_EQ(invokeOp.getCallee(), "generated_invoke"); + EXPECT_EQ(invokeOp->getAttr("callee"), invokeOp.getCalleeAttr()); + EXPECT_FALSE(invokeOp->hasAttr("binding")); + + mlir::OpBuilder builder(&context); + builder.setInsertionPointToEnd(file->getBody()); + InlineOp builtInline = InlineOp::create( + builder, builder.getUnknownLoc(), + ExprType::get(&context, + mlir::FlatSymbolRefAttr::get(&context, "cpp_i32")), + mlir::ValueRange{}, "built_inline"); + InvokeOp builtInvoke = + InvokeOp::create(builder, builder.getUnknownLoc(), mlir::TypeRange{}, + mlir::ValueRange{}, "built_invoke"); + EXPECT_EQ(builtInline.getCalleeAttr().getValue(), "built_inline"); + EXPECT_EQ(builtInvoke.getCalleeAttr().getValue(), "built_invoke"); +} + +TEST(ACSimOpsTest, ExportRetainsItsPublicSymbolNameApi) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ExportOp exportOp = firstOp(*file); + ASSERT_TRUE(exportOp); + + EXPECT_TRUE(HasPublicSymbolNameAccessor::value); + EXPECT_TRUE(exportOp->getAttrOfType( + mlir::SymbolTable::getSymbolAttrName())); + EXPECT_FALSE(exportOp->hasAttr("export_name")); +} + +TEST(ACSimOpsTest, ModuleInterfaceRecordsAreClosedOrderedAndExact) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto extraKeyFile = parseValidModel(context); + ASSERT_TRUE(extraKeyFile); + ModuleOp module = firstOp(*extraKeyFile); + mlir::NamedAttrList interface(module.getInterface()); + interface.set("runtime", mlir::UnitAttr::get(&context)); + module.setInterfaceAttr(interface.getDictionary(&context)); + EXPECT_TRUE(llvm::StringRef(expectDirectVerificationFailure(context, [&] { + return module.verify(); + })).contains("exactly ports, resources, and results")); + + auto orderingFile = parseValidModel(context); + ASSERT_TRUE(orderingFile); + module = firstOp(*orderingFile); + auto ports = module.getInterface().getAs("ports"); + llvm::SmallVector duplicated{ports[0], ports[0]}; + interface = mlir::NamedAttrList(module.getInterface()); + interface.set("ports", mlir::ArrayAttr::get(&context, duplicated)); + module.setInterfaceAttr(interface.getDictionary(&context)); + EXPECT_TRUE(llvm::StringRef(expectDirectVerificationFailure(context, [&] { + return module.verify(); + })).contains("strictly name-sorted")); + + auto accessorFile = parseValidModel(context); + ASSERT_TRUE(accessorFile); + module = firstOp(*accessorFile); + replaceModuleInterfaceRecordField( + module, "resources", 0, "accessor", + mlir::FlatSymbolRefAttr::get(&context, "port_accessor")); + EXPECT_TRUE(llvm::StringRef(expectDirectVerificationFailure(context, [&] { + return module.verify(); + })).contains("globally unique accessors")); + + auto closedRecordFile = parseValidModel(context); + ASSERT_TRUE(closedRecordFile); + module = firstOp(*closedRecordFile); + auto resultRecords = module.getInterface().getAs("results"); + mlir::NamedAttrList result( + mlir::cast(resultRecords[0])); + result.set("role", mlir::FlatSymbolRefAttr::get(&context, "role")); + interface = mlir::NamedAttrList(module.getInterface()); + interface.set("results", mlir::ArrayAttr::get( + &context, {result.getDictionary(&context)})); + module.setInterfaceAttr(interface.getDictionary(&context)); + EXPECT_TRUE(llvm::StringRef(expectDirectVerificationFailure(context, [&] { + return module.verify(); + })).contains("exact closed fields")); + + auto wrongKindFile = parseValidModel(context); + ASSERT_TRUE(wrongKindFile); + module = firstOp(*wrongKindFile); + replaceModuleInterfaceRecordField( + module, "ports", 0, "interface", + mlir::FlatSymbolRefAttr::get(&context, "role")); + EXPECT_TRUE( + llvm::StringRef(expectVerificationFailure(*wrongKindFile)) + .contains("interface kind reference '@role' has incompatible")); + + auto delegationFile = parseValidModel(context); + ASSERT_TRUE(delegationFile); + module = firstOp(*delegationFile); + replaceModuleInterfaceRecordField( + module, "ports", 0, "delegation", + mlir::StringAttr::get(&context, "optional")); + EXPECT_TRUE(llvm::StringRef(expectDirectVerificationFailure(context, [&] { + return module.verify(); + })).contains("exact typed endpoint metadata")); + + auto roleMismatchFile = parseValidModel(context); + ASSERT_TRUE(roleMismatchFile); + module = firstOp(*roleMismatchFile); + replaceModuleInterfaceRecordField( + module, "ports", 0, "role", + mlir::FlatSymbolRefAttr::get(&context, "consumer")); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*roleMismatchFile)) + .contains("port export must exactly match")); + + auto typeMismatchFile = parseValidModel(context); + ASSERT_TRUE(typeMismatchFile); + module = firstOp(*typeMismatchFile); + replaceModuleInterfaceRecordField( + module, "results", 0, "cpp_type", + mlir::FlatSymbolRefAttr::get(&context, "payload")); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*typeMismatchFile)) + .contains("result export must exactly match")); + + auto exportMismatchFile = parseValidModel(context); + ASSERT_TRUE(exportMismatchFile); + module = firstOp(*exportMismatchFile); + replaceModuleInterfaceRecordField( + module, "ports", 0, "accessor", + mlir::FlatSymbolRefAttr::get(&context, "port_in_accessor")); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*exportMismatchFile)) + .contains("port export must exactly match")); +} + +TEST(ACSimOpsTest, ModuleExportsRequireExactEndpointDelegation) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto portFile = parseValidModel(context); + ASSERT_TRUE(portFile); + ModuleOp module = firstOp(*portFile); + replaceModuleInterfaceRecordField(module, "ports", 0, "delegation", + mlir::StringAttr::get(&context, "allowed")); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*portFile)) + .contains("port export must exactly match")); + + auto resourceFile = parseValidModel(context); + ASSERT_TRUE(resourceFile); + module = firstOp(*resourceFile); + replaceModuleInterfaceRecordField(module, "resources", 0, "delegation", + mlir::StringAttr::get(&context, "allowed")); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*resourceFile)) + .contains("resource export must exactly match")); +} + +TEST(ACSimOpsTest, PortAndResourceExportsMayShareAnInterfaceName) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ModuleOp module = firstOp(*file); + auto shared = mlir::StringAttr::get(&context, "shared"); + replaceModuleInterfaceRecordField(module, "ports", 0, "name", shared); + replaceModuleInterfaceRecordField(module, "resources", 0, "name", shared); + llvm::SmallVector exports; + module.walk([&](ExportOp exportOp) { exports.push_back(exportOp); }); + ASSERT_GE(exports.size(), 2u); + exports[0].setSymName("shared"); + exports[1].setSymName("shared"); + module.setExportsAttr(mlir::ArrayAttr::get( + &context, {mlir::FlatSymbolRefAttr::get(&context, "shared"), + mlir::FlatSymbolRefAttr::get(&context, "shared"), + mlir::FlatSymbolRefAttr::get(&context, "out")})); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); +} + +TEST(ACSimOpsTest, GeneratedWrapperEndpointsReachChildRuntimeObjects) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseWrapperActivationModel(context); + ASSERT_TRUE(file); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); + + llvm::SmallVector> edges; + file->walk([&](ActivateOp activate) { + edges.emplace_back( + activate.getSource().getDefiningOp().getObjectId(), + activate.getTarget().getDefiningOp().getObjectId()); + }); + EXPECT_TRUE(llvm::is_contained(edges, std::pair{0, 1})); + EXPECT_TRUE(llvm::is_contained(edges, std::pair{0, 2})); +} + +TEST(ACSimOpsTest, GeneratedModuleWrappersOwnChildrenButHaveNoRuntimeRows) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseReusableModel(context); + ASSERT_TRUE(file); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*file))); + + ModelOp model = firstOp(*file); + EXPECT_EQ(model.getConstructionOrder().size(), 9u); + unsigned dispatches = 0; + unsigned processRows = 0; + file->walk([&](DispatchOp dispatch) { + ++dispatches; + EXPECT_EQ(dispatch.getTarget().getRootReference().getValue(), "Leaf"); + EXPECT_TRUE( + llvm::is_contained({llvm::StringRef("child"), llvm::StringRef("pulse")}, + dispatch.getTarget().getLeafReference().getValue())); + processRows += + dispatch.getTarget().getLeafReference().getValue() == "pulse"; + }); + EXPECT_EQ(dispatches, 6u); + EXPECT_EQ(processRows, 3u); + + InstanceOp wrapper; + file->walk([&](InstanceOp candidate) { + if (candidate.getSymName() == "left") + wrapper = candidate; + }); + ASSERT_TRUE(wrapper); + auto owner = mlir::dyn_cast(wrapper.getResult().getType()); + ASSERT_TRUE(owner); + EXPECT_EQ(owner.getRealization(), wrapper.getTargetAttr()); + + auto invalid = parseValidModel(context); + ASSERT_TRUE(invalid); + firstOp(*invalid).getResult().setType( + RefType::get(&context, mlir::FlatSymbolRefAttr::get(&context, "role"))); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*invalid)) + .contains("realization reference '@role' resolves to " + "incompatible operation")); +} + +TEST(ACSimOpsTest, BindingArraysAndProcessesReceiveExactRuntimeRows) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*file))); + + unsigned laneRows = 0; + unsigned processRows = 0; + file->walk([&](DispatchOp dispatch) { + laneRows += dispatch.getPath().starts_with("Top.lanes["); + processRows += dispatch.getTarget().getLeafReference().getValue() == "tick"; + }); + EXPECT_EQ(laneRows, 2u); + EXPECT_EQ(processRows, 1u); +} + +TEST(ACSimOpsTest, + DeclarationPermutationsFailBeforeExpansionDeterministically) { + mlir::MLIRContext context; + loadTestDialects(context); + auto diagnose = [&]() { + auto file = parseReusableModel(context); + EXPECT_TRUE(file); + ModuleOp top; + file->walk([&](ModuleOp candidate) { + if (candidate.getSymName() == "Top") + top = candidate; + }); + auto left = *top.getBody().front().getOps().begin(); + auto right = *top.getBody().front().getOps().begin(); + right->moveBefore(left); + return expectVerificationFailure(*file); + }; + std::string first = diagnose(); + std::string second = diagnose(); + EXPECT_EQ(first, second); + EXPECT_TRUE(llvm::StringRef(first).contains( + "owned placements must be strictly symbol-sorted")); +} + +TEST(ACSimOpsTest, + ProcessDeclarationPermutationsFailBeforeExpansionDeterministically) { + mlir::MLIRContext context; + loadTestDialects(context); + auto diagnose = [&]() { + auto file = parseReusableModel(context); + EXPECT_TRUE(file); + ProcessOp pulse = firstOp(*file); + auto *zetaOperation = pulse->clone(); + auto zeta = mlir::cast(zetaOperation); + zeta.setSymName("zeta"); + zeta.setSpecializationFingerprint( + "sha256:" + "c000000000000000000000000000000000000000000000000000000000000000"); + pulse->getBlock()->getOperations().insert(pulse->getIterator(), + zetaOperation); + return expectVerificationFailure(*file); + }; + std::string first = diagnose(); + std::string second = diagnose(); + EXPECT_EQ(first, second); + EXPECT_TRUE(llvm::StringRef(first).contains( + "process declarations must be strictly symbol-sorted")); +} + +TEST(ACSimOpsTest, CustomModelIndexOwnsProcessReferenceResolution) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*file))); + + ProcessOp process = firstOp(*file); + LiveLoadOp load = firstOp(*file); + LiveStoreOp store = firstOp(*file); + DispatchOp processDispatch; + file->walk([&](DispatchOp dispatch) { + if (dispatch.getTarget().getLeafReference().getValue() == "tick") + processDispatch = dispatch; + }); + ASSERT_TRUE(process); + ASSERT_TRUE(load); + ASSERT_TRUE(store); + ASSERT_TRUE(processDispatch); + EXPECT_EQ(load.getProcess(), process.getSymName()); + EXPECT_EQ(store.getProcess(), process.getSymName()); + EXPECT_EQ(processDispatch.getTarget().getRootReference().getValue(), "Top"); + EXPECT_EQ(processDispatch.getTarget().getLeafReference().getValue(), + process.getSymName()); +} + +TEST(ACSimOpsTest, + CustomModelIndexRejectsDuplicateLocalPlacementsAndProcesses) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto instanceFile = parseReusableModel(context); + ASSERT_TRUE(instanceFile); + InstanceOp left; + instanceFile->walk([&](InstanceOp instance) { + if (instance.getSymName() == "left") + left = instance; + }); + ASSERT_TRUE(left); + left->getBlock()->getOperations().insert(left->getIterator(), left->clone()); + std::string instanceDiagnostic = expectVerificationFailure(*instanceFile); + EXPECT_TRUE(llvm::StringRef(instanceDiagnostic) + .contains("duplicate canonical symbol or placement " + "'Top::left'")) + << instanceDiagnostic; + + auto arrayFile = parseReusableModel(context); + ASSERT_TRUE(arrayFile); + ArrayOp right = firstOp(*arrayFile); + right->getBlock()->getOperations().insert(right->getIterator(), + right->clone()); + std::string arrayDiagnostic = expectVerificationFailure(*arrayFile); + EXPECT_TRUE(llvm::StringRef(arrayDiagnostic) + .contains("duplicate canonical symbol or placement " + "'Top::right'")) + << arrayDiagnostic; + + auto processFile = parseReusableModel(context); + ASSERT_TRUE(processFile); + ProcessOp pulse = firstOp(*processFile); + auto *duplicate = pulse->clone(); + pulse->getBlock()->getOperations().insert(pulse->getIterator(), duplicate); + std::string processDiagnostic = expectVerificationFailure(*processFile); + EXPECT_TRUE(llvm::StringRef(processDiagnostic) + .contains("duplicate canonical symbol or placement " + "'Leaf::pulse'")) + << processDiagnostic; +} + +TEST(ACSimOpsTest, CustomModelIndexRejectsUnresolvedProcessReferences) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto localFile = parseValidModel(context); + ASSERT_TRUE(localFile); + firstOp(*localFile) + .setProcessAttr(mlir::FlatSymbolRefAttr::get(&context, "missing")); + std::string localDiagnostic = expectVerificationFailure(*localFile); + EXPECT_TRUE(llvm::StringRef(localDiagnostic) + .contains("live load must resolve to an exact typed slot of " + "this process")) + << localDiagnostic; + + auto nestedFile = parseValidModel(context); + ASSERT_TRUE(nestedFile); + DispatchOp processDispatch; + nestedFile->walk([&](DispatchOp dispatch) { + if (dispatch.getTarget().getLeafReference().getValue() == "tick") + processDispatch = dispatch; + }); + ASSERT_TRUE(processDispatch); + processDispatch.setTargetAttr(mlir::SymbolRefAttr::get( + &context, "Top", {mlir::FlatSymbolRefAttr::get(&context, "missing")})); + std::string nestedDiagnostic = expectVerificationFailure(*nestedFile); + EXPECT_TRUE(llvm::StringRef(nestedDiagnostic) + .contains("dispatch target reference '@Top::@missing' is " + "unresolved")) + << nestedDiagnostic; +} + +TEST(ACSimOpsTest, EveryPublicOperationHasItsTypedCppClass) { + mlir::MLIRContext context; + context.loadDialect(); + +#define EXPECT_REGISTERED(OP) \ + EXPECT_TRUE( \ + mlir::OperationName(OP::getOperationName(), &context).isRegistered()) \ + << OP::getOperationName().str() + EXPECT_REGISTERED(ModelOp); + EXPECT_REGISTERED(TypeOp); + EXPECT_REGISTERED(BindingOp); + EXPECT_REGISTERED(ModuleOp); + EXPECT_REGISTERED(InstanceOp); + EXPECT_REGISTERED(ArrayOp); + EXPECT_REGISTERED(ElementOp); + EXPECT_REGISTERED(PortOp); + EXPECT_REGISTERED(ResourceOp); + EXPECT_REGISTERED(BindOp); + EXPECT_REGISTERED(InlineOp); + EXPECT_REGISTERED(ProcessOp); + EXPECT_REGISTERED(LiveLoadOp); + EXPECT_REGISTERED(LiveStoreOp); + EXPECT_REGISTERED(InvokeOp); + EXPECT_REGISTERED(ContinueOp); + EXPECT_REGISTERED(SuspendOp); + EXPECT_REGISTERED(TerminateOp); + EXPECT_REGISTERED(ExportOp); + EXPECT_REGISTERED(DispatchOp); + EXPECT_REGISTERED(ActivateOp); + EXPECT_REGISTERED(ReturnOp); +#undef EXPECT_REGISTERED +} + +TEST(ACSimOpsTest, IndexedWholeModelVerificationHasExactLinearWorkDelta) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto measure = [&](unsigned extraTypes) { + auto file = mlir::parseSourceString( + scalableModel(extraTypes), &context); + EXPECT_TRUE(file); + detail::ModelVerificationWork work; + if (file) { + detail::ScopedModelVerificationWorkCollector collector(work); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); + } + return work; + }; + + constexpr unsigned smallSize = 128; + constexpr unsigned largeSize = 512; + detail::ModelVerificationWork small = measure(smallSize); + detail::ModelVerificationWork large = measure(largeSize); + RecordProperty("small_declarations", smallSize); + RecordProperty("large_declarations", largeSize); + RecordProperty("small_work_units", small.total()); + RecordProperty("large_work_units", large.total()); + EXPECT_EQ(large.total() - small.total(), + uint64_t{9} * (largeSize - smallSize)); +} + +TEST(ACSimOpsTest, CanonicalFixtureCoversInventoryEffectsAndPerPcRoundTrip) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*file))); + + llvm::StringMap counts; + file->walk([&](mlir::Operation *operation) { + if (operation->getName().getDialectNamespace() == "acsim") + ++counts[operation->getName().getStringRef()]; + }); + for (mlir::RegisteredOperationName operation : + context.getRegisteredOperationsByDialect("acsim")) + EXPECT_GT(counts.lookup(operation.getStringRef()), 0u) + << operation.getStringRef().str(); + const std::array, 22> exact = {{ + {"acsim.model", 1}, {"acsim.type", 24}, {"acsim.binding", 2}, + {"acsim.module", 1}, {"acsim.instance", 1}, {"acsim.array", 1}, + {"acsim.element", 2}, {"acsim.port", 2}, {"acsim.resource", 2}, + {"acsim.bind", 3}, {"acsim.inline", 5}, {"acsim.process", 1}, + {"acsim.live.load", 1}, {"acsim.live.store", 1}, {"acsim.invoke", 3}, + {"acsim.continue", 1}, {"acsim.suspend", 1}, {"acsim.terminate", 1}, + {"acsim.export", 3}, {"acsim.dispatch", 4}, {"acsim.activate", 6}, + {"acsim.return", 1}, + }}; + for (auto [name, expected] : exact) + EXPECT_EQ(counts.lookup(name), expected) << name.str(); + + ProcessOp process; + file->walk([&](ProcessOp candidate) { process = candidate; }); + ASSERT_TRUE(process); + EXPECT_EQ(process.getPcs().size(), 3u); + EXPECT_EQ(process.getStates().size(), process.getPcs().size()); + for (mlir::Region &state : process.getStates()) + EXPECT_FALSE(state.empty()); + + const std::array pureOperations = { + "acsim.type", "acsim.binding", "acsim.element", "acsim.port", + "acsim.resource", "acsim.inline", "acsim.export"}; + file->walk([&](mlir::Operation *operation) { + if (llvm::is_contained(pureOperations, operation->getName().getStringRef())) + EXPECT_TRUE(mlir::isMemoryEffectFree(operation)) + << operation->getName().getStringRef().str(); + }); + file->walk([&](mlir::Operation *operation) { + if (mlir::isa(operation)) + EXPECT_FALSE(mlir::isMemoryEffectFree(operation)) + << operation->getName().getStringRef().str(); + }); + + std::string printed; + llvm::raw_string_ostream(printed) << *file; + auto reparsed = mlir::parseSourceString(printed, &context); + ASSERT_TRUE(reparsed); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*reparsed))); + std::string reprinted; + llvm::raw_string_ostream(reprinted) << *reparsed; + EXPECT_EQ(reprinted, printed); +} + +TEST(ACSimOpsTest, ClosedSchemaSourceMapActivationAndFairnessRegressions) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto missingBindingField = parseValidModel(context); + ASSERT_TRUE(missingBindingField); + BindingOp binding; + missingBindingField->walk([&](BindingOp candidate) { + if (!binding) + binding = candidate; + }); + mlir::NamedAttrList fields(binding.getRecord()); + fields.erase("availability"); + binding.setRecordAttr(fields.getDictionary(&context)); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*missingBindingField)) + .contains("binding lock must contain exactly")); + + auto sourceMapFile = parseValidModel(context); + ASSERT_TRUE(sourceMapFile); + ModelOp model; + sourceMapFile->walk([&](ModelOp candidate) { model = candidate; }); + model->setAttr("acsim.source_map", mlir::StringAttr::get(&context, "bad")); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*sourceMapFile)) + .contains("acsim.source_map must be an array")); + + auto validSourceMapFile = parseValidModel(context); + ASSERT_TRUE(validSourceMapFile); + validSourceMapFile->walk([&](ModelOp candidate) { model = candidate; }); + mlir::NamedAttrList sourceRecord; + sourceRecord.set("file", mlir::StringAttr::get(&context, "model.acir")); + for (llvm::StringRef field : {"line", "column", "end_line", "end_column"}) + sourceRecord.set( + field, mlir::IntegerAttr::get(mlir::IntegerType::get(&context, 64), + field == "end_column" ? 2 : 1)); + model->setAttr( + "acsim.source_map", + mlir::ArrayAttr::get(&context, {sourceRecord.getDictionary(&context)})); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*validSourceMapFile))); + + auto unknownAttrFile = parseValidModel(context); + ASSERT_TRUE(unknownAttrFile); + unknownAttrFile->walk([&](ModelOp candidate) { model = candidate; }); + model->setAttr("acsim.unknown", mlir::UnitAttr::get(&context)); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*unknownAttrFile)) + .contains("unknown public attribute 'acsim.unknown'")); + + auto fileAttrFile = parseValidModel(context); + ASSERT_TRUE(fileAttrFile); + (*fileAttrFile)->setAttr("ac.extra", mlir::UnitAttr::get(&context)); + std::string fileAttrDiagnostic; + mlir::ScopedDiagnosticHandler fileAttrHandler( + &context, [&](mlir::Diagnostic &value) { + llvm::raw_string_ostream(fileAttrDiagnostic) << value; + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(verifyCanonicalACSimFile(*fileAttrFile))); + EXPECT_TRUE(llvm::StringRef(fileAttrDiagnostic) + .contains("canonical ACSim file attributes must be exactly")); + + auto activationFile = parseValidModel(context); + ASSERT_TRUE(activationFile); + ActivateOp lastActivation; + activationFile->walk( + [&](ActivateOp candidate) { lastActivation = candidate; }); + lastActivation.erase(); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*activationFile)) + .contains("activation edges must exactly equal computed")); + + auto fairnessFile = parseValidModel(context); + ASSERT_TRUE(fairnessFile); + ProcessOp process; + fairnessFile->walk([&](ProcessOp candidate) { process = candidate; }); + process.setFairnessCap(5); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*fairnessFile)) + .contains("below maximum local execution path 6")); + process.setFairnessCap(6); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*fairnessFile))); + + auto captureFile = parseValidModel(context); + ASSERT_TRUE(captureFile); + captureFile->walk([&](ProcessOp candidate) { process = candidate; }); + process.setCaptureNamesAttr(mlir::ArrayAttr::get(&context, {})); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*captureFile)) + .contains("one exact ordered name per operand")); + + auto typeClosureFile = parseValidModel(context); + ASSERT_TRUE(typeClosureFile); + typeClosureFile->walk([&](ProcessOp candidate) { process = candidate; }); + process.getStates().front().front().getArgument(0).setType( + mlir::MemRefType::get({1}, mlir::IntegerType::get(&context, 8))); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*typeClosureFile)) + .contains("is not legal in canonical ACSim")); + + auto specializationFile = parseValidModel(context); + ASSERT_TRUE(specializationFile); + ArrayOp lanes; + specializationFile->walk([&](ArrayOp candidate) { lanes = candidate; }); + lanes.setSpecializationFingerprint( + "sha256:" + "2400000000000000000000000000000000000000000000000000000000000000"); + EXPECT_TRUE( + llvm::StringRef(expectVerificationFailure(*specializationFile)) + .contains("identical target and static arguments require one")); +} + +TEST(ACSimOpsTest, SemanticMutationsProduceDeterministicDiagnostics) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto destructionFile = parseValidModel(context); + ASSERT_TRUE(destructionFile); + ModelOp destructionModel = *destructionFile->getOps().begin(); + destructionModel.setDestructionOrderAttr( + destructionModel.getConstructionOrderAttr()); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*destructionFile)) + .contains("destruction order must be the exact reverse")); + + auto exportFile = parseValidModel(context); + ASSERT_TRUE(exportFile); + ModuleOp module; + exportFile->walk([&](ModuleOp candidate) { module = candidate; }); + module.setExportsAttr(mlir::ArrayAttr::get(&context, {})); + EXPECT_TRUE(llvm::StringRef(expectVerificationFailure(*exportFile)) + .contains("exports must exactly cover")); + + auto wakeFile = parseValidModel(context); + ASSERT_TRUE(wakeFile); + SuspendOp suspend; + wakeFile->walk([&](SuspendOp candidate) { suspend = candidate; }); + ASSERT_TRUE(suspend); + suspend.getWake().setType(ValueType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "cpp_bool"))); + std::string wakeDiagnostic = expectVerificationFailure(*wakeFile); + EXPECT_TRUE( + llvm::StringRef(wakeDiagnostic).contains("requires one exact typed wake")) + << wakeDiagnostic; + + auto castFile = parseValidModel(context); + ASSERT_TRUE(castFile); + TerminateOp terminate; + castFile->walk([&](TerminateOp candidate) { terminate = candidate; }); + ASSERT_TRUE(terminate); + mlir::OpBuilder builder(terminate); + auto constant = mlir::arith::ConstantOp::create(builder, terminate.getLoc(), + builder.getI32IntegerAttr(0)); + mlir::UnrealizedConversionCastOp::create( + builder, terminate.getLoc(), mlir::TypeRange{builder.getI64Type()}, + mlir::ValueRange{constant}); + std::string castDiagnostic = expectVerificationFailure(*castFile); + EXPECT_TRUE(llvm::StringRef(castDiagnostic) + .contains("conversion placeholders are not legal")) + << castDiagnostic; + + auto processShapeFile = parseValidModel(context); + ASSERT_TRUE(processShapeFile); + ProcessOp malformedProcess; + processShapeFile->walk( + [&](ProcessOp candidate) { malformedProcess = candidate; }); + malformedProcess.setPcsAttr(mlir::ArrayAttr::get( + &context, {mlir::FlatSymbolRefAttr::get(&context, "entry")})); + std::string processDiagnostic = expectVerificationFailure(*processShapeFile); + EXPECT_TRUE(llvm::StringRef(processDiagnostic) + .contains("exactly one ordered state region per PC")) + << processDiagnostic; + + auto thunkFile = parseValidModel(context); + ASSERT_TRUE(thunkFile); + DispatchOp dispatch; + thunkFile->walk([&](DispatchOp candidate) { + if (!dispatch) + dispatch = candidate; + }); + dispatch.setWorkAttr(mlir::StringAttr::get(&context, "work();")); + std::string thunkDiagnostic = expectVerificationFailure(*thunkFile); + EXPECT_TRUE(llvm::StringRef(thunkDiagnostic) + .contains("dispatch thunks must exactly match")) + << thunkDiagnostic; + + auto objectOrderFile = parseValidModel(context); + ASSERT_TRUE(objectOrderFile); + DispatchOp wrongId; + objectOrderFile->walk([&](DispatchOp candidate) { + if (candidate.getObjectId() == 0) + wrongId = candidate; + }); + wrongId.setObjectId(2); + std::string objectOrderDiagnostic = + expectVerificationFailure(*objectOrderFile); + EXPECT_TRUE( + llvm::StringRef(objectOrderDiagnostic) + .contains("target, path, indices, and IDs must jointly match")) + << objectOrderDiagnostic; + + auto bindingKindFile = parseValidModel(context); + ASSERT_TRUE(bindingKindFile); + BindingOp binding; + bindingKindFile->walk([&](BindingOp candidate) { + if (!binding) + binding = candidate; + }); + replaceDictionaryField(binding, "component_schema", + mlir::FlatSymbolRefAttr::get(&context, "gfsim")); + std::string bindingKindDiagnostic = + expectVerificationFailure(*bindingKindFile); + EXPECT_TRUE(llvm::StringRef(bindingKindDiagnostic) + .contains("schema reference '@gfsim' has incompatible")) + << bindingKindDiagnostic; + + auto implementationFingerprintFile = parseValidModel(context); + ASSERT_TRUE(implementationFingerprintFile); + implementationFingerprintFile->walk([&](BindingOp candidate) { + if (candidate.getSymName() == "pure") + binding = candidate; + }); + replaceDictionaryField( + binding, "provider_implementation_fingerprint", + mlir::StringAttr::get( + &context, + "sha256:" + "3000000000000000000000000000000000000000000000000000000000000000")); + std::string implementationFingerprintDiagnostic = + expectVerificationFailure(*implementationFingerprintFile); + EXPECT_TRUE(llvm::StringRef(implementationFingerprintDiagnostic) + .contains("implementation fingerprint must exactly match")) + << implementationFingerprintDiagnostic; + + auto invokeEffectFile = parseValidModel(context); + ASSERT_TRUE(invokeEffectFile); + InvokeOp invoke; + invokeEffectFile->walk([&](InvokeOp candidate) { + if (!invoke) + invoke = candidate; + }); + invoke.setCalleeAttr(mlir::FlatSymbolRefAttr::get(&context, "pure")); + std::string invokeEffectDiagnostic = + expectVerificationFailure(*invokeEffectFile); + EXPECT_TRUE(llvm::StringRef(invokeEffectDiagnostic) + .contains("requires effect 'stateful'")) + << invokeEffectDiagnostic; +} + +TEST(ACSimOpsTest, EveryOperationHasATableDrivenBehavioralNegative) { + mlir::MLIRContext context; + loadTestDialects(context); + struct OperationCase { + llvm::StringLiteral operation; + std::function mutate; + llvm::StringLiteral expected; + }; + const std::array cases = {{ + {"model", + [&](mlir::ModuleOp file) { + ModelOp op = firstOp(file); + op.setDestructionOrderAttr(op.getConstructionOrderAttr()); + }, + "destruction order must be the exact reverse"}, + {"type", + [&](mlir::ModuleOp file) { + firstOp(file).setKind("runtime_descriptor"); + }, + "runtime_descriptor"}, + {"binding", + [&](mlir::ModuleOp file) { + BindingOp op = firstOp(file); + mlir::NamedAttrList fields(op.getRecord()); + fields.erase("availability"); + op.setRecordAttr(fields.getDictionary(&context)); + }, + "binding lock must contain exactly"}, + {"module", + [&](mlir::ModuleOp file) { + firstOp(file).setExportsAttr( + mlir::ArrayAttr::get(&context, {})); + }, + "exports must exactly cover"}, + {"instance", + [&](mlir::ModuleOp file) { + firstOp(file).getResult().setType(OwnerType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "pure"))); + }, + "owner/ref type requires a generated module or stateful binding"}, + {"array", + [&](mlir::ModuleOp file) { + ArrayOp op = firstOp(file); + op.getResult().setType(ArrayType::get( + &context, mlir::DenseI64ArrayAttr::get(&context, {3}), + OwnerType::get(&context, mlir::FlatSymbolRefAttr::get( + &context, "stateful")))); + }, + "array result shape and owning element realization must be exact"}, + {"element", + [&](mlir::ModuleOp file) { + firstOp(file).setIndicesAttr( + mlir::DenseI64ArrayAttr::get(&context, {2})); + }, + "element index is out of static bounds"}, + {"port", + [&](mlir::ModuleOp file) { + PortOp op = firstOp(file); + PortType type = mlir::cast(op.getResult().getType()); + op.getResult().setType( + PortType::get(&context, type.getInterface(), + mlir::FlatSymbolRefAttr::get(&context, "initiator"), + type.getPayload(), type.getProtocol())); + }, + "port projection must exactly match"}, + {"resource", + [&](mlir::ModuleOp file) { + ResourceOp op = firstOp(file); + ResourceType type = mlir::cast(op.getResult().getType()); + op.getResult().setType(ResourceType::get( + &context, type.getResource(), + mlir::FlatSymbolRefAttr::get(&context, "consumer"))); + }, + "resource projection must exactly match"}, + {"bind", + [&](mlir::ModuleOp file) { firstOp(file).setKind("resource"); }, + "resource binding must connect exact initiator and target"}, + {"inline", + [&](mlir::ModuleOp file) { + firstOp(file).setCallee("stateful"); + }, + "requires effect 'pure'"}, + {"process", + [&](mlir::ModuleOp file) { + firstOp(file).setCaptureNamesAttr( + mlir::ArrayAttr::get(&context, {})); + }, + "one exact ordered name per operand"}, + {"live.load", + [&](mlir::ModuleOp file) { + firstOp(file).setSlot("missing"); + }, + "live load must resolve to an exact typed slot"}, + {"live.store", + [&](mlir::ModuleOp file) { + firstOp(file).setSlot("missing"); + }, + "live store must resolve to an exact typed slot"}, + {"invoke", + [&](mlir::ModuleOp file) { firstOp(file).setCallee("pure"); }, + "requires effect 'stateful'"}, + {"continue", + [&](mlir::ModuleOp file) { + firstOp(file).setTargetPc("missing"); + }, + "target PC '@missing' is not in the closed PC list"}, + {"suspend", + [&](mlir::ModuleOp file) { + firstOp(file).getWake().setType(ValueType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "cpp_bool"))); + }, + "suspend requires one exact typed wake"}, + {"terminate", + [&](mlir::ModuleOp file) { + firstOp(file).setStatus("cancelled"); + }, + "terminal status must be exactly"}, + {"export", + [&](mlir::ModuleOp file) { + firstOp(file).setRole("event_kind"); + }, + "export role reference '@event_kind' has incompatible"}, + {"dispatch", + [&](mlir::ModuleOp file) { + firstOp(file).setPath("Top.alias"); + }, + "target, path, indices, and IDs must jointly match"}, + {"activate", + [&](mlir::ModuleOp file) { + ActivateOp op; + file.walk([&](ActivateOp candidate) { op = candidate; }); + op.erase(); + }, + "activation edges must exactly equal computed"}, + {"return", + [&](mlir::ModuleOp file) { firstOp(file)->setOperands({}); }, + "return values must exactly match ordered module exports"}, + }}; + + for (const OperationCase &testCase : cases) { + SCOPED_TRACE(testCase.operation.str()); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + testCase.mutate(*file); + std::string diagnostic = expectVerificationFailure(*file); + EXPECT_TRUE(llvm::StringRef(diagnostic).contains(testCase.expected)) + << diagnostic; + } +} + +TEST(ACSimOpsTest, EveryTypeHasATableDrivenSemanticNegative) { + mlir::MLIRContext context; + loadTestDialects(context); + struct TypeCase { + llvm::StringLiteral type; + std::function mutate; + llvm::StringLiteral expected; + bool directExport = false; + }; + const std::array cases = {{ + {"value", + [&](mlir::ModuleOp file) { + firstOp(file).getResult().setType(ValueType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "event_kind"))); + }, + "live load must resolve to an exact typed slot"}, + {"expr", + [&](mlir::ModuleOp file) { + firstOp(file).getResult().setType(ExprType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "event_kind"))); + }, + "C++ type reference '@event_kind' has incompatible"}, + {"owner", + [&](mlir::ModuleOp file) { + firstOp(file).getResult().setType(OwnerType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "pure"))); + }, + "owner/ref type requires a generated module or stateful binding"}, + {"ref", + [&](mlir::ModuleOp file) { + firstOp(file).getResult().setType(RefType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "pure"))); + }, + "owner/ref type requires a generated module or stateful binding"}, + {"port", + [&](mlir::ModuleOp file) { + PortOp op = firstOp(file); + PortType type = mlir::cast(op.getResult().getType()); + op.getResult().setType( + PortType::get(&context, type.getInterface(), + mlir::FlatSymbolRefAttr::get(&context, "initiator"), + type.getPayload(), type.getProtocol())); + }, + "port projection must exactly match"}, + {"resource", + [&](mlir::ModuleOp file) { + ResourceOp op = firstOp(file); + ResourceType type = mlir::cast(op.getResult().getType()); + op.getResult().setType(ResourceType::get( + &context, type.getResource(), + mlir::FlatSymbolRefAttr::get(&context, "consumer"))); + }, + "resource projection must exactly match"}, + {"array", + [&](mlir::ModuleOp file) { + ArrayOp op = firstOp(file); + op.getResult().setType(ArrayType::get( + &context, mlir::DenseI64ArrayAttr::get(&context, {3}), + OwnerType::get(&context, mlir::FlatSymbolRefAttr::get( + &context, "stateful")))); + }, + "array result shape and owning element realization must be exact"}, + {"object_id", + [&](mlir::ModuleOp file) { + ActivateOp op = firstOp(file); + llvm::SmallVector dispatches; + file.walk( + [&](DispatchOp candidate) { dispatches.push_back(candidate); }); + op->setOperand(1, dispatches[1].getObject()); + }, + "activation edges must exactly equal computed"}, + {"activation_id", + [&](mlir::ModuleOp file) { + ActivateOp op = firstOp(file); + llvm::SmallVector dispatches; + file.walk( + [&](DispatchOp candidate) { dispatches.push_back(candidate); }); + op->setOperand(0, dispatches[1].getActivation()); + }, + "activation edges must exactly equal computed"}, + {"pc", + [&](mlir::ModuleOp file) { + ExportOp op = firstOp(file); + auto type = PcType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "tick")); + op.getValue().setType(type); + op.getResult().setType(type); + }, + "owners, IDs, PCs, and wakes cannot escape", true}, + {"wake", + [&](mlir::ModuleOp file) { + firstOp(file).getWake().setType(WakeType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "cpp_bool"))); + }, + "wake kind reference '@cpp_bool' has incompatible"}, + }}; + + for (const TypeCase &testCase : cases) { + SCOPED_TRACE(testCase.type.str()); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + testCase.mutate(*file); + std::string diagnostic; + if (testCase.directExport) { + ExportOp exportOp = firstOp(*file); + diagnostic = expectDirectVerificationFailure( + context, [&] { return exportOp.verify(); }); + } else { + diagnostic = expectVerificationFailure(*file); + } + EXPECT_TRUE(llvm::StringRef(diagnostic).contains(testCase.expected)) + << diagnostic; + } +} + +TEST(ACSimOpsTest, BindingLockNestedDomainsAreClosedAndDeclarative) { + mlir::MLIRContext context; + loadTestDialects(context); + + struct MutationCase { + llvm::StringLiteral name; + llvm::StringLiteral bindingName; + std::function mutate; + llvm::StringLiteral expected; + }; + const std::array cases = {{ + {"cpp-header", "stateful", + [&](BindingOp binding) { + replaceNestedDictionaryField( + binding, "cpp", "header", + mlir::StringAttr::get(&context, "../escape.hpp")); + }, + "cpp.header must be a repository-relative header path"}, + {"cpp-symbol", "stateful", + [&](BindingOp binding) { + replaceNestedDictionaryField( + binding, "cpp", "symbol", + mlir::StringAttr::get(&context, "gfsim::make()")); + }, + "must be declarative qualified names"}, + {"construction-kind", "stateful", + [&](BindingOp binding) { + replaceNestedDictionaryField( + binding, "construction", "kind", + mlir::StringAttr::get(&context, "factory")); + }, + "construction kind must be exactly 'constructor'"}, + {"construction-raw-argument", "stateful", + [&](BindingOp binding) { + replaceNestedDictionaryField( + binding, "construction", "arguments", + mlir::ArrayAttr::get( + &context, {mlir::StringAttr::get(&context, "make();")})); + }, + "construction arguments must be canonical static data"}, + {"pure-ownership-kind", "pure", + [&](BindingOp binding) { + replaceNestedDictionaryField( + binding, "ownership", "kind", + mlir::StringAttr::get(&context, "unique")); + }, + "pure binding ownership must be exactly none/inline"}, + {"pure-ownership-placement", "pure", + [&](BindingOp binding) { + replaceNestedDictionaryField( + binding, "ownership", "placement", + mlir::StringAttr::get(&context, "member_or_array")); + }, + "pure binding ownership must be exactly none/inline"}, + {"stateful-ownership-kind", "stateful", + [&](BindingOp binding) { + replaceNestedDictionaryField( + binding, "ownership", "kind", + mlir::StringAttr::get(&context, "shared")); + }, + "stateful binding ownership must be exactly unique/member_or_array or " + "unique/root_or_process"}, + {"stateful-ownership-placement", "stateful", + [&](BindingOp binding) { + replaceNestedDictionaryField(binding, "ownership", "placement", + mlir::StringAttr::get(&context, "root")); + }, + "stateful binding ownership must be exactly unique/member_or_array or " + "unique/root_or_process"}, + {"cardinality-string", "stateful", + [&](BindingOp binding) { + replaceRecordArrayField(binding, "ports", 0, "cardinality", + mlir::StringAttr::get(&context, "many")); + }, + "cardinality must be exactly 'exclusive' or 'shared'"}, + {"cardinality-integer", "stateful", + [&](BindingOp binding) { + replaceRecordArrayField( + binding, "ports", 0, "cardinality", + mlir::IntegerAttr::get(mlir::IntegerType::get(&context, 64), 1)); + }, + "cardinality must be exactly 'exclusive' or 'shared'"}, + {"delegation", "stateful", + [&](BindingOp binding) { + replaceRecordArrayField(binding, "ports", 0, "delegation", + mlir::StringAttr::get(&context, "optional")); + }, + "delegation must be forbidden, allowed, or required"}, + {"endpoint-ownership", "stateful", + [&](BindingOp binding) { + replaceRecordArrayField(binding, "ports", 0, "ownership", + mlir::StringAttr::get(&context, "temporary")); + }, + "endpoint ownership must be owned, borrowed, or shared"}, + {"time-domain-string", "stateful", + [&](BindingOp binding) { + replaceRecordArrayField( + binding, "ports", 0, "time_domain", + mlir::StringAttr::get(&context, "combinational")); + }, + "time_domain must be a flat symbol reference"}, + {"resource-time-domain-string", "stateful", + [&](BindingOp binding) { + replaceRecordArrayField( + binding, "resources", 0, "time_domain", + mlir::StringAttr::get(&context, "combinational")); + }, + "time_domain must be a flat symbol reference"}, + {"result-name", "pure", + [&](BindingOp binding) { + replaceRecordArrayField(binding, "results", 0, "name", + mlir::StringAttr::get(&context, "bad.name")); + }, + "result name must be a canonical identifier"}, + {"activation-name", "stateful", + [&](BindingOp binding) { + replaceRecordArrayField(binding, "activation_sources", 0, "name", + mlir::StringAttr::get(&context, "wake()")); + }, + "activation-source name must be a canonical identifier"}, + }}; + + for (const MutationCase &testCase : cases) { + SCOPED_TRACE(testCase.name.str()); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + BindingOp binding; + file->walk([&](BindingOp candidate) { + if (candidate.getSymName() == testCase.bindingName) + binding = candidate; + }); + ASSERT_TRUE(binding); + testCase.mutate(binding); + std::string diagnostic = expectDirectVerificationFailure( + context, [&] { return binding.verify(); }); + EXPECT_TRUE(llvm::StringRef(diagnostic).contains(testCase.expected)) + << diagnostic; + } +} + +TEST(ACSimOpsTest, BindingTimeDomainReferencesMustResolveToTimeDomainTypes) { + mlir::MLIRContext context; + loadTestDialects(context); + + struct ReferenceCase { + llvm::StringLiteral name; + llvm::StringLiteral reference; + llvm::StringLiteral expected; + }; + const std::array cases = {{ + {"unresolved", "missing_domain", + "time-domain reference '@missing_domain' is unresolved"}, + {"wrong-kind", "protocol", + "time-domain reference '@protocol' has incompatible acsim.type kind " + "'protocol'"}, + }}; + + for (const ReferenceCase &testCase : cases) { + SCOPED_TRACE(testCase.name.str()); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + BindingOp binding; + file->walk([&](BindingOp candidate) { + if (candidate.getSymName() == "stateful") + binding = candidate; + }); + ASSERT_TRUE(binding); + replaceRecordArrayField( + binding, "ports", 0, "time_domain", + mlir::FlatSymbolRefAttr::get(&context, testCase.reference)); + replaceRecordArrayField( + binding, "ports", 1, "time_domain", + mlir::FlatSymbolRefAttr::get(&context, testCase.reference)); + std::string diagnostic = expectVerificationFailure(*file); + EXPECT_TRUE(llvm::StringRef(diagnostic).contains(testCase.expected)) + << diagnostic; + } +} + +TEST(ACSimOpsTest, BindLocalVerifierUsesExactAccessorRoleRecords) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto wrongPortRole = parseValidModel(context); + ASSERT_TRUE(wrongPortRole); + PortOp sourcePort; + BindOp portBind; + wrongPortRole->walk([&](PortOp candidate) { + if (!sourcePort) + sourcePort = candidate; + }); + wrongPortRole->walk([&](BindOp candidate) { + if (candidate.getKind() == "port") + portBind = candidate; + }); + auto sourcePortType = mlir::cast(sourcePort.getResult().getType()); + sourcePort.getResult().setType( + PortType::get(&context, sourcePortType.getInterface(), + mlir::FlatSymbolRefAttr::get(&context, "initiator"), + sourcePortType.getPayload(), sourcePortType.getProtocol())); + std::string portDiagnostic = expectDirectVerificationFailure( + context, [&] { return portBind.verify(); }); + EXPECT_TRUE(llvm::StringRef(portDiagnostic) + .contains("port bind endpoints must match exact output/input " + "binding-lock records")) + << portDiagnostic; + + auto wrongResourceAccessor = parseValidModel(context); + ASSERT_TRUE(wrongResourceAccessor); + ResourceOp sourceResource; + BindOp resourceBind; + wrongResourceAccessor->walk([&](ResourceOp candidate) { + if (!sourceResource) + sourceResource = candidate; + }); + wrongResourceAccessor->walk([&](BindOp candidate) { + if (candidate.getKind() == "resource") + resourceBind = candidate; + }); + sourceResource.setAccessorAttr( + mlir::FlatSymbolRefAttr::get(&context, "resource_in_accessor")); + std::string resourceDiagnostic = expectDirectVerificationFailure( + context, [&] { return resourceBind.verify(); }); + EXPECT_TRUE(llvm::StringRef(resourceDiagnostic) + .contains("resource bind endpoints must match exact " + "initiator/target binding-lock records")) + << resourceDiagnostic; + + auto wrongPortAccessor = parseValidModel(context); + ASSERT_TRUE(wrongPortAccessor); + sourcePort = {}; + portBind = {}; + wrongPortAccessor->walk([&](PortOp candidate) { + if (!sourcePort) + sourcePort = candidate; + }); + wrongPortAccessor->walk([&](BindOp candidate) { + if (candidate.getKind() == "port") + portBind = candidate; + }); + sourcePort.setAccessorAttr( + mlir::FlatSymbolRefAttr::get(&context, "port_in_accessor")); + portDiagnostic = expectDirectVerificationFailure( + context, [&] { return portBind.verify(); }); + EXPECT_TRUE(llvm::StringRef(portDiagnostic) + .contains("port bind endpoints must match exact output/input " + "binding-lock records")) + << portDiagnostic; + + auto wrongResourceRole = parseValidModel(context); + ASSERT_TRUE(wrongResourceRole); + sourceResource = {}; + resourceBind = {}; + wrongResourceRole->walk([&](ResourceOp candidate) { + if (!sourceResource) + sourceResource = candidate; + }); + wrongResourceRole->walk([&](BindOp candidate) { + if (candidate.getKind() == "resource") + resourceBind = candidate; + }); + ResourceType sourceResourceType = + mlir::cast(sourceResource.getResult().getType()); + sourceResource.getResult().setType( + ResourceType::get(&context, sourceResourceType.getResource(), + mlir::FlatSymbolRefAttr::get(&context, "consumer"))); + resourceDiagnostic = expectDirectVerificationFailure( + context, [&] { return resourceBind.verify(); }); + EXPECT_TRUE(llvm::StringRef(resourceDiagnostic) + .contains("resource bind endpoints must match exact " + "initiator/target binding-lock records")) + << resourceDiagnostic; +} + +TEST(ACSimOpsTest, BindLocalVerifierRejectsMutuallyIncompatibleEndpoints) { + mlir::MLIRContext context; + loadTestDialects(context); + + struct PortCase { + llvm::StringLiteral name; + std::function replacementType; + mlir::FlatSymbolRefAttr replacementReference; + }; + const std::array portCases = {{ + {"interface", + [&](PortType type) { + return PortType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "interface_alt"), + type.getRole(), type.getPayload(), type.getProtocol()); + }, + mlir::FlatSymbolRefAttr::get(&context, "interface_alt")}, + {"payload", + [&](PortType type) { + return PortType::get( + &context, type.getInterface(), type.getRole(), + mlir::FlatSymbolRefAttr::get(&context, "payload_alt"), + type.getProtocol()); + }, + mlir::FlatSymbolRefAttr::get(&context, "payload_alt")}, + {"protocol", + [&](PortType type) { + return PortType::get( + &context, type.getInterface(), type.getRole(), type.getPayload(), + mlir::FlatSymbolRefAttr::get(&context, "protocol_alt")); + }, + mlir::FlatSymbolRefAttr::get(&context, "protocol_alt")}, + }}; + + for (const PortCase &testCase : portCases) { + SCOPED_TRACE(testCase.name.str()); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + BindingOp binding; + PortOp targetPort; + BindOp portBind; + file->walk([&](BindingOp candidate) { + if (candidate.getSymName() == "stateful") + binding = candidate; + }); + file->walk([&](PortOp candidate) { + if (candidate.getAccessor() == "port_in_accessor") + targetPort = candidate; + }); + file->walk([&](BindOp candidate) { + if (candidate.getKind() == "port") + portBind = candidate; + }); + ASSERT_TRUE(binding); + ASSERT_TRUE(targetPort); + ASSERT_TRUE(portBind); + replaceRecordArrayField(binding, "ports", 1, testCase.name, + testCase.replacementReference); + auto type = mlir::cast(targetPort.getResult().getType()); + targetPort.getResult().setType(testCase.replacementType(type)); + std::string diagnostic = expectDirectVerificationFailure( + context, [&] { return portBind.verify(); }); + EXPECT_TRUE(llvm::StringRef(diagnostic) + .contains("port bind endpoints must have identical " + "interface, payload, protocol, cardinality, " + "delegation, ownership, and time domain")) + << diagnostic; + } + + auto file = parseValidModel(context); + ASSERT_TRUE(file); + BindingOp binding; + ResourceOp targetResource; + BindOp resourceBind; + file->walk([&](BindingOp candidate) { + if (candidate.getSymName() == "stateful") + binding = candidate; + }); + file->walk([&](ResourceOp candidate) { + if (candidate.getAccessor() == "resource_in_accessor") + targetResource = candidate; + }); + file->walk([&](BindOp candidate) { + if (candidate.getKind() == "resource") + resourceBind = candidate; + }); + ASSERT_TRUE(binding); + ASSERT_TRUE(targetResource); + ASSERT_TRUE(resourceBind); + replaceRecordArrayField( + binding, "resources", 1, "resource", + mlir::FlatSymbolRefAttr::get(&context, "resource_alt")); + auto resourceType = + mlir::cast(targetResource.getResult().getType()); + targetResource.getResult().setType(ResourceType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "resource_alt"), + resourceType.getRole())); + std::string diagnostic = expectDirectVerificationFailure( + context, [&] { return resourceBind.verify(); }); + EXPECT_TRUE(llvm::StringRef(diagnostic) + .contains("resource bind endpoints must have identical " + "resource kind, delegation, ownership, and time " + "domain")) + << diagnostic; +} + +TEST(ACSimOpsTest, ExportBindIsACanonicalTypedConstructionRelation) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ExportOp exportOp; + file->walk([&](ExportOp candidate) { exportOp = candidate; }); + ASSERT_TRUE(exportOp); + mlir::OpBuilder builder(exportOp); + builder.setInsertionPointAfter(exportOp); + BindOp::create(builder, exportOp.getLoc(), exportOp.getValue(), + exportOp.getResult(), builder.getStringAttr("export")); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); +} + +TEST(ACSimOpsTest, ExportBindRejectsUnrelatedEqualTypedSource) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ExportOp exportOp; + InlineOp unrelated; + file->walk([&](ExportOp candidate) { exportOp = candidate; }); + file->walk([&](InlineOp candidate) { + if (!unrelated) + unrelated = candidate; + }); + ASSERT_TRUE(exportOp); + ASSERT_TRUE(unrelated); + ASSERT_NE(unrelated.getResult(), exportOp.getValue()); + ASSERT_EQ(unrelated.getResult().getType(), exportOp.getResult().getType()); + mlir::OpBuilder builder(exportOp); + builder.setInsertionPointAfter(exportOp); + BindOp::create(builder, exportOp.getLoc(), unrelated.getResult(), + exportOp.getResult(), builder.getStringAttr("export")); + std::string diagnostic = expectVerificationFailure(*file); + EXPECT_TRUE(llvm::StringRef(diagnostic) + .contains("export bind source must be the exact input of its " + "target acsim.export")) + << diagnostic; +} + +TEST(ACSimOpsTest, CapabilityPreflightUsesExactPrivateLimits) { + mlir::MLIRContext context; + loadTestDialects(context); + auto checkLimit = [&](detail::ModelVerificationLimits limits, + llvm::StringRef expected) { + auto file = parseValidModel(context); + ASSERT_TRUE(file); + detail::ScopedModelVerificationLimits scopedLimits(limits); + std::string diagnostic = expectVerificationFailure(*file); + EXPECT_TRUE(llvm::StringRef(diagnostic).contains(expected)) << diagnostic; + }; + + detail::ModelVerificationLimits nodeLimits; + nodeLimits.maxNodes = 8; + checkLimit(nodeLimits, "model node count exceeds ACSim capability 8"); + + detail::ModelVerificationLimits depthLimits; + depthLimits.maxRegionDepth = 1; + checkLimit(depthLimits, "region nesting exceeds ACSim capability 1"); + + detail::ModelVerificationLimits expansionLimits; + expansionLimits.maxExpandedObjects = 1; + checkLimit(expansionLimits, + "expanded array volume exceeds ACSim capability 1"); + + detail::ModelVerificationLimits edgeLimits; + edgeLimits.maxEdges = 0; + checkLimit(edgeLimits, "model edge count exceeds ACSim capability 0"); + + detail::ModelVerificationLimits attributeLimits; + attributeLimits.maxAttributeElements = 8; + checkLimit(attributeLimits, + "attribute element count exceeds ACSim capability"); + + detail::ModelVerificationLimits stringLimits; + stringLimits.maxAttributeStringBytes = 8; + checkLimit(stringLimits, "attribute string bytes exceed ACSim capability"); + + detail::ModelVerificationLimits dependencyLimits; + dependencyLimits.maxDependencyNodes = 4; + checkLimit(dependencyLimits, "dependency graph exceeds ACSim capability 4"); +} + +TEST(ACSimOpsTest, CyclicSsaDependencyFailsExplicitlyWithoutRecursion) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ModelOp model = *file->getOps().begin(); + llvm::SmallVector inlineOps; + file->walk([&](InlineOp operation) { inlineOps.push_back(operation); }); + ASSERT_GE(inlineOps.size(), 2u); + inlineOps.front()->insertOperands(0, inlineOps[1].getResult()); + + std::string diagnostic = + expectDirectVerificationFailure(context, [&] { return model.verify(); }); + EXPECT_TRUE(llvm::StringRef(diagnostic) + .contains("typed SSA dependency graph contains a cycle")) + << diagnostic; +} + +TEST(ACSimOpsTest, CyclicProcessPcDependencyFailsExplicitly) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + SuspendOp suspend = firstOp(*file); + ASSERT_TRUE(suspend); + mlir::OpBuilder builder(suspend); + ContinueOp::create(builder, suspend.getLoc(), "entry"); + suspend.erase(); + std::string diagnostic = expectVerificationFailure(*file); + EXPECT_TRUE( + llvm::StringRef(diagnostic) + .contains( + "process continue graph must prove bounded acyclic progress")) + << diagnostic; +} + +TEST(ACSimOpsTest, ReusableExpansionCycleAndTotalCapFailExplicitly) { + mlir::MLIRContext context; + loadTestDialects(context); + + auto cycleFile = parseReusableModel(context); + ASSERT_TRUE(cycleFile); + InstanceOp child; + cycleFile->walk([&](InstanceOp candidate) { + if (candidate.getSymName() == "child") + child = candidate; + }); + ASSERT_TRUE(child); + child.setTargetAttr(mlir::SymbolRefAttr::get(&context, "Leaf")); + child.setStaticArgsAttr(mlir::ArrayAttr::get( + &context, + {mlir::IntegerAttr::get(mlir::IntegerType::get(&context, 64), 2)})); + child.setSpecializationFingerprint( + "sha256:" + "8000000000000000000000000000000000000000000000000000000000000000"); + child.getResult().setType( + OwnerType::get(&context, mlir::FlatSymbolRefAttr::get(&context, "Leaf"))); + std::string cycleDiagnostic = expectVerificationFailure(*cycleFile); + EXPECT_TRUE(llvm::StringRef(cycleDiagnostic) + .contains("module instantiation cycle has no canonical " + "declaration order")) + << cycleDiagnostic; + + auto capFile = parseReusableModel(context); + ASSERT_TRUE(capFile); + detail::ModelVerificationLimits limits; + limits.maxExpandedObjects = 3; + detail::ScopedModelVerificationLimits scopedLimits(limits); + std::string capDiagnostic = expectVerificationFailure(*capFile); + EXPECT_TRUE(llvm::StringRef(capDiagnostic) + .contains("expanded hierarchy exceeds ACSim capability 3")) + << capDiagnostic; +} + +TEST(ACSimOpsTest, DeepSsaDependenciesUseABoundedIterativeWorklist) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + InlineOp firstInline; + file->walk([&](InlineOp candidate) { + if (!firstInline) + firstInline = candidate; + }); + ASSERT_TRUE(firstInline); + mlir::OpBuilder builder(firstInline); + mlir::Value previous; + constexpr unsigned depth = 20000; + for (unsigned index = 0; index != depth; ++index) { + auto operation = InlineOp::create( + builder, firstInline.getLoc(), firstInline.getResult().getType(), + previous ? mlir::ValueRange{previous} : mlir::ValueRange{}, "pure"); + previous = operation.getResult(); + } + firstInline->insertOperands(0, previous); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); +} + +TEST(ACSimOpsTest, DeepProcessPcChainUsesATopologicalWorklist) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ProcessOp oldProcess; + file->walk([&](ProcessOp candidate) { oldProcess = candidate; }); + ASSERT_TRUE(oldProcess); + constexpr unsigned depth = 4096; + llvm::SmallVector pcs; + pcs.reserve(depth); + for (unsigned index = 0; index != depth; ++index) + pcs.push_back( + mlir::FlatSymbolRefAttr::get(&context, ("pc" + std::to_string(index)))); + mlir::OpBuilder builder(oldProcess); + ProcessOp process = ProcessOp::create( + builder, oldProcess.getLoc(), oldProcess.getCaptures(), "tick", + oldProcess.getCaptureNamesAttr(), "pc0", + mlir::ArrayAttr::get(&context, pcs), oldProcess.getLiveSlotsAttr(), depth, + oldProcess.getSpecializationFingerprint(), depth); + for (unsigned index = 0; index != depth; ++index) { + mlir::Region &state = process.getStates()[index]; + mlir::Block *block = &state.emplaceBlock(); + for (mlir::Value capture : process.getCaptures()) + block->addArgument(capture.getType(), process.getLoc()); + builder.setInsertionPointToEnd(block); + if (index + 1 == depth) + TerminateOp::create(builder, process.getLoc(), "success"); + else + ContinueOp::create(builder, process.getLoc(), + "pc" + std::to_string(index + 1)); + } + oldProcess.erase(); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); +} + +TEST(ACSimOpsTest, ForwardOnlyMultiBlockProcessCfgIsCanonical) { + mlir::MLIRContext context; + loadTestDialects(context); + auto file = parseValidModel(context); + ASSERT_TRUE(file); + ProcessOp process = firstOp(*file); + mlir::Region &entryState = process.getStates().front(); + mlir::Block &entry = entryState.front(); + auto continuation = mlir::cast(entry.getTerminator()); + mlir::FlatSymbolRefAttr target = continuation.getTargetPcAttr(); + mlir::Block *next = &entryState.emplaceBlock(); + mlir::OpBuilder builder(continuation); + mlir::cf::BranchOp::create(builder, continuation.getLoc(), next); + continuation.erase(); + builder.setInsertionPointToEnd(next); + ContinueOp::create(builder, process.getLoc(), target); + EXPECT_TRUE(mlir::succeeded(mlir::verify(*file))); +} + +} // namespace +} // namespace acir::acsim diff --git a/components/agentic-circuit/unittests/Dialect/ACSim/TypesTest.cpp b/components/agentic-circuit/unittests/Dialect/ACSim/TypesTest.cpp new file mode 100644 index 00000000..e15eec52 --- /dev/null +++ b/components/agentic-circuit/unittests/Dialect/ACSim/TypesTest.cpp @@ -0,0 +1,87 @@ +#include "acir/Dialect/ACSim/ACSimDialect.h" +#include "acir/Dialect/ACSim/ACSimTypes.h" + +#include "mlir/AsmParser/AsmParser.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/MLIRContext.h" +#include "gtest/gtest.h" + +#include + +namespace acir::acsim { +namespace { + +TEST(ACSimTypesTest, ExactPublicInventoryConstructsPrintsAndRoundTrips) { + mlir::MLIRContext context; + context.loadDialect(); + + struct TypeCase { + llvm::StringLiteral spelling; + mlir::TypeID typeID; + }; + const std::array cases = {{ + {"!acsim.value<@cpp_i32>", ValueType::getTypeID()}, + {"!acsim.expr<@cpp_i32>", ExprType::getTypeID()}, + {"!acsim.owner<@fifo_binding>", OwnerType::getTypeID()}, + {"!acsim.ref<@fifo_binding>", RefType::getTypeID()}, + {"!acsim.port<@stream, @producer, @packet, @ready_valid>", + PortType::getTypeID()}, + {"!acsim.resource<@memory, @initiator>", ResourceType::getTypeID()}, + {"!acsim.array<[2, 3], !acsim.owner<@fifo_binding>>", + ArrayType::getTypeID()}, + {"!acsim.object_id", ObjectIdType::getTypeID()}, + {"!acsim.activation_id", ActivationIdType::getTypeID()}, + {"!acsim.pc<@tick>", PcType::getTypeID()}, + {"!acsim.wake<@event>", WakeType::getTypeID()}, + }}; + + for (const TypeCase &testCase : cases) { + mlir::Type type = mlir::parseType(testCase.spelling, &context); + ASSERT_TRUE(type) << testCase.spelling.str(); + EXPECT_EQ(type.getTypeID(), testCase.typeID) << testCase.spelling.str(); + + std::string printed; + llvm::raw_string_ostream(printed) << type; + EXPECT_EQ(printed, testCase.spelling) << testCase.spelling.str(); + EXPECT_EQ(type, mlir::parseType(printed, &context)); + } +} + +TEST(ACSimTypesTest, + CheckedArrayBuilderRejectsDynamicNegativeAndExcessiveShape) { + mlir::MLIRContext context; + context.loadDialect(); + mlir::ScopedDiagnosticHandler suppressExpectedDiagnostics( + &context, [](mlir::Diagnostic &) { return mlir::success(); }); + auto location = mlir::UnknownLoc::get(&context); + auto emitError = [location] { return mlir::emitError(location); }; + auto owner = OwnerType::get( + &context, mlir::FlatSymbolRefAttr::get(&context, "fifo_binding")); + + EXPECT_TRUE(ArrayType::getChecked( + emitError, &context, mlir::DenseI64ArrayAttr::get(&context, {2, 3}), + mlir::Type(owner))); + EXPECT_FALSE(ArrayType::getChecked(emitError, &context, + mlir::DenseI64ArrayAttr::get(&context, {}), + mlir::Type(owner))); + EXPECT_FALSE(ArrayType::getChecked( + emitError, &context, mlir::DenseI64ArrayAttr::get(&context, {2, -1}), + mlir::Type(owner))); + EXPECT_FALSE(ArrayType::getChecked( + emitError, &context, + mlir::DenseI64ArrayAttr::get(&context, {1048576, 1048576}), + mlir::Type(owner))); +} + +TEST(ACSimTypesTest, PublicTypesAreUniquedByExactTypedMetadata) { + mlir::MLIRContext context; + context.loadDialect(); + auto left = mlir::FlatSymbolRefAttr::get(&context, "left"); + auto right = mlir::FlatSymbolRefAttr::get(&context, "right"); + EXPECT_EQ(ValueType::get(&context, left), ValueType::get(&context, left)); + EXPECT_NE(ValueType::get(&context, left), ValueType::get(&context, right)); + EXPECT_NE(OwnerType::get(&context, left), RefType::get(&context, left)); +} + +} // namespace +} // namespace acir::acsim diff --git a/components/agentic-circuit/unittests/Dialect/CMakeLists.txt b/components/agentic-circuit/unittests/Dialect/CMakeLists.txt new file mode 100644 index 00000000..a0fe9b40 --- /dev/null +++ b/components/agentic-circuit/unittests/Dialect/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(ACIR) +add_subdirectory(ACSim) diff --git a/components/agentic-circuit/unittests/gfsim/CMakeLists.txt b/components/agentic-circuit/unittests/gfsim/CMakeLists.txt new file mode 100644 index 00000000..382b0116 --- /dev/null +++ b/components/agentic-circuit/unittests/gfsim/CMakeLists.txt @@ -0,0 +1,18 @@ +find_package(GTest CONFIG REQUIRED) +add_executable(GfsimTests + NpuTest.cpp + QueueBlocksTest.cpp + ShowcaseTest.cpp + core_test.cpp +) +target_include_directories(GfsimTests PRIVATE + ${PROJECT_SOURCE_DIR}/include +) +target_compile_definitions(GfsimTests PRIVATE + ACIR_TEST_SOURCE_DIR="${PROJECT_SOURCE_DIR}" +) +target_link_libraries(GfsimTests PRIVATE + gfsim + GTest::gtest_main +) +add_test(NAME GfsimTests COMMAND GfsimTests) diff --git a/components/agentic-circuit/unittests/gfsim/NpuTest.cpp b/components/agentic-circuit/unittests/gfsim/NpuTest.cpp new file mode 100644 index 00000000..c93c2e61 --- /dev/null +++ b/components/agentic-circuit/unittests/gfsim/NpuTest.cpp @@ -0,0 +1,845 @@ +#include "gfsim/npu.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include + +namespace gfsim { +namespace { + +PtoValue value(std::string text) { return {.value = std::move(text)}; } + +PtoValue value(uint64_t integer) { return {.value = integer}; } + +PtoValue array(PtoValue::Array values) { return {.value = std::move(values)}; } + +PtoValue object(PtoValue::Object members) { + return {.value = std::move(members)}; +} + +PtoValue tile(std::string address) { + return object({{"address", value(std::move(address))}, + {"dtype", value("float32")}, + {"layout", value("ND")}, + {"shape", array({value(2), value(4)})}}); +} + +PtoValue scalar(std::string type, std::string scalarValue) { + return object({{"dtype", value(std::move(type))}, + {"value", value(std::move(scalarValue))}}); +} + +PtoTraceOperand tileOperand(std::string id) { + return {.kind = PtoOperandKind::Tile, .id = std::move(id)}; +} + +PtoTraceOperand scalarOperand(std::string type, std::string scalarValue) { + return {.kind = PtoOperandKind::Immediate, + .type = std::move(type), + .immediate = value(std::move(scalarValue))}; +} + +PtoTraceRecord record(std::string opcode, uint64_t sequenceId, uint64_t blockId, + PtoValue::Array inputTiles, PtoValue::Array scalarInputs, + PtoValue::Array outputTiles, + std::vector roles, + std::vector operands) { + PtoValue::Array roleValues; + for (std::string &role : roles) + roleValues.push_back(value(std::move(role))); + PtoTraceRecord result; + result.sequenceId = sequenceId; + result.opcode = std::move(opcode); + result.operands = std::move(operands); + result.attributes.emplace( + "davincioo", object({{"block_idx", value(blockId)}, + {"input_tiles", array(std::move(inputTiles))}, + {"operand_roles", array(std::move(roleValues))}, + {"output_tiles", array(std::move(outputTiles))}, + {"scalar_inputs", array(std::move(scalarInputs))}})); + return result; +} + +PtoTraceRecord representative(std::string opcode) { + if (opcode == "TASSIGN") + return record( + std::move(opcode), 7, 3, {}, {scalar("uint64", "64")}, {tile("0x40")}, + {"scalar_input", "output_tile"}, + {scalarOperand("uint64", "64"), tileOperand("block/3/tile/0x40")}); + if (opcode == "TLOAD") + return record( + std::move(opcode), 7, 3, {tile("0x20")}, {}, {tile("0x40")}, + {"input_tile", "output_tile"}, + {tileOperand("block/3/tile/0x20"), tileOperand("block/3/tile/0x40")}); + if (opcode == "TMATMUL") + return record(std::move(opcode), 7, 3, {tile("0x10"), tile("0x20")}, {}, + {tile("0x40")}, {"input_tile", "input_tile", "output_tile"}, + {tileOperand("block/3/tile/0x10"), + tileOperand("block/3/tile/0x20"), + tileOperand("block/3/tile/0x40")}); + return record( + std::move(opcode), 7, 3, {tile("0x20")}, {scalar("float32", "1.25")}, + {tile("0x40")}, {"input_tile", "scalar_input", "output_tile"}, + {tileOperand("block/3/tile/0x20"), scalarOperand("float32", "1.25"), + tileOperand("block/3/tile/0x40")}); +} + +NpuInstruction tileInstruction(std::string opcode, uint64_t sequenceId, + uint64_t blockId, + std::vector inputAddresses, + std::vector outputAddresses) { + PtoValue::Array inputs; + PtoValue::Array outputs; + std::vector roles; + std::vector operands; + for (std::string &address : inputAddresses) { + inputs.push_back(tile(address)); + roles.push_back("input_tile"); + operands.push_back( + tileOperand("block/" + std::to_string(blockId) + "/tile/" + address)); + } + for (std::string &address : outputAddresses) { + outputs.push_back(tile(address)); + roles.push_back("output_tile"); + operands.push_back( + tileOperand("block/" + std::to_string(blockId) + "/tile/" + address)); + } + NpuDecodeResult decoded = NpuDecoder{}.decode( + record(std::move(opcode), sequenceId, blockId, std::move(inputs), {}, + std::move(outputs), std::move(roles), std::move(operands))); + EXPECT_TRUE(decoded.succeeded()); + return std::move(*decoded.instruction); +} + +void commit(NpuDependencyTracker &tracker, Epoch epoch) { + tracker.doArbitrate(epoch); + tracker.doXfer(epoch); +} + +void commit(NpuExecutionPipeline &pipeline, Epoch epoch) { + pipeline.doArbitrate(epoch); + pipeline.doXfer(epoch); +} + +NpuIssueEntry issueEntry(NpuInstruction instruction, ObjectId objectId) { + return {.instruction = std::move(instruction), .stableObjectId = objectId}; +} + +struct RecorderSink final : ObservationSink { + ObservationRecorder recorder; + bool proposeObservation(EventProposal proposal) override { + return recorder.propose(std::move(proposal)); + } +}; + +TEST(NpuDecoderTest, ClassifiesRepresentativePinnedDavinciOOOpcodes) { + NpuDecoder decoder; + const std::array cases = { + std::pair{"TASSIGN", NpuEngineClass::Scalar}, + std::pair{"TADDS", NpuEngineClass::Vector}, + std::pair{"TMATMUL", NpuEngineClass::Cube}, + std::pair{"TLOAD", NpuEngineClass::Tma}, + }; + + for (const auto &[opcode, engine] : cases) { + SCOPED_TRACE(opcode); + NpuDecodeResult decoded = decoder.decode(representative(opcode)); + ASSERT_TRUE(decoded.succeeded()); + ASSERT_TRUE(decoded.instruction); + EXPECT_EQ(decoded.instruction->opcode, opcode); + EXPECT_EQ(decoded.instruction->engine, engine); + } +} + +TEST(NpuDecoderTest, PreservesOrderedOperandsTilesScalarsAndDependencies) { + NpuDecoder decoder; + PtoTraceRecord source = representative("TADDS"); + source.dependencies = {1, 5}; + + NpuDecodeResult decoded = decoder.decode(source); + ASSERT_TRUE(decoded.succeeded()); + const NpuInstruction &instruction = *decoded.instruction; + EXPECT_EQ(instruction.sequenceId, 7u); + EXPECT_EQ(instruction.blockId, 3u); + EXPECT_EQ(instruction.dependencies, (std::vector{1, 5})); + EXPECT_EQ(instruction.operands, source.operands); + EXPECT_EQ(instruction.inputTiles, + (std::vector{"block/3/tile/0x20"})); + EXPECT_EQ(instruction.outputTiles, + (std::vector{"block/3/tile/0x40"})); + ASSERT_EQ(instruction.scalarInputs.size(), 1u); + EXPECT_EQ(instruction.scalarInputs[0].type, "float32"); + EXPECT_EQ(instruction.scalarInputs[0].value, value("1.25")); + EXPECT_EQ(instruction.timestamps, NpuTimestamps{}); +} + +TEST(NpuDecoderTest, ProducesDecodeAndDispatchProposalsWithRootIdentity) { + NpuDecodeResult decoded = NpuDecoder{}.decode(representative("TMATMUL")); + ASSERT_TRUE(decoded.succeeded()); + ASSERT_EQ(decoded.observations.size(), 2u); + EXPECT_EQ(decoded.observations[0].category, "instruction"); + EXPECT_EQ(decoded.observations[0].name, "decode"); + EXPECT_EQ(decoded.observations[1].category, "instruction"); + EXPECT_EQ(decoded.observations[1].name, "dispatch"); + for (const EventProposal &proposal : decoded.observations) { + EXPECT_EQ(proposal.rootSequenceId, 7u); + EXPECT_EQ(proposal.arguments, + (std::vector{ + {.name = "block_id", .value = uint64_t{3}}, + {.name = "engine", .value = std::string("cube")}, + {.name = "opcode", .value = std::string("TMATMUL")}})); + } +} + +TEST(NpuDecoderTest, RejectsUnsupportedAndMalformedImportedRecords) { + NpuDecoder decoder; + + PtoTraceRecord unsupported = representative("TADDS"); + unsupported.opcode = "TUNSUPPORTED"; + EXPECT_EQ(decoder.decode(unsupported).primaryDiagnostic(), + "npu_unsupported_opcode"); + + PtoTraceRecord missing = representative("TADDS"); + missing.attributes.clear(); + EXPECT_EQ(decoder.decode(missing).primaryDiagnostic(), + "npu_missing_davincioo_attributes"); + + PtoTraceRecord unknownField = representative("TADDS"); + auto &attributes = + std::get(unknownField.attributes.at("davincioo").value); + attributes.emplace("unexpected", value(1)); + EXPECT_EQ(decoder.decode(unknownField).primaryDiagnostic(), + "npu_invalid_davincioo_attributes"); + + PtoTraceRecord wrongRoleCount = representative("TADDS"); + auto &roleArray = std::get( + std::get( + wrongRoleCount.attributes.at("davincioo").value) + .at("operand_roles") + .value); + roleArray.pop_back(); + EXPECT_EQ(decoder.decode(wrongRoleCount).primaryDiagnostic(), + "npu_operand_role_mismatch"); + + PtoTraceRecord wrongTile = representative("TADDS"); + wrongTile.operands.front().id = "block/3/tile/0x21"; + EXPECT_EQ(decoder.decode(wrongTile).primaryDiagnostic(), + "npu_tile_identity_mismatch"); + + PtoTraceRecord wrongScalar = representative("TADDS"); + wrongScalar.operands[1].type = "float16"; + EXPECT_EQ(decoder.decode(wrongScalar).primaryDiagnostic(), + "npu_scalar_mismatch"); +} + +TEST(NpuDecoderTest, RepeatedDecodeIsByteIndependentAndValueIdentical) { + NpuDecoder decoder; + const PtoTraceRecord source = representative("TADDS"); + const NpuDecodeResult first = decoder.decode(source); + const NpuDecodeResult second = decoder.decode(source); + ASSERT_TRUE(first.succeeded()); + ASSERT_TRUE(second.succeeded()); + EXPECT_EQ(first.instruction, second.instruction); + EXPECT_EQ(first.observations, second.observations); + EXPECT_EQ(source, representative("TADDS")); +} + +TEST(NpuDecoderTest, UnsupportedOpcodeNeverCommitsATraceOffer) { + PtoTraceDocument document; + document.records.push_back(representative("TADDS")); + document.records.front().opcode = "TUNSUPPORTED"; + TraceSource source("trace", 1, nullptr, + std::move(document)); + + source.doWork({0, 0}); + source.doXfer({0, 0}); + + EXPECT_FALSE(source.hasOffer()); + EXPECT_EQ(source.position().nextRecordIndex, 0u); + EXPECT_EQ(source.runtimeFailureCode(), "trace_decode_failed"); +} + +TEST(NpuDependencyTrackerTest, RawDependencyWakesOnlyAtCompletionXfer) { + RecorderSink sink; + NpuDependencyTracker tracker("dependencies", 40, nullptr, {2, 2, 2, 2}, + &sink); + const NpuInstruction producer = tileInstruction("TADD", 10, 0, {}, {"0x10"}); + NpuInstruction consumer = tileInstruction("TADD", 11, 0, {"0x10"}, {"0x20"}); + consumer.dependencies = {4, 9}; + + ASSERT_TRUE(tracker.proposeDispatch(producer, 7)); + commit(tracker, {0, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(40, {0, 0})); + ASSERT_TRUE(tracker.proposeDispatch(consumer, 8)); + commit(tracker, {1, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(40, {1, 0})); + + ASSERT_EQ(tracker.dependencies(11).size(), 1u); + EXPECT_EQ(tracker.dependencies(11).front().producerSequenceId, 10u); + ASSERT_EQ(tracker.queued(NpuEngineClass::Vector).size(), 2u); + EXPECT_EQ( + tracker.queued(NpuEngineClass::Vector).back().instruction.dependencies, + (std::vector{4, 9})); + EXPECT_FALSE(tracker.isReady(11)); + ASSERT_TRUE(tracker.proposeIssue(NpuEngineClass::Vector)); + EXPECT_EQ( + tracker.proposedIssue(NpuEngineClass::Vector)->instruction.sequenceId, + 10u); + commit(tracker, {2, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(40, {2, 0})); + + ASSERT_TRUE(tracker.proposeComplete(10)); + EXPECT_FALSE(tracker.isReady(11)); + commit(tracker, {3, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(40, {3, 0})); + EXPECT_TRUE(tracker.isReady(11)); + ASSERT_TRUE(tracker.proposeIssue(NpuEngineClass::Vector)); + EXPECT_EQ( + tracker.proposedIssue(NpuEngineClass::Vector)->instruction.sequenceId, + 11u); + commit(tracker, {4, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(40, {4, 0})); + + std::vector dependencyPhases; + for (const CommittedEvent &event : sink.recorder.events()) + if (event.category == "dependency" && event.name == "tile") { + dependencyPhases.push_back(event.phase); + ASSERT_TRUE(event.flowId); + EXPECT_LE(*event.flowId, 9007199254740991ULL); + } + EXPECT_EQ(dependencyPhases, + (std::vector{TraceEventPhase::FlowStart, + TraceEventPhase::FlowEnd})); + + std::vector statistics; + tracker.collectStatistics(statistics); + auto valueOf = [&](std::string_view name) { + auto position = std::ranges::find(statistics, name, &StatSnapshot::name); + EXPECT_NE(position, statistics.end()); + return position == statistics.end() ? uint64_t{0} : position->value; + }; + EXPECT_EQ(valueOf("issued_instructions"), 2u); + EXPECT_EQ(valueOf("dependency_wakeups"), 1u); +} + +TEST(NpuDependencyTrackerTest, FourFiniteEngineQueuesIssueIndependently) { + NpuDependencyTracker tracker("dependencies", 40, nullptr, {1, 1, 1, 1}); + const std::array instructions = { + tileInstruction("TASSIGN", 1, 0, {}, {"0x10"}), + tileInstruction("TADD", 2, 0, {}, {"0x20"}), + tileInstruction("TMATMUL", 3, 0, {}, {"0x30"}), + tileInstruction("TLOAD", 4, 0, {}, {"0x40"}), + }; + for (size_t index = 0; index < instructions.size(); ++index) + ASSERT_TRUE(tracker.proposeDispatch(instructions[index], + static_cast(10 + index))); + commit(tracker, {0, 0}); + + EXPECT_EQ(tracker.queueSize(NpuEngineClass::Scalar), 1u); + EXPECT_EQ(tracker.queueSize(NpuEngineClass::Vector), 1u); + EXPECT_EQ(tracker.queueSize(NpuEngineClass::Cube), 1u); + EXPECT_EQ(tracker.queueSize(NpuEngineClass::Tma), 1u); + EXPECT_TRUE(tracker.proposeIssue(NpuEngineClass::Tma)); + EXPECT_TRUE(tracker.proposeIssue(NpuEngineClass::Cube)); + EXPECT_TRUE(tracker.proposeIssue(NpuEngineClass::Vector)); + EXPECT_TRUE(tracker.proposeIssue(NpuEngineClass::Scalar)); + commit(tracker, {1, 0}); + + ASSERT_EQ(tracker.issued().size(), 4u); + for (size_t index = 0; index < tracker.issued().size(); ++index) { + EXPECT_EQ(tracker.issued()[index].instruction.sequenceId, index + 1); + EXPECT_EQ(tracker.issued()[index].instruction.timestamps.issued, 1u); + } +} + +TEST(NpuDependencyTrackerTest, OldestReadySelectionIgnoresInsertionOrder) { + auto run = [](std::array order) { + NpuDependencyTracker tracker("dependencies", 40, nullptr, {3, 3, 3, 3}); + const std::array instructions = { + tileInstruction("TADD", 30, 0, {}, {"0x30"}), + tileInstruction("TADD", 10, 0, {}, {"0x10"}), + tileInstruction("TADD", 20, 0, {}, {"0x20"}), + }; + for (size_t index : order) + EXPECT_TRUE(tracker.proposeDispatch(instructions[index], + static_cast(9 - index))); + commit(tracker, {0, 0}); + EXPECT_TRUE(tracker.proposeIssue(NpuEngineClass::Vector)); + return tracker.proposedIssue(NpuEngineClass::Vector) + ->instruction.sequenceId; + }; + + EXPECT_EQ(run({0, 1, 2}), 10u); + EXPECT_EQ(run({2, 0, 1}), 10u); +} + +TEST(NpuDependencyTrackerTest, RenameUsesLatestProducerAndIsBlockLocal) { + NpuDependencyTracker tracker("dependencies", 40, nullptr, {8, 8, 8, 8}); + const NpuInstruction first = tileInstruction("TADD", 1, 0, {}, {"0x10"}); + const NpuInstruction overwrite = tileInstruction("TADD", 2, 0, {}, {"0x10"}); + const NpuInstruction sameBlock = + tileInstruction("TADD", 3, 0, {"0x10"}, {"0x20"}); + const NpuInstruction otherBlock = + tileInstruction("TADD", 4, 1, {"0x10"}, {"0x20"}); + + ASSERT_TRUE(tracker.proposeDispatch(first, 1)); + ASSERT_TRUE(tracker.proposeDispatch(overwrite, 2)); + commit(tracker, {0, 0}); + ASSERT_TRUE(tracker.proposeDispatch(sameBlock, 3)); + ASSERT_TRUE(tracker.proposeDispatch(otherBlock, 4)); + commit(tracker, {1, 0}); + + ASSERT_EQ(tracker.dependencies(3).size(), 1u); + EXPECT_EQ(tracker.dependencies(3).front().producerSequenceId, 2u); + EXPECT_TRUE(tracker.dependencies(4).empty()); + EXPECT_TRUE(tracker.isReady(4)); +} + +TEST(NpuDependencyTrackerTest, FiniteCapacityRejectsWithoutMutatingOffer) { + RecorderSink sink; + NpuDependencyTracker tracker("dependencies", 40, nullptr, {1, 1, 1, 1}, + &sink); + tracker.setPath("/npu/dependencies"); + const NpuInstruction first = tileInstruction("TADD", 1, 0, {}, {"0x10"}); + const NpuInstruction blocked = tileInstruction("TADD", 2, 0, {}, {"0x20"}); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + const NpuInstruction original = blocked; + + ASSERT_TRUE(tracker.proposeDispatch(first, 1)); + commit(tracker, {0, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(40, {0, 0})); + ASSERT_TRUE(tracker.proposeDispatch(blocked, 2)); + commit(tracker, {1, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(40, {1, 0})); + + EXPECT_EQ(blocked, original); + EXPECT_EQ(tracker.queueSize(NpuEngineClass::Vector), 1u); + EXPECT_EQ(tracker.rejectedDispatches(), (std::vector{2})); + EXPECT_FALSE(tracker.dispatchAccepted(2)); + ASSERT_FALSE(sink.recorder.events().empty()); + EXPECT_EQ(sink.recorder.events().back().category, "stall"); + EXPECT_EQ(sink.recorder.events().back().name, "issue_queue_capacity"); + + ASSERT_TRUE(tracker.proposeIssue(NpuEngineClass::Vector)); + ASSERT_TRUE(tracker.proposeDispatch(blocked, 2)); + commit(tracker, {2, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(40, {2, 0})); + EXPECT_TRUE(tracker.dispatchAccepted(2)); + ASSERT_EQ(tracker.queued(NpuEngineClass::Vector).size(), 1u); + EXPECT_EQ( + tracker.queued(NpuEngineClass::Vector).front().instruction.sequenceId, + 2u); + + std::vector statistics; + tracker.collectStatistics(statistics); + auto find = [&](std::string_view name) -> const StatSnapshot * { + auto position = std::ranges::find(statistics, name, &StatSnapshot::name); + return position == statistics.end() ? nullptr : &*position; + }; + ASSERT_NE(find("dispatch_stalls"), nullptr); + EXPECT_EQ(find("dispatch_stalls")->value, 1u); + ASSERT_NE(find("issue_queue_occupancy_vector"), nullptr); + EXPECT_EQ(find("issue_queue_occupancy_vector")->value, 1u); +} + +TEST(NpuDependencyTrackerTest, + WorkPermutationPreservesQueuesStatisticsAndObservations) { + struct Snapshot { + std::vector queue; + std::vector> statistics; + std::vector observations; + bool operator==(const Snapshot &) const = default; + }; + auto run = [](std::array order) { + RecorderSink sink; + NpuDependencyTracker tracker("dependencies", 40, nullptr, {1, 2, 1, 1}, + &sink); + tracker.setPath("/npu/dependencies"); + const std::array instructions = { + tileInstruction("TADD", 3, 0, {}, {"0x30"}), + tileInstruction("TADD", 1, 0, {}, {"0x10"}), + tileInstruction("TADD", 2, 0, {"0x10"}, {"0x20"}), + }; + for (size_t index : order) + EXPECT_TRUE(tracker.proposeDispatch(instructions[index], + static_cast(10 + index))); + commit(tracker, {0, 0}); + EXPECT_TRUE(sink.recorder.commitOwner(40, {0, 0})); + std::vector statistics; + tracker.collectStatistics(statistics); + std::vector> statisticValues; + for (const StatSnapshot &statistic : statistics) + statisticValues.emplace_back(statistic.name, statistic.value); + return Snapshot{tracker.queued(NpuEngineClass::Vector), + std::move(statisticValues), + std::vector(sink.recorder.events().begin(), + sink.recorder.events().end())}; + }; + + EXPECT_EQ(run({0, 1, 2}), run({2, 0, 1})); +} + +TEST(NpuExecutionPipelineTest, FrozenLatenciesCompleteFourEnginesPrecisely) { + NpuExecutionPipeline pipeline("execution", 50, nullptr, + {.scalarUnits = 1, + .vectorUnits = 1, + .cubeUnits = 1, + .tmaUnits = 1, + .memoryRequests = 2, + .scratchpadTiles = 8}); + const std::array entries = { + issueEntry(tileInstruction("TASSIGN", 1, 0, {}, {"0x10"}), 10), + issueEntry(tileInstruction("TADD", 2, 0, {}, {"0x20"}), 11), + issueEntry(tileInstruction("TMATMUL", 3, 0, {}, {"0x30"}), 12), + issueEntry(tileInstruction("TLOAD", 4, 0, {"0x100"}, {"0x40"}), 13), + }; + for (const NpuIssueEntry &entry : entries) + ASSERT_TRUE(pipeline.proposeAdmit(entry.instruction)); + commit(pipeline, {0, 0}); + for (const NpuIssueEntry &entry : entries) + ASSERT_TRUE(pipeline.proposeExecute(entry, {1, 0})); + commit(pipeline, {1, 0}); + + EXPECT_EQ(pipeline.completionEpoch(1), (Epoch{2, 0})); + EXPECT_EQ(pipeline.completionEpoch(2), (Epoch{3, 0})); + EXPECT_EQ(pipeline.completionEpoch(3), (Epoch{5, 0})); + EXPECT_EQ(pipeline.completionEpoch(4), (Epoch{4, 0})); + + pipeline.doWork({2, 0}); + commit(pipeline, {2, 0}); + ASSERT_EQ(pipeline.completed().size(), 1u); + EXPECT_EQ(pipeline.completed().front().instruction.sequenceId, 1u); + pipeline.doWork({3, 0}); + commit(pipeline, {3, 0}); + ASSERT_EQ(pipeline.completed().size(), 1u); + EXPECT_EQ(pipeline.completed().front().instruction.sequenceId, 2u); + pipeline.doWork({4, 0}); + commit(pipeline, {4, 0}); + ASSERT_EQ(pipeline.completed().size(), 1u); + EXPECT_EQ(pipeline.completed().front().instruction.sequenceId, 4u); + pipeline.doWork({5, 0}); + commit(pipeline, {5, 0}); + ASSERT_EQ(pipeline.completed().size(), 1u); + EXPECT_EQ(pipeline.completed().front().instruction.sequenceId, 3u); +} + +TEST(NpuExecutionPipelineTest, UnitPressureRejectsWithoutMutatingIssue) { + NpuExecutionPipeline pipeline("execution", 50, nullptr, + {.scalarUnits = 1, + .vectorUnits = 1, + .cubeUnits = 1, + .tmaUnits = 1, + .memoryRequests = 1, + .scratchpadTiles = 2}); + const NpuIssueEntry first = + issueEntry(tileInstruction("TADD", 1, 0, {}, {"0x10"}), 10); + const NpuIssueEntry blocked = + issueEntry(tileInstruction("TADD", 2, 0, {}, {"0x20"}), 11); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + const NpuIssueEntry original = blocked; + ASSERT_TRUE(pipeline.proposeAdmit(first.instruction)); + ASSERT_TRUE(pipeline.proposeAdmit(blocked.instruction)); + commit(pipeline, {0, 0}); + + EXPECT_TRUE(pipeline.proposeExecute(first, {1, 0})); + EXPECT_FALSE(pipeline.proposeExecute(blocked, {1, 0})); + EXPECT_EQ(blocked, original); + commit(pipeline, {1, 0}); + EXPECT_EQ(pipeline.activeExecutions(NpuEngineClass::Vector), 1u); +} + +TEST(NpuExecutionPipelineTest, TmaUsesExactAddressAndCorrelation) { + NpuExecutionPipeline pipeline("execution", 50, nullptr, + {.scalarUnits = 1, + .vectorUnits = 1, + .cubeUnits = 1, + .tmaUnits = 1, + .memoryRequests = 1, + .scratchpadTiles = 2}); + const NpuIssueEntry load = + issueEntry(tileInstruction("TLOAD", 7, 0, {"0x100"}, {"0x0"}), 10); + ASSERT_TRUE(pipeline.proposeAdmit(load.instruction)); + commit(pipeline, {0, 0}); + ASSERT_TRUE(pipeline.proposeTraceExhausted()); + ASSERT_TRUE(pipeline.proposeExecute(load, {1, 0})); + commit(pipeline, {1, 0}); + + ASSERT_EQ(pipeline.memoryRequests().size(), 1u); + EXPECT_EQ(pipeline.memoryRequests().front().sequenceId, 7u); + EXPECT_EQ(pipeline.memoryRequests().front().correlationId, 7u); + EXPECT_EQ(pipeline.memoryRequests().front().address, 0x100u); + EXPECT_FALSE(pipeline.memoryRequests().front().write); + + pipeline.doWork({2, 0}); + commit(pipeline, {2, 0}); + pipeline.doWork({4, 0}); + commit(pipeline, {4, 0}); + ASSERT_EQ(pipeline.memoryResponses().size(), 1u); + EXPECT_EQ(pipeline.memoryResponses().front().correlationId, 7u); + EXPECT_TRUE(pipeline.scratchpadContains("block/0/tile/0x0")); + EXPECT_FALSE(pipeline.complete()); + pipeline.doWork({5, 0}); + commit(pipeline, {5, 0}); + EXPECT_TRUE(pipeline.complete()); +} + +TEST(NpuExecutionPipelineTest, StoreUsesScratchpadValueAndGlobalAddress) { + NpuExecutionPipeline pipeline("execution", 50, nullptr, + {.scalarUnits = 1, + .vectorUnits = 1, + .cubeUnits = 1, + .tmaUnits = 1, + .memoryRequests = 1, + .scratchpadTiles = 2}); + const NpuIssueEntry produce = + issueEntry(tileInstruction("TASSIGN", 1, 0, {}, {"0x0"}), 10); + const NpuIssueEntry store = + issueEntry(tileInstruction("TSTORE", 2, 0, {"0x0"}, {"0x200"}), 11); + ASSERT_TRUE(pipeline.proposeAdmit(produce.instruction)); + ASSERT_TRUE(pipeline.proposeAdmit(store.instruction)); + commit(pipeline, {0, 0}); + ASSERT_TRUE(pipeline.proposeExecute(produce, {1, 0})); + commit(pipeline, {1, 0}); + pipeline.doWork({2, 0}); + commit(pipeline, {2, 0}); + ASSERT_TRUE(pipeline.proposeExecute(store, {3, 0})); + commit(pipeline, {3, 0}); + + ASSERT_EQ(pipeline.memoryRequests().size(), 1u); + EXPECT_TRUE(pipeline.memoryRequests().front().write); + EXPECT_EQ(pipeline.memoryRequests().front().address, 0x200u); + EXPECT_EQ(pipeline.memoryRequests().front().tileIdentity, "block/0/tile/0x0"); + pipeline.doWork({4, 0}); + commit(pipeline, {4, 0}); + EXPECT_EQ(pipeline.completionEpoch(2), (Epoch{7, 0})); + pipeline.doWork({7, 0}); + commit(pipeline, {7, 0}); + EXPECT_EQ(pipeline.globalMemoryValue(0x200), 1u); + EXPECT_FALSE(pipeline.scratchpadContains("block/0/tile/0x200")); +} + +TEST(NpuExecutionPipelineTest, MemoryOrderingIgnoresExecuteProposalOrder) { + NpuExecutionPipeline pipeline("execution", 50, nullptr, + {.scalarUnits = 1, + .vectorUnits = 1, + .cubeUnits = 1, + .tmaUnits = 2, + .memoryRequests = 2, + .scratchpadTiles = 4}); + const NpuIssueEntry first = + issueEntry(tileInstruction("TLOAD", 1, 0, {"0x100"}, {"0x10"}), 11); + const NpuIssueEntry second = + issueEntry(tileInstruction("TLOAD", 2, 0, {"0x200"}, {"0x20"}), 10); + ASSERT_TRUE(pipeline.proposeAdmit(first.instruction)); + ASSERT_TRUE(pipeline.proposeAdmit(second.instruction)); + commit(pipeline, {0, 0}); + ASSERT_TRUE(pipeline.proposeExecute(second, {1, 0})); + ASSERT_TRUE(pipeline.proposeExecute(first, {1, 0})); + commit(pipeline, {1, 0}); + + ASSERT_EQ(pipeline.memoryRequests().size(), 2u); + EXPECT_EQ(pipeline.memoryRequests()[0].correlationId, 1u); + EXPECT_EQ(pipeline.memoryRequests()[1].correlationId, 2u); + pipeline.doWork({2, 0}); + commit(pipeline, {2, 0}); + pipeline.doWork({4, 0}); + commit(pipeline, {4, 0}); + ASSERT_EQ(pipeline.memoryResponses().size(), 2u); + EXPECT_EQ(pipeline.memoryResponses()[0].correlationId, 1u); + EXPECT_EQ(pipeline.memoryResponses()[1].correlationId, 2u); +} + +TEST(NpuExecutionPipelineTest, CompletionIsDrivenByScheduledRuntimeEvent) { + SimSystem system; + NpuExecutionPipeline pipeline("execution", 50, nullptr, + {.scalarUnits = 1, + .vectorUnits = 1, + .cubeUnits = 1, + .tmaUnits = 1, + .memoryRequests = 1, + .scratchpadTiles = 2}, + &system); + system.registerObject(&pipeline); + const NpuIssueEntry entry = + issueEntry(tileInstruction("TADD", 1, 0, {}, {"0x10"}), 10); + ASSERT_TRUE(pipeline.proposeAdmit(entry.instruction)); + commit(pipeline, {0, 0}); + ASSERT_TRUE(pipeline.proposeExecute(entry, {0, 0})); + commit(pipeline, {0, 0}); + + EXPECT_EQ(system.nextEvent(), std::nullopt); + ASSERT_TRUE(system.step()); + ASSERT_TRUE(system.nextEvent()); + EXPECT_EQ(system.nextEvent()->readyTime, (Epoch{2, 0})); + EXPECT_EQ(system.nextEvent()->targetId, 50u); + EXPECT_FALSE(system.step()); + ASSERT_EQ(pipeline.completed().size(), 1u); + EXPECT_EQ(pipeline.completed().front().instruction.sequenceId, 1u); +} + +TEST(NpuExecutionPipelineTest, + OutOfOrderCompletionStillRetiresInBlockSequenceOrder) { + NpuExecutionPipeline pipeline("execution", 50, nullptr, + {.scalarUnits = 1, + .vectorUnits = 1, + .cubeUnits = 1, + .tmaUnits = 1, + .memoryRequests = 1, + .scratchpadTiles = 4}); + const NpuIssueEntry slow = + issueEntry(tileInstruction("TMATMUL", 1, 0, {}, {"0x10"}), 10); + const NpuIssueEntry fast = + issueEntry(tileInstruction("TASSIGN", 2, 0, {}, {"0x20"}), 11); + ASSERT_TRUE(pipeline.proposeAdmit(slow.instruction)); + ASSERT_TRUE(pipeline.proposeAdmit(fast.instruction)); + commit(pipeline, {0, 0}); + ASSERT_TRUE(pipeline.proposeExecute(slow, {1, 0})); + ASSERT_TRUE(pipeline.proposeExecute(fast, {1, 0})); + commit(pipeline, {1, 0}); + + pipeline.doWork({2, 0}); + commit(pipeline, {2, 0}); + ASSERT_EQ(pipeline.completed().size(), 1u); + EXPECT_EQ(pipeline.completed().front().instruction.sequenceId, 2u); + EXPECT_TRUE(pipeline.retired().empty()); + EXPECT_EQ(pipeline.architecturalResult(), NpuArchitecturalResult{}); + pipeline.doWork({5, 0}); + commit(pipeline, {5, 0}); + ASSERT_EQ(pipeline.retired().size(), 2u); + EXPECT_EQ(pipeline.retired()[0].sequenceId, 1u); + EXPECT_EQ(pipeline.retired()[1].sequenceId, 2u); + EXPECT_EQ(pipeline.retired()[0].timestamps.retired, 5u); + EXPECT_EQ(pipeline.retired()[1].timestamps.retired, 5u); + EXPECT_EQ(pipeline.architecturalResult().retiredInstructions, 2u); + EXPECT_EQ(pipeline.architecturalResult().retiredSequenceIds, + (std::vector{1, 2})); +} + +TEST(NpuExecutionPipelineTest, CompletionNeedsTraceExhaustionAndQuiescence) { + NpuExecutionPipeline pipeline("execution", 50, nullptr, + {.scalarUnits = 1, + .vectorUnits = 1, + .cubeUnits = 1, + .tmaUnits = 1, + .memoryRequests = 1, + .scratchpadTiles = 2}); + const NpuIssueEntry entry = + issueEntry(tileInstruction("TASSIGN", 1, 0, {}, {"0x10"}), 10); + ASSERT_TRUE(pipeline.proposeAdmit(entry.instruction)); + commit(pipeline, {0, 0}); + ASSERT_TRUE(pipeline.proposeTraceExhausted()); + ASSERT_TRUE(pipeline.proposeExecute(entry, {1, 0})); + commit(pipeline, {1, 0}); + EXPECT_FALSE(pipeline.complete()); + pipeline.doWork({2, 0}); + commit(pipeline, {2, 0}); + EXPECT_TRUE(pipeline.complete()); +} + +TEST(NpuTraceSourceTest, + ExecutesFourEnginesWithDependenciesAndCommitsArchitecturalState) { + PtoTraceDocument document; + document.records.push_back(record( + "TLOAD", 0, 0, {tile("0x100")}, {}, {tile("0x0")}, + {"input_tile", "output_tile"}, + {tileOperand("block/0/tile/0x100"), tileOperand("block/0/tile/0x0")})); + document.records.push_back( + record("TASSIGN", 1, 0, {}, {scalar("uint64", "7")}, {tile("0x10")}, + {"scalar_input", "output_tile"}, + {scalarOperand("uint64", "7"), tileOperand("block/0/tile/0x10")})); + document.records.push_back( + record("TADD", 2, 0, {tile("0x0"), tile("0x10")}, {}, {tile("0x20")}, + {"input_tile", "input_tile", "output_tile"}, + {tileOperand("block/0/tile/0x0"), tileOperand("block/0/tile/0x10"), + tileOperand("block/0/tile/0x20")})); + document.records.push_back(record( + "TMATMUL", 3, 0, {tile("0x20")}, {}, {tile("0x40")}, + {"input_tile", "output_tile"}, + {tileOperand("block/0/tile/0x20"), tileOperand("block/0/tile/0x40")})); + document.records.push_back(record("TADD", 4, 0, {}, {}, {tile("0x50")}, + {"output_tile"}, + {tileOperand("block/0/tile/0x50")})); + document.records.push_back(record( + "TSTORE", 5, 0, {tile("0x40")}, {}, {tile("0x200")}, + {"input_tile", "output_tile"}, + {tileOperand("block/0/tile/0x40"), tileOperand("block/0/tile/0x200")})); + + RecorderSink sink; + NpuTraceSource provider("trace_source", 7, nullptr, &sink); + ASSERT_TRUE(provider.loadDocument(std::move(document))); + provider.doWork({0, 0}); + ASSERT_TRUE(provider.runtimeFailureCode().empty()) + << provider.runtimeFailureCode(); + ASSERT_TRUE(provider.hasPendingCommit()); + provider.doXfer({0, 0}); + ASSERT_TRUE(sink.recorder.commitOwner(provider.id(), {0, 0})); + + RuntimeObjectState state = provider.runtimeState({0, 0}); + EXPECT_TRUE(state.quiescent); + EXPECT_TRUE(state.traceEof); + EXPECT_EQ(state.tracePosition, 6u); + std::vector statistics; + provider.collectStatistics(statistics); + auto statistic = [&](std::string_view name) { + return std::ranges::find(statistics, name, &StatSnapshot::name); + }; + ASSERT_NE(statistic("architectural_retired_instructions"), statistics.end()); + EXPECT_EQ(statistic("architectural_retired_instructions")->value, 6u); + ASSERT_NE(statistic("architectural_digest"), statistics.end()); + EXPECT_NE(statistic("architectural_digest")->value, + NpuArchitecturalResult{}.digest); + EXPECT_GT(sink.recorder.events().size(), 24u); +} + +TEST(NpuTraceSourceTest, RejectsUnsupportedOpcodeWithoutCommit) { + PtoTraceDocument document; + document.records.push_back(representative("TUNSUPPORTED")); + NpuTraceSource provider("trace_source", 7, nullptr); + ASSERT_TRUE(provider.loadDocument(std::move(document))); + provider.doWork({0, 0}); + EXPECT_EQ(provider.runtimeFailureCode(), "npu_decode_failed"); + EXPECT_FALSE(provider.hasPendingCommit()); + EXPECT_FALSE(provider.runtimeState({0, 0}).traceEof); +} + +TEST(NpuTraceSourceTest, RunsAsTheSingleSystemTraceOwner) { + PtoTraceDocument document; + document.records.push_back(representative("TASSIGN")); + SimSystem system; + NpuTraceSource provider("trace_source", 7, &system.root()); + provider.setObservationSink(&system); + system.registerObject(&provider); + ASSERT_TRUE(provider.loadDocument(std::move(document))); + + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed) + << result.diagnosticCode << ": " << result.message.value_or(""); + EXPECT_EQ(result.tracePosition, 1u); + EXPECT_FALSE(system.observations().empty()); +} + +TEST(NpuTraceSourceTest, DecodesTheCheckedInDavinciOOFixture) { + std::ifstream input(std::string(ACIR_TEST_SOURCE_DIR) + + "/examples/workspaces/npu/traces/pto-trace.json", + std::ios::binary); + ASSERT_TRUE(input); + std::string bytes((std::istreambuf_iterator(input)), + std::istreambuf_iterator()); + TraceLoadResult loaded = parsePtoTrace(bytes); + ASSERT_TRUE(loaded.succeeded()) << loaded.primaryDiagnostic(); + ASSERT_TRUE(loaded.document); + EXPECT_EQ(loaded.document->records.size(), 6u); + for (const PtoTraceRecord &source : loaded.document->records) { + SCOPED_TRACE(source.sequenceId); + NpuDecodeResult decoded = NpuDecoder{}.decode(source); + EXPECT_TRUE(decoded.succeeded()) << decoded.primaryDiagnostic(); + } +} + +} // namespace +} // namespace gfsim diff --git a/components/agentic-circuit/unittests/gfsim/QueueBlocksTest.cpp b/components/agentic-circuit/unittests/gfsim/QueueBlocksTest.cpp new file mode 100644 index 00000000..a57fc902 --- /dev/null +++ b/components/agentic-circuit/unittests/gfsim/QueueBlocksTest.cpp @@ -0,0 +1,919 @@ +#include "gfsim/queue_blocks.h" + +#include "gtest/gtest.h" + +#include +#include +#include + +namespace gfsim { +namespace { + +struct Increment { + int operator()(const int &value) const { return value + 1; } +}; + +struct SelectParity { + size_t operator()(const int &value) const { + return static_cast(value & 1); + } +}; + +struct SelectIndex { + size_t operator()(const int &value) const { + return static_cast(value); + } +}; + +struct Positive { + bool operator()(const int &value) const { return value > 0; } +}; + +struct Decrement { + int operator()(const int &value) const { return value - 1; } +}; + +struct IncrementAndDouble { + std::tuple operator()(const int &left, const int &right) const { + return {left + 1, right * 2}; + } +}; + +struct SumToWide { + std::tuple operator()(const int &left, const int64_t &right) const { + return {static_cast(left) + right}; + } +}; + +struct SequencedValue { + uint64_t sequence = 0; + int value = 0; + + bool operator==(const SequencedValue &) const = default; +}; + +struct SequenceKey { + uint64_t operator()(const SequencedValue &value) const { + return value.sequence; + } +}; + +struct SignedSequencedValue { + int64_t sequence = 0; +}; + +struct SignedSequenceKey { + int64_t operator()(const SignedSequencedValue &value) const { + return value.sequence; + } +}; + +struct DependencyValue { + uint64_t sequence = 0; + uint64_t predecessor = 255; + uint64_t resource = 0; + uint64_t cycles = 1; + + bool operator==(const DependencyValue &) const = default; +}; + +struct DependencyKey { + uint64_t operator()(const DependencyValue &value) const { + return value.sequence; + } +}; + +struct DependencyPredecessor { + uint64_t operator()(const DependencyValue &value) const { + return value.predecessor; + } +}; + +struct DependencyCost { + uint64_t operator()(const DependencyValue &value) const { + return value.cycles; + } +}; + +struct DependencyResource { + uint64_t operator()(const DependencyValue &value) const { + return value.resource; + } +}; + +struct MemoryRequest { + uint8_t address = 0; + bool write = false; + uint16_t data = 0; + + bool operator==(const MemoryRequest &) const = default; +}; + +struct MemoryAddress { + uint8_t operator()(const MemoryRequest &request) const { + return request.address; + } +}; + +struct MemoryWrite { + bool operator()(const MemoryRequest &request) const { return request.write; } +}; + +struct MemoryWriteData { + uint16_t operator()(const MemoryRequest &request) const { + return request.data; + } +}; + +struct MemoryResponse { + MemoryRequest operator()(const MemoryRequest &request, + const uint16_t &oldData) const { + MemoryRequest response = request; + response.data = oldData; + return response; + } +}; + +TEST(QueueBlocksTest, HighLevelProvidersFreezeStructuralTemplateParameters) { + using Schedule4 = + Schedule; + using Schedule2 = + Schedule; + using Engine4 = Engine; + using ComputeInt = Compute; + using Pipeline2 = Pipeline; + using Ordered64 = Reorder; + using Table32 = Table; + + static_assert(!std::is_same_v); + EXPECT_EQ(Schedule4::contractName, "ac.schedule"); + EXPECT_EQ(Engine4::contractName, "ac.engine"); + EXPECT_EQ(ComputeInt::contractName, "ac.compute"); + EXPECT_EQ(Pipeline2::contractName, "ac.pipeline"); + EXPECT_EQ(Ordered64::contractName, "ac.reorder"); + EXPECT_EQ(Table32::contractName, "ac.table"); + + SimQueue dependencyInput("dependency_input", 1, nullptr, 2); + SimQueue dependencyOutput("dependency_output", 2, nullptr, + 2); + Schedule4 schedule("schedule", 3, nullptr, dependencyInput, dependencyOutput); + Engine4 engine("engine", 4, nullptr, dependencyInput, dependencyOutput); + SimQueue orderedInput("ordered_input", 5, nullptr, 2); + SimQueue orderedOutput("ordered_output", 6, nullptr, 2); + Ordered64 reorder("reorder", 7, nullptr, orderedInput, orderedOutput); + SimQueue memoryInput("memory_input", 8, nullptr, 2); + SimQueue memoryOutput("memory_output", 9, nullptr, 2); + Table32 table("table", 10, nullptr, memoryInput, memoryOutput); + SimQueue computeInput("compute_input", 11, nullptr, 2); + SimQueue computeOutput("compute_output", 12, nullptr, 2); + ComputeInt compute("compute", 13, nullptr, computeInput, computeOutput); + SimQueue pipelineOutput("pipeline_output", 14, nullptr, 2, + std::numeric_limits::max(), nullptr, 2); + Pipeline2 pipeline("pipeline", 15, nullptr, computeOutput, pipelineOutput); + + EXPECT_EQ(schedule.active(), 0u); + EXPECT_EQ(engine.active(), 0u); + EXPECT_EQ(reorder.active(), 0u); + EXPECT_EQ(table.at(0), 0u); + EXPECT_FALSE(compute.hasPendingCommit()); + EXPECT_FALSE(pipeline.hasPendingCommit()); +} + +struct SharedMemoryAddress { + uint8_t operator()(size_t, const MemoryRequest &request) const { + return request.address; + } +}; +struct SharedMemoryWrite { + bool operator()(size_t, const MemoryRequest &request) const { + return request.write; + } +}; +struct SharedMemoryWriteData { + uint16_t operator()(size_t, const MemoryRequest &request) const { + return request.data; + } +}; +struct SharedMemoryResponse { + MemoryRequest operator()(size_t, const MemoryRequest &request, + const uint16_t &oldData) const { + MemoryRequest response = request; + response.data = oldData; + return response; + } +}; + +TEST(QueueBlocksTest, TransformCommitsOnlyAcrossTheQueueBarrier) { + SimQueue input("input", 1, nullptr, 2); + SimQueue output("output", 2, nullptr, 2); + QueueTransform transform("transform", 3, nullptr, input, + output); + + ASSERT_TRUE(input.proposePush(41)); + input.doXfer({0, 0}); + transform.doWork({1, 0}); + + ASSERT_NE(input.peek(), nullptr); + EXPECT_EQ(*input.peek(), 41); + EXPECT_TRUE(output.isEmpty()); + + input.doXfer({1, 0}); + output.doXfer({1, 0}); + transform.doXfer({1, 0}); + EXPECT_TRUE(input.isEmpty()); + ASSERT_NE(output.peek(), nullptr); + EXPECT_EQ(*output.peek(), 42); +} + +TEST(QueueBlocksTest, SimQueueRateBoundsOneEpochProposals) { + SimQueue queue("rate_two", 1, nullptr, 4, + std::numeric_limits::max(), nullptr, 1, 2); + EXPECT_EQ(queue.rate(), 2u); + EXPECT_TRUE(queue.proposePush(10)); + EXPECT_TRUE(queue.proposePush(20)); + EXPECT_FALSE(queue.proposePush(30)); + queue.doXfer({0, 0}); + + ASSERT_NE(queue.peekProposable(), nullptr); + EXPECT_EQ(*queue.peekProposable(), 10); + EXPECT_EQ(queue.proposePop(), 10); + ASSERT_NE(queue.peekProposable(), nullptr); + EXPECT_EQ(*queue.peekProposable(), 20); + EXPECT_EQ(queue.proposePop(), 20); + EXPECT_EQ(queue.peekProposable(), nullptr); + EXPECT_EQ(queue.proposePop(), std::nullopt); + queue.doXfer({1, 0}); + EXPECT_TRUE(queue.isEmpty()); + + EXPECT_THROW( + (SimQueue("invalid", 2, nullptr, 1, + std::numeric_limits::max(), nullptr, 1, 0)), + std::invalid_argument); + EXPECT_THROW( + (SimQueue("too_wide", 3, nullptr, 1, + std::numeric_limits::max(), nullptr, 1, 2)), + std::invalid_argument); +} + +TEST(QueueBlocksTest, ComputeConsumesAndProducesItsStaticRate) { + SimQueue input("input", 1, nullptr, 4, + std::numeric_limits::max(), nullptr, 1, 2); + SimQueue output("output", 2, nullptr, 4, + std::numeric_limits::max(), nullptr, 1, 2); + Compute compute("compute", 3, nullptr, input, output); + ASSERT_TRUE(input.proposePush(10)); + ASSERT_TRUE(input.proposePush(20)); + input.doXfer({0, 0}); + + compute.doWork({1, 0}); + input.doXfer({1, 0}); + output.doXfer({1, 0}); + compute.doXfer({1, 0}); + + EXPECT_TRUE(input.isEmpty()); + ASSERT_EQ(output.committedSize(), 2u); + EXPECT_EQ(output.proposePop(), 11); + EXPECT_EQ(output.proposePop(), 21); +} + +TEST(QueueBlocksTest, TransformDoesNotConsumeWhenOutputIsBackpressured) { + SimQueue input("input", 1, nullptr, 1); + SimQueue output("output", 2, nullptr, 1); + QueueTransform transform("transform", 3, nullptr, input, + output); + ASSERT_TRUE(input.proposePush(7)); + ASSERT_TRUE(output.proposePush(99)); + input.doXfer({0, 0}); + output.doXfer({0, 0}); + + transform.doWork({1, 0}); + input.doXfer({1, 0}); + output.doXfer({1, 0}); + ASSERT_NE(input.peek(), nullptr); + EXPECT_EQ(*input.peek(), 7); + ASSERT_NE(output.peek(), nullptr); + EXPECT_EQ(*output.peek(), 99); +} + +TEST(QueueBlocksTest, AtomicTransformCommitsAllQueuesTogether) { + SimQueue left("left", 1, nullptr, 1); + SimQueue right("right", 2, nullptr, 1); + SimQueue leftOutput("left_output", 3, nullptr, 1); + SimQueue rightOutput("right_output", 4, nullptr, 1); + QueueAtomicTransform, + std::tuple> + atomic("atomic", 5, nullptr, {&left, &right}, + {&leftOutput, &rightOutput}); + ASSERT_TRUE(left.proposePush(4)); + ASSERT_TRUE(right.proposePush(7)); + left.doXfer({0, 0}); + right.doXfer({0, 0}); + atomic.doWork({1, 0}); + EXPECT_EQ(left.committedSize(), 1u); + EXPECT_EQ(right.committedSize(), 1u); + EXPECT_TRUE(leftOutput.isEmpty()); + EXPECT_TRUE(rightOutput.isEmpty()); + left.doXfer({1, 0}); + right.doXfer({1, 0}); + leftOutput.doXfer({1, 0}); + rightOutput.doXfer({1, 0}); + atomic.doXfer({1, 0}); + EXPECT_TRUE(left.isEmpty()); + EXPECT_TRUE(right.isEmpty()); + ASSERT_NE(leftOutput.peek(), nullptr); + ASSERT_NE(rightOutput.peek(), nullptr); + EXPECT_EQ(*leftOutput.peek(), 5); + EXPECT_EQ(*rightOutput.peek(), 14); +} + +TEST(QueueBlocksTest, AtomicTransformSupportsIndependentInputOutputArity) { + SimQueue left("left", 1, nullptr, 1); + SimQueue right("right", 2, nullptr, 1); + SimQueue output("output", 3, nullptr, 1); + QueueAtomicTransform, std::tuple> + atomic("atomic", 4, nullptr, {&left, &right}, {&output}); + + ASSERT_TRUE(left.proposePush(4)); + ASSERT_TRUE(right.proposePush(8)); + left.doXfer({0, 0}); + right.doXfer({0, 0}); + atomic.doWork({1, 0}); + + EXPECT_EQ(left.committedSize(), 1u); + EXPECT_EQ(right.committedSize(), 1u); + EXPECT_TRUE(output.isEmpty()); + left.doXfer({1, 0}); + right.doXfer({1, 0}); + output.doXfer({1, 0}); + atomic.doXfer({1, 0}); + EXPECT_TRUE(left.isEmpty()); + EXPECT_TRUE(right.isEmpty()); + ASSERT_NE(output.peek(), nullptr); + EXPECT_EQ(*output.peek(), 12); +} + +TEST(QueueBlocksTest, BarrierTransfersHeterogeneousQueuesAtomically) { + SimQueue left("left", 1, nullptr, 1); + SimQueue right("right", 2, nullptr, 1); + SimQueue leftOutput("left_output", 3, nullptr, 1); + SimQueue rightOutput("right_output", 4, nullptr, 1); + QueueBarrier> barrier( + "barrier", 5, nullptr, {&left, &right}, {&leftOutput, &rightOutput}); + ASSERT_TRUE(left.proposePush(4)); + ASSERT_TRUE(right.proposePush(8)); + ASSERT_TRUE(rightOutput.proposePush(99)); + left.doXfer({0, 0}); + right.doXfer({0, 0}); + rightOutput.doXfer({0, 0}); + + barrier.doWork({1, 0}); + EXPECT_FALSE(barrier.hasPendingCommit()); + EXPECT_EQ(left.committedSize(), 1u); + EXPECT_EQ(right.committedSize(), 1u); + EXPECT_TRUE(leftOutput.isEmpty()); + + rightOutput.proposePop(); + rightOutput.doXfer({1, 0}); + barrier.doWork({2, 0}); + ASSERT_TRUE(barrier.hasPendingCommit()); + left.doXfer({2, 0}); + right.doXfer({2, 0}); + leftOutput.doXfer({2, 0}); + rightOutput.doXfer({2, 0}); + barrier.doXfer({2, 0}); + EXPECT_TRUE(left.isEmpty()); + EXPECT_TRUE(right.isEmpty()); + ASSERT_NE(leftOutput.peek(), nullptr); + ASSERT_NE(rightOutput.peek(), nullptr); + EXPECT_EQ(*leftOutput.peek(), 4); + EXPECT_EQ(*rightOutput.peek(), 8); +} + +TEST(QueueBlocksTest, ReorderRetiresOutOfOrderArrivalsBySequenceKey) { + SimQueue input("input", 1, nullptr, 4); + SimQueue output("output", 2, nullptr, 4); + QueueReorder reorder("reorder", 3, nullptr, + input, output, 4, 0); + QueueSink sink("sink", 4, nullptr, output); + ASSERT_TRUE(input.proposePush({2, 20})); + ASSERT_TRUE(input.proposePush({0, 0})); + ASSERT_TRUE(input.proposePush({1, 10})); + input.doXfer({0, 0}); + + for (uint64_t tick = 1; tick < 12; ++tick) { + const Epoch epoch{tick, 0}; + reorder.doWork(epoch); + sink.doWork(epoch); + input.doXfer(epoch); + output.doXfer(epoch); + reorder.doXfer(epoch); + sink.doXfer(epoch); + } + + ASSERT_EQ(sink.received().size(), 3u); + EXPECT_EQ(sink.received()[0], (SequencedValue{0, 0})); + EXPECT_EQ(sink.received()[1], (SequencedValue{1, 10})); + EXPECT_EQ(sink.received()[2], (SequencedValue{2, 20})); +} + +TEST(QueueBlocksTest, ReorderRejectsDuplicateSequenceKey) { + SimQueue input("input", 1, nullptr, 2); + SimQueue output("output", 2, nullptr, 2); + QueueReorder reorder("reorder", 3, nullptr, + input, output, 2, 0); + ASSERT_TRUE(input.proposePush({0, 1})); + ASSERT_TRUE(input.proposePush({0, 2})); + input.doXfer({0, 0}); + reorder.doWork({1, 0}); + input.doXfer({1, 0}); + output.doXfer({1, 0}); + reorder.doXfer({1, 0}); + reorder.doWork({2, 0}); + EXPECT_EQ(reorder.runtimeFailureCode(), "reorder_duplicate_key"); + EXPECT_FALSE(reorder.hasPendingCommit()); + EXPECT_TRUE(output.isEmpty()); +} + +TEST(QueueBlocksTest, ReorderRejectsNegativeSequenceKey) { + SimQueue input("input", 1, nullptr, 1); + SimQueue output("output", 2, nullptr, 1); + QueueReorder reorder( + "reorder", 3, nullptr, input, output, 1, 0); + ASSERT_TRUE(input.proposePush({-1})); + input.doXfer({0, 0}); + reorder.doWork({1, 0}); + EXPECT_EQ(reorder.runtimeFailureCode(), "reorder_negative_key"); + EXPECT_FALSE(reorder.hasPendingCommit()); + EXPECT_TRUE(output.isEmpty()); +} + +TEST(QueueBlocksTest, DependencyCompletesReadyTokensOutOfOrder) { + SimQueue input("input", 1, nullptr, 4); + SimQueue output("output", 2, nullptr, 4); + QueueDependency + dependency("dependency", 3, nullptr, input, output, 4, 2, 255); + QueueSink sink("sink", 4, nullptr, output); + ASSERT_TRUE(input.proposePush({0, 255, 0, 4})); + ASSERT_TRUE(input.proposePush({1, 255, 0, 1})); + ASSERT_TRUE(input.proposePush({2, 255, 1, 1})); + ASSERT_TRUE(input.proposePush({3, 0, 1, 1})); + input.doXfer({0, 0}); + + for (uint64_t tick = 1; tick < 16; ++tick) { + const Epoch epoch{tick, 0}; + dependency.doWork(epoch); + sink.doWork(epoch); + input.doXfer(epoch); + output.doXfer(epoch); + dependency.doXfer(epoch); + sink.doXfer(epoch); + } + + ASSERT_EQ(sink.received().size(), 4u); + EXPECT_EQ(sink.received()[0].sequence, 2u); + EXPECT_EQ(sink.received()[1].sequence, 0u); + EXPECT_EQ(sink.received()[2].sequence, 1u); + EXPECT_EQ(sink.received()[3].sequence, 3u); +} + +TEST(QueueBlocksTest, DependencyRejectsZeroExecutionCost) { + SimQueue input("input", 1, nullptr, 1); + SimQueue output("output", 2, nullptr, 1); + QueueDependency + dependency("dependency", 3, nullptr, input, output, 1, 1, 255); + ASSERT_TRUE(input.proposePush({0, 255, 0, 0})); + input.doXfer({0, 0}); + dependency.doWork({1, 0}); + EXPECT_EQ(dependency.runtimeFailureCode(), "dependency_nonpositive_cost"); + EXPECT_FALSE(dependency.hasPendingCommit()); + EXPECT_TRUE(output.isEmpty()); +} + +TEST(QueueBlocksTest, DependencyRejectsOutOfRangeResource) { + SimQueue input("input", 1, nullptr, 1); + SimQueue output("output", 2, nullptr, 1); + QueueDependency + dependency("dependency", 3, nullptr, input, output, 1, 1, 255); + ASSERT_TRUE(input.proposePush({0, 255, 1, 1})); + input.doXfer({0, 0}); + dependency.doWork({1, 0}); + EXPECT_EQ(dependency.runtimeFailureCode(), + "dependency_resource_out_of_range"); + EXPECT_FALSE(dependency.hasPendingCommit()); + EXPECT_TRUE(output.isEmpty()); +} + +TEST(QueueBlocksTest, CreditWindowCompletesParallelTokensAndReturnsSlots) { + SimQueue input("input", 1, nullptr, 4); + SimQueue output("output", 2, nullptr, 4); + QueueCredit credit("credit", 3, nullptr, + input, output, 2); + QueueSink sink("sink", 4, nullptr, output); + ASSERT_TRUE(input.proposePush({0, 255, 0, 4})); + ASSERT_TRUE(input.proposePush({1, 255, 0, 1})); + ASSERT_TRUE(input.proposePush({2, 255, 0, 1})); + input.doXfer({0, 0}); + + for (uint64_t tick = 1; tick < 12; ++tick) { + const Epoch epoch{tick, 0}; + credit.doWork(epoch); + sink.doWork(epoch); + input.doXfer(epoch); + output.doXfer(epoch); + credit.doXfer(epoch); + sink.doXfer(epoch); + } + + ASSERT_EQ(sink.received().size(), 3u); + EXPECT_EQ(sink.received()[0].sequence, 1u); + EXPECT_EQ(sink.received()[1].sequence, 0u); + EXPECT_EQ(sink.received()[2].sequence, 2u); + EXPECT_EQ(credit.active(), 0u); +} + +TEST(QueueBlocksTest, CreditRejectsZeroCostWithoutConsumingInput) { + SimQueue input("input", 1, nullptr, 1); + SimQueue output("output", 2, nullptr, 1); + QueueCredit credit("credit", 3, nullptr, + input, output, 1); + ASSERT_TRUE(input.proposePush({0, 255, 0, 0})); + input.doXfer({0, 0}); + credit.doWork({1, 0}); + EXPECT_EQ(credit.runtimeFailureCode(), "credit_nonpositive_cost"); + EXPECT_FALSE(credit.hasPendingCommit()); + EXPECT_EQ(input.committedSize(), 1u); + EXPECT_TRUE(output.isEmpty()); +} + +TEST(QueueBlocksTest, MemoryReturnsOldDataAndCommitsWriteAtXfer) { + SimQueue input("input", 1, nullptr, 2); + SimQueue output("output", 2, nullptr, 2); + QueueMemory + memory("memory", 3, nullptr, input, output, 16); + QueueSink sink("sink", 4, nullptr, output); + ASSERT_TRUE(input.proposePush({3, true, 42})); + ASSERT_TRUE(input.proposePush({3, false, 0})); + input.doXfer({0, 0}); + + for (uint64_t tick = 1; tick < 8; ++tick) { + const Epoch epoch{tick, 0}; + memory.doWork(epoch); + sink.doWork(epoch); + input.doXfer(epoch); + output.doXfer(epoch); + memory.doXfer(epoch); + sink.doXfer(epoch); + } + + ASSERT_EQ(sink.received().size(), 2u); + EXPECT_EQ(sink.received()[0].data, 0u); + EXPECT_EQ(sink.received()[1].data, 42u); + EXPECT_EQ(memory.at(3), 42u); +} + +TEST(QueueBlocksTest, MemoryRejectsOutOfRangeAddress) { + SimQueue input("input", 1, nullptr, 1); + SimQueue output("output", 2, nullptr, 1); + QueueMemory + memory("memory", 3, nullptr, input, output, 4); + ASSERT_TRUE(input.proposePush({4, false, 0})); + input.doXfer({0, 0}); + memory.doWork({1, 0}); + EXPECT_EQ(memory.runtimeFailureCode(), "memory_address_out_of_range"); + EXPECT_FALSE(memory.hasPendingCommit()); + EXPECT_TRUE(output.isEmpty()); +} + +TEST(QueueBlocksTest, SharedMemoryUsesPriorityAndBlocksUntilResponseAccepted) { + SimQueue input0("input0", 1, nullptr, 2); + SimQueue input1("input1", 2, nullptr, 2); + SimQueue output0("output0", 3, nullptr, 1); + SimQueue output1("output1", 4, nullptr, 1); + QueueMemoryArbiter + memory("memory", 5, nullptr, {&input0, &input1}, {&output0, &output1}, + 16); + ASSERT_TRUE(input0.proposePush({3, true, 42})); + ASSERT_TRUE(input1.proposePush({3, false, 0})); + ASSERT_TRUE(output0.proposePush({0, false, 99})); + input0.doXfer({0, 0}); + input1.doXfer({0, 0}); + output0.doXfer({0, 0}); + + memory.doWork({1, 0}); + input0.doXfer({1, 0}); + input1.doXfer({1, 0}); + memory.doXfer({1, 0}); + EXPECT_TRUE(memory.busy()); + EXPECT_EQ(memory.selectedEndpoint(), 0u); + EXPECT_EQ(input0.committedSize(), 0u); + EXPECT_EQ(input1.committedSize(), 1u); + EXPECT_EQ(memory.at(3), 42u); + + memory.doWork({2, 0}); + EXPECT_FALSE(memory.hasPendingCommit()); + ASSERT_TRUE(output0.proposePop()); + output0.doXfer({2, 0}); + memory.doWork({2, 1}); + output0.doXfer({2, 1}); + memory.doXfer({2, 1}); + EXPECT_FALSE(memory.busy()); + memory.doWork({2, 1}); + EXPECT_EQ(input1.committedSize(), 1u); + + memory.doWork({3, 0}); + input1.doXfer({3, 0}); + memory.doXfer({3, 0}); + EXPECT_EQ(input1.committedSize(), 0u); +} + +TEST(QueueBlocksTest, + SharedMemoryLatencyDelaysResponseAndBackpressuresRequests) { + SimQueue input("input", 1, nullptr, 2); + SimQueue output("output", 2, nullptr, 1); + QueueMemoryArbiter + memory("memory", 3, nullptr, {&input}, {&output}, 16, 0, 3); + EXPECT_EQ(memory.latency(), 3u); + ASSERT_TRUE(input.proposePush({3, false, 0})); + input.doXfer({0, 0}); + + memory.doWork({1, 0}); + input.doXfer({1, 0}); + memory.doXfer({1, 0}); + ASSERT_TRUE(memory.busy()); + ASSERT_TRUE(input.proposePush({7, false, 0})); + input.doXfer({2, 0}); + + for (uint64_t tick : {2, 3}) { + memory.doWork({tick, 0}); + EXPECT_TRUE(memory.hasPendingCommit()); + EXPECT_EQ(input.committedSize(), 1u); + EXPECT_TRUE(output.isEmpty()); + memory.doXfer({tick, 0}); + } + + memory.doWork({4, 0}); + EXPECT_TRUE(memory.hasPendingCommit()); + output.doXfer({4, 0}); + memory.doXfer({4, 0}); + EXPECT_FALSE(memory.busy()); + EXPECT_EQ(input.committedSize(), 1u); + EXPECT_EQ(output.committedSize(), 1u); + + memory.doWork({4, 0}); + EXPECT_FALSE(memory.hasPendingCommit()); + memory.doWork({5, 0}); + input.doXfer({5, 0}); + memory.doXfer({5, 0}); + EXPECT_TRUE(memory.busy()); + EXPECT_EQ(input.committedSize(), 0u); +} + +TEST(QueueBlocksTest, SharedMemoryRejectsZeroLatency) { + SimQueue input("input", 1, nullptr, 1); + SimQueue output("output", 2, nullptr, 1); + EXPECT_THROW((QueueMemoryArbiter( + "memory", 3, nullptr, {&input}, {&output}, 16, 0, 0)), + std::invalid_argument); +} + +TEST(QueueBlocksTest, QueueLatencyDelaysVisibilityButReservesCapacity) { + SimQueue queue("queue", 1, nullptr, 1, + std::numeric_limits::max(), nullptr, 3); + EXPECT_EQ(queue.latency(), 3u); + ASSERT_TRUE(queue.proposePush(5)); + queue.doXfer({0, 0}); + EXPECT_TRUE(queue.isEmpty()); + EXPECT_TRUE(queue.isFull()); + queue.doXfer({1, 0}); + EXPECT_TRUE(queue.isEmpty()); + queue.doXfer({2, 0}); + ASSERT_NE(queue.peek(), nullptr); + EXPECT_EQ(*queue.peek(), 5); +} + +TEST(QueueBlocksTest, SinkConsumesAtWorkAndPublishesAtXfer) { + SimQueue input("input", 1, nullptr, 1); + QueueSink sink("sink", 2, nullptr, input); + ASSERT_TRUE(input.proposePush(13)); + input.doXfer({0, 0}); + + sink.doWork({1, 0}); + EXPECT_TRUE(sink.received().empty()); + input.doXfer({1, 0}); + sink.doXfer({1, 0}); + ASSERT_EQ(sink.received().size(), 1u); + EXPECT_EQ(sink.received().front(), 13); +} + +TEST(QueueBlocksTest, ObserveCommitsWithoutConsumingOrBackpressure) { + SimQueue input("input", 1, nullptr, 2); + QueueObserve observe("observe", 2, nullptr, input); + ASSERT_TRUE(input.proposePush(13)); + input.doXfer({0, 0}); + observe.doWork({1, 0}); + EXPECT_EQ(input.committedSize(), 1u); + EXPECT_TRUE(observe.observed().empty()); + observe.doXfer({1, 0}); + ASSERT_EQ(observe.observed().size(), 1u); + EXPECT_EQ(observe.observed().front(), 13); + observe.doWork({2, 0}); + observe.doXfer({2, 0}); + EXPECT_EQ(observe.observed().size(), 1u); + EXPECT_EQ(input.committedSize(), 1u); +} + +TEST(QueueBlocksTest, ExpectChecksHeadWithoutConsumingIt) { + SimQueue input("input", 1, nullptr, 2); + QueueExpect expect("expect", 2, nullptr, input, + "must be positive"); + ASSERT_TRUE(input.proposePush(7)); + input.doXfer({0, 0}); + expect.doWork({1, 0}); + EXPECT_TRUE(expect.hasPendingCommit()); + expect.doXfer({1, 0}); + EXPECT_EQ(input.committedSize(), 1u); + EXPECT_TRUE(expect.runtimeFailureCode().empty()); + EXPECT_EQ(expect.message(), "must be positive"); +} + +TEST(QueueBlocksTest, ExpectReportsPredicateFailure) { + SimQueue input("input", 1, nullptr, 1); + QueueExpect expect("expect", 2, nullptr, input, + "must be positive"); + ASSERT_TRUE(input.proposePush(-1)); + input.doXfer({0, 0}); + expect.doWork({1, 0}); + EXPECT_EQ(expect.runtimeFailureCode(), "expectation_failed"); + EXPECT_FALSE(expect.hasPendingCommit()); + EXPECT_EQ(input.committedSize(), 1u); +} + +TEST(QueueBlocksTest, BroadcastWaitsForEveryOutput) { + SimQueue input("input", 1, nullptr, 1); + SimQueue left("left", 2, nullptr, 1); + SimQueue right("right", 3, nullptr, 1); + QueueBroadcast broadcast("broadcast", 4, nullptr, input, + {&left, &right}); + ASSERT_TRUE(input.proposePush(9)); + ASSERT_TRUE(right.proposePush(4)); + input.doXfer({0, 0}); + right.doXfer({0, 0}); + broadcast.doWork({1, 0}); + input.doXfer({1, 0}); + left.doXfer({1, 0}); + ASSERT_NE(input.peek(), nullptr); + EXPECT_TRUE(left.isEmpty()); +} + +TEST(QueueBlocksTest, ForkDeliversOutputsIndependentlyBeforeInputPop) { + SimQueue input("input", 1, nullptr, 1); + SimQueue left("left", 2, nullptr, 1); + SimQueue right("right", 3, nullptr, 1); + QueueFork fork("fork", 4, nullptr, input, {&left, &right}); + ASSERT_TRUE(input.proposePush(9)); + ASSERT_TRUE(right.proposePush(4)); + input.doXfer({0, 0}); + right.doXfer({0, 0}); + + fork.doWork({1, 0}); + input.doXfer({1, 0}); + left.doXfer({1, 0}); + right.doXfer({1, 0}); + fork.doXfer({1, 0}); + ASSERT_NE(input.peek(), nullptr); + ASSERT_NE(left.peek(), nullptr); + EXPECT_EQ(*left.peek(), 9); + EXPECT_EQ(*right.peek(), 4); + + right.proposePop(); + right.doXfer({2, 0}); + fork.doWork({2, 0}); + input.doXfer({2, 0}); + left.doXfer({2, 0}); + right.doXfer({2, 0}); + fork.doXfer({2, 0}); + EXPECT_TRUE(input.isEmpty()); + EXPECT_EQ(left.committedSize(), 1u); + ASSERT_NE(right.peek(), nullptr); + EXPECT_EQ(*right.peek(), 9); +} + +TEST(QueueBlocksTest, RouteSelectsExactlyOneOutput) { + SimQueue input("input", 1, nullptr, 1); + SimQueue even("even", 2, nullptr, 1); + SimQueue odd("odd", 3, nullptr, 1); + QueueRoute route("route", 4, nullptr, input, + {&even, &odd}); + ASSERT_TRUE(input.proposePush(7)); + input.doXfer({0, 0}); + route.doWork({1, 0}); + input.doXfer({1, 0}); + even.doXfer({1, 0}); + odd.doXfer({1, 0}); + EXPECT_TRUE(even.isEmpty()); + ASSERT_NE(odd.peek(), nullptr); + EXPECT_EQ(*odd.peek(), 7); +} + +TEST(QueueBlocksTest, SelectConsumesControlAndChosenInputOnly) { + SimQueue control("control", 1, nullptr, 1); + SimQueue left("left", 2, nullptr, 1); + SimQueue right("right", 3, nullptr, 1); + SimQueue output("output", 4, nullptr, 1); + QueueSelect select("select", 5, nullptr, control, + {&left, &right}, output); + ASSERT_TRUE(control.proposePush(1)); + ASSERT_TRUE(left.proposePush(10)); + ASSERT_TRUE(right.proposePush(20)); + control.doXfer({0, 0}); + left.doXfer({0, 0}); + right.doXfer({0, 0}); + select.doWork({1, 0}); + control.doXfer({1, 0}); + left.doXfer({1, 0}); + right.doXfer({1, 0}); + output.doXfer({1, 0}); + select.doXfer({1, 0}); + EXPECT_TRUE(control.isEmpty()); + ASSERT_NE(left.peek(), nullptr); + EXPECT_EQ(*left.peek(), 10); + EXPECT_TRUE(right.isEmpty()); + ASSERT_NE(output.peek(), nullptr); + EXPECT_EQ(*output.peek(), 20); +} + +TEST(QueueBlocksTest, SelectRejectsOutOfRangeSelector) { + SimQueue control("control", 1, nullptr, 1); + SimQueue left("left", 2, nullptr, 1); + SimQueue right("right", 3, nullptr, 1); + SimQueue output("output", 4, nullptr, 1); + QueueSelect select("select", 5, nullptr, control, + {&left, &right}, output); + ASSERT_TRUE(control.proposePush(2)); + control.doXfer({0, 0}); + select.doWork({1, 0}); + EXPECT_EQ(select.runtimeFailureCode(), "select_selector_out_of_range"); + EXPECT_FALSE(select.hasPendingCommit()); +} + +TEST(QueueBlocksTest, MergeRoundRobinIgnoresWorkInsertionOrder) { + SimQueue left("left", 1, nullptr, 2); + SimQueue right("right", 2, nullptr, 2); + SimQueue output("output", 3, nullptr, 2); + QueueMerge merge("merge", 4, nullptr, {&left, &right}, output); + ASSERT_TRUE(left.proposePush(10)); + ASSERT_TRUE(right.proposePush(20)); + left.doXfer({0, 0}); + right.doXfer({0, 0}); + merge.doWork({1, 0}); + left.doXfer({1, 0}); + right.doXfer({1, 0}); + output.doXfer({1, 0}); + merge.doXfer({1, 0}); + ASSERT_NE(output.peek(), nullptr); + EXPECT_EQ(*output.peek(), 10); + output.proposePop(); + output.doXfer({2, 0}); + merge.doWork({2, 0}); + right.doXfer({2, 0}); + output.doXfer({2, 0}); + ASSERT_NE(output.peek(), nullptr); + EXPECT_EQ(*output.peek(), 20); +} + +TEST(QueueBlocksTest, FeedbackUsesParentOwnedStateQueue) { + using State = FeedbackToken; + SimQueue input("input", 1, nullptr, 1); + SimQueue feedback("feedback", 2, nullptr, 1); + SimQueue output("output", 3, nullptr, 1); + QueueFeedback loop("feedback_block", 4, nullptr, + input, feedback, output, 8); + ASSERT_TRUE(input.proposePush(3)); + input.doXfer({0, 0}); + for (uint64_t tick = 1; tick <= 4; ++tick) { + loop.doWork({tick, 0}); + input.doXfer({tick, 0}); + feedback.doXfer({tick, 0}); + output.doXfer({tick, 0}); + loop.doXfer({tick, 0}); + } + ASSERT_NE(output.peek(), nullptr); + EXPECT_EQ(*output.peek(), 0); +} + +} // namespace +} // namespace gfsim diff --git a/components/agentic-circuit/unittests/gfsim/ShowcaseTest.cpp b/components/agentic-circuit/unittests/gfsim/ShowcaseTest.cpp new file mode 100644 index 00000000..ac602134 --- /dev/null +++ b/components/agentic-circuit/unittests/gfsim/ShowcaseTest.cpp @@ -0,0 +1,138 @@ +#include "gfsim/showcase.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include + +namespace gfsim { +namespace { + +const CommittedEvent *findEvent(const ShowcaseResult &result, + std::string_view category, + std::string_view name) { + auto event = std::ranges::find_if(result.events, [&](const auto &candidate) { + return candidate.category == category && candidate.name == name; + }); + return event == result.events.end() ? nullptr : &*event; +} + +TEST(ShowcaseTest, ProducerQueueConsumerUsesDenseDispatchAndCommittedFlow) { + ProducerQueueConsumerPolicy policy; + policy.values = {3, 5, 8}; + policy.queueCapacity = 2; + + ShowcaseResult result = runShowcase(policy, ShowcaseWorkOrder::Ascending); + + EXPECT_EQ(result.termination.classification, TerminationClass::Completed); + EXPECT_EQ(result.architecturalValues.at("consumed_count"), 3u); + EXPECT_EQ(result.architecturalValues.at("consumed_sum"), 16u); + ASSERT_EQ(result.hierarchy.size(), 3u); + for (ObjectId id = 0; id != result.hierarchy.size(); ++id) + EXPECT_EQ(result.hierarchy[id].id, id); + EXPECT_EQ(result.hierarchy[0].path, "/showcase/producer"); + EXPECT_EQ(result.hierarchy[1].path, "/showcase/queue"); + EXPECT_EQ(result.hierarchy[2].path, "/showcase/consumer"); + EXPECT_NE(findEvent(result, "queue", "occupancy"), nullptr); + EXPECT_NE(findEvent(result, "transaction", "completed"), nullptr); +} + +TEST(ShowcaseTest, BackpressuredPipelineRetainsOfferUntilExactTransfer) { + BackpressuredPipelinePolicy policy; + policy.values = {21, 34}; + policy.readyTicks = {2, 4}; + + ShowcaseResult result = runShowcase(policy, ShowcaseWorkOrder::Descending); + + EXPECT_EQ(result.termination.classification, TerminationClass::Completed); + EXPECT_EQ(result.architecturalValues.at("consumed_count"), 2u); + EXPECT_EQ(result.architecturalValues.at("consumed_sum"), 55u); + EXPECT_EQ(result.architecturalValues.at("transfer_count"), 2u); + EXPECT_NE(findEvent(result, "stall", "backpressure"), nullptr); +} + +TEST(ShowcaseTest, RequestResponseMemoryPreservesCorrelationAndState) { + RequestResponseMemoryPolicy policy; + policy.requests = {{.correlationId = 10, .address = 1, .value = 17}, + {.correlationId = 11, .address = 3, .value = 29}}; + policy.memoryCapacity = 4; + + ShowcaseResult result = runShowcase(policy, ShowcaseWorkOrder::Seeded, 91); + + EXPECT_EQ(result.termination.classification, TerminationClass::Completed); + EXPECT_EQ(result.architecturalValues.at("completed_responses"), 2u); + EXPECT_EQ(result.architecturalValues.at("memory.1"), 17u); + EXPECT_EQ(result.architecturalValues.at("memory.3"), 29u); + const CommittedEvent *response = findEvent(result, "transaction", "response"); + ASSERT_NE(response, nullptr); + EXPECT_TRUE(response->rootSequenceId == 10 || response->rootSequenceId == 11); +} + +TEST(ShowcaseTest, NestedArraysExposeDenseIndependentLaneHierarchy) { + NestedArraysPolicy policy; + policy.laneValues = {{1, 2}, {10, 20}, {100, 200}}; + + ShowcaseResult result = runShowcase(policy, ShowcaseWorkOrder::Ascending); + + EXPECT_EQ(result.termination.classification, TerminationClass::Completed); + EXPECT_EQ(result.architecturalValues.at("lane.0.sum"), 3u); + EXPECT_EQ(result.architecturalValues.at("lane.1.sum"), 30u); + EXPECT_EQ(result.architecturalValues.at("lane.2.sum"), 300u); + ASSERT_EQ(result.hierarchy.size(), 7u); + for (ObjectId id = 0; id != result.hierarchy.size(); ++id) + EXPECT_EQ(result.hierarchy[id].id, id); + EXPECT_EQ(result.hierarchy.back().path, "/showcase/lanes/2/sink"); +} + +TEST(ShowcaseTest, TimeDomainBridgeUsesExactIntegerDomainAdvancement) { + MultiTimeDomainBridgePolicy policy; + policy.values = {4, 6, 9}; + policy.sourcePeriod = 2; + policy.targetPeriod = 3; + + ShowcaseResult result = runShowcase(policy, ShowcaseWorkOrder::Ascending); + + EXPECT_EQ(result.termination.classification, TerminationClass::Completed); + EXPECT_EQ(result.architecturalValues.at("bridged_sum"), 19u); + EXPECT_EQ(result.architecturalValues.at("bridged_count"), 3u); + EXPECT_GT(result.termination.domainCycles.at("source"), 0u); + EXPECT_GT(result.termination.domainCycles.at("target"), 0u); + EXPECT_EQ(result.architecturalValues.at("last_transfer_tick") % 3, 0u); +} + +TEST(ShowcaseTest, SuspendedProcessWakesResumesAndPreservesLiveState) { + SuspendedProcessPolicy policy; + policy.initialValue = 40; + policy.incrementAfterWake = 2; + policy.wakeTick = 3; + + ShowcaseResult result = runShowcase(policy, ShowcaseWorkOrder::Ascending); + + EXPECT_EQ(result.termination.classification, TerminationClass::Completed); + EXPECT_EQ(result.architecturalValues.at("process_result"), 42u); + EXPECT_EQ(result.architecturalValues.at("resume_count"), 1u); + EXPECT_EQ(result.architecturalValues.at("wake_tick"), 3u); +} + +TEST(ShowcaseTest, LegalWorkOrdersHaveByteIdenticalCommittedResults) { + const std::array policies = { + ProducerQueueConsumerPolicy{}, BackpressuredPipelinePolicy{}, + RequestResponseMemoryPolicy{}, NestedArraysPolicy{}, + MultiTimeDomainBridgePolicy{}, SuspendedProcessPolicy{}}; + + for (const ShowcasePolicy &policy : policies) { + const std::string ascending = canonicalShowcaseResult( + runShowcase(policy, ShowcaseWorkOrder::Ascending, 73)); + const std::string descending = canonicalShowcaseResult( + runShowcase(policy, ShowcaseWorkOrder::Descending, 73)); + const std::string seeded = canonicalShowcaseResult( + runShowcase(policy, ShowcaseWorkOrder::Seeded, 73)); + EXPECT_EQ(descending, ascending); + EXPECT_EQ(seeded, ascending); + } +} + +} // namespace +} // namespace gfsim diff --git a/components/agentic-circuit/unittests/gfsim/core_test.cpp b/components/agentic-circuit/unittests/gfsim/core_test.cpp new file mode 100644 index 00000000..fc7f6df9 --- /dev/null +++ b/components/agentic-circuit/unittests/gfsim/core_test.cpp @@ -0,0 +1,3083 @@ +#include "gfsim/components.h" +#include "gfsim/core.h" +#include "gfsim/dispatch.h" +#include "gfsim/harness.h" +#include "gfsim/object.h" +#include "gfsim/observation.h" +#include "gfsim/packet.h" +#include "gfsim/process.h" +#include "gfsim/queue.h" +#include "gfsim/resource.h" +#include "gfsim/statistics.h" +#include "gfsim/trace.h" + +#include "gtest/gtest.h" + +#include "acir/Bindings/Binding.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include + +namespace gfsim { + +struct TestPacket { + uint16_t opcode = 0; + uint32_t payload = 0; + auto operator<=>(const TestPacket &) const = default; +}; + +struct InvalidPacket {}; + +template <> struct PacketTraits { + static constexpr bool isPacket = true; + static constexpr std::string_view schema = {}; + static constexpr size_t serializedSize = 1; + static constexpr size_t maximumSerializedSize = serializedSize; + static constexpr size_t alignment = 1; + static constexpr PacketEndianness endianness = PacketEndianness::Little; + static constexpr std::array fields{{{"field", 1, 1}}}; + static constexpr std::optional routingField = std::nullopt; + static constexpr std::optional correlationField = + std::nullopt; +}; + +template <> struct PacketTraits { + static constexpr bool isPacket = true; + static constexpr std::string_view schema = "test.Packet@0.1"; + static constexpr size_t serializedSize = 6; + static constexpr size_t maximumSerializedSize = serializedSize; + static constexpr size_t alignment = alignof(TestPacket); + static constexpr PacketEndianness endianness = PacketEndianness::Little; + static constexpr std::array fields{{ + {"opcode", 0, 2}, + {"payload", 2, 4}, + }}; + static constexpr std::optional routingField = std::nullopt; + static constexpr std::optional correlationField = + std::nullopt; + + using Serialized = std::array; + static Serialized serialize(const TestPacket &packet) { + return {std::byte(packet.opcode), std::byte(packet.opcode >> 8), + std::byte(packet.payload), std::byte(packet.payload >> 8), + std::byte(packet.payload >> 16), std::byte(packet.payload >> 24)}; + } + static std::optional + deserialize(std::span bytes) { + if (bytes.size() != serializedSize) + return std::nullopt; + return TestPacket{ + static_cast(std::to_integer(bytes[0]) | + (std::to_integer(bytes[1]) << 8)), + std::to_integer(bytes[2]) | + (std::to_integer(bytes[3]) << 8) | + (std::to_integer(bytes[4]) << 16) | + (std::to_integer(bytes[5]) << 24), + }; + } +}; + +namespace { + +constexpr std::string_view kHarnessFingerprint = + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + +class HarnessTempRoot { +public: + HarnessTempRoot() { + EXPECT_FALSE(llvm::sys::fs::createUniqueDirectory("gfsim-harness", path_)); + } + ~HarnessTempRoot() { llvm::sys::fs::remove_directories(path_); } + + std::string path() const { return path_.str().str(); } + + void write(std::string_view relativePath, std::string_view bytes) const { + llvm::SmallString<256> path(path_); + llvm::sys::path::append(path, relativePath); + llvm::SmallString<256> parent(path); + llvm::sys::path::remove_filename(parent); + ASSERT_FALSE(llvm::sys::fs::create_directories(parent)); + std::error_code error; + llvm::raw_fd_ostream output(path, error); + ASSERT_FALSE(error); + output << bytes; + } + + std::string read(std::string_view relativePath) const { + llvm::SmallString<256> path(path_); + llvm::sys::path::append(path, relativePath); + auto buffer = llvm::MemoryBuffer::getFile(path); + EXPECT_TRUE(static_cast(buffer)); + return buffer ? buffer.get()->getBuffer().str() : std::string(); + } + + bool exists(std::string_view relativePath) const { + llvm::SmallString<256> path(path_); + llvm::sys::path::append(path, relativePath); + return llvm::sys::fs::exists(path); + } + +private: + llvm::SmallString<256> path_; +}; + +std::string harnessBuildManifest() { + return R"json({"schema":"agentic-circuit-build-manifest","version":"0.1","contract_epoch":"0.3","project":{"name":"project","identity":"project:test"},"system":{"name":"system","identity":"system:test"},"source_files":[],"normalized_acir_sha256":"sha256:0000000000000000000000000000000000000000000000000000000000000000","compiler":{"name":"clang","build_id":"clang test","toolchain_target":"test-target"},"pass_pipeline":["compile","link"],"providers":[],"component_specializations":[],"protocol_identities":[],"artifacts":[],"validation_gates":[],"build_profile":"fast","instrumentation_layers":[],"specialization_inputs":[],"build_fingerprint":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})json"; +} + +std::string harnessTrace() { + return R"json({"schema":"pto-trace","version":"0.1","contract_epoch":"0.3","metadata":{"record_count":0},"records":[]})json"; +} + +std::string harnessTraceWithOneRecord() { + return R"json({"schema":"pto-trace","version":"0.1","contract_epoch":"0.3","metadata":{"record_count":1},"records":[{"sequence_id":17,"opcode":"pto.test","operands":[],"dependencies":[],"attributes":{}}]})json"; +} + +std::string harnessRunManifest(std::string_view buildHash, + std::string_view traceHash, + std::string_view eventLog = "disabled") { + return "{\"schema\":\"agentic-circuit-run-manifest\",\"version\":\"0.1\"," + "\"contract_epoch\":\"0.3\",\"build_manifest\":{\"path\":" + "\"build-manifest.json\",\"sha256\":\"" + + std::string(buildHash) + + "\"},\"trace\":{\"path\":\"trace.json\",\"schema\":\"pto-trace\"," + "\"version\":\"0.1\",\"sha256\":\"" + + std::string(traceHash) + + "\"},\"seed\":1,\"output_directory\":\"run\",\"deadlock_window\":null," + "\"max_ticks\":100,\"max_domain_cycles\":{\"core\":25}," + "\"stats_format\":\"json\",\"event_log\":\"" + + std::string(eventLog) + + "\"," + "\"termination_expectation\":{\"kind\":\"incomplete\"," + "\"reason\":\"max_domain_cycles\"}}"; +} + +class HarnessModel { +public: + bool loadTrace(PtoTraceDocument document) { + ++traceLoadCount; + loadedTrace = std::move(document); + return acceptTrace; + } + void configure(const RuntimeLimits &limits) { + configuredAfterTrace = traceLoadCount == 1; + configured = limits; + } + TerminationResult run() { + ++workCount; + TerminationResult result; + result.classification = TerminationClass::Incomplete; + result.finalEpoch = {50, 0}; + result.committedEventCount = 7; + result.tracePosition = 3; + result.traceLastCommittedSequenceId = 11; + result.diagnosticCode = "max_domain_cycles_reached"; + result.domainCycles = {{"core", 25}}; + return result; + } + std::string_view buildFingerprint() const { return kHarnessFingerprint; } + std::span timeDomains() const { return domains; } + std::vector statistics() const { return snapshots; } + std::span observations() const { return events; } + + RuntimeLimits configured; + uint64_t workCount = 0; + uint64_t traceLoadCount = 0; + bool acceptTrace = true; + bool configuredAfterTrace = false; + PtoTraceDocument loadedTrace; + std::vector snapshots = {{.name = "accepted_transactions", + .objectPath = "/generated/root/queue", + .kind = StatisticKind::Counter, + .value = 2, + .lastUpdate = {4, 0}}}; + std::vector events = { + {.epoch = {2, 3}, + .ownerId = 5, + .localCommittedIndex = 0, + .category = "transaction", + .name = "accepted", + .phase = TraceEventPhase::Instant, + .rootSequenceId = 11, + .arguments = {{"accepted", true}, {"bytes", uint64_t{64}}}}, + {.epoch = {4, 0}, + .ownerId = 7, + .localCommittedIndex = 0, + .category = "execution", + .name = "execute", + .phase = TraceEventPhase::Complete, + .rootSequenceId = 12, + .duration = 3, + .arguments = {{"engine", std::string("cube")}}}}; + +private: + static constexpr std::array domains = { + TimeDomainRuntime{"core", 2, 0, 1}}; +}; + +class DomainClockObject : public SimObject { +public: + DomainClockObject(ObjectId id, SimSystem &system) + : SimObject(ObjectKind::Process, "clock", id), system_(system) {} + + void doWork(Epoch epoch) override { + ++workCount; + ASSERT_TRUE(system_.scheduleEvent({{epoch.time + 1, 0}, id(), 0, 0})); + } + + uint64_t workCount = 0; + +private: + SimSystem &system_; +}; + +class NoProgressClockObject : public SimObject { +public: + NoProgressClockObject(ObjectId id, SimSystem &system) + : SimObject(ObjectKind::Process, "no_progress", id), system_(system) {} + + void doWork(Epoch epoch) override { + ++workCount; + ASSERT_TRUE(system_.scheduleWork(id(), {epoch.time + 1, 0})); + } + + uint64_t workCount = 0; + +private: + SimSystem &system_; +}; + +TEST(RuntimeHarnessTest, ExactManifestConfiguresLimitsAndProducesClosedResult) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = harnessTrace(); + root.write("build-manifest.json", build); + root.write("trace.json", trace); + auto manifest = loadRunManifest( + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace)), + root.path()); + ASSERT_TRUE(static_cast(manifest)) + << llvm::toString(manifest.takeError()); + + HarnessModel model; + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + auto result = runGeneratedModel(model, *manifest, resultStage); + ASSERT_TRUE(static_cast(result)) << llvm::toString(result.takeError()); + EXPECT_EQ(result->status, RunStatus::Incomplete); + EXPECT_EQ(result->terminationReason, "max_domain_cycles"); + EXPECT_EQ(result->domainCycles.at("core"), 25u); + EXPECT_EQ(model.traceLoadCount, 1u); + EXPECT_TRUE(model.configuredAfterTrace); + EXPECT_TRUE(model.loadedTrace.records.empty()); + ASSERT_TRUE(model.configured.maxTicks); + EXPECT_EQ(*model.configured.maxTicks, 100u); + auto document = + acir::bindings::parseIJson(root.read("result-stage/run-result.json")); + ASSERT_TRUE(static_cast(document)) + << llvm::toString(document.takeError()); + const llvm::json::Object *object = document->getAsObject(); + ASSERT_NE(object, nullptr); + EXPECT_EQ(object->size(), 12u); + EXPECT_EQ(object->getString("status"), "incomplete"); + EXPECT_EQ(object->getString("termination_reason"), "max_domain_cycles"); + const llvm::json::Object *tracePosition = object->getObject("trace_position"); + ASSERT_NE(tracePosition, nullptr); + EXPECT_EQ(tracePosition->getInteger("next_record_index"), 3); + EXPECT_EQ(tracePosition->getInteger("last_committed_sequence_id"), 11); + auto statistics = + acir::bindings::parseIJson(root.read("result-stage/stats.json")); + ASSERT_TRUE(static_cast(statistics)) + << llvm::toString(statistics.takeError()); + const llvm::json::Array *array = statistics->getAsArray(); + ASSERT_NE(array, nullptr); + ASSERT_EQ(array->size(), 1u); + const llvm::json::Object *snapshot = array->front().getAsObject(); + ASSERT_NE(snapshot, nullptr); + EXPECT_EQ(snapshot->getString("object_path"), "/generated/root/queue"); + EXPECT_EQ(snapshot->getString("name"), "accepted_transactions"); + EXPECT_EQ(snapshot->getString("kind"), "counter"); + EXPECT_EQ(snapshot->getInteger("value"), 2); + EXPECT_FALSE(root.exists("result-stage/events.jsonl")); +} + +TEST(RuntimeHarnessTest, MovesTypedTraceIntoModelExactlyOnceBeforeConfigure) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = harnessTraceWithOneRecord(); + root.write("build-manifest.json", build); + root.write("trace.json", trace); + auto manifest = loadRunManifest( + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace)), + root.path()); + ASSERT_TRUE(static_cast(manifest)); + + HarnessModel model; + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + auto result = runGeneratedModel(model, *manifest, resultStage); + ASSERT_TRUE(static_cast(result)) << llvm::toString(result.takeError()); + ASSERT_EQ(model.traceLoadCount, 1u); + ASSERT_TRUE(model.configuredAfterTrace); + ASSERT_EQ(model.loadedTrace.records.size(), 1u); + EXPECT_EQ(model.loadedTrace.records.front().sequenceId, 17u); + EXPECT_EQ(model.loadedTrace.records.front().opcode, "pto.test"); +} + +TEST(RuntimeHarnessTest, TraceOwnerRejectionStopsBeforeConfigureAndRun) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = harnessTraceWithOneRecord(); + root.write("build-manifest.json", build); + root.write("trace.json", trace); + auto manifest = loadRunManifest( + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace)), + root.path()); + ASSERT_TRUE(static_cast(manifest)); + + HarnessModel model; + model.acceptTrace = false; + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + EXPECT_FALSE(runGeneratedModel(model, *manifest, resultStage)); + EXPECT_EQ(model.traceLoadCount, 1u); + EXPECT_FALSE(model.configuredAfterTrace); + EXPECT_EQ(model.workCount, 0u); + EXPECT_FALSE(root.exists("result-stage")); +} + +TEST(RuntimeHarnessTest, RawDavinciJsonlNeverReachesModelCode) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = + R"json({"block_idx":0,"sequence_id":0,"opcode":"TASSIGN","input_tiles":[],"scalar_inputs":[],"output_tiles":[]})json" + "\n"; + root.write("build-manifest.json", build); + root.write("trace.json", trace); + auto manifest = loadRunManifest( + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace)), + root.path()); + ASSERT_TRUE(static_cast(manifest)); + + HarnessModel model; + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + EXPECT_FALSE(runGeneratedModel(model, *manifest, resultStage)); + EXPECT_EQ(model.traceLoadCount, 0u); + EXPECT_EQ(model.workCount, 0u); +} + +TEST(RuntimeHarnessTest, JsonlEventLogContainsCommittedStableKeys) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = harnessTrace(); + root.write("build-manifest.json", build); + root.write("trace.json", trace); + auto manifest = loadRunManifest( + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace), "jsonl"), + root.path()); + ASSERT_TRUE(static_cast(manifest)); + + HarnessModel model; + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + auto result = runGeneratedModel(model, *manifest, resultStage); + ASSERT_TRUE(static_cast(result)) << llvm::toString(result.takeError()); + + llvm::SmallVector lines; + std::string eventBytes = root.read("result-stage/events.jsonl"); + llvm::StringRef(eventBytes).split(lines, '\n', -1, false); + ASSERT_EQ(lines.size(), 2u); + auto first = acir::bindings::parseIJson(lines.front()); + ASSERT_TRUE(static_cast(first)) << llvm::toString(first.takeError()); + const llvm::json::Object *event = first->getAsObject(); + ASSERT_NE(event, nullptr); + EXPECT_EQ(event->getString("name"), "accepted"); + EXPECT_EQ(event->getString("cat"), "transaction"); + EXPECT_EQ(event->getString("ph"), "i"); + EXPECT_EQ(event->getInteger("ts"), 2 * kMaxDeltasPerTick + 3); + EXPECT_EQ(event->getInteger("pid"), 0); + EXPECT_EQ(event->getInteger("tid"), 5); + const llvm::json::Object *arguments = event->getObject("args"); + ASSERT_NE(arguments, nullptr); + EXPECT_EQ(arguments->getInteger("gfsim_epoch_time"), 2); + EXPECT_EQ(arguments->getInteger("gfsim_epoch_delta"), 3); + EXPECT_EQ(arguments->getInteger("gfsim_object_id"), 5); + EXPECT_EQ(arguments->getInteger("gfsim_local_committed_index"), 0); + EXPECT_EQ(arguments->getInteger("gfsim_root_sequence_id"), 11); + EXPECT_EQ(arguments->getBoolean("accepted"), true); + EXPECT_EQ(arguments->getInteger("bytes"), 64); + + auto second = acir::bindings::parseIJson(lines.back()); + ASSERT_TRUE(static_cast(second)) << llvm::toString(second.takeError()); + const llvm::json::Object *complete = second->getAsObject(); + ASSERT_NE(complete, nullptr); + EXPECT_EQ(complete->getString("ph"), "X"); + EXPECT_EQ(complete->getInteger("dur"), 3); + EXPECT_EQ(complete->getInteger("tid"), 7); + EXPECT_EQ(complete->getObject("args")->getString("engine"), "cube"); + + auto resultDocument = + acir::bindings::parseIJson(root.read("result-stage/run-result.json")); + ASSERT_TRUE(static_cast(resultDocument)); + const llvm::json::Array *outputs = + resultDocument->getAsObject()->getArray("outputs"); + ASSERT_NE(outputs, nullptr); + auto eventOutput = std::find_if( + outputs->begin(), outputs->end(), [](const llvm::json::Value &value) { + const llvm::json::Object *object = value.getAsObject(); + return object && object->getString("path") == "events.jsonl"; + }); + ASSERT_NE(eventOutput, outputs->end()); + EXPECT_EQ(eventOutput->getAsObject()->getString("sha256"), + acir::bindings::sha256Fingerprint(eventBytes)); +} + +TEST(RuntimeHarnessTest, RejectsNoncanonicalObservationsBeforePublication) { + auto runInvalid = [](bool invalidStatistics) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = harnessTrace(); + root.write("build-manifest.json", build); + root.write("trace.json", trace); + auto manifest = loadRunManifest( + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace), "jsonl"), + root.path()); + EXPECT_TRUE(static_cast(manifest)); + HarnessModel model; + if (invalidStatistics) + model.snapshots.push_back({.name = "earlier", + .objectPath = "/a", + .kind = StatisticKind::Counter}); + else + std::swap(model.events[0], model.events[1]); + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + EXPECT_FALSE(runGeneratedModel(model, *manifest, resultStage)); + EXPECT_FALSE(root.exists("result-stage")); + }; + + runInvalid(true); + runInvalid(false); +} + +TEST(RuntimeHarnessTest, BuildFingerprintMismatchFailsBeforeModelWork) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = harnessTrace(); + root.write("build-manifest.json", build); + root.write("trace.json", trace); + auto manifest = loadRunManifest( + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace)), + root.path()); + ASSERT_TRUE(static_cast(manifest)); + manifest->buildManifest.sha256 = + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + + HarnessModel model; + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + EXPECT_FALSE(runGeneratedModel(model, *manifest, resultStage)); + EXPECT_EQ(model.workCount, 0u); +} + +TEST(RuntimeHarnessTest, UnknownDomainLimitFailsBeforeModelWork) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = harnessTrace(); + root.write("build-manifest.json", build); + root.write("trace.json", trace); + std::string bytes = + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace)); + const size_t domain = bytes.find("\"core\":25"); + ASSERT_NE(domain, std::string::npos); + bytes.replace(domain, std::string("\"core\":25").size(), "\"unknown\":25"); + auto manifest = loadRunManifest(bytes, root.path()); + ASSERT_TRUE(static_cast(manifest)); + + HarnessModel model; + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + EXPECT_FALSE(runGeneratedModel(model, *manifest, resultStage)); + EXPECT_EQ(model.workCount, 0u); +} + +TEST(RuntimeHarnessTest, UnmetExpectationPublishesFailedValidation) { + HarnessTempRoot root; + const std::string build = harnessBuildManifest(); + const std::string trace = harnessTrace(); + root.write("build-manifest.json", build); + root.write("trace.json", trace); + std::string bytes = + harnessRunManifest(acir::bindings::sha256Fingerprint(build), + acir::bindings::sha256Fingerprint(trace)); + const size_t reason = bytes.find("\"reason\":\"max_domain_cycles\""); + ASSERT_NE(reason, std::string::npos); + bytes.replace(reason, std::string("\"reason\":\"max_domain_cycles\"").size(), + "\"reason\":\"max_ticks\""); + auto manifest = loadRunManifest(bytes, root.path()); + ASSERT_TRUE(static_cast(manifest)); + + HarnessModel model; + llvm::SmallString<256> resultStage(root.path()); + llvm::sys::path::append(resultStage, "result-stage"); + auto result = runGeneratedModel(model, *manifest, resultStage); + ASSERT_TRUE(static_cast(result)) << llvm::toString(result.takeError()); + EXPECT_EQ(result->status, RunStatus::Failed); + EXPECT_EQ(result->terminationReason, "invariant_violation"); + EXPECT_EQ(result->validation.status, "failed"); + EXPECT_NE(root.read("result-stage/validation-report.json") + .find("ACRUN-EXPECTATION-001"), + std::string::npos); +} + +TEST(RuntimeHarnessTest, DomainCycleCapStopsBeforeWorkBeyondTheBound) { + SimSystem system("test"); + DomainClockObject object(0, system); + std::array rows = {makeDispatchRow(&object)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE( + system.setTimeDomains(std::array{TimeDomainRuntime{"core", 2, 1, 1}})); + RuntimeLimits limits; + limits.maxDomainCycles = {{"core", 2}}; + ASSERT_TRUE(system.setRuntimeLimits(limits)); + + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Incomplete); + EXPECT_EQ(result.diagnosticCode, "max_domain_cycles_reached"); + EXPECT_EQ(result.domainCycles.at("core"), 2u); + EXPECT_EQ(result.finalEpoch, (Epoch{5, 0})); + EXPECT_EQ(object.workCount, 5u); +} + +TEST(RuntimeHarnessTest, DeadlockWindowProducesExactFailedEpoch) { + SimSystem system("test"); + Queue queue("queue", 1, nullptr, 2); + queue.setPath("/test/queue"); + ASSERT_TRUE(queue.proposePush(7)); + queue.doXfer({0, 0}); + system.registerObject(&queue); + ASSERT_TRUE(system.setDeadlockWindow(3)); + + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Failed); + EXPECT_EQ(result.diagnosticCode, "deadlock_window_reached"); + EXPECT_EQ(result.finalEpoch, (Epoch{3, 0})); + EXPECT_EQ(result.terminationCap, std::nullopt); +} + +TEST(RuntimeHarnessTest, DeadlockWindowStopsPollingWithoutDeclaredProgress) { + SimSystem system("test"); + NoProgressClockObject object(0, system); + std::array rows = {makeDispatchRow(&object)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.setDeadlockWindow(3)); + + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Failed); + EXPECT_EQ(result.diagnosticCode, "deadlock_window_reached"); + EXPECT_EQ(result.finalEpoch, (Epoch{3, 0})); + EXPECT_EQ(object.workCount, 3u); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Core types +// ═══════════════════════════════════════════════════════════════════════ + +TEST(GfsimCoreTest, EpochComparison) { + Epoch a{0, 0}, b{0, 1}, c{1, 0}; + EXPECT_LT(a, b); + EXPECT_LT(a, c); + EXPECT_LT(b, c); + EXPECT_EQ(a.nextDelta(), b); +} + +TEST(GfsimCoreTest, EpochSameTime) { + Epoch a{5, 0}, b{5, 3}; + EXPECT_TRUE(a.sameTime(b)); + EXPECT_FALSE(a.sameTime({6, 0})); +} + +TEST(GfsimCoreTest, EpochNextDeltaCorrect) { + Epoch e{10, 5}; + EXPECT_EQ(e.nextDelta(), (Epoch{10, 6})); +} + +TEST(GfsimCoreTest, EventOrdering) { + Event e1{{0, 0}, 1, 0, 0}; + Event e2{{0, 1}, 1, 0, 0}; + Event e3{{1, 0}, 1, 0, 0}; + EXPECT_LT(e1, e2); + EXPECT_LT(e2, e3); +} + +TEST(GfsimCoreTest, EventSameTimeOrderedByDelta) { + Event e1{{5, 0}, 1, 0, 0}; + Event e2{{5, 1}, 1, 0, 0}; + Event e3{{5, 2}, 1, 0, 0}; + EXPECT_LT(e1, e2); + EXPECT_LT(e2, e3); +} + +TEST(GfsimCoreTest, EventSameTimeOrderedByKindThenPayload) { + Event e1{{0, 0}, 1, 0, 0}; + Event e2{{0, 0}, 1, 1, 0}; + Event e3{{0, 0}, 1, 1, 1}; + EXPECT_LT(e1, e2); + EXPECT_LT(e2, e3); + EXPECT_LT(e1, e3); +} + +TEST(GfsimCoreTest, EventTieBreaksByStableTargetId) { + Event first{{0, 0}, 3, 1, 9}; + Event second{{0, 0}, 7, 1, 9}; + EXPECT_LT(first, second); +} + +TEST(GfsimCoreTest, TerminationResultDefaults) { + TerminationResult r; + EXPECT_EQ(r.classification, TerminationClass::Incomplete); + EXPECT_EQ(r.committedEventCount, 0u); + EXPECT_EQ(r.tracePosition, 0u); +} + +TEST(GfsimCoreTest, MaxDeltasPerTickIsFinite) { + EXPECT_GT(kMaxDeltasPerTick, 0u); + EXPECT_LT(kMaxDeltasPerTick, 1u << 20); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Object hierarchy +// ═══════════════════════════════════════════════════════════════════════ + +class ResetTrackingObject final : public SimObject { +public: + ResetTrackingObject(std::string name, ObjectId id) + : SimObject(ObjectKind::Compute, std::move(name), id) {} + + void reset() override { ++resetCount; } + + unsigned resetCount = 0; +}; + +TEST(GfsimObjectTest, AttachedByValueChildSharesWalkPathAndResetBehavior) { + Module root("top", 1); + root.setPath("/top"); + ResetTrackingObject child("worker", 2); + + ASSERT_TRUE(root.attachChild(child)); + EXPECT_EQ(child.parent(), &root); + EXPECT_EQ(child.path(), "/top/worker"); + + std::vector visited; + root.walk([&](SimObject &object) { visited.push_back(object.id()); }); + EXPECT_EQ(visited, (std::vector{1, 2})); + + root.reset(); + EXPECT_EQ(child.resetCount, 1u); +} + +TEST(GfsimObjectTest, DuplicateAndCrossParentAttachmentAreRejected) { + Module first("first", 1); + Module second("second", 2); + ResetTrackingObject child("worker", 3); + + ASSERT_TRUE(first.attachChild(child)); + EXPECT_FALSE(first.attachChild(child)); + EXPECT_FALSE(second.attachChild(child)); + EXPECT_EQ(child.parent(), &first); + EXPECT_EQ(first.children(), (std::vector{&child})); + EXPECT_TRUE(second.children().empty()); +} + +TEST(GfsimObjectTest, ReparentingAttachedModuleRefreshesNestedPaths) { + Module root("top", 1); + root.setPath("/top"); + Module nested("nested", 2); + ResetTrackingObject leaf("leaf", 3); + + ASSERT_TRUE(nested.attachChild(leaf)); + ASSERT_TRUE(root.attachChild(nested)); + EXPECT_EQ(nested.path(), "/top/nested"); + EXPECT_EQ(leaf.path(), "/top/nested/leaf"); +} + +TEST(GfsimObjectTest, OwnedAndAttachedChildrenShareInsertionOrder) { + Module root("top", 1); + root.addChild( + std::make_unique(ObjectKind::Compute, "owned_first", 2)); + ResetTrackingObject attached("attached", 3); + ASSERT_TRUE(root.attachChild(attached)); + root.addChild(std::make_unique(ObjectKind::Sink, "owned_last", 4)); + + std::vector visited; + root.walk([&](SimObject &object) { visited.push_back(object.id()); }); + EXPECT_EQ(visited, (std::vector{1, 2, 3, 4})); +} + +TEST(GfsimObjectTest, ModuleAddsChildWithPath) { + Module root("root", 1); + auto child = std::make_unique(ObjectKind::Compute, "comp", 2); + root.addChild(std::move(child)); + ASSERT_EQ(root.children().size(), 1u); + EXPECT_EQ(root.children()[0]->name(), "comp"); + EXPECT_EQ(root.children()[0]->parent(), &root); +} + +TEST(GfsimObjectTest, ModuleWalkVisitsAll) { + Module root("root", 1); + root.addChild(std::make_unique(ObjectKind::Compute, "a", 2)); + root.addChild(std::make_unique(ObjectKind::Sink, "b", 3)); + int count = 0; + root.walk([&](SimObject &) { ++count; }); + EXPECT_EQ(count, 3); +} + +TEST(GfsimObjectTest, NestedModuleWalkVisitsAllLevels) { + Module root("root", 1); + auto mid = std::make_unique("mid", 2); + auto leaf = std::make_unique(ObjectKind::Compute, "leaf", 3); + mid->addChild(std::move(leaf)); + root.addChild(std::move(mid)); + int count = 0; + root.walk([&](SimObject &) { ++count; }); + EXPECT_EQ(count, 3); +} + +TEST(GfsimObjectTest, WalkUsesModuleCapabilityRatherThanKindTag) { + Module root("root", 1); + root.addChild(std::make_unique(ObjectKind::Module, "tagged", 2)); + + std::vector visited; + root.walk([&](SimObject &object) { visited.push_back(object.id()); }); + EXPECT_EQ(visited, (std::vector{1, 2})); +} + +TEST(GfsimObjectTest, GeneratedModuleWrappersNeedPathsButNoRuntimeObjectIds) { + SimSystem system("system"); + Module generated("generated", kInvalidObjectId); + ASSERT_TRUE(system.root().attachChild(generated)); + + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed); + EXPECT_FALSE(generated.path().empty()); +} + +TEST(GfsimObjectTest, FindChildByName) { + Module root("root", 1); + root.addChild(std::make_unique(ObjectKind::Compute, "alu", 2)); + root.addChild(std::make_unique(ObjectKind::Sink, "mem", 3)); + EXPECT_NE(root.findChild("alu"), nullptr); + EXPECT_NE(root.findChild("mem"), nullptr); + EXPECT_EQ(root.findChild("nonexistent"), nullptr); +} + +TEST(GfsimObjectTest, ResetPropagatesToChildren) { + Module root("root", 1); + auto q = std::make_unique>("q", 2, nullptr, 10); + auto *qptr = q.get(); + root.addChild(std::move(q)); + qptr->proposePush(42); + qptr->doArbitrate({0, 0}); + qptr->doXfer({0, 0}); + EXPECT_EQ(qptr->committedSize(), 1u); + root.reset(); + EXPECT_EQ(qptr->committedSize(), 0u); +} + +TEST(GfsimObjectTest, EmptyModuleWalkOnlySelf) { + Module root("root", 1); + int count = 0; + root.walk([&](SimObject &) { ++count; }); + EXPECT_EQ(count, 1); +} + +TEST(GfsimObjectTest, ChildPathReflectsHierarchy) { + Module root("top", 1); + auto mid = std::make_unique("mid", 2); + auto leaf = std::make_unique(ObjectKind::Sink, "leaf", 3); + mid->addChild(std::move(leaf)); + root.addChild(std::move(mid)); + // Paths are hierarchical; verify they are non-empty and distinct + EXPECT_FALSE(root.children()[0]->path().empty()); + EXPECT_NE(root.path(), root.children()[0]->path()); +} + +TEST(GfsimObjectTest, ReparentingRefreshesAllNestedCanonicalPaths) { + Module root("top", 1); + root.setPath("/top"); + auto mid = std::make_unique("mid", 2); + mid->addChild( + std::make_unique(ObjectKind::Sink, "leaf", 3, nullptr)); + root.addChild(std::move(mid)); + + ASSERT_EQ(root.children()[0]->children().size(), 1u); + EXPECT_EQ(root.children()[0]->path(), "/top/mid"); + EXPECT_EQ(root.children()[0]->children()[0]->path(), "/top/mid/leaf"); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Queue +// ═══════════════════════════════════════════════════════════════════════ + +TEST(GfsimQueueTest, PushPopRoundTrip) { + SimQueue q("q", 1, nullptr, 10); + EXPECT_TRUE(q.isEmpty()); + EXPECT_TRUE(q.proposePush(42)); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + EXPECT_EQ(q.committedSize(), 1u); + EXPECT_EQ(*q.peek(), 42); +} + +TEST(GfsimQueueTest, CapacityEnforcement) { + SimQueue q("q", 1, nullptr, 2); + EXPECT_TRUE(q.proposePush(1)); + EXPECT_TRUE(q.proposePush(2)); + EXPECT_FALSE(q.proposePush(3)); +} + +TEST(GfsimQueueTest, PopAfterPushRoundTrip) { + SimQueue q("q", 1, nullptr, 10); + q.proposePush(42); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + auto popped = q.proposePop(); + ASSERT_TRUE(popped.has_value()); + EXPECT_EQ(*popped, 42); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + EXPECT_TRUE(q.isEmpty()); +} + +TEST(GfsimQueueTest, WatermarkTracksPeak) { + SimQueue q("q", 1, nullptr, 10); + q.proposePush(1); + q.proposePush(2); + q.proposePush(3); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + EXPECT_EQ(q.highWatermark(), 3u); + q.proposePop(); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + EXPECT_EQ(q.highWatermark(), 3u); // persists after pop + EXPECT_EQ(q.committedSize(), 2u); +} + +TEST(GfsimQueueTest, MultiplePopsDrainQueue) { + SimQueue q("q", 1, nullptr, 3); + q.proposePush(10); + q.proposePush(20); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + q.proposePop(); + q.proposePop(); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + EXPECT_TRUE(q.isEmpty()); +} + +TEST(GfsimQueueTest, PeekReturnsFrontWithoutConsuming) { + SimQueue q("q", 1, nullptr, 10); + q.proposePush(7); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + EXPECT_EQ(*q.peek(), 7); + EXPECT_EQ(q.committedSize(), 1u); +} + +TEST(GfsimQueueTest, PopFromEmptyReturnsNullopt) { + SimQueue q("q", 1, nullptr, 10); + auto popped = q.proposePop(); + EXPECT_FALSE(popped.has_value()); +} + +TEST(GfsimQueueTest, PeekFromEmptyReturnsNull) { + SimQueue q("q", 1, nullptr, 10); + EXPECT_EQ(q.peek(), nullptr); +} + +TEST(GfsimQueueTest, PendingCommitTracksAcceptedProposals) { + SimQueue queue("queue", 1, nullptr, 2); + EXPECT_FALSE(queue.hasPendingCommit()); + ASSERT_TRUE(queue.proposePush(7)); + EXPECT_TRUE(queue.hasPendingCommit()); + queue.doXfer({0, 0}); + EXPECT_FALSE(queue.hasPendingCommit()); +} + +// ═══════════════════════════════════════════════════════════════════════ +// EventQueue +// ═══════════════════════════════════════════════════════════════════════ + +TEST(GfsimEventQueueTest, EventsOrderedByEpoch) { + EventQueue eq("events", 1, nullptr); + EXPECT_TRUE(eq.proposeSchedule({{2, 0}, 1, 0, 0})); + EXPECT_TRUE(eq.proposeSchedule({{1, 0}, 1, 0, 0})); + eq.doXfer({0, 0}); + auto e = eq.popNext(); + ASSERT_TRUE(e.has_value()); + EXPECT_EQ(e->readyTime.time, 1u); +} + +TEST(GfsimEventQueueTest, SameTickOrderedByDelta) { + EventQueue eq("events", 1, nullptr); + eq.proposeSchedule({{5, 2}, 1, 0, 0}); + eq.proposeSchedule({{5, 0}, 1, 0, 0}); + eq.proposeSchedule({{5, 1}, 1, 0, 0}); + eq.doXfer({0, 0}); + EXPECT_EQ(eq.popNext()->readyTime.delta, 0u); + EXPECT_EQ(eq.popNext()->readyTime.delta, 1u); + EXPECT_EQ(eq.popNext()->readyTime.delta, 2u); +} + +TEST(GfsimEventQueueTest, HasEventAtChecksExactEpoch) { + EventQueue eq("events", 1, nullptr); + eq.proposeSchedule({{7, 0}, 1, 0, 0}); + eq.doXfer({0, 0}); + EXPECT_TRUE(eq.hasEventAt({7, 0})); + EXPECT_FALSE(eq.hasEventAt({7, 1})); + EXPECT_FALSE(eq.hasEventAt({8, 0})); +} + +TEST(GfsimEventQueueTest, CapacityEnforcement) { + EventQueue eq("events", 1, nullptr, 3); + EXPECT_TRUE(eq.proposeSchedule({{1, 0}, 1, 0, 0})); + EXPECT_TRUE(eq.proposeSchedule({{2, 0}, 1, 0, 0})); + EXPECT_TRUE(eq.proposeSchedule({{3, 0}, 1, 0, 0})); + EXPECT_FALSE(eq.proposeSchedule({{4, 0}, 1, 0, 0})); +} + +TEST(GfsimEventQueueTest, PopNextFromEmptyReturnsNullopt) { + EventQueue eq("events", 1, nullptr); + EXPECT_FALSE(eq.popNext().has_value()); +} + +TEST(GfsimEventQueueTest, NextEventDoesNotConsume) { + EventQueue eq("events", 1, nullptr); + eq.proposeSchedule({{1, 0}, 1, 0, 0}); + eq.doXfer({0, 0}); + auto next = eq.nextEvent(); + ASSERT_TRUE(next.has_value()); + EXPECT_EQ(next->readyTime.time, 1u); + auto popped = eq.popNext(); + ASSERT_TRUE(popped.has_value()); + EXPECT_EQ(popped->readyTime.time, 1u); +} + +TEST(GfsimEventQueueTest, ResetClearsAllEvents) { + EventQueue eq("events", 1, nullptr); + eq.proposeSchedule({{1, 0}, 1, 0, 0}); + eq.proposeSchedule({{2, 0}, 1, 0, 0}); + eq.doXfer({0, 0}); + EXPECT_EQ(eq.size(), 2u); + eq.reset(); + EXPECT_EQ(eq.size(), 0u); + EXPECT_FALSE(eq.nextEvent().has_value()); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Resource +// ═══════════════════════════════════════════════════════════════════════ + +TEST(GfsimResourceTest, ReserveWithinCapacity) { + Resource r("r", 1, nullptr, 10); + EXPECT_TRUE(r.canReserve(5)); + EXPECT_TRUE(r.proposeReserve(1, 5, {0, 0}, 100)); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + EXPECT_EQ(r.activeReservations(), 5u); + EXPECT_EQ(r.availableCapacity(), 5u); +} + +TEST(GfsimResourceTest, ReserveExceedsCapacityFails) { + Resource r("r", 1, nullptr, 3); + EXPECT_FALSE(r.proposeReserve(1, 5, {0, 0}, 100)); +} + +TEST(GfsimResourceTest, ReleaseAfterReserve) { + Resource r("r", 1, nullptr, 10); + r.proposeReserve(1, 5, {0, 0}, 100); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + EXPECT_EQ(r.activeReservations(), 5u); + r.proposeRelease(1, 3); + r.doXfer({0, 0}); + EXPECT_EQ(r.activeReservations(), 2u); + EXPECT_EQ(r.availableCapacity(), 8u); +} + +TEST(GfsimResourceTest, HighWatermarkPersistsAfterRelease) { + Resource r("r", 1, nullptr, 10); + r.proposeReserve(1, 7, {0, 0}, 100); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + EXPECT_EQ(r.highWatermark(), 7u); + r.proposeRelease(1, 5); + r.doXfer({0, 0}); + EXPECT_EQ(r.highWatermark(), 7u); + EXPECT_EQ(r.activeReservations(), 2u); +} + +TEST(GfsimResourceTest, MultipleReservationsUseStableOwnerOrder) { + Resource r("r", 1, nullptr, 5); + // Two reservers each want 3, total capacity is 5 + r.proposeReserve(10, 3, {0, 0}, 100); + r.proposeReserve(20, 3, {0, 0}, 200); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + // First gets granted (3), second rejected (only 2 remaining) + EXPECT_EQ(r.activeReservations(), 3u); + EXPECT_EQ(r.availableCapacity(), 2u); + EXPECT_TRUE(r.canReserve(2)); + EXPECT_FALSE(r.canReserve(3)); +} + +TEST(GfsimResourceTest, TotalStatisticsAccumulate) { + Resource r("r", 1, nullptr, 10); + r.proposeReserve(1, 3, {0, 0}, 100); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + r.proposeRelease(1, 1); + r.doXfer({0, 0}); + r.proposeReserve(1, 4, {0, 0}, 200); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + EXPECT_EQ(r.totalReservations(), 7u); + EXPECT_EQ(r.totalReleases(), 1u); + EXPECT_EQ(r.activeReservations(), 6u); +} + +TEST(GfsimResourceTest, ResetClearsAllState) { + Resource r("r", 1, nullptr, 10); + r.proposeReserve(1, 5, {0, 0}, 100); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + EXPECT_EQ(r.activeReservations(), 5u); + r.reset(); + EXPECT_EQ(r.activeReservations(), 0u); + EXPECT_EQ(r.availableCapacity(), 10u); + EXPECT_EQ(r.highWatermark(), 0u); + EXPECT_EQ(r.totalReservations(), 0u); +} + +TEST(GfsimResourceTest, PendingCommitBeginsAfterArbitration) { + Resource resource("resource", 1, nullptr, 1); + ASSERT_TRUE(resource.proposeReserve(2, 1, {0, 0}, 3)); + EXPECT_FALSE(resource.hasPendingCommit()); + resource.doArbitrate({0, 0}); + EXPECT_TRUE(resource.hasPendingCommit()); + resource.doXfer({0, 0}); + EXPECT_FALSE(resource.hasPendingCommit()); +} + +TEST(GfsimResourceTest, ContentionUsesStableKeysNotProposalOrder) { + for (unsigned seed = 0; seed < 32; ++seed) { + Resource resource("resource", 1, nullptr, 5); + struct Request { + ObjectId owner; + uint32_t amount; + uint64_t transaction; + }; + std::array requests = {Request{2, 3, 20}, Request{1, 3, 30}, + Request{1, 2, 10}}; + std::mt19937 random(seed); + std::shuffle(requests.begin(), requests.end(), random); + for (const Request &request : requests) + ASSERT_TRUE(resource.proposeReserve(request.owner, request.amount, {0, 0}, + request.transaction)); + + resource.doArbitrate({0, 0}); + resource.doXfer({0, 0}); + EXPECT_TRUE(resource.hasReservation(10)); + EXPECT_TRUE(resource.hasReservation(30)); + EXPECT_FALSE(resource.hasReservation(20)); + EXPECT_EQ(resource.rejectedTransactions(), (std::vector{20})); + EXPECT_TRUE(resource.validate()); + } +} + +TEST(GfsimResourceTest, ArbitrationKeepsRejectedStatePrivateUntilXfer) { + Resource resource("resource", 1, nullptr, 1); + ASSERT_TRUE(resource.proposeReserve(1, 1, {0, 0}, 10)); + ASSERT_TRUE(resource.proposeReserve(2, 1, {0, 0}, 20)); + + resource.doArbitrate({0, 0}); + EXPECT_TRUE(resource.rejectedTransactions().empty()); + EXPECT_TRUE(resource.hasPendingCommit()); + resource.doXfer({0, 0}); + EXPECT_EQ(resource.rejectedTransactions(), (std::vector{20})); +} + +TEST(GfsimResourceTest, ExplicitPriorityPrecedesStableTieBreakKeys) { + Resource resource("resource", 1, nullptr, 1); + ASSERT_TRUE(resource.proposeReserve({.ownerId = 1, + .amount = 1, + .issueTime = {0, 0}, + .readyTime = {1, 0}, + .transactionId = 10, + .rootTransactionId = 10, + .priority = 5})); + ASSERT_TRUE(resource.proposeReserve({.ownerId = 2, + .amount = 1, + .issueTime = {0, 0}, + .readyTime = {1, 0}, + .transactionId = 20, + .rootTransactionId = 20, + .priority = 1})); + resource.doArbitrate({0, 0}); + resource.doXfer({0, 0}); + EXPECT_TRUE(resource.hasReservation(20)); + EXPECT_FALSE(resource.hasReservation(10)); +} + +TEST(GfsimResourceTest, ReservationIdentityAndOwnerAreUnique) { + Resource resource("resource", 1, nullptr, 4); + ASSERT_TRUE(resource.proposeReserve(7, 2, {0, 0}, 100, {4, 0}, 55)); + EXPECT_FALSE(resource.proposeReserve(8, 1, {0, 0}, 100, {5, 0}, 66)); + resource.doArbitrate({0, 0}); + resource.doXfer({0, 0}); + + const auto *reservation = resource.findReservation(100); + ASSERT_NE(reservation, nullptr); + EXPECT_EQ(reservation->ownerId, 7u); + EXPECT_EQ(reservation->rootTransactionId, 55u); + EXPECT_EQ(reservation->readyTime, (Epoch{4, 0})); + EXPECT_TRUE(resource.validate()); +} + +TEST(GfsimResourceTest, ReleaseAndCancellationAreOwnerScoped) { + Resource resource("resource", 1, nullptr, 4); + ASSERT_TRUE(resource.proposeReserve(7, 2, {0, 0}, 100)); + ASSERT_TRUE(resource.proposeReserve(8, 2, {0, 0}, 200)); + resource.doArbitrate({0, 0}); + resource.doXfer({0, 0}); + + EXPECT_FALSE(resource.proposeCancel(8, 100)); + EXPECT_TRUE(resource.proposeCancel(7, 100)); + EXPECT_FALSE(resource.proposeRelease(7, 1)); + EXPECT_TRUE(resource.proposeRelease(8, 1)); + resource.doXfer({1, 0}); + + EXPECT_FALSE(resource.hasReservation(100)); + const auto *remaining = resource.findReservation(200); + ASSERT_NE(remaining, nullptr); + EXPECT_EQ(remaining->amount, 1u); + EXPECT_EQ(resource.activeReservations(), 1u); + EXPECT_TRUE(resource.validate()); +} + +TEST(GfsimResourceTest, ReadyReservationsUseExactEpochAndStableOrder) { + Resource resource("resource", 1, nullptr, 3); + ASSERT_TRUE(resource.proposeReserve(2, 1, {0, 0}, 20, {3, 1}, 20)); + ASSERT_TRUE(resource.proposeReserve(1, 1, {0, 0}, 10, {3, 1}, 10)); + resource.doArbitrate({0, 0}); + resource.doXfer({0, 0}); + + EXPECT_TRUE(resource.readyReservations({3, 0}).empty()); + auto ready = resource.readyReservations({3, 1}); + ASSERT_EQ(ready.size(), 2u); + EXPECT_EQ(ready[0]->transactionId, 10u); + EXPECT_EQ(ready[1]->transactionId, 20u); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Components +// ═══════════════════════════════════════════════════════════════════════ + +struct DoublePolicy { + uint64_t operator()(uint64_t value) const { return value * 2; } +}; + +struct IncrementPolicy { + uint64_t operator()(uint64_t value) const { return value + 1; } +}; + +struct AddThreePolicy { + uint64_t operator()(uint64_t value) const { return value + 3; } +}; + +struct StringSizePolicy { + size_t operator()(const std::string &value) const { return value.size(); } +}; + +TEST(GfsimComponentsTest, ComputeTransformsInput) { + Compute c("c", 1, nullptr); + c.setInput(21); + c.doWork({0, 0}); + c.doXfer({0, 0}); + EXPECT_EQ(c.output(), 42u); +} + +TEST(GfsimComponentsTest, SinkAccumulatesValues) { + Sink<> s("s", 1, nullptr); + s.receive(10); + s.receive(20); + s.doXfer({0, 0}); + EXPECT_EQ(s.totalReceived(), 2u); + ASSERT_EQ(s.received().size(), 2u); + EXPECT_EQ(s.received()[0], 10u); + EXPECT_EQ(s.received()[1], 20u); +} + +TEST(GfsimComponentsTest, MemoryReadWrite) { + Memory<> m("m", 1, nullptr, 4); + EXPECT_TRUE(m.proposeWrite(0, 42)); + EXPECT_TRUE(m.proposeWrite(2, 99)); + m.doXfer({0, 0}); + EXPECT_EQ(m.read(0), 42u); + EXPECT_EQ(m.read(1), 0u); + EXPECT_EQ(m.read(2), 99u); +} + +TEST(GfsimComponentsTest, MemoryRejectsOutOfBoundsWrite) { + Memory<> m("m", 1, nullptr, 2); + EXPECT_TRUE(m.proposeWrite(0, 42)); + EXPECT_TRUE(m.proposeWrite(1, 43)); + EXPECT_FALSE(m.proposeWrite(2, 44)); +} + +TEST(GfsimComponentsTest, MemoryReadOutOfBoundsReturnsZero) { + Memory<> m("m", 1, nullptr, 4); + EXPECT_EQ(m.read(100), 0u); +} + +TEST(GfsimComponentsTest, LinkForwardsValue) { + Link<> l("l", 1, nullptr); + l.forward(123); + l.doXfer({0, 0}); + EXPECT_EQ(l.value(), 123u); + EXPECT_TRUE(l.hasValue()); +} + +TEST(GfsimComponentsTest, LinkResetClearsValue) { + Link<> l("l", 1, nullptr); + l.forward(99); + l.doXfer({0, 0}); + l.reset(); + EXPECT_EQ(l.value(), 0u); + EXPECT_FALSE(l.hasValue()); +} + +TEST(GfsimComponentsTest, ComputeResetClearsOutput) { + Compute c("c", 1, nullptr); + c.setInput(5); + c.doWork({0, 0}); + c.doXfer({0, 0}); + EXPECT_EQ(c.output(), 6u); + c.reset(); + EXPECT_EQ(c.output(), 0u); +} + +TEST(GfsimComponentsTest, SinkResetClearsAll) { + Sink<> s("s", 1, nullptr); + s.receive(1); + s.receive(2); + s.doXfer({0, 0}); + EXPECT_EQ(s.totalReceived(), 2u); + s.reset(); + EXPECT_EQ(s.totalReceived(), 0u); + EXPECT_TRUE(s.received().empty()); +} + +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); +static_assert(Component>); + +TEST(GfsimComponentsTest, BaselineTemplatesExposeCanonicalObjectKinds) { + TraceSource<> trace("trace", 0, nullptr); + Queue queue("queue", 1, nullptr, 2); + Scheduler scheduler("scheduler", 2, nullptr, 2); + Compute<> compute("compute", 3, nullptr); + Link<> link("link", 4, nullptr); + Memory<> memory("memory", 5, nullptr, 2); + Sink<> sink("sink", 6, nullptr); + + EXPECT_EQ(trace.kind(), ObjectKind::TraceSource); + EXPECT_EQ(queue.kind(), ObjectKind::Queue); + EXPECT_EQ(scheduler.kind(), ObjectKind::Scheduler); + EXPECT_EQ(compute.kind(), ObjectKind::Compute); + EXPECT_EQ(link.kind(), ObjectKind::Link); + EXPECT_EQ(memory.kind(), ObjectKind::Memory); + EXPECT_EQ(sink.kind(), ObjectKind::Sink); + EXPECT_EQ(TraceSource<>::contractName, "ac.TraceSource"); + EXPECT_EQ(Queue::contractName, "ac.Queue"); + EXPECT_EQ(Scheduler::contractName, "ac.Scheduler"); + EXPECT_EQ(Compute<>::contractName, "ac.Compute"); + EXPECT_EQ(Link<>::contractName, "ac.Link"); + EXPECT_EQ(Memory<>::contractName, "ac.Memory"); + EXPECT_EQ(Sink<>::contractName, "ac.Sink"); + EXPECT_EQ(ReadyValid::contractName, "ac.ready_valid"); + EXPECT_EQ((RequestResponse::contractName), + "ac.request_response"); +} + +TEST(GfsimComponentsTest, QueueCommitsThroughBarrierAndTracksStatistics) { + Queue queue("queue", 1, nullptr, 2); + EXPECT_TRUE(queue.proposePush(10)); + EXPECT_TRUE(queue.proposePush(20)); + EXPECT_FALSE(queue.proposePush(30)); + EXPECT_TRUE(queue.hasPendingCommit()); + + queue.doArbitrate({0, 0}); + queue.doXfer({0, 0}); + EXPECT_EQ(queue.committedSize(), 2u); + EXPECT_EQ(queue.totalPushes(), 2u); + EXPECT_EQ(queue.highWatermark(), 2u); + + EXPECT_EQ(queue.proposePop(), 10); + queue.doArbitrate({1, 0}); + queue.doXfer({1, 0}); + EXPECT_EQ(queue.totalPops(), 1u); + ASSERT_NE(queue.peek(), nullptr); + EXPECT_EQ(*queue.peek(), 20); +} + +TEST(GfsimComponentsTest, QueueEnforcesStaticPacketByteCapacity) { + Queue queue("queue", 1, nullptr, 4, 2 * sizeof(uint32_t)); + EXPECT_TRUE(queue.proposePush(10)); + EXPECT_TRUE(queue.proposePush(20)); + EXPECT_FALSE(queue.proposePush(30)); + queue.doXfer({0, 0}); + + EXPECT_EQ(queue.committedSize(), 2u); + EXPECT_EQ(queue.committedBytes(), 2 * sizeof(uint32_t)); + EXPECT_TRUE(queue.isFull()); +} + +TEST(GfsimComponentsTest, LinkTreatsZeroAsACommittedValue) { + Link<> link("link", 1, nullptr); + link.forward(0); + link.doXfer({0, 0}); + EXPECT_TRUE(link.hasValue()); + EXPECT_EQ(link.value(), 0u); +} + +struct ScheduledValue { + uint64_t value = 0; + auto operator<=>(const ScheduledValue &) const = default; +}; + +TEST(GfsimComponentsTest, SchedulerOrderIsIndependentOfProposalInsertion) { + struct Candidate { + ScheduledValue value; + uint32_t priority; + uint32_t port; + uint32_t instance; + ObjectId owner; + uint64_t transaction; + }; + const std::array candidates{{ + {{10}, 1, 0, 0, 3, 10}, + {{20}, 0, 1, 0, 2, 20}, + {{30}, 0, 0, 1, 4, 30}, + {{40}, 0, 0, 0, 5, 40}, + }}; + + auto schedule = [&](std::array order) { + Scheduler scheduler("scheduler", 1, nullptr, 4); + for (size_t index : order) { + const Candidate &candidate = candidates[index]; + EXPECT_TRUE(scheduler.proposeSchedule( + candidate.value, candidate.priority, candidate.port, + candidate.instance, candidate.owner, candidate.transaction)); + } + scheduler.doArbitrate({0, 0}); + scheduler.doXfer({0, 0}); + + std::vector result; + while (auto value = scheduler.proposePop()) + result.push_back(value->value); + scheduler.doXfer({1, 0}); + return result; + }; + + EXPECT_EQ(schedule({0, 1, 2, 3}), (std::vector{40, 30, 20, 10})); + EXPECT_EQ(schedule({2, 0, 3, 1}), (std::vector{40, 30, 20, 10})); +} + +TEST(GfsimComponentsTest, SchedulerAppliesFiniteCapacityAfterArbitration) { + Scheduler scheduler("scheduler", 1, nullptr, 2); + EXPECT_TRUE(scheduler.proposeSchedule(30, 2, 0, 0, 3, 30)); + EXPECT_TRUE(scheduler.proposeSchedule(10, 0, 0, 0, 1, 10)); + EXPECT_TRUE(scheduler.proposeSchedule(20, 1, 0, 0, 2, 20)); + scheduler.doArbitrate({0, 0}); + EXPECT_TRUE(scheduler.hasPendingCommit()); + scheduler.doXfer({0, 0}); + + EXPECT_EQ(scheduler.size(), 2u); + EXPECT_EQ(scheduler.rejectedTransactions(), (std::vector{30})); + EXPECT_EQ(scheduler.proposePop(), 10u); + EXPECT_EQ(scheduler.proposePop(), 20u); + EXPECT_EQ(scheduler.proposePop(), std::nullopt); +} + +TEST(GfsimComponentsTest, SchedulerPreservesFifoAcrossCommittedEpochs) { + Scheduler scheduler("scheduler", 1, nullptr, 2); + EXPECT_TRUE(scheduler.proposeSchedule(90, 0, 0, 0, 9, 90)); + scheduler.doArbitrate({0, 0}); + scheduler.doXfer({0, 0}); + + EXPECT_TRUE(scheduler.proposeSchedule(10, 0, 0, 0, 1, 10)); + scheduler.doArbitrate({1, 0}); + scheduler.doXfer({1, 0}); + + EXPECT_EQ(scheduler.proposePop(), 90u); + EXPECT_EQ(scheduler.proposePop(), 10u); +} + +TEST(GfsimComponentsTest, SchedulerRejectsDuplicateIdentityAndResets) { + Scheduler scheduler("scheduler", 1, nullptr, 2); + EXPECT_TRUE(scheduler.proposeSchedule(10, 0, 0, 0, 7, 99)); + EXPECT_FALSE(scheduler.proposeSchedule(20, 0, 0, 0, 7, 99)); + scheduler.doArbitrate({0, 0}); + scheduler.doXfer({0, 0}); + EXPECT_EQ(scheduler.size(), 1u); + + scheduler.reset(); + EXPECT_EQ(scheduler.size(), 0u); + EXPECT_TRUE(scheduler.rejectedTransactions().empty()); + EXPECT_TRUE(scheduler.proposeSchedule(20, 0, 0, 0, 7, 99)); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Protocols and packets +// ═══════════════════════════════════════════════════════════════════════ + +static_assert(Packet); +static_assert(!Packet); +static_assert(!Packet); + +TEST(GfsimPacketTest, FixedPacketRoundTripsWithStableReflection) { + const TestPacket packet{0x1234, 0x89abcdef}; + const auto bytes = serializePacket(packet); + EXPECT_EQ(bytes, (PacketTraits::Serialized{ + std::byte{0x34}, std::byte{0x12}, std::byte{0xef}, + std::byte{0xcd}, std::byte{0xab}, std::byte{0x89}})); + EXPECT_EQ(deserializePacket(bytes), packet); + EXPECT_EQ(PacketTraits::maximumSerializedSize, bytes.size()); + EXPECT_EQ(PacketTraits::endianness, PacketEndianness::Little); + ASSERT_EQ(PacketTraits::fields.size(), 2u); + EXPECT_EQ(PacketTraits::fields[0], (PacketField{"opcode", 0, 2})); + EXPECT_EQ(PacketTraits::fields[1], + (PacketField{"payload", 2, 4})); +} + +TEST(GfsimPacketTest, DeserializationRejectsWrongSerializedSize) { + const std::array shortBytes{}; + EXPECT_EQ(deserializePacket(shortBytes), std::nullopt); +} + +TEST(GfsimPacketTest, QueueUsesSerializedPacketSizeInsteadOfNativeLayout) { + static_assert(sizeof(TestPacket) > PacketTraits::serializedSize); + Queue queue("packets", 1, nullptr, 2, + PacketTraits::serializedSize); + EXPECT_TRUE(queue.proposePush({1, 2})); + EXPECT_FALSE(queue.proposePush({3, 4})); + queue.doXfer({0, 0}); + EXPECT_EQ(queue.committedBytes(), PacketTraits::serializedSize); +} + +TEST(GfsimProtocolTest, ReadyValidRetainsOfferAndTransfersExactlyOnce) { + ReadyValid channel("channel", 1, nullptr); + EXPECT_TRUE(channel.proposeOffer(7)); + EXPECT_FALSE(channel.proposeOffer(8)); + channel.doXfer({0, 0}); + + ASSERT_NE(channel.peekOffer(), nullptr); + EXPECT_EQ(*channel.peekOffer(), 7u); + EXPECT_EQ(channel.transferCount(), 0u); + + channel.proposeReady(true); + channel.doXfer({1, 0}); + EXPECT_EQ(channel.peekOffer(), nullptr); + EXPECT_EQ(channel.lastTransferred(), 7u); + EXPECT_EQ(channel.transferCount(), 1u); + + channel.proposeReady(true); + channel.doXfer({2, 0}); + EXPECT_EQ(channel.transferCount(), 1u); + EXPECT_TRUE(channel.validate()); +} + +TEST(GfsimProtocolTest, RequestResponseEnforcesBoundsAndCorrelation) { + RequestResponse channel("channel", 1, nullptr, 2); + EXPECT_TRUE(channel.proposeRequest(11, 101)); + EXPECT_TRUE(channel.proposeRequest(22, 102)); + EXPECT_FALSE(channel.proposeRequest(33, 103)); + EXPECT_FALSE(channel.proposeRequest(44, 101)); + channel.doXfer({0, 0}); + EXPECT_EQ(channel.inFlight(), 2u); + + auto first = channel.proposePopRequest(); + auto second = channel.proposePopRequest(); + ASSERT_TRUE(first.has_value()); + ASSERT_TRUE(second.has_value()); + EXPECT_EQ(first->payload, 11u); + EXPECT_EQ(first->correlationId, 101u); + EXPECT_EQ(second->payload, 22u); + EXPECT_EQ(second->correlationId, 102u); + channel.doXfer({1, 0}); + + EXPECT_FALSE(channel.proposeResponse(999, 999)); + EXPECT_TRUE(channel.proposeResponse(111, 101)); + EXPECT_FALSE(channel.proposeResponse(222, 101)); + channel.doXfer({2, 0}); + EXPECT_EQ(channel.inFlight(), 1u); + EXPECT_EQ(channel.totalCompleted(), 1u); + + auto response = channel.proposePopResponse(); + ASSERT_TRUE(response.has_value()); + EXPECT_EQ(response->payload, 111u); + EXPECT_EQ(response->correlationId, 101u); + channel.doXfer({3, 0}); + EXPECT_FALSE(channel.proposePopResponse().has_value()); + EXPECT_TRUE(channel.validate()); +} + +TEST(GfsimProtocolTest, ProtocolStatePreservesCreditAndPhaseInvariants) { + ProtocolState state(2); + EXPECT_TRUE(state.validate()); + EXPECT_TRUE(state.startRequest()); + EXPECT_TRUE(state.startRequest()); + EXPECT_FALSE(state.startRequest()); + EXPECT_EQ(state.credits(), 0u); + EXPECT_EQ(state.inFlight(), 2u); + + EXPECT_TRUE(state.beginResponse()); + EXPECT_TRUE(state.completeResponse()); + EXPECT_EQ(state.phase(), ProtocolPhase::Transfer); + EXPECT_EQ(state.credits(), 1u); + EXPECT_EQ(state.inFlight(), 1u); + + EXPECT_TRUE(state.setBackpressure(true)); + EXPECT_EQ(state.phase(), ProtocolPhase::Backpressure); + EXPECT_FALSE(state.startRequest()); + EXPECT_TRUE(state.setBackpressure(false)); + EXPECT_EQ(state.phase(), ProtocolPhase::Transfer); + + EXPECT_TRUE(state.beginResponse()); + EXPECT_TRUE(state.completeResponse()); + EXPECT_EQ(state.phase(), ProtocolPhase::Idle); + EXPECT_EQ(state.credits(), 2u); + EXPECT_EQ(state.inFlight(), 0u); + EXPECT_FALSE(state.completeResponse()); + EXPECT_TRUE(state.validate()); +} + +// ═══════════════════════════════════════════════════════════════════════ +// PTO trace streaming +// ═══════════════════════════════════════════════════════════════════════ + +constexpr std::string_view ValidPtoTrace = R"json({ + "schema": "pto-trace", + "version": "0.1", + "contract_epoch": "0.3", + "metadata": { + "producer": "gfsim-test", + "address_spaces": ["global"], + "record_count": 2 + }, + "records": [ + { + "sequence_id": 10, + "opcode": "pto.load", + "operands": [{"kind": "buffer", "id": "A"}], + "dependencies": [], + "attributes": {"width": 32}, + "issue_time": 2 + }, + { + "sequence_id": 20, + "opcode": "pto.store", + "operands": [ + {"kind": "address", "space": "global", "value": 64}, + {"kind": "record_result", "sequence_id": 10, "result_index": 0} + ], + "dependencies": [10], + "attributes": {"ordered": true} + } + ] +})json"; + +TEST(GfsimTraceTest, ParsesClosedTypedPtoTraceDocument) { + TraceLoadResult result = parsePtoTrace(ValidPtoTrace); + ASSERT_TRUE(result.succeeded()) << result.primaryDiagnostic(); + ASSERT_TRUE(result.document.has_value()); + EXPECT_EQ(result.document->metadata.producer, "gfsim-test"); + ASSERT_EQ(result.document->records.size(), 2u); + EXPECT_EQ(result.document->records[0].sequenceId, 10u); + EXPECT_EQ(result.document->records[0].opcode, "pto.load"); + EXPECT_EQ(result.document->records[0].issueTime, 2u); + ASSERT_EQ(result.document->records[1].operands.size(), 2u); + EXPECT_EQ(result.document->records[1].operands[0].kind, + PtoOperandKind::Address); + EXPECT_EQ(result.document->records[1].dependencies, + (std::vector{10})); +} + +TEST(GfsimTraceTest, ParsesCanonicalDavinciOOAdapterFixture) { + auto buffer = llvm::MemoryBuffer::getFile( + ACIR_TEST_SOURCE_DIR + "/tests/tools/fixtures/davincioo-valid.pto-trace.json"); + ASSERT_TRUE(static_cast(buffer)); + TraceLoadResult result = parsePtoTrace((*buffer)->getBuffer()); + ASSERT_TRUE(result.succeeded()) << result.primaryDiagnostic(); + ASSERT_TRUE(result.document.has_value()); + EXPECT_EQ(result.document->metadata.producer, + "davincioo@e73633301cabed0d871ea5ff66e76a91df870aeb"); + EXPECT_EQ(result.document->metadata.ptoIdentity, + "pto-isa@f6d0567c1cae2d6a7b0ebaf7ad0e3b93f8a39da3"); + ASSERT_EQ(result.document->records.size(), 2u); + const PtoTraceRecord &record = result.document->records.front(); + EXPECT_EQ(record.sequenceId, 0u); + EXPECT_EQ(record.opcode, "TASSIGN"); + ASSERT_EQ(record.operands.size(), 2u); + EXPECT_EQ(record.operands[0].kind, PtoOperandKind::Immediate); + EXPECT_EQ(record.operands[0].type, "uint64"); + EXPECT_EQ(record.operands[1].kind, PtoOperandKind::Tile); + EXPECT_EQ(record.operands[1].id, "block/0/tile/0x0"); + auto attributes = record.attributes.find("davincioo"); + ASSERT_NE(attributes, record.attributes.end()); + const auto *davincioo = + std::get_if(&attributes->second.value); + ASSERT_NE(davincioo, nullptr); + EXPECT_TRUE(davincioo->contains("block_idx")); + EXPECT_TRUE(davincioo->contains("input_tiles")); + EXPECT_TRUE(davincioo->contains("scalar_inputs")); + EXPECT_TRUE(davincioo->contains("output_tiles")); + EXPECT_TRUE(davincioo->contains("operand_roles")); +} + +TEST(GfsimTraceTest, StreamingAndBufferedParsingProduceIdenticalDocument) { + TraceLoadResult buffered = parsePtoTrace(ValidPtoTrace); + ASSERT_TRUE(buffered.succeeded()); + + PtoTraceStream stream; + for (size_t offset = 0; offset < ValidPtoTrace.size(); offset += 17) + ASSERT_TRUE(stream.append(ValidPtoTrace.substr( + offset, std::min(17, ValidPtoTrace.size() - offset)))); + TraceLoadResult streamed = stream.finish(); + ASSERT_TRUE(streamed.succeeded()) << streamed.primaryDiagnostic(); + EXPECT_EQ(streamed.document, buffered.document); +} + +TEST(GfsimTraceTest, RejectsForwardDependencyWithStableDiagnostic) { + constexpr std::string_view invalid = R"json({ + "schema":"pto-trace","version":"0.1","contract_epoch":"0.3", + "metadata":{},"records":[ + {"sequence_id":1,"opcode":"pto.a","operands":[], + "dependencies":[2],"attributes":{}}, + {"sequence_id":2,"opcode":"pto.b","operands":[], + "dependencies":[],"attributes":{}} + ]})json"; + TraceLoadResult result = parsePtoTrace(invalid); + ASSERT_FALSE(result.succeeded()); + ASSERT_FALSE(result.diagnostics.empty()); + EXPECT_EQ(result.diagnostics.front().code, "ACTRACE-DEPENDENCY"); + EXPECT_EQ(result.diagnostics.front().jsonPointer, + "/records/0/dependencies/0"); + EXPECT_EQ(result.diagnostics.front().sequenceId, 1u); +} + +TEST(GfsimTraceTest, RejectsUnknownFieldsAndRepresentationCaps) { + constexpr std::string_view unknown = R"json({ + "schema":"pto-trace","version":"0.1","contract_epoch":"0.3", + "metadata":{"extension":true},"records":[]})json"; + TraceLoadResult closed = parsePtoTrace(unknown); + ASSERT_FALSE(closed.succeeded()); + EXPECT_EQ(closed.diagnostics.front().code, "ACTRACE-SCHEMA"); + EXPECT_EQ(closed.diagnostics.front().jsonPointer, "/metadata/extension"); + + TraceValidationLimits limits; + limits.maxDocumentBytes = 32; + TraceLoadResult capped = parsePtoTrace(ValidPtoTrace, limits); + ASSERT_FALSE(capped.succeeded()); + EXPECT_EQ(capped.diagnostics.front().code, "ACTRACE-LIMIT"); +} + +TEST(GfsimTraceTest, ValidatesDuplicateKeysRecordCountAndContentHash) { + constexpr std::string_view validEmpty = R"json({ + "schema":"pto-trace","version":"0.1","contract_epoch":"0.3", + "metadata":{ + "record_count":0, + "content_hash":"sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945" + },"records":[]})json"; + EXPECT_TRUE(parsePtoTrace(validEmpty).succeeded()); + + constexpr std::string_view wrongCount = R"json({ + "schema":"pto-trace","version":"0.1","contract_epoch":"0.3", + "metadata":{"record_count":1},"records":[]})json"; + TraceLoadResult count = parsePtoTrace(wrongCount); + ASSERT_FALSE(count.succeeded()); + EXPECT_EQ(count.diagnostics.front().code, "ACTRACE-METADATA"); + + constexpr std::string_view duplicateKey = R"json({ + "schema":"pto-trace","schema":"pto-trace","version":"0.1", + "contract_epoch":"0.3","metadata":{},"records":[]})json"; + TraceLoadResult duplicate = parsePtoTrace(duplicateKey); + ASSERT_FALSE(duplicate.succeeded()); + EXPECT_EQ(duplicate.diagnostics.front().code, "ACTRACE-JSON"); +} + +struct DecodedTraceTransaction { + uint64_t sequenceId; + std::string opcode; + auto operator<=>(const DecodedTraceTransaction &) const = default; +}; + +struct CountingTraceDecoder { + size_t *decodeCount; + std::optional + operator()(const PtoTraceRecord &record) const { + ++*decodeCount; + return DecodedTraceTransaction{record.sequenceId, record.opcode}; + } +}; + +TEST(GfsimTraceTest, CursorAdvancesOnlyOnAcceptedXferAndHonorsDependencies) { + TraceLoadResult loaded = parsePtoTrace(ValidPtoTrace); + ASSERT_TRUE(loaded.succeeded()); + size_t decodeCount = 0; + TraceSource source( + "trace", 1, nullptr, std::move(*loaded.document), {&decodeCount}); + + source.doWork({2, 0}); + source.doXfer({2, 0}); + ASSERT_NE(source.peekOffer(), nullptr); + EXPECT_EQ(source.peekOffer()->sequenceId, 10u); + EXPECT_EQ(source.position().nextRecordIndex, 0u); + EXPECT_FALSE(source.position().lastCommittedSequenceId.has_value()); + EXPECT_EQ(decodeCount, 1u); + + source.doWork({3, 0}); + source.doWork({3, 1}); + EXPECT_EQ(source.peekOffer()->sequenceId, 10u); + EXPECT_EQ(decodeCount, 1u); + ASSERT_TRUE(source.proposeAccept()); + source.doXfer({3, 1}); + EXPECT_EQ(source.position().nextRecordIndex, 1u); + EXPECT_EQ(source.position().lastCommittedSequenceId, 10u); + EXPECT_FALSE(source.eof()); + + source.doWork({4, 0}); + source.doXfer({4, 0}); + EXPECT_EQ(source.peekOffer(), nullptr); + EXPECT_EQ(decodeCount, 1u); + EXPECT_TRUE(source.markDependencyComplete(10)); + source.doWork({4, 1}); + source.doXfer({4, 1}); + ASSERT_NE(source.peekOffer(), nullptr); + EXPECT_EQ(source.peekOffer()->sequenceId, 20u); + EXPECT_EQ(decodeCount, 2u); + + ASSERT_TRUE(source.proposeAccept()); + source.doXfer({5, 0}); + EXPECT_TRUE(source.eof()); + EXPECT_EQ(source.position().nextRecordIndex, 2u); + EXPECT_EQ(source.position().lastCommittedSequenceId, 20u); + EXPECT_TRUE(source.validate()); +} + +TEST(GfsimTraceTest, UnloadedSourceAcceptsOneTypedDocumentBeforeExecution) { + TraceLoadResult loaded = parsePtoTrace(ValidPtoTrace); + ASSERT_TRUE(loaded.succeeded()); + TraceSource<> source("trace", 1, nullptr); + + EXPECT_TRUE(source.loadDocument(std::move(*loaded.document))); + EXPECT_FALSE(source.loadDocument(PtoTraceDocument{})); + EXPECT_FALSE(source.eof()); + source.doWork({2, 0}); + source.doXfer({2, 0}); + ASSERT_NE(source.peekOffer(), nullptr); + EXPECT_EQ(source.peekOffer()->sequenceId, 10u); +} + +TEST(GfsimTraceTest, IssueTimeSchedulesAnExactWakeInsteadOfPolling) { + TraceLoadResult loaded = parsePtoTrace(ValidPtoTrace); + ASSERT_TRUE(loaded.succeeded()); + SimSystem system; + auto source = std::make_unique>( + "trace", 0, nullptr, std::move(*loaded.document), + IdentityTraceDecoder{}, &system); + TraceSource<> *sourcePtr = source.get(); + system.registerObject(sourcePtr); + system.root().addChild(std::move(source)); + + TerminationResult result = system.run(); + EXPECT_EQ(result.finalEpoch, (Epoch{2, 0})); + EXPECT_EQ(result.classification, TerminationClass::Failed); + EXPECT_EQ(result.diagnosticCode, "no_progress"); + ASSERT_NE(sourcePtr->peekOffer(), nullptr); + EXPECT_EQ(sourcePtr->peekOffer()->sequenceId, 10u); + EXPECT_EQ(sourcePtr->position().nextRecordIndex, 0u); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Statistics, diagnostics, and termination +// ═══════════════════════════════════════════════════════════════════════ + +TEST(GfsimStatisticsTest, CounterGaugeAndHistogramCommitDeterministically) { + Statistic counter("accepted", 1, nullptr, StatisticKind::Counter); + EXPECT_TRUE(counter.proposeAdd(2)); + EXPECT_TRUE(counter.proposeAdd(3)); + counter.doXfer({4, 0}); + StatSnapshot counterSnapshot = counter.snapshot(); + EXPECT_EQ(counterSnapshot.value, 5u); + EXPECT_EQ(counterSnapshot.lastUpdate, (Epoch{4, 0})); + + Statistic gauge("occupancy", 2, nullptr, StatisticKind::Gauge); + EXPECT_TRUE(gauge.proposeSet(7)); + EXPECT_FALSE(gauge.proposeSet(8)); + gauge.doXfer({4, 0}); + EXPECT_EQ(gauge.snapshot().value, 7u); + + Statistic histogram("latency", 3, nullptr, {10, 20}); + EXPECT_TRUE(histogram.proposeObserve(25)); + EXPECT_TRUE(histogram.proposeObserve(5)); + EXPECT_TRUE(histogram.proposeObserve(15)); + histogram.doXfer({5, 0}); + StatSnapshot histogramSnapshot = histogram.snapshot(); + EXPECT_EQ(histogramSnapshot.count, 3u); + EXPECT_EQ(histogramSnapshot.sum, 45u); + EXPECT_EQ(histogramSnapshot.minimum, 5u); + EXPECT_EQ(histogramSnapshot.maximum, 25u); + EXPECT_EQ(histogramSnapshot.buckets, + (std::vector{{10, 1}, {20, 1}, {UINT64_MAX, 1}})); +} + +TEST(GfsimStatisticsTest, SystemSnapshotsAreStableByPathAndName) { + SimSystem system; + Statistic second("zeta", 2, nullptr, StatisticKind::Counter); + Statistic first("alpha", 1, nullptr, StatisticKind::Gauge); + second.setPath("/system/zeta"); + first.setPath("/system/alpha"); + ASSERT_TRUE(second.proposeAdd(2)); + ASSERT_TRUE(first.proposeSet(1)); + second.doXfer({1, 0}); + first.doXfer({1, 0}); + system.registerObject(&second); + system.registerObject(&first); + + std::vector snapshots = system.statistics(); + ASSERT_EQ(snapshots.size(), 2u); + EXPECT_EQ(snapshots[0].objectPath, "/system/alpha"); + EXPECT_EQ(snapshots[0].name, "alpha"); + EXPECT_EQ(snapshots[1].objectPath, "/system/zeta"); + EXPECT_EQ(snapshots[1].name, "zeta"); +} + +TEST(GfsimStatisticsTest, QueueSnapshotsUseFrozenNamesAndKinds) { + Queue queue("queue", 1, nullptr, 2); + queue.setPath("/system/queue"); + ASSERT_TRUE(queue.proposePush(7)); + queue.doXfer({3, 0}); + std::vector snapshots; + queue.collectStatistics(snapshots); + ASSERT_EQ(snapshots.size(), 4u); + + auto find = [&](std::string_view name) -> const StatSnapshot * { + auto position = std::ranges::find(snapshots, name, &StatSnapshot::name); + return position == snapshots.end() ? nullptr : &*position; + }; + ASSERT_NE(find("queue_occupancy"), nullptr); + EXPECT_EQ(find("queue_occupancy")->kind, StatisticKind::Gauge); + ASSERT_NE(find("queue_occupancy_peak"), nullptr); + EXPECT_EQ(find("queue_occupancy_peak")->kind, StatisticKind::Gauge); + ASSERT_NE(find("accepted_transactions"), nullptr); + EXPECT_EQ(find("accepted_transactions")->kind, StatisticKind::Counter); + ASSERT_NE(find("completed_transactions"), nullptr); + EXPECT_EQ(find("completed_transactions")->kind, StatisticKind::Counter); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Committed observations +// ═══════════════════════════════════════════════════════════════════════ + +EventProposal testObservation(ObjectId owner, std::string name, + uint64_t sequenceId) { + return {.ownerId = owner, + .category = "transaction", + .name = std::move(name), + .phase = TraceEventPhase::Instant, + .rootSequenceId = sequenceId, + .arguments = {{"accepted", true}, {"bytes", uint64_t{64}}}}; +} + +TEST(GfsimObservationTest, ProposalsBecomeVisibleOnlyAtCommit) { + ObservationRecorder recorder; + ASSERT_TRUE(recorder.propose(testObservation(4, "accepted", 10))); + EXPECT_TRUE(recorder.events().empty()); + recorder.rejectOwner(4); + EXPECT_TRUE(recorder.events().empty()); + + ASSERT_TRUE(recorder.propose(testObservation(4, "accepted", 11))); + ASSERT_TRUE(recorder.commitOwner(4, {3, 1})); + ASSERT_EQ(recorder.events().size(), 1u); + const CommittedEvent &event = recorder.events().front(); + EXPECT_EQ(event.epoch, (Epoch{3, 1})); + EXPECT_EQ(event.ownerId, 4u); + EXPECT_EQ(event.localCommittedIndex, 0u); + EXPECT_EQ(event.rootSequenceId, 11u); +} + +TEST(GfsimObservationTest, StableKeyDoesNotDependOnProposalOrCommitOrder) { + auto run = [](std::array proposalOrder, + std::array commitOrder) { + ObservationRecorder recorder; + for (ObjectId owner : proposalOrder) + EXPECT_TRUE(recorder.propose( + testObservation(owner, "owner_" + std::to_string(owner), owner))); + for (ObjectId owner : commitOrder) + EXPECT_TRUE(recorder.commitOwner(owner, {7, 0})); + return std::vector(recorder.events().begin(), + recorder.events().end()); + }; + + const auto ascending = run({1, 3, 7}, {1, 3, 7}); + const auto permuted = run({7, 1, 3}, {3, 7, 1}); + EXPECT_EQ(ascending, permuted); + ASSERT_EQ(permuted.size(), 3u); + EXPECT_EQ(permuted[0].ownerId, 1u); + EXPECT_EQ(permuted[1].ownerId, 3u); + EXPECT_EQ(permuted[2].ownerId, 7u); +} + +TEST(GfsimObservationTest, RejectionConsumesNoOwnerLocalIndex) { + ObservationRecorder recorder; + ASSERT_TRUE(recorder.propose(testObservation(2, "rejected", 1))); + recorder.rejectOwner(2); + ASSERT_TRUE(recorder.propose(testObservation(2, "first_commit", 2))); + ASSERT_TRUE(recorder.commitOwner(2, {1, 0})); + ASSERT_TRUE(recorder.propose(testObservation(2, "second_commit", 3))); + ASSERT_TRUE(recorder.commitOwner(2, {2, 0})); + ASSERT_EQ(recorder.events().size(), 2u); + EXPECT_EQ(recorder.events()[0].localCommittedIndex, 0u); + EXPECT_EQ(recorder.events()[1].localCommittedIndex, 1u); +} + +TEST(GfsimObservationTest, ValidatesNamesArgumentsFlowsAndMonotonicTime) { + ObservationRecorder recorder; + EventProposal invalid = testObservation(kInvalidObjectId, "accepted", 1); + EXPECT_FALSE(recorder.propose(invalid)); + invalid = testObservation(1, "not valid", 1); + EXPECT_FALSE(recorder.propose(invalid)); + invalid = testObservation(1, "accepted", 1); + invalid.arguments = {{"z", uint64_t{1}}, {"a", uint64_t{2}}}; + EXPECT_FALSE(recorder.propose(invalid)); + invalid.arguments = {{"gfsim_object_id", uint64_t{2}}}; + EXPECT_FALSE(recorder.propose(invalid)); + + EventProposal start = testObservation(1, "dependency", 1); + start.phase = TraceEventPhase::FlowStart; + start.flowId = 9; + ASSERT_TRUE(recorder.propose(start)); + ASSERT_TRUE(recorder.commitOwner(1, {4, 0})); + EventProposal end = start; + end.phase = TraceEventPhase::FlowEnd; + ASSERT_TRUE(recorder.propose(end)); + ASSERT_TRUE(recorder.commitOwner(1, {5, 0})); + EXPECT_FALSE(recorder.commitOwner(1, {3, 0})); + + recorder.reset(); + EXPECT_TRUE(recorder.events().empty()); + ASSERT_TRUE(recorder.propose(start)); + EXPECT_EQ(recorder.pendingCount(1), 1u); +} + +class ObservedCommitObject final : public SimObject { +public: + ObservedCommitObject(ObjectId id, SimSystem &system, bool commit) + : SimObject(ObjectKind::Compute, "observed", id), system_(system), + commit_(commit) {} + + void doWork(Epoch) override { + EXPECT_TRUE( + system_.proposeObservation(testObservation(id(), "work", id()))); + pending_ = commit_; + } + bool hasPendingCommit() const override { return pending_; } + void doXfer(Epoch) override { pending_ = false; } + +private: + SimSystem &system_; + bool commit_ = false; + bool pending_ = false; +}; + +TEST(GfsimObservationTest, SystemAdmitsOnlyObjectsThatCommitAtXfer) { + SimSystem system("observed"); + ObservedCommitObject committed(0, system, true); + ObservedCommitObject rejected(1, system, false); + system.registerObject(&committed); + system.registerObject(&rejected); + ASSERT_TRUE(system.scheduleWork(rejected.id(), {0, 0})); + ASSERT_TRUE(system.scheduleWork(committed.id(), {0, 0})); + + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed); + ASSERT_EQ(system.observations().size(), 1u); + EXPECT_EQ(system.observations().front().ownerId, committed.id()); + EXPECT_EQ(system.observations().front().localCommittedIndex, 0u); + + system.reset(); + EXPECT_TRUE(system.observations().empty()); +} + +const CommittedEvent *findObservation(std::span events, + ObjectId owner, std::string_view name, + size_t occurrence = 0) { + for (const CommittedEvent &event : events) { + if (event.ownerId == owner && event.name == name) { + if (occurrence == 0) + return &event; + --occurrence; + } + } + return nullptr; +} + +const ObservationValue *findArgument(const CommittedEvent &event, + std::string_view name) { + auto argument = + std::ranges::find(event.arguments, name, &ObservationArgument::name); + return argument == event.arguments.end() ? nullptr : &argument->value; +} + +TEST(GfsimComponentObservationTest, + QueueSchedulerAndResourcePublishAcceptedOutcomesAndOccupancy) { + SimSystem system("observed"); + Queue queue("queue", 0, nullptr, 2, SIZE_MAX, &system); + Scheduler scheduler("scheduler", 1, nullptr, 1, &system); + Resource resource("resource", 2, nullptr, 1, &system); + system.registerObject(&queue); + system.registerObject(&scheduler); + system.registerObject(&resource); + + ASSERT_TRUE(queue.proposePush(5)); + queue.doXfer({0, 0}); + ASSERT_TRUE(queue.proposePop().has_value()); + ASSERT_TRUE(queue.proposePush(10)); + ASSERT_TRUE(scheduler.proposeSchedule(1, 0, 0, 0, 7, 70)); + ASSERT_TRUE(scheduler.proposeSchedule(2, 0, 0, 0, 8, 80)); + ASSERT_TRUE(resource.proposeReserve(9, 1, {1, 0}, 100, {4, 0}, 900)); + ASSERT_TRUE(resource.proposeReserve(10, 1, {1, 0}, 101, {5, 0}, 901)); + ASSERT_TRUE(system.scheduleWork(queue.id(), {1, 0})); + ASSERT_TRUE(system.scheduleWork(scheduler.id(), {1, 0})); + ASSERT_TRUE(system.scheduleWork(resource.id(), {1, 0})); + + system.run(); + auto events = system.observations(); + ASSERT_NE(findObservation(events, queue.id(), "accepted", 0), nullptr); + ASSERT_NE(findObservation(events, queue.id(), "completed"), nullptr); + const CommittedEvent *queueOccupancy = + findObservation(events, queue.id(), "occupancy"); + ASSERT_NE(queueOccupancy, nullptr); + EXPECT_EQ(queueOccupancy->phase, TraceEventPhase::Counter); + ASSERT_NE(findArgument(*queueOccupancy, "occupancy"), nullptr); + EXPECT_EQ(std::get(*findArgument(*queueOccupancy, "occupancy")), + 1u); + + const CommittedEvent *scheduled = + findObservation(events, scheduler.id(), "accepted"); + ASSERT_NE(scheduled, nullptr); + EXPECT_EQ(scheduled->rootSequenceId, 70u); + ASSERT_NE(findArgument(*scheduled, "child_owner_id"), nullptr); + EXPECT_EQ(std::get(*findArgument(*scheduled, "child_owner_id")), + 7u); + const CommittedEvent *schedulerStall = + findObservation(events, scheduler.id(), "capacity"); + ASSERT_NE(schedulerStall, nullptr); + EXPECT_EQ(schedulerStall->category, "stall"); + EXPECT_EQ(schedulerStall->rootSequenceId, 80u); + + const CommittedEvent *reservation = + findObservation(events, resource.id(), "reservation"); + ASSERT_NE(reservation, nullptr); + EXPECT_EQ(reservation->phase, TraceEventPhase::Complete); + EXPECT_EQ(reservation->duration, 3 * kMaxDeltasPerTick); + EXPECT_EQ(reservation->rootSequenceId, 900u); + const CommittedEvent *resourceStall = + findObservation(events, resource.id(), "capacity"); + ASSERT_NE(resourceStall, nullptr); + EXPECT_EQ(resourceStall->rootSequenceId, 901u); +} + +TEST(GfsimComponentObservationTest, + ComputeLinkMemoryAndSinkPublishOnlyCommittedActivity) { + SimSystem system("observed"); + Compute<> compute("compute", 0, nullptr, {}, &system); + Link<> link("link", 1, nullptr, &system); + Memory<> memory("memory", 2, nullptr, 4, &system); + Sink<> sink("sink", 3, nullptr, &system); + system.registerObject(&compute); + system.registerObject(&link); + system.registerObject(&memory); + system.registerObject(&sink); + compute.setInput(12); + link.forward(13); + ASSERT_TRUE(memory.proposeWrite(2, 14)); + sink.receive(15); + for (ObjectId id = 0; id != 4; ++id) + ASSERT_TRUE(system.scheduleWork(id, {0, 0})); + + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed); + EXPECT_EQ(compute.output(), 12u); + EXPECT_EQ(link.value(), 13u); + EXPECT_EQ(memory.read(2), 14u); + EXPECT_EQ(sink.received(), (std::vector{15})); + auto events = system.observations(); + EXPECT_NE(findObservation(events, compute.id(), "completed"), nullptr); + EXPECT_NE(findObservation(events, link.id(), "completed"), nullptr); + const CommittedEvent *write = findObservation(events, memory.id(), "write"); + ASSERT_NE(write, nullptr); + EXPECT_EQ(std::get(*findArgument(*write, "address")), 2u); + EXPECT_NE(findObservation(events, sink.id(), "accepted"), nullptr); +} + +class ProtocolObservationDriver final : public SimObject { +public: + ProtocolObservationDriver( + ObjectId id, ReadyValid &readyValid, + RequestResponse &requestResponse, + TraceSource<> &trace) + : SimObject(ObjectKind::Process, "driver", id), readyValid_(readyValid), + requestResponse_(requestResponse), trace_(trace) {} + + void doWork(Epoch epoch) override { + if (epoch.time == 1) { + readyValid_.proposeReady(true); + EXPECT_TRUE(requestResponse_.proposePopRequest().has_value()); + EXPECT_TRUE(trace_.proposeAccept()); + } else if (epoch.time == 2) { + EXPECT_TRUE(requestResponse_.proposeResponse(99, 44)); + } else if (epoch.time == 3) { + EXPECT_TRUE(requestResponse_.proposePopResponse().has_value()); + } + } + +private: + ReadyValid &readyValid_; + RequestResponse &requestResponse_; + TraceSource<> &trace_; +}; + +TEST(GfsimComponentObservationTest, + ProtocolBackpressureRequestsResponsesAndTraceCursorAreCommitted) { + constexpr std::string_view oneRecord = R"json({ + "schema":"pto-trace","version":"0.1","contract_epoch":"0.3", + "metadata":{"record_count":1},"records":[{ + "sequence_id":17,"opcode":"pto.done","operands":[], + "dependencies":[],"attributes":{}}]})json"; + TraceLoadResult loaded = parsePtoTrace(oneRecord); + ASSERT_TRUE(loaded.succeeded()); + SimSystem system("observed"); + ReadyValid readyValid("ready_valid", 0, nullptr, &system); + RequestResponse requestResponse("request_response", 1, + nullptr, 2, &system); + TraceSource<> trace("trace", 2, nullptr, std::move(*loaded.document), {}, + &system, &system); + ProtocolObservationDriver driver(3, readyValid, requestResponse, trace); + system.registerObject(&readyValid); + system.registerObject(&requestResponse); + system.registerObject(&trace); + system.registerObject(&driver); + ASSERT_TRUE(readyValid.proposeOffer(42)); + ASSERT_TRUE(requestResponse.proposeRequest(7, 44)); + ASSERT_TRUE(system.scheduleWork(readyValid.id(), {0, 0})); + ASSERT_TRUE(system.scheduleWork(requestResponse.id(), {0, 0})); + for (Tick tick = 1; tick <= 3; ++tick) { + ASSERT_TRUE(system.scheduleWork(driver.id(), {tick, 0})); + ASSERT_TRUE(system.scheduleWork(requestResponse.id(), {tick, 0})); + } + ASSERT_TRUE(system.scheduleWork(readyValid.id(), {1, 0})); + ASSERT_TRUE(system.scheduleWork(trace.id(), {1, 0})); + + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed); + EXPECT_EQ(result.tracePosition, 1u); + EXPECT_EQ(result.traceLastCommittedSequenceId, 17u); + auto events = system.observations(); + EXPECT_NE(findObservation(events, readyValid.id(), "backpressure"), nullptr); + EXPECT_NE(findObservation(events, readyValid.id(), "completed"), nullptr); + EXPECT_NE(findObservation(events, requestResponse.id(), "accepted"), nullptr); + EXPECT_NE(findObservation(events, requestResponse.id(), "request"), nullptr); + EXPECT_NE(findObservation(events, requestResponse.id(), "completed"), + nullptr); + EXPECT_NE(findObservation(events, requestResponse.id(), "response"), nullptr); + const CommittedEvent *traceAccepted = + findObservation(events, trace.id(), "accepted"); + ASSERT_NE(traceAccepted, nullptr); + EXPECT_EQ(traceAccepted->rootSequenceId, 17u); + EXPECT_EQ(std::get(*findArgument(*traceAccepted, "trace_position")), + 1u); +} + +TEST(GfsimComponentObservationTest, + DisabledAndEnabledObservationProduceEqualFunctionalResults) { + auto run = [](bool enabled) { + SimSystem system("observed"); + Compute compute("compute", 0, nullptr, {}, + enabled ? &system : nullptr); + system.registerObject(&compute); + compute.setInput(23); + EXPECT_TRUE(system.scheduleWork(compute.id(), {0, 0})); + TerminationResult result = system.run(); + std::vector< + std::tuple> + statistics; + for (const StatSnapshot &snapshot : system.statistics()) + statistics.emplace_back(snapshot.objectPath, snapshot.name, snapshot.kind, + snapshot.value, snapshot.lastUpdate); + return std::tuple(result.classification, result.finalEpoch, + result.diagnosticCode, compute.output(), + std::move(statistics), system.observations().size()); + }; + + auto disabled = run(false); + auto enabled = run(true); + EXPECT_EQ(std::get<0>(disabled), std::get<0>(enabled)); + EXPECT_EQ(std::get<1>(disabled), std::get<1>(enabled)); + EXPECT_EQ(std::get<2>(disabled), std::get<2>(enabled)); + EXPECT_EQ(std::get<3>(disabled), std::get<3>(enabled)); + EXPECT_EQ(std::get<4>(disabled), std::get<4>(enabled)); + EXPECT_EQ(std::get<5>(disabled), 0u); + EXPECT_GT(std::get<5>(enabled), 0u); +} + +TEST(GfsimSystemTest, NonEmptyQueueWithoutWakeFailsWithNoProgressReport) { + SimSystem system; + Queue queue("queue", 1, nullptr, 2); + queue.setPath("/system/queue"); + ASSERT_TRUE(queue.proposePush(7)); + queue.doXfer({0, 0}); + system.registerObject(&queue); + + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Failed); + EXPECT_EQ(result.diagnosticCode, "no_progress"); + NoProgressReport report = system.noProgressReport(); + ASSERT_EQ(report.blockedObjects.size(), 1u); + EXPECT_EQ(report.blockedObjects[0].id, 1u); + EXPECT_EQ(report.blockedObjects[0].reason, "queue_not_empty"); + EXPECT_EQ(report.queueOccupancy, 1u); +} + +TEST(GfsimSystemTest, TraceLimitIsIncompleteAndReportsExactPosition) { + TraceLoadResult loaded = parsePtoTrace(ValidPtoTrace); + ASSERT_TRUE(loaded.succeeded()); + SimSystem system; + TraceSource<> source("trace", 1, nullptr, std::move(*loaded.document)); + source.setPath("/system/trace"); + system.registerObject(&source); + system.setMaxTraceRecords(0); + + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Incomplete); + EXPECT_EQ(result.diagnosticCode, "max_trace_records_reached"); + EXPECT_EQ(result.tracePosition, 0u); + EXPECT_EQ(result.terminationCap, 0u); +} + +TEST(GfsimSystemTest, ExhaustedTraceCompletesWithCommittedCursorIdentity) { + constexpr std::string_view oneRecord = R"json({ + "schema":"pto-trace","version":"0.1","contract_epoch":"0.3", + "metadata":{"record_count":1},"records":[{ + "sequence_id":9,"opcode":"pto.done","operands":[], + "dependencies":[],"attributes":{}}]})json"; + TraceLoadResult loaded = parsePtoTrace(oneRecord); + ASSERT_TRUE(loaded.succeeded()); + TraceSource<> source("trace", 1, nullptr, std::move(*loaded.document)); + source.doWork({0, 0}); + source.doXfer({0, 0}); + ASSERT_TRUE(source.proposeAccept()); + source.doXfer({1, 0}); + ASSERT_TRUE(source.eof()); + + SimSystem system; + source.setPath("/system/trace"); + system.registerObject(&source); + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed); + EXPECT_EQ(result.tracePosition, 1u); + EXPECT_EQ(result.traceLastCommittedSequenceId, 9u); + EXPECT_TRUE(system.noProgressReport().empty()); +} + +TEST(GfsimSystemTest, MultipleTraceCursorOwnersFailDeterministically) { + TraceLoadResult firstDocument = parsePtoTrace(ValidPtoTrace); + TraceLoadResult secondDocument = parsePtoTrace(ValidPtoTrace); + ASSERT_TRUE(firstDocument.succeeded()); + ASSERT_TRUE(secondDocument.succeeded()); + TraceSource<> first("first", 1, nullptr, std::move(*firstDocument.document)); + TraceSource<> second("second", 2, nullptr, + std::move(*secondDocument.document)); + SimSystem system; + system.registerObject(&first); + system.registerObject(&second); + + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Failed); + EXPECT_EQ(result.diagnosticCode, "multiple_trace_owners"); +} + +TEST(GfsimSystemTest, + NoProgressReportIncludesReservationsProtocolsAndCorrelations) { + SimSystem system; + Resource resource("resource", 1, nullptr, 1); + ReadyValid readyValid("ready_valid", 2, nullptr); + RequestResponse requestResponse("request_response", 3, + nullptr, 1); + resource.setPath("/system/resource"); + readyValid.setPath("/system/ready_valid"); + requestResponse.setPath("/system/request_response"); + + ASSERT_TRUE(resource.proposeReserve(9, 1, {0, 0}, 101)); + resource.doArbitrate({0, 0}); + resource.doXfer({0, 0}); + ASSERT_TRUE(readyValid.proposeOffer(5)); + readyValid.doXfer({0, 0}); + ASSERT_TRUE(requestResponse.proposeRequest(6, 202)); + requestResponse.doXfer({0, 0}); + system.registerObject(&resource); + system.registerObject(&readyValid); + system.registerObject(&requestResponse); + + TerminationResult result = system.run(); + EXPECT_EQ(result.diagnosticCode, "no_progress"); + NoProgressReport report = system.noProgressReport(); + ASSERT_EQ(report.blockedObjects.size(), 3u); + EXPECT_EQ(report.activeReservations, 1u); + EXPECT_EQ(report.pendingOffers, 2u); + EXPECT_EQ(report.blockedObjects[0].reason, "resource_reservation_live"); + EXPECT_EQ(report.blockedObjects[1].protocolState, "backpressure"); + EXPECT_EQ(report.blockedObjects[2].correlationChain, + (std::vector{202})); +} + +// ═══════════════════════════════════════════════════════════════════════ +// SimSystem +// ═══════════════════════════════════════════════════════════════════════ + +class RecordingDispatchObject : public SimObject { +public: + RecordingDispatchObject(ObjectId id, std::vector &log) + : SimObject(ObjectKind::Compute, "object", id), log_(log) {} + + void doWork(Epoch) override { + log_.push_back("work:" + std::to_string(id())); + } + void doArbitrate(Epoch) override { + log_.push_back("arbitrate:" + std::to_string(id())); + } + void doXfer(Epoch) override { + log_.push_back("xfer:" + std::to_string(id())); + } + void reset() override { log_.push_back("reset:" + std::to_string(id())); } + bool validate() const { return true; } + +private: + std::vector &log_; +}; + +class SelfWakingDispatchObject : public SimObject { +public: + SelfWakingDispatchObject(ObjectId id, SimSystem &system) + : SimObject(ObjectKind::Process, "self", id), system_(system) {} + + void doWork(Epoch epoch) override { + ++workCount; + system_.scheduleWork(id(), epoch); + } + bool validate() const { return true; } + + uint64_t workCount = 0; + +private: + SimSystem &system_; +}; + +class CommittingDispatchObject : public SimObject { +public: + CommittingDispatchObject(ObjectId id, std::vector &workLog, + bool commitOnWork) + : SimObject(ObjectKind::Compute, "committing", id), workLog_(workLog), + commitOnWork_(commitOnWork) {} + + void doWork(Epoch) override { + workLog_.push_back(id()); + pending_ = commitOnWork_; + } + void doXfer(Epoch) override { + if (pending_) + ++commitCount; + pending_ = false; + } + bool hasPendingCommit() const override { return pending_; } + bool validate() const { return true; } + + uint64_t commitCount = 0; + +private: + std::vector &workLog_; + bool commitOnWork_ = false; + bool pending_ = false; +}; + +class RepeatedCommitDispatchObject : public SimObject { +public: + RepeatedCommitDispatchObject(ObjectId id, SimSystem &system) + : SimObject(ObjectKind::Memory, "repeated", id), system_(system) {} + + void doWork(Epoch epoch) override { + pending_ = true; + system_.scheduleWork(id(), epoch); + } + void doXfer(Epoch) override { pending_ = false; } + bool hasPendingCommit() const override { return pending_; } + bool validate() const { return true; } + +private: + SimSystem &system_; + bool pending_ = false; +}; + +class SameTickEventChainObject : public SimObject { +public: + SameTickEventChainObject(ObjectId id, SimSystem &system) + : SimObject(ObjectKind::Process, "event_chain", id), system_(system) {} + + void doWork(Epoch epoch) override { + system_.scheduleEvent({epoch, id(), 1, eventCount++}); + } + bool validate() const { return true; } + +private: + SimSystem &system_; + uint64_t eventCount = 0; +}; + +class SnapshotWriter : public SimObject { +public: + SnapshotWriter(ObjectId id, uint64_t &state) + : SimObject(ObjectKind::Memory, "writer", id), state_(state) {} + void doWork(Epoch) override { pending_ = true; } + void doXfer(Epoch) override { + state_ = 1; + pending_ = false; + } + bool hasPendingCommit() const override { return pending_; } + bool validate() const { return true; } + +private: + uint64_t &state_; + bool pending_ = false; +}; + +class SnapshotReader : public SimObject { +public: + SnapshotReader(ObjectId id, const uint64_t &state) + : SimObject(ObjectKind::Sink, "reader", id), state_(state) {} + void doWork(Epoch) override { observed.push_back(state_); } + bool validate() const { return true; } + + std::vector observed; + +private: + const uint64_t &state_; +}; + +TEST(GfsimSystemTest, StaticDispatchUsesDenseStableOrderAndBarrierPhases) { + for (unsigned seed = 0; seed < 32; ++seed) { + SimSystem system("test"); + std::vector log; + RecordingDispatchObject first(0, log); + RecordingDispatchObject second(1, log); + std::array rows = {makeDispatchRow(&first), makeDispatchRow(&second)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + + std::array insertionOrder = {0, 1}; + std::mt19937 random(seed); + std::shuffle(insertionOrder.begin(), insertionOrder.end(), random); + for (ObjectId id : insertionOrder) + EXPECT_TRUE(system.scheduleWork(id, {0, 0})); + + system.step(); + EXPECT_EQ(log, + (std::vector{"work:0", "work:1", "arbitrate:0", + "arbitrate:1", "xfer:0", "xfer:1"})); + } +} + +TEST(GfsimSystemTest, StaticDispatchRejectsNonDenseRows) { + SimSystem system("test"); + std::vector log; + RecordingDispatchObject object(1, log); + std::array rows = {makeDispatchRow(&object)}; + EXPECT_FALSE(system.setDispatchTable(rows)); + EXPECT_EQ(system.terminationResult().classification, + TerminationClass::Failed); + EXPECT_EQ(system.terminationResult().diagnosticCode, + "invalid_dispatch_table"); +} + +TEST(GfsimSystemTest, StaticDispatchRejectsMismatchedObjectKind) { + SimSystem system("test"); + std::vector log; + RecordingDispatchObject object(0, log); + std::array rows = {makeDispatchRow(&object)}; + rows.front().kind = ObjectKind::Sink; + EXPECT_FALSE(system.setDispatchTable(rows)); + EXPECT_EQ(system.terminationResult().diagnosticCode, + "invalid_dispatch_table"); +} + +TEST(GfsimSystemTest, RegistryAndDispatchCannotShadowOneStableObjectId) { + SimSystem system("test"); + std::vector log; + RecordingDispatchObject registered(0, log); + RecordingDispatchObject dispatched(0, log); + system.registerObject(®istered); + std::array rows = {makeDispatchRow(&dispatched)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Failed); + EXPECT_EQ(result.diagnosticCode, "duplicate_object_identity"); +} + +TEST(GfsimSystemTest, StaticDispatchResetUsesDenseThunkOrder) { + SimSystem system("test"); + std::vector log; + RecordingDispatchObject first(0, log); + RecordingDispatchObject second(1, log); + std::array rows = {makeDispatchRow(&first), makeDispatchRow(&second)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + system.reset(); + EXPECT_EQ(log, (std::vector{"reset:0", "reset:1"})); +} + +TEST(GfsimSystemTest, ActivationAdjacencyWakesOnlyDeclaredTargetsNextTick) { + SimSystem system("test"); + std::vector workLog; + CommittingDispatchObject source(0, workLog, true); + CommittingDispatchObject target(1, workLog, false); + CommittingDispatchObject inactive(2, workLog, false); + std::array rows = {makeDispatchRow(&source), makeDispatchRow(&target), + makeDispatchRow(&inactive)}; + constexpr std::array offsets = {0, 1, 1, 1}; + constexpr std::array targets = {1}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.setActivationPlan(offsets, targets)); + ASSERT_TRUE(system.scheduleWork(0, {0, 0})); + + EXPECT_TRUE(system.step()); + EXPECT_EQ(system.currentEpoch(), (Epoch{1, 0})); + EXPECT_EQ(workLog, (std::vector{0})); + EXPECT_EQ(source.commitCount, 1u); + + EXPECT_FALSE(system.step()); + EXPECT_EQ(workLog, (std::vector{0, 1})); + EXPECT_EQ(std::count(workLog.begin(), workLog.end(), 2), 0); +} + +TEST(GfsimSystemTest, ActivationPlanRejectsNonCanonicalTargets) { + SimSystem system("test"); + std::vector workLog; + CommittingDispatchObject first(0, workLog, false); + CommittingDispatchObject second(1, workLog, false); + std::array rows = {makeDispatchRow(&first), makeDispatchRow(&second)}; + constexpr std::array offsets = {0, 2, 2}; + constexpr std::array targets = {1, 1}; + ASSERT_TRUE(system.setDispatchTable(rows)); + EXPECT_FALSE(system.setActivationPlan(offsets, targets)); + EXPECT_EQ(system.terminationResult().diagnosticCode, + "invalid_activation_plan"); +} + +TEST(GfsimSystemTest, WorkPhaseReadsOneImmutableCommittedSnapshot) { + SimSystem system("test"); + uint64_t committedState = 0; + SnapshotWriter writer(0, committedState); + SnapshotReader reader(1, committedState); + std::array rows = {makeDispatchRow(&writer), makeDispatchRow(&reader)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.scheduleWork(0, {0, 0})); + ASSERT_TRUE(system.scheduleWork(1, {0, 0})); + + system.step(); + EXPECT_EQ(reader.observed, (std::vector{0})); + EXPECT_EQ(committedState, 1u); +} + +TEST(GfsimSystemTest, SameTimeFutureDeltaEventRemainsPending) { + SimSystem system("test"); + std::vector log; + RecordingDispatchObject object(0, log); + std::array rows = {makeDispatchRow(&object)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.scheduleEvent({{0, 2}, 0, 0, 0})); + + EXPECT_TRUE(system.step()); + EXPECT_EQ(system.currentEpoch(), (Epoch{0, 2})); + ASSERT_TRUE(system.nextEvent()); + EXPECT_EQ(system.nextEvent()->readyTime, (Epoch{0, 2})); + + system.step(); + EXPECT_EQ(log.front(), "work:0"); + EXPECT_FALSE(system.nextEvent()); +} + +TEST(GfsimSystemTest, SchedulingBeforeCommittedEpochFails) { + SimSystem system("test"); + ASSERT_TRUE(system.scheduleEvent({{1, 0}, kSystemObjectId, 0, 0})); + ASSERT_TRUE(system.step()); + ASSERT_EQ(system.currentEpoch(), (Epoch{1, 0})); + EXPECT_FALSE(system.scheduleEvent({{0, 0}, kSystemObjectId, 0, 0})); + EXPECT_TRUE(system.isTerminated()); + EXPECT_EQ(system.terminationResult().classification, + TerminationClass::Failed); + EXPECT_EQ(system.terminationResult().diagnosticCode, + "event_before_current_epoch"); +} + +TEST(GfsimSystemTest, DeltaCycleLimitFailsAtExactBoundary) { + SimSystem system("test"); + SelfWakingDispatchObject object(0, system); + std::array rows = {makeDispatchRow(&object)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.scheduleWork(0, {0, 0})); + + while (system.step()) { + } + EXPECT_EQ(system.terminationResult().classification, + TerminationClass::Failed); + EXPECT_EQ(system.terminationResult().diagnosticCode, "max_deltas_exceeded"); + EXPECT_EQ(system.currentEpoch().delta, kMaxDeltasPerTick - 1); + EXPECT_EQ(object.workCount, kMaxDeltasPerTick); +} + +TEST(GfsimSystemTest, StatefulObjectCannotCommitTwiceInOneTick) { + SimSystem system("test"); + RepeatedCommitDispatchObject object(0, system); + std::array rows = {makeDispatchRow(&object)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.scheduleWork(0, {0, 0})); + + EXPECT_TRUE(system.step()); + EXPECT_EQ(system.currentEpoch(), (Epoch{0, 1})); + EXPECT_FALSE(system.step()); + EXPECT_EQ(system.terminationResult().classification, + TerminationClass::Failed); + EXPECT_EQ(system.terminationResult().diagnosticCode, + "multiple_stateful_commits"); +} + +TEST(GfsimSystemTest, EventQueueCannotCommitTwiceInOneTick) { + SimSystem system("test"); + SameTickEventChainObject object(0, system); + std::array rows = {makeDispatchRow(&object)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.scheduleWork(0, {0, 0})); + + EXPECT_TRUE(system.step()); + EXPECT_EQ(system.currentEpoch(), (Epoch{0, 1})); + EXPECT_FALSE(system.step()); + EXPECT_EQ(system.terminationResult().diagnosticCode, + "multiple_stateful_commits"); +} + +TEST(GfsimSystemTest, SchedulingOutOfRangeDeltaFailsImmediately) { + SimSystem system("test"); + EXPECT_FALSE( + system.scheduleEvent({{1, kMaxDeltasPerTick}, kSystemObjectId, 0, 0})); + EXPECT_EQ(system.terminationResult().classification, + TerminationClass::Failed); + EXPECT_EQ(system.terminationResult().diagnosticCode, "max_deltas_exceeded"); +} + +TEST(GfsimSystemTest, SchedulingUnknownEventTargetFailsImmediately) { + SimSystem system("test"); + EXPECT_FALSE(system.scheduleEvent({{1, 0}, 999, 0, 0})); + EXPECT_EQ(system.terminationResult().classification, + TerminationClass::Failed); + EXPECT_EQ(system.terminationResult().diagnosticCode, "unknown_event_target"); +} + +TEST(GfsimSystemTest, EmptySystemCompletesImmediately) { + SimSystem sys("test"); + auto result = sys.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed); + EXPECT_EQ(result.finalEpoch.time, 0u); + EXPECT_EQ(result.finalEpoch.delta, 0u); +} + +TEST(GfsimSystemTest, MaxTicksCapTriggersIncomplete) { + SimSystem sys("test"); + sys.setMaxTicks(3); + sys.scheduleEvent({{100, 0}, kSystemObjectId, 0, 0}); + auto result = sys.run(); + EXPECT_EQ(result.classification, TerminationClass::Incomplete); + EXPECT_EQ(result.diagnosticCode, "max_ticks_reached"); +} + +TEST(GfsimSystemTest, MaxTicksCapDoesNotJumpToAFutureEvent) { + SimSystem system("test"); + system.setMaxTicks(3); + ASSERT_TRUE(system.scheduleEvent({{100, 0}, kSystemObjectId, 0, 0})); + + TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Incomplete); + EXPECT_EQ(result.finalEpoch, (Epoch{3, 0})); + EXPECT_EQ(result.terminationCap, 3u); + ASSERT_TRUE(system.nextEvent()); + EXPECT_EQ(system.nextEvent()->readyTime, (Epoch{100, 0})); +} + +TEST(GfsimSystemTest, MaxEventsCapTriggersIncomplete) { + SimSystem sys("test"); + sys.setMaxEvents(0); + auto result = sys.run(); + EXPECT_EQ(result.classification, TerminationClass::Incomplete); +} + +TEST(GfsimSystemTest, MaxEventsCapCannotBeExceededWithinOneEpoch) { + SimSystem system("test"); + std::vector log; + RecordingDispatchObject object(0, log); + std::array rows = {makeDispatchRow(&object)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.scheduleEvent({{0, 0}, 0, 0, 0})); + ASSERT_TRUE(system.scheduleEvent({{0, 0}, 0, 0, 1})); + system.setMaxEvents(1); + + EXPECT_FALSE(system.step()); + EXPECT_EQ(system.terminationResult().classification, + TerminationClass::Incomplete); + EXPECT_EQ(system.terminationResult().diagnosticCode, "max_events_reached"); + EXPECT_EQ(system.terminationResult().committedEventCount, 1u); + EXPECT_TRUE(system.nextEvent()); +} + +TEST(GfsimSystemTest, ObjectRegistryLookup) { + SimSystem sys("test"); + sys.registerObject(&sys.root()); + EXPECT_EQ(sys.lookup(kSystemObjectId), &sys); + EXPECT_EQ(sys.lookup(999), nullptr); +} + +TEST(GfsimSystemTest, DeterministicEmptyRuns) { + SimSystem sys1("a"), sys2("a"); + auto r1 = sys1.run(); + auto r2 = sys2.run(); + EXPECT_EQ(r1.classification, r2.classification); + EXPECT_EQ(r1.finalEpoch, r2.finalEpoch); +} + +TEST(GfsimSystemTest, ScheduleEventAndCommitThenQuery) { + SimSystem sys("test"); + sys.scheduleEvent({{5, 0}, kSystemObjectId, 0, 0}); + // Step once to commit the event into the event queue + bool hasWork = sys.step(); + (void)hasWork; + auto next = sys.nextEvent(); + ASSERT_TRUE(next.has_value()); + EXPECT_EQ(next->readyTime.time, 5u); +} + +TEST(GfsimSystemTest, SystemCanBeReset) { + SimSystem sys("test"); + sys.scheduleEvent({{5, 0}, kSystemObjectId, 0, 0}); + sys.run(); + EXPECT_TRUE(sys.isTerminated()); + sys.reset(); + EXPECT_FALSE(sys.isTerminated()); + EXPECT_EQ(sys.currentEpoch(), (Epoch{0, 0})); + EXPECT_FALSE(sys.nextEvent()); +} + +TEST(GfsimSystemTest, ResetIncludesRegisteredObjectsOutsideTheRootTree) { + SimSystem system("test"); + Statistic statistic("counter", 1, nullptr, StatisticKind::Counter); + ASSERT_TRUE(statistic.proposeAdd(4)); + statistic.doXfer({0, 0}); + system.registerObject(&statistic); + ASSERT_EQ(statistic.snapshot().value, 4u); + + system.reset(); + EXPECT_EQ(statistic.snapshot().value, 0u); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Process runtime +// ═══════════════════════════════════════════════════════════════════════ + +class SuspendingProcess final : public ProcessRuntime { +public: + SuspendingProcess() : ProcessRuntime("process", 0, nullptr, 0, 4) {} + + ProcessStep executeProcessStep(uint32_t pc, Epoch) { + ++stepCount; + switch (pc) { + case 0: + return ProcessStep::continueAt(1); + case 1: + return ProcessStep::suspendAt( + 2, {.kind = ProcessWakeKind::EventQueue, .id = 7}, 42); + default: + return ProcessStep::terminate(); + } + } + + uint64_t stepCount = 0; +}; + +class UnboundedProcess final : public ProcessRuntime { +public: + UnboundedProcess() : ProcessRuntime("unbounded", 0, nullptr, 0, 3) {} + + ProcessStep executeProcessStep(uint32_t pc, Epoch) { + ++stepCount; + return ProcessStep::continueAt(pc); + } + + uint64_t stepCount = 0; +}; + +class YieldingProcess final : public ProcessRuntime { +public: + YieldingProcess() : ProcessRuntime("yielding", 0, nullptr, 0, 2) {} + + ProcessStep executeProcessStep(uint32_t, Epoch) { + return ProcessStep::suspendAt( + 0, {.kind = ProcessWakeKind::NextDelta, .id = 0}, 1); + } +}; + +class InvalidContinuationProcess final + : public ProcessRuntime { +public: + InvalidContinuationProcess() : ProcessRuntime("invalid", 0, nullptr, 0, 1) {} + + ProcessStep executeProcessStep(uint32_t, Epoch) { + return ProcessStep::suspendAt( + 1, {.kind = ProcessWakeKind::Condition, .id = 1}, 0); + } +}; + +TEST(GfsimProcessTest, SuspensionCommitsContinuationAndRequiresExactWake) { + SuspendingProcess process; + EXPECT_TRUE(process.isRunnable({0, 0})); + process.doWork({0, 0}); + EXPECT_EQ(process.stepCount, 2u); + EXPECT_EQ(process.pc(), 0u); + EXPECT_TRUE(process.hasPendingCommit()); + process.doXfer({0, 0}); + + EXPECT_EQ(process.status(), ProcessStatus::Suspended); + EXPECT_EQ(process.pc(), 2u); + EXPECT_EQ(process.continuationId(), 42u); + EXPECT_FALSE(process.wake({ProcessWakeKind::EventQueue, 8}, 42)); + EXPECT_FALSE(process.wake({ProcessWakeKind::EventQueue, 7}, 41)); + EXPECT_TRUE(process.wake({ProcessWakeKind::EventQueue, 7}, 42)); + EXPECT_TRUE(process.isRunnable({1, 0})); + + process.doWork({1, 0}); + process.doXfer({1, 0}); + EXPECT_EQ(process.status(), ProcessStatus::Terminated); +} + +TEST(GfsimProcessTest, FairnessCapProducesDeterministicFailure) { + UnboundedProcess process; + process.doWork({0, 0}); + EXPECT_EQ(process.stepCount, 3u); + EXPECT_TRUE(process.hasPendingCommit()); + process.doXfer({0, 0}); + EXPECT_EQ(process.status(), ProcessStatus::Failed); + EXPECT_EQ(process.diagnosticCode(), "process_fairness_exceeded"); + EXPECT_FALSE(process.isRunnable({1, 0})); +} + +TEST(GfsimProcessTest, TraceEndTerminatesOnlyVoluntaryYieldSuspension) { + { + SimSystem system("test"); + YieldingProcess process; + TraceSource<> trace("trace", 1, nullptr); + std::array rows = {makeDispatchRow(&process), makeDispatchRow(&trace)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Completed); + EXPECT_EQ(result.finalEpoch, (Epoch{1, 0})); + EXPECT_EQ(process.status(), ProcessStatus::Terminated); + } + { + SimSystem system("test"); + SuspendingProcess process; + TraceSource<> trace("trace", 1, nullptr); + std::array rows = {makeDispatchRow(&process), makeDispatchRow(&trace)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + + const TerminationResult result = system.run(); + EXPECT_EQ(result.classification, TerminationClass::Failed); + EXPECT_EQ(result.diagnosticCode, "no_progress"); + EXPECT_EQ(process.status(), ProcessStatus::Suspended); + } +} + +TEST(GfsimProcessTest, ResetRestoresEntryState) { + SuspendingProcess process; + process.doWork({0, 0}); + process.doXfer({0, 0}); + ASSERT_EQ(process.status(), ProcessStatus::Suspended); + process.reset(); + EXPECT_EQ(process.status(), ProcessStatus::Runnable); + EXPECT_EQ(process.pc(), 0u); + EXPECT_EQ(process.continuationId(), 0u); + EXPECT_FALSE(process.hasPendingCommit()); +} + +TEST(GfsimProcessTest, InvalidContinuationFailsAtCommitBarrier) { + InvalidContinuationProcess process; + process.doWork({0, 0}); + process.doXfer({0, 0}); + EXPECT_EQ(process.status(), ProcessStatus::Failed); + EXPECT_EQ(process.diagnosticCode(), "invalid_process_continuation"); +} + +TEST(GfsimProcessTest, ProcessFailureTerminatesTheSystem) { + SimSystem system("test"); + UnboundedProcess process; + std::array rows = {makeDispatchRow(&process)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.scheduleWork(0, {0, 0})); + + EXPECT_FALSE(system.step()); + EXPECT_EQ(system.terminationResult().classification, + TerminationClass::Failed); + EXPECT_EQ(system.terminationResult().diagnosticCode, + "process_fairness_exceeded"); +} + +TEST(GfsimProcessTest, SuspendedProcessProducesExactNoProgressSubscription) { + SimSystem system("test"); + SuspendingProcess process; + std::array rows = {makeDispatchRow(&process)}; + ASSERT_TRUE(system.setDispatchTable(rows)); + ASSERT_TRUE(system.scheduleWork(0, {0, 0})); + + EXPECT_FALSE(system.step()); + EXPECT_EQ(system.terminationResult().diagnosticCode, "no_progress"); + NoProgressReport report = system.noProgressReport(); + ASSERT_EQ(report.blockedObjects.size(), 1u); + EXPECT_EQ(report.blockedObjects[0].reason, "process_suspended"); + EXPECT_EQ(report.blockedObjects[0].subscriptions, + (std::vector{"event_queue:7"})); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Integration +// ═══════════════════════════════════════════════════════════════════════ + +TEST(GfsimIntegrationTest, ComputeToSinkPipeline) { + auto comp = std::make_unique>( + "adder", 10, nullptr); + auto *cptr = comp.get(); + + auto snk = std::make_unique>("sink", 11, nullptr); + auto *sptr = snk.get(); + + cptr->setInput(21); + cptr->doWork({0, 0}); + cptr->doXfer({0, 0}); + sptr->receive(cptr->output()); + sptr->doXfer({0, 0}); + + ASSERT_EQ(sptr->received().size(), 1u); + EXPECT_EQ(sptr->received()[0], 42u); +} + +TEST(GfsimIntegrationTest, QueueProducerConsumer) { + SimQueue q("fifo", 1, nullptr, 5); + q.proposePush(10); + q.proposePush(20); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + auto v1 = q.proposePop(); + auto v2 = q.proposePop(); + auto v3 = q.proposePop(); + q.doArbitrate({0, 0}); + q.doXfer({0, 0}); + ASSERT_TRUE(v1.has_value()); + EXPECT_EQ(*v1, 10); + ASSERT_TRUE(v2.has_value()); + EXPECT_EQ(*v2, 20); + EXPECT_FALSE(v3.has_value()); + EXPECT_TRUE(q.isEmpty()); +} + +TEST(GfsimIntegrationTest, ResourceProducerConsumer) { + Resource r("r", 1, nullptr, 3); + EXPECT_TRUE(r.proposeReserve(100, 2, {0, 0}, 1)); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + EXPECT_EQ(r.activeReservations(), 2u); + EXPECT_TRUE(r.proposeReserve(200, 2, {0, 0}, 2)); + r.doArbitrate({0, 0}); + r.doXfer({0, 0}); + EXPECT_FALSE(r.hasReservation(2)); + EXPECT_EQ(r.rejectedTransactions(), (std::vector{2})); + r.proposeRelease(100, 1); + r.doXfer({0, 0}); + EXPECT_EQ(r.activeReservations(), 1u); +} + +TEST(GfsimIntegrationTest, MemoryToComputeToSinkChain) { + Memory<> mem("mem", 1, nullptr, 4); + Compute comp("comp", 2, nullptr); + Sink<> snk("snk", 3, nullptr); + + mem.proposeWrite(0, 5); + mem.proposeWrite(1, 7); + mem.doXfer({0, 0}); + + comp.setInput(mem.read(0)); + comp.doWork({0, 0}); + comp.doXfer({0, 0}); + + snk.receive(comp.output()); + snk.doXfer({0, 0}); + + EXPECT_EQ(snk.received()[0], 8u); +} + +} // namespace +} // namespace gfsim diff --git a/docs/acir/agentic-circuit-collaboration.md b/docs/acir/agentic-circuit-collaboration.md new file mode 100644 index 00000000..3d3da537 --- /dev/null +++ b/docs/acir/agentic-circuit-collaboration.md @@ -0,0 +1,34 @@ +# Agentic Circuit Collaboration Migration + +Source repository: `PTO-ISA/agentic-circuit` +Inventory baseline: `756002e2998b11dfe1fed14dc3d63cdad8be694c` + +## Open pull requests + +| Source | Title | Source head | Migration disposition | +| --- | --- | --- | --- | +| [#18](https://github.com/PTO-ISA/agentic-circuit/pull/18) | Popcount and round-robin ACIR-to-PYC lowering | `feature/issue11-pyc-phase1-wsl` | Source head preserved as `agentic-circuit/pr-18-head`; [pyCircuit issue #6](https://github.com/PTO-ISA/pyCircuit/issues/6) records the pyc6 replacement and superseded disposition | +| [#23](https://github.com/PTO-ISA/agentic-circuit/pull/23) | ACIR-to-C++ generation pipeline | `xiekunpeng:feature/acir-emit-cxx` | Four unique commits migrated with authorship to draft [pyCircuit PR #5](https://github.com/PTO-ISA/pyCircuit/pull/5); source head preserved as `agentic-circuit/pr-23-head` | + +Source PRs remain open until the target-side closure conditions are satisfied. + +## Open issues + +| Source | Title | Target | +| --- | --- | --- | +| [source #7](https://github.com/PTO-ISA/agentic-circuit/issues/7) | Generated C++ embeds full SHA-256 in names | Transferred to [pyCircuit #7](https://github.com/PTO-ISA/pyCircuit/issues/7) with `area:acir` | +| [source #8](https://github.com/PTO-ISA/agentic-circuit/issues/8) | Add atomic `ac.try_transfer` primitive | Transferred to [pyCircuit #8](https://github.com/PTO-ISA/pyCircuit/issues/8) with `area:acir` | +| [source #14](https://github.com/PTO-ISA/agentic-circuit/issues/14) | Parameterized blocks, SimQueue ABI and provider specialization | Transferred to [pyCircuit #9](https://github.com/PTO-ISA/pyCircuit/issues/9) with `area:acir` | +| [source #26](https://github.com/PTO-ISA/agentic-circuit/issues/26) | Suggested ACPy/ACIR table design | Transferred to [pyCircuit #10](https://github.com/PTO-ISA/pyCircuit/issues/10) with `area:acir` | +| [source #27](https://github.com/PTO-ISA/agentic-circuit/issues/27) | Correlated messages across independent queues | Transferred to [pyCircuit #11](https://github.com/PTO-ISA/pyCircuit/issues/11) with `area:acir` | + +## Branch and access audit + +- Preserve `agentic-circuit/import-0.3` at the source main baseline. +- Record every source branch as imported, migrated, superseded, or intentionally + retained before repository closure. +- Revoke old Actions, environments, deploy keys, webhooks and publishing + credentials after the final gate bundle passes. +- Remove teams and outside collaborators before changing visibility. +- Verify effective private-repository access after cutover and attach the audit + result to the final migration evidence. diff --git a/docs/acir/index.md b/docs/acir/index.md new file mode 100644 index 00000000..bcea231a --- /dev/null +++ b/docs/acir/index.md @@ -0,0 +1,63 @@ +# Agentic Circuit and ACIR + +Agentic Circuit is the architecture-modeling frontend hosted in the pyCircuit +repository. It retains its own Python distribution, import namespace, command +line interface, MLIR dialect, schemas, and simulator backend while sharing one +repository, release authority, LLVM/MLIR toolchain, and review process with +pyCircuit 6. + +## Contract boundaries + +- `agentic_circuit` captures architecture, process, resource, and queue models. +- ACPy schema `agentic-circuit-acpy` version `0.1`, contract epoch `0.3`, is the + stable frontend interchange contract. +- ACIR (`ac`) is an upper-level MLIR dialect. It remains separate from the PYC + hardware dialect. +- ACSim is the canonical simulator-oriented lowering of ACIR. +- gfsim executes generated ACSim C++ models and remains independent of + `libpyc6_runtime`. +- The synthesizable ACIR subset lowers through PYC and the normal pyCircuit 6 + `pycc` flow to C++ simulation and Verilog. +- Cycle-Aware Signal remains the canonical pyCircuit 6 hardware-authoring + model. ACIR-to-PYC adapts to that accepted semantic contract. + +## Source layout + +The imported component is rooted at `components/agentic-circuit/`: + +| Path | Responsibility | +| --- | --- | +| `src/agentic_circuit/` | Python frontend, ACPy, CLI, workspace and JIT APIs | +| `include/acir/`, `lib/` | ACIR/ACSim dialects, analysis, transformations and code generation | +| `include/gfsim/`, `lib/gfsim/` | Architecture simulation runtime | +| `tools/` | `acir-*` tools and AC-to-PYC orchestration | +| `contracts/`, `schemas/`, `resources/` | Machine-readable public contracts | +| `test/`, `tests/`, `unittests/` | MLIR, Python, end-to-end and C++ coverage | +| `docs/spec/` | Detailed ACIR and frontend specification | + +The component directory is an integration boundary, not a second repository. +Its source is versioned and released only from `PTO-ISA/pyCircuit`. + +## Public names + +The two frontend namespaces are intentionally separate: + +```python +import pycircuit +import agentic_circuit +``` + +Do not re-export Agentic Circuit's generic `module`, `process`, `queue`, +`pipeline`, or `memory` names from `pycircuit`. Both frontends may lower toward +PYC, but they do not share a top-level Python API. + +## Verification + +AC changes must run the applicable AC G0/G1/G2 lanes documented in +[Testing and Gates](../development/testing-and-gates.md). Integration changes +must also run the full pyCircuit 6 closure because ACIR-to-PYC consumes current +PYC, `pycc`, and `libpyc6_runtime` contracts. + +Detailed AC specifications remain under +`components/agentic-circuit/docs/spec/`. The pyCircuit 6 specification remains +the authority for PYC and Cycle-Aware Signal semantics. diff --git a/docs/acir/migration.md b/docs/acir/migration.md new file mode 100644 index 00000000..94dfe498 --- /dev/null +++ b/docs/acir/migration.md @@ -0,0 +1,58 @@ +# Agentic Circuit Repository Migration + +Agentic Circuit is being consolidated from +`PTO-ISA/agentic-circuit` into `PTO-ISA/pyCircuit`. The goal is one active +source repository without collapsing the ACIR and PYC semantic layers. + +## Recorded baselines + +| Item | Revision | +| --- | --- | +| pyCircuit import parent | `1f1651f9bff4293deb1613324ab575b3322ab38b` | +| Agentic Circuit import parent | `756002e2998b11dfe1fed14dc3d63cdad8be694c` | +| Import tag | `agentic-circuit/import-0.3` | + +The import is a non-squash merge. Original Agentic Circuit commits remain +reachable, while the imported working tree is namespaced under +`components/agentic-circuit/`. + +The implementation is reviewed in +[pyCircuit PR #4](https://github.com/PTO-ISA/pyCircuit/pull/4). + +## Compatibility commitments + +- Preserve `agentic-circuit` as a distribution and CLI. +- Preserve `agentic_circuit` as the import namespace. +- Preserve ACPy epoch `0.3` and canonical ACIR unless a later accepted decision + records an intentional break. +- Preserve ACIR and ACSim dialect/tool names. +- Replace obsolete external pyCircuit and `libpyc4_runtime` assumptions with + repository-local pyCircuit 6 and `libpyc6_runtime` contracts. +- Keep Cycle-Aware Signal as the canonical pyCircuit 6 hardware frontend. + +## Collaboration migration + +Every open issue, pull request, and source branch receives an explicit target +record. Pull requests cannot be transferred across GitHub repositories, so +their commits, authorship, descriptions, review conclusions, and source links +must be preserved in replacement pyCircuit pull requests or a documented +superseded record. + +The collaboration inventory is maintained in +[`agentic-circuit-collaboration.md`](agentic-circuit-collaboration.md). + +## Cutover rule + +The standalone repository remains active until: + +1. the AC frontend, ACIR/ACSim, gfsim, and ACIR-to-PYC closure passes; +2. the full pyCircuit 6 and required Linx integration closure passes on the + same integrated revision; +3. evidence is archived under one `docs/gates/logs//` bundle; and +4. issues, pull requests, branches, packages, secrets and webhooks have a + recorded disposition. + +After those conditions pass, publishing authority is removed from the old +repository. It is converted to private, direct/team/outside access is removed, +`zhoubot` remains the only explicitly granted repository user (subject to +GitHub organization-owner access), and the repository is archived read-only. diff --git a/docs/development/repository-management.md b/docs/development/repository-management.md index feff0219..a6698675 100644 --- a/docs/development/repository-management.md +++ b/docs/development/repository-management.md @@ -9,9 +9,11 @@ ownership for pyCircuit. | --- | --- | --- | | [`PTO-ISA/pyCircuit`](https://github.com/PTO-ISA/pyCircuit) | Canonical upstream | Product decisions, default branch, releases, packages, documentation, CI policy | | [`LinxISA/pyCircuit`](https://github.com/LinxISA/pyCircuit) | Downstream fork | Linx integration staging and downstream validation | +| `PTO-ISA/agentic-circuit` | Private historical archive after closure | Original issues, pull requests and audit history only; no active development or publishing | The upstream repository is the only source of truth. Do not maintain a second -independent product history in the LinxISA fork. +independent product history in the LinxISA fork or the retired Agentic Circuit +repository. ## Change flow @@ -46,6 +48,7 @@ Only PTO-ISA/pyCircuit may: - create canonical version tags and GitHub releases; - publish the `pycircuit-hisi` package; +- publish the `agentic-circuit` package; - publish canonical compiler or runtime artifacts; and - announce a language, ABI, trace-schema, or toolchain compatibility level. @@ -90,3 +93,20 @@ Historical gate logs and compatibility identifiers may retain earlier version labels. Keep them stable unless an accepted migration decision defines the replacement and compatibility window. Current product documentation must still identify the language as pyCircuit 6. + +## Agentic Circuit retirement + +Agentic Circuit source, ACIR/ACSim, gfsim, tests, schemas and frontend are owned +by the pyCircuit repository under `components/agentic-circuit/`. Do not land new +source changes in the standalone repository after the migration freeze. + +The standalone repository may be made private and archived only after the AC +closure and full PYC closure pass on the integrated revision and all open +collaboration items have a target record. Before visibility changes: + +1. publish a final tombstone linking to the canonical pyCircuit revision; +2. disable Actions, releases, packages, secrets, environments and webhooks; +3. transfer issues and recreate or supersede pull requests with links; +4. remove direct, team and outside-collaborator access; and +5. verify `zhoubot` is the only explicitly granted repository user, subject to + GitHub organization-owner access. diff --git a/docs/development/testing-and-gates.md b/docs/development/testing-and-gates.md index 03cfa4bf..ba262fdd 100644 --- a/docs/development/testing-and-gates.md +++ b/docs/development/testing-and-gates.md @@ -39,6 +39,44 @@ bash flows/scripts/run_semantic_regressions_v6.sh | Examples, testbenches, simulation entrypoint behavior | `pytest tests/unit -m unit`; `pytest tests/system -m system`; `bash flows/scripts/run_examples.sh`; `bash flows/scripts/run_sims.sh`; `bash flows/scripts/run_sims_nightly.sh` | | MLIR dialect, passes, legality, runtime, codegen, trace semantics | `bash flows/scripts/run_examples.sh`; `bash flows/scripts/run_sims.sh`; `bash flows/scripts/run_sims_nightly.sh`; `bash flows/scripts/run_semantic_regressions_v6.sh`; strict decision-status check | | Linx integration changes under `contrib/linx/` or cross-repo interface behavior | Required pyCircuit lanes plus the `linx-pycircuit` mandatory gates | +| Agentic Circuit Python frontend, ACPy, schemas or CLI | AC G0 plus changed-file pre-commit checks | +| ACIR/ACSim dialect, verifier, transformation or gfsim | AC G0 and AC G1 | +| ACIR-to-PYC, pyc6 runtime integration or synthesizable AC semantics | AC G0/G1/G2 plus full pyCircuit closure; add Linx interface/trace lanes when the change touches those contracts | +| Retiring the standalone Agentic Circuit repository | AC G0/G1/G2, full pyCircuit closure, Linx interface/trace lanes, and a current QEMU/PYC comparison | + +## Agentic Circuit gates + +Agentic Circuit uses three stable gate classes. All commands run from the +pyCircuit repository root and use generated output directories outside tracked +source. + +### AC G0: frontend and contracts + +- install/import the `agentic-circuit` distribution from the current worktree; +- validate ACPy epoch `0.3` golden serialization; +- run Python frontend, schema, contract and CLI inventory tests; and +- verify that `agentic_circuit` remains separate from `pycircuit` exports. + +### AC G1: ACIR, ACSim and gfsim + +- build `acir-opt`, ACIR/ACSim libraries and gfsim from the current worktree; +- run ACIR/ACSim parser, printer, verifier and lit suites; +- run the AC C++ unit suites; and +- run at least one ACIR-to-ACSim-to-gfsim end-to-end case. + +### AC G2: pyCircuit 6 hardware integration + +- run the synthesizable ACIR subset through ACIR-to-PYC-to-`pycc`; +- execute C++ simulation linked against `libpyc6_runtime`; +- generate and validate Verilog for the same canonical cases; +- compare applicable gfsim, pyc6 C++ and Verilator observations; and +- prove unsupported ACIR constructs fail at the intended verifier boundary. + +AC G2 consumes current pyCircuit 6 contracts. Code merge therefore requires the +full examples, normal/nightly simulation, V6 semantic, and strict +decision-status lanes. The Linx integration lanes become merge gates when their +interfaces or traces change, and remain unconditional operational gates before +the old Agentic Circuit repository is retired. ## When strict decision-status validation is required diff --git a/docs/gates/decision_status_v6.md b/docs/gates/decision_status_v6.md index 2bae107c..6dd7b953 100644 --- a/docs/gates/decision_status_v6.md +++ b/docs/gates/decision_status_v6.md @@ -4,7 +4,9 @@ This table tracks implementation status for all decisions in `docs/rfcs/pyc6-decisions.md`. Decisions 0001–0147 retain the pyc4.0 closure evidence and historical owner labels recorded when those contracts were verified. Decision 0010 is superseded by Decision 0148 and is not an active V6 -semantic contract. +semantic contract. Decision 0150 covers repository consolidation and is +verified by the archived AC and PYC closure evidence; retirement of the old +repository remains a separate operational gate. Allowed statuses: @@ -164,3 +166,4 @@ Allowed statuses: | 0147 | implemented-verified | docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json | pyc4-core | Keep gates green; refresh evidence on behavior changes | | 0148 | implemented-verified | tests/unit/test_pyc6_surface.py, tests/unit/test_v6_forward_signal.py, tests/v6/test_v6_vector.py, docs/gates/logs/pyc6-unification/pyc6_migration_summary.md, docs/gates/logs/pyc6-unification/semantic_regressions_summary.json | pyc6-frontend | Keep CycleAwareSignal and automatic cycle balancing gates green | | 0149 | implemented-verified | CMakeLists.txt, runtime/cpp/CMakeLists.txt, tests/unit/test_pyc6_surface.py, tests/system/test_smoke_system.py, docs/gates/logs/pyc6-unification/pyc6_migration_summary.md, docs/gates/logs/pyc6-unification/summary.json | pyc6-runtime | Keep runtime, trace, packaging, and semantic-gate names on V6 | +| 0150 | implemented-verified | docs/gates/logs/20260831-agentic-consolidation-r2/agentic_circuit_summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_nightly_summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/semantic_regressions_summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/migration_closure.md | acir-integration | Keep AC/PYC gates green; old-repository retirement remains blocked on the current QEMU/PYC trace producer or fixture | diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/agentic_circuit_commands.txt b/docs/gates/logs/20260831-agentic-consolidation-r2/agentic_circuit_commands.txt new file mode 100644 index 00000000..498d023e --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/agentic_circuit_commands.txt @@ -0,0 +1,7 @@ +bash flows/scripts/run_agentic_circuit.sh +python3 -m unittest discover -s components/agentic-circuit/tests/python_frontend -p 'test_*.py' +python3 -m unittest discover -s components/agentic-circuit/tests/cli -p 'test_*.py' +cmake --build components/agentic-circuit/build/dev-llvm22 --target check-acir +ctest --test-dir components/agentic-circuit/build/dev-llvm22 --output-on-failure +bash flows/scripts/pyc build +components/agentic-circuit/tools/ac-queue-pyc-build.py ... diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/agentic_circuit_summary.json b/docs/gates/logs/20260831-agentic-consolidation-r2/agentic_circuit_summary.json new file mode 100644 index 00000000..d26e4331 --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/agentic_circuit_summary.json @@ -0,0 +1,9 @@ +{ + "run_id": "20260831-agentic-consolidation-r2", + "script": "run_agentic_circuit.sh", + "status": "pass", + "lanes": ["AC G0", "AC G1", "AC G2"], + "contract_epoch": "0.3", + "pyc_interface": "pyc6", + "cases": ["arbiter", "atomic-transform", "popcount"] +} diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/commands.txt b/docs/gates/logs/20260831-agentic-consolidation-r2/commands.txt new file mode 100644 index 00000000..9baef9e6 --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/commands.txt @@ -0,0 +1,4 @@ +bash flows/scripts/run_examples.sh +python3 flows/tools/check_api_hygiene.py compiler/frontend/pycircuit designs/examples docs README.md +python3 flows/tools/check_decision_status.py --status docs/gates/decision_status_v6.md --out .pycircuit_out/gates/20260831-agentic-consolidation-r2/decision_status_report.json --require-no-deferred --require-all-verified --require-concrete-evidence --require-existing-evidence +PYC_GATE_RUN_ID=20260831-agentic-consolidation-r2 bash flows/scripts/run_semantic_regressions_v6.sh diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/commands_run_sims.txt b/docs/gates/logs/20260831-agentic-consolidation-r2/commands_run_sims.txt new file mode 100644 index 00000000..cd2e8148 --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/commands_run_sims.txt @@ -0,0 +1,6 @@ +bash flows/scripts/run_sims.sh +env: + PYC_GATE_RUN_ID=20260831-agentic-consolidation-r2 + PYC_SIM_CASE_TIMEOUT_SEC=3600 + PYC_SIM_RETRY_ON_TIMEOUT=1 + PYC_SIM_RESUME_FROM_CASE= diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/commands_run_sims_nightly.txt b/docs/gates/logs/20260831-agentic-consolidation-r2/commands_run_sims_nightly.txt new file mode 100644 index 00000000..e37c3daf --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/commands_run_sims_nightly.txt @@ -0,0 +1,6 @@ +bash flows/scripts/run_sims_nightly.sh +env: + PYC_GATE_RUN_ID=20260831-agentic-consolidation-r2 + PYC_SIM_CASE_TIMEOUT_SEC=3600 + PYC_SIM_RETRY_ON_TIMEOUT=1 + PYC_SIM_RESUME_FROM_CASE= diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/decision_status_report.json b/docs/gates/logs/20260831-agentic-consolidation-r2/decision_status_report.json new file mode 100644 index 00000000..f7f233f9 --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/decision_status_report.json @@ -0,0 +1,1080 @@ +{ + "checks": { + "require_all_verified": true, + "require_concrete_evidence": true, + "require_existing_evidence": true, + "require_no_deferred": true + }, + "decisions": [ + { + "decision": "0001", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0002", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0003", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0004", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0005", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0006", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0007", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0008", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0009", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0010", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Historical closure only; superseded by Decision 0148", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0011", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0012", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0013", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-runtime", + "status": "implemented-verified" + }, + { + "decision": "0014", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-runtime", + "status": "implemented-verified" + }, + { + "decision": "0015", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0016", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0017", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0018", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0019", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0020", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0021", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0022", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0023", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0024", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0025", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0026", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0027", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0028", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0029", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0030", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0031", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0032", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0033", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0034", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0035", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0036", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0037", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0038", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0039", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0040", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0041", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0042", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0043", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0044", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0045", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0046", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0047", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0048", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0049", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0050", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0051", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0052", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0053", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0054", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0055", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0056", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0057", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0058", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0059", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0060", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0061", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0062", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0063", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0064", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0065", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0066", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0067", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0068", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0069", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0070", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0071", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0072", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0073", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0074", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0075", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0076", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0077", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0078", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0079", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0080", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0081", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0082", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0083", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0084", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0085", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0086", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0087", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0088", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0089", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0090", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0091", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0092", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0093", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0094", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0095", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0096", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0097", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0098", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0099", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0100", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0101", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0102", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0103", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0104", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0105", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0106", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0107", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0108", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0109", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0110", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0111", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0112", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0113", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0114", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0115", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0116", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0117", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0118", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0119", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0120", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0121", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0122", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0123", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0124", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0125", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0126", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0127", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0128", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0129", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0130", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0131", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-semantic-lane", + "status": "implemented-verified" + }, + { + "decision": "0132", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0133", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0134", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0135", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0136", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0137", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0138", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0139", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0140", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0141", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0142", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0143", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0144", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0145", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0146", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0147", + "evidence": "docs/gates/logs/20260303-closure-v40-r2/summary.json, docs/gates/logs/20260303-closure-v40-r2/decision_status_report.json", + "next_action": "Keep gates green; refresh evidence on behavior changes", + "owner": "pyc4-core", + "status": "implemented-verified" + }, + { + "decision": "0148", + "evidence": "tests/unit/test_pyc6_surface.py, tests/unit/test_v6_forward_signal.py, tests/v6/test_v6_vector.py, docs/gates/logs/pyc6-unification/pyc6_migration_summary.md, docs/gates/logs/pyc6-unification/semantic_regressions_summary.json", + "next_action": "Keep CycleAwareSignal and automatic cycle balancing gates green", + "owner": "pyc6-frontend", + "status": "implemented-verified" + }, + { + "decision": "0149", + "evidence": "CMakeLists.txt, runtime/cpp/CMakeLists.txt, tests/unit/test_pyc6_surface.py, tests/system/test_smoke_system.py, docs/gates/logs/pyc6-unification/pyc6_migration_summary.md, docs/gates/logs/pyc6-unification/summary.json", + "next_action": "Keep runtime, trace, packaging, and semantic-gate names on V6", + "owner": "pyc6-runtime", + "status": "implemented-verified" + }, + { + "decision": "0150", + "evidence": "docs/gates/logs/20260831-agentic-consolidation-r2/agentic_circuit_summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_nightly_summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/semantic_regressions_summary.json, docs/gates/logs/20260831-agentic-consolidation-r2/migration_closure.md", + "next_action": "Keep AC/PYC gates green; old-repository retirement remains blocked on the current QEMU/PYC trace producer or fixture", + "owner": "acir-integration", + "status": "implemented-verified" + } + ], + "deferred_decisions": [], + "empty_evidence_decisions": [], + "extra_decisions": [], + "gap_in_scope_decisions": [], + "invalid_status_decisions": [], + "missing_decisions": [], + "missing_evidence_paths": {}, + "non_verified_decisions": [], + "ok": true, + "placeholder_evidence_decisions": [], + "rfc_path": "/Users/zhoubot/linx-isa/tools/pyCircuit/docs/rfcs/pyc6-decisions.md", + "rows": 150, + "status_counts": { + "deferred": 0, + "gap-in-scope": 0, + "implemented-unverified": 0, + "implemented-verified": 150 + }, + "status_path": "/Users/zhoubot/linx-isa/tools/pyCircuit/docs/gates/decision_status_v6.md", + "total_decisions": 150 +} diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/migration_closure.md b/docs/gates/logs/20260831-agentic-consolidation-r2/migration_closure.md new file mode 100644 index 00000000..cf121a19 --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/migration_closure.md @@ -0,0 +1,66 @@ +# Agentic Circuit Consolidation Closure + +Run ID: `20260831-agentic-consolidation-r2` +Integration revision: `83f7f746` +Decision: 0150 + +## Passed AC closure + +- Agentic Circuit Python frontend: 113 passed, 1 skipped. +- Agentic Circuit CLI: 52 passed. +- ACIR/ACSim lit: 128/128 passed. +- AC CTest: 14/14 passed, including gfsim and workspace E2E. +- AC G2 canonical cases: arbiter, atomic transform, and popcount. +- Each AC G2 case passed ACIR-to-PYC, pyc6 C++ generation/syntax checking, + pyc6 Verilog generation, and Verilator lint. +- The isolated install tree contains `agentic-circuit`, public `acir-*` tools, + ACIR/ACSim/gfsim libraries, and `libpyc6_runtime`. + +Primary command: + +```bash +PYC_GATE_RUN_ID=20260831-agentic-consolidation-r2 \ + bash flows/scripts/run_agentic_circuit.sh +``` + +## Passed PYC closure + +- `pytest tests/unit -m unit`: 51 passed. +- `pytest tests/system -m system`: 3 passed. +- API hygiene: passed. +- `mkdocs build --strict`: passed. +- Examples: passed. +- Normal simulations: passed. +- Nightly simulations: passed. +- V6 semantic regressions: passed. +- Linx CPU pyc6 C++ smoke: passed. +- pyCircuit interface contract 2.0: passed. +- LinxTrace SemVer contract 1.0: passed. + +## Open blocking gate + +The QEMU-versus-pyCircuit trace gate is not closed. + +The current `linx_cpu_pyc` design is a contract smoke and does not execute the +ELF supplied to `run_linx_qemu_vs_pyc.sh`; the gate therefore uses the archived +`MODEL-SCALAR-MCOPY-MSET` pyCircuit trace. That archived trace is semantically +stale against the current QEMU/assembler line: + +- at PC `0x10020`, QEMU reports instruction `0x164`, while the archived PYC + trace records `0x154`; +- a later current instruction is six bytes, while the archived trace advances + by four bytes and has different writeback data. + +Regenerating the PYC trace from QEMU or guessing legacy instruction length, +store/source, trap, or writeback semantics would make the comparison circular +or weaken the gate, so neither workaround is accepted. + +## Retirement decision + +Decision 0150 is `implemented-verified` for repository consolidation, semantic +boundaries, build/install integration, and AC/PYC closure. The standalone +Agentic Circuit repository must nevertheless remain public and active until a +current independent PYC commit-trace producer or reviewed current PYC fixture +makes the QEMU/PYC gate pass. Do not disable publishing authority, change +visibility, close the source PRs, or archive the repository before that gate +passes. diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_nightly_summary.json b/docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_nightly_summary.json new file mode 100644 index 00000000..18954fc6 --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_nightly_summary.json @@ -0,0 +1,7 @@ +{ + "run_id": "20260831-agentic-consolidation-r2", + "script": "run_sims_nightly.sh", + "status": "pass", + "case_timeout_sec": 3600, + "retry_on_timeout": 1 +} diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_summary.json b/docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_summary.json new file mode 100644 index 00000000..3b74c10a --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/run_sims_summary.json @@ -0,0 +1,7 @@ +{ + "run_id": "20260831-agentic-consolidation-r2", + "script": "run_sims.sh", + "status": "pass", + "case_timeout_sec": 3600, + "retry_on_timeout": 1 +} diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/semantic_regressions_summary.json b/docs/gates/logs/20260831-agentic-consolidation-r2/semantic_regressions_summary.json new file mode 100644 index 00000000..c23008c5 --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/semantic_regressions_summary.json @@ -0,0 +1,10 @@ +{ + "run_id": "20260831-agentic-consolidation-r2", + "script": "run_semantic_regressions_v6.sh", + "status": "pass", + "cases": [ + "xz_value_model_smoke", + "reset_invalidate_order_smoke", + "net_resolution_depth_smoke" + ] +} diff --git a/docs/gates/logs/20260831-agentic-consolidation-r2/summary.json b/docs/gates/logs/20260831-agentic-consolidation-r2/summary.json new file mode 100644 index 00000000..326fa89a --- /dev/null +++ b/docs/gates/logs/20260831-agentic-consolidation-r2/summary.json @@ -0,0 +1 @@ +{"run_id":"20260831-agentic-consolidation-r2","script":"run_examples.sh","status":"pass"} diff --git a/docs/legal/AC-RELICENSE-BSD-3-CLAUSE.md b/docs/legal/AC-RELICENSE-BSD-3-CLAUSE.md new file mode 100644 index 00000000..e16cdac8 --- /dev/null +++ b/docs/legal/AC-RELICENSE-BSD-3-CLAUSE.md @@ -0,0 +1,58 @@ +# Agentic Circuit BSD-3-Clause Relicensing Record + +## Purpose + +This record documents the repository-owner direction to relicense Agentic +Circuit from Apache License 2.0 to the BSD 3-Clause License while consolidating +Agentic Circuit into `PTO-ISA/pyCircuit`. + +## Rights statement and direction + +The repository owner has stated that PTO-ISA is the copyright owner for both +Agentic Circuit and pyCircuit and has directed that the consolidated source be +licensed uniformly under BSD-3-Clause. This record captures that owner +direction; it is not a fabricated signature, CLA, or representation that a +particular natural person executed a separate legal instrument. + +The relicensing scope is pinned to these source objects: + +| source | Git object | disposition | +| --- | --- | --- | +| Agentic Circuit `main` import baseline | `756002e2998b11dfe1fed14dc3d63cdad8be694c` | Relicense imported source under BSD-3-Clause | +| Agentic Circuit PR #18 head | `cb037558a6b900f4106f4b756c62b0bb0d56d83b` | Relicense any migrated changes under BSD-3-Clause | +| Agentic Circuit PR #23 head | `cfe048d6923bc69a1985b95f007330279b2fc8d1` | Relicense any migrated changes under BSD-3-Clause | + +The original commits remain reachable for provenance. Replacing license text +does not rewrite their authorship, timestamps, or commit identities. + +## Effective repository policy + +- The root `LICENSE` is the canonical license for the consolidated repository. +- Imported Agentic Circuit source and future contributions are BSD-3-Clause. +- Package metadata, source archives, binary distributions, generated install + trees, and component documentation must identify BSD-3-Clause consistently. +- Any independently licensed third-party dependency or vendored artifact keeps + its own license; this decision does not purport to relicense third-party work. + +## Authorization record + +The repository-owner direction in the migration task is recorded as follows: + +- Authorized representative: `zhoubot` +- PTO-ISA role or authority basis: active PTO-ISA GitHub organization admin +- Approval date: 2026-08-31 +- Approved source scope: Agentic Circuit main baseline and PR #18/#23 heads + listed above, plus their explicitly identified migrated successor commits in + the reviewed pyCircuit migration pull requests + +The organization role was verified through GitHub's organization-membership +API before this record was completed. The migration pull request remains the +reviewable approval record; this document does not invent a handwritten or +cryptographic signature. + +## Verification + +Release review must confirm that active first-party license declarations no +longer name Apache-2.0 or MIT and that produced distributions include the root +BSD-3-Clause license. This record is governance evidence, not evidence that the +ACIR, gfsim, PYC, C++, or Verilog test gates have passed. diff --git a/docs/pyc6-plan.md b/docs/pyc6-plan.md index eecdd422..8c06ba27 100644 --- a/docs/pyc6-plan.md +++ b/docs/pyc6-plan.md @@ -29,8 +29,13 @@ builds, and cosimulation. The active status index is evidence paths. V6 adds Decision 0148: the global CycleAwareSignal design is the primary -authoring model. This supersedes Decision 0010 without invalidating unrelated -the earlier closure evidence. +authoring model. This supersedes Decision 0010 without invalidating the +unrelated earlier closure evidence. + +Decision 0150 adds Agentic Circuit as an independent upper-level ACIR and +frontend in this repository. Its synthesizable path converges on verified PYC +IR and the pyCircuit 6 backends; its ACSim/gfsim architecture-simulation path +remains distinct. ## Milestones @@ -90,6 +95,32 @@ V6 tests plus the examples, simulation, and semantic lanes archived under - [x] Ensure package metadata, documentation URLs, badges, and source links name PTO-ISA/pyCircuit. +### Agentic Circuit consolidation + +**Goal:** Make this repository the only development and release authority for +ACIR, Agentic Circuit, and pyCircuit without weakening the CycleAwareSignal or +PYC semantic contracts. + +- [x] Import the Agentic Circuit `main` history at + `756002e2998b11dfe1fed14dc3d63cdad8be694c` with provenance intact. +- [x] Record PTO-ISA's BSD-3-Clause owner direction and the imported main and + open-PR head objects in `docs/legal/AC-RELICENSE-BSD-3-CLAUSE.md`. +- [x] Migrate the unique work and review disposition from Agentic Circuit PR + #18 and PR #23 into reviewable pyCircuit changes. +- [x] Integrate the `agentic_circuit` distribution, ACPy/ACIR, ACSim/gfsim, + and ACIR-to-PYC targets without merging their Python or MLIR namespaces. +- [x] Replace prior-version pyCircuit runtime references with repo-local + pyCircuit 6 targets and `libpyc6_runtime`. +- [x] Run the ACIR/ACSim verifier and unit lanes, ACIR-to-gfsim execution, and + ACIR-to-PYC-to-C++/Verilog gates from the consolidated checkout. +- [x] Run the existing pyCircuit 6 API, examples, simulation, and semantic + closure lanes from the same checkout. +- [x] Attach gate evidence and promote Decision 0150 to + `implemented-verified` for repository and compiler consolidation. +- [ ] After the independent current QEMU/PYC comparison passes, disable the + old repository's publishing/CI authority and make it + private with only `zhoubot` as a direct repository collaborator. + ## Gate mapping Use the minimum applicable lanes from @@ -102,6 +133,7 @@ Use the minimum applicable lanes from | MLIR semantics or legality | examples, normal and nightly simulations, semantic regressions, strict decision status | | C++ or Verilog behavior | both simulation lanes and backend-equivalence evidence | | Linx integration | pyCircuit lanes plus the Linx interface and model-comparison gates | +| ACIR or Agentic Circuit integration | ACIR/ACSim verifier and unit lanes, ACIR-to-gfsim, ACIR-to-PYC-to-C++/Verilog, plus pyCircuit examples, simulations, and semantic regressions | Use one `PYC_GATE_RUN_ID` for related semantic lanes. Record skipped gates and their risk in the pull request. @@ -115,4 +147,6 @@ The pyCircuit 6 transition is complete when: - supported examples use the V6 CycleAwareSignal contract; - repository metadata and release automation point only to PTO-ISA/pyCircuit; - the LinxISA repository is maintained only as a downstream fork; and +- Decision 0150 has archived AC and PYC closure evidence, and the retired + Agentic Circuit repository has no publishing or CI authority; and - all required gates pass from a clean worktree. diff --git a/docs/rfcs/pyc6-decisions.md b/docs/rfcs/pyc6-decisions.md index 16608d12..15522874 100644 --- a/docs/rfcs/pyc6-decisions.md +++ b/docs/rfcs/pyc6-decisions.md @@ -3382,3 +3382,71 @@ gate documentation. **Source** - pyCircuit 6 repository convergence (2026-08-31). + +## Decision 0150: Agentic Circuit remains a distinct upper-level IR and frontend inside the pyCircuit repository + +**Status:** Accepted + +**Context / Goal** +PTO-ISA is consolidating Agentic Circuit into `PTO-ISA/pyCircuit` so one +repository owns the complete architecture-to-hardware flow. Consolidation must +not collapse architecture/process semantics into the PYC hardware dialect or +create a second timing model that competes with pyCircuit 6. + +**Decision (strong constraint)** +- ACIR remains an independent, upper-level MLIR dialect. Its architecture, + process, and queue operations must not be folded into the `pyc` dialect. +- The `agentic_circuit` and `pycircuit` Python distributions and import + namespaces remain distinct public surfaces in the shared repository. +- The Agentic Circuit frontend continues to emit ACPy and ACIR. Existing ACPy + contract epochs and schemas may change only through an explicit contract + decision and matching compatibility evidence. +- ACSim and gfsim remain the architecture-simulation lowering and runtime path. + They are not aliases for, or implementations inside, `libpyc6_runtime`. +- Synthesizable ACIR lowers into verified PYC IR. ACIR-to-PYC integration must + adapt to pyCircuit 6, `libpyc6_runtime`, and the CycleAwareSignal contract in + Decision 0148; it must not reintroduce prior-version runtime names or bypass + PYC verifiers. +- ACIR-to-PYC and the native pyCircuit frontend share the same verified PYC + semantics and downstream C++ and Verilog backends. +- First-party Agentic Circuit and pyCircuit code in the consolidated repository + is licensed under BSD-3-Clause. The owner-direction record and exact imported + Git objects are maintained in + `docs/legal/AC-RELICENSE-BSD-3-CLAUSE.md`. + +**Migration and retirement rules** +- Preserve the Agentic Circuit `main` history and migrate open PR work with + original commit and author provenance. Each old PR must receive an explicit + migrated, superseded, or rejected disposition; no unique reviewed work is + silently discarded. +- The original Agentic Circuit repository remains active until both the AC + lanes and the existing PYC lanes pass from the consolidated checkout. +- Retirement requires at least ACIR/ACSim parser and verifier coverage, + ACIR-to-gfsim execution, ACIR-to-PYC-to-C++/Verilog coverage, and the normal + pyCircuit 6 frontend, simulation, and semantic-regression gates. +- After those gates pass, disable release, package, and CI authority in the old + repository, make it private, and retain only `zhoubot` as a direct repository + collaborator. PTO-ISA organization owners may retain access inherent to + GitHub organization administration; the repository policy must not claim it + can remove that inherited authority. +- The old repository is provenance only after retirement. New source changes, + issues, releases, and packages belong in `PTO-ISA/pyCircuit`. + +**Compatibility and verification** +- Keeping two Python namespaces does not authorize duplicate semantic + implementations: both hardware paths converge on verified PYC IR. +- C++20 may remain target-local to Agentic Circuit targets during integration; + repository-wide compiler-standard changes require a separate decision. +- Decision 0150 is implemented when the imported source, namespace boundaries, + repo-local pyc6 lowering, build/install contracts, and AC/PYC closure evidence + are present in `PTO-ISA/pyCircuit`. +- Changing the old repository's visibility, credentials, and archive state is + a separate operational cutover. A blocked cutover does not make the merged + compiler implementation unverified; it requires the old repository to remain + active until the operational gate is resolved. + +**Source** +- Repository-owner direction (2026-08-31): consolidate Agentic Circuit into + pyCircuit; retain ACIR and its frontend; use BSD-3-Clause; preserve both + Python namespaces; migrate existing PRs; test AC and PYC before retiring the + old private repository. diff --git a/flows/scripts/pyc b/flows/scripts/pyc index e9fc0aa2..6f3b89ec 100755 --- a/flows/scripts/pyc +++ b/flows/scripts/pyc @@ -21,6 +21,7 @@ Env: LLVM_DIR, MLIR_DIR Optional. If unset, flows/scripts/pyc infers them via llvm-config-22. PYC_BUILD_DIR Optional build directory override. PYC_INSTALL_PREFIX Optional install prefix override. + PYC_BUILD_AGENTIC_CIRCUIT Build/install ACIR, ACSim, gfsim, and AC tools (default: ON). PYCC Path to pycc (overrides auto-detect). EOF } @@ -110,6 +111,8 @@ case "${cmd}" in -DCMAKE_INSTALL_PREFIX="${install_prefix}" -DLLVM_DIR="${LLVM_DIR}" -DMLIR_DIR="${MLIR_DIR}" + -DPYC_BUILD_AGENTIC_CIRCUIT="${PYC_BUILD_AGENTIC_CIRCUIT:-ON}" + -DPYC_BUILD_AGENTIC_CIRCUIT_TESTS=OFF ) if [[ "${MSYSTEM:-}" == MINGW64* || "${uname_s}" == MINGW* || "${uname_s}" == MSYS* ]]; then cmake_args+=( @@ -122,6 +125,10 @@ case "${cmd}" in pyc_log "build pycc + runtime (${build_dir})" ninja -C "${build_dir}" pycc pyc6_runtime + if [[ "${PYC_BUILD_AGENTIC_CIRCUIT:-ON}" == "ON" ]]; then + pyc_log "build integrated ACIR/ACSim/gfsim toolchain" + ninja -C "${build_dir}" all + fi ninja -C "${build_dir}" pyc-opt 2>/dev/null || true cmake --install "${build_dir}" --prefix "${install_prefix}" export PYC_TOOLCHAIN_ROOT="${install_prefix}" diff --git a/flows/scripts/run_agentic_circuit.sh b/flows/scripts/run_agentic_circuit.sh new file mode 100755 index 00000000..fdec7c05 --- /dev/null +++ b/flows/scripts/run_agentic_circuit.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/lib.sh" + +gate_run_id="${PYC_GATE_RUN_ID:-$(date +%Y%m%d-%H%M%S)}" +docs_gate_dir="${PYC_ROOT_DIR}/docs/gates/logs/${gate_run_id}" +gate_out_dir="$(pyc_out_root)/gates/${gate_run_id}/agentic-circuit" +ac_root="${PYC_ROOT_DIR}/components/agentic-circuit" +venv="$(pyc_out_root)/agentic-circuit/venv" +mkdir -p "${docs_gate_dir}" "${gate_out_dir}" "$(dirname "${venv}")" + +exec > >(tee -a "${docs_gate_dir}/agentic_circuit.stdout") \ + 2> >(tee -a "${docs_gate_dir}/agentic_circuit.stderr" >&2) + +cat > "${docs_gate_dir}/agentic_circuit_commands.txt" < ... +EOF + +pyc_log "Agentic Circuit closure run-id=${gate_run_id}" + +resume_from="${AC_GATE_RESUME_FROM:-g0}" +if [[ "${resume_from}" != "g0" && "${resume_from}" != "g2" ]]; then + pyc_die "AC_GATE_RESUME_FROM must be g0 or g2" +fi + +if [[ "${resume_from}" == "g0" ]]; then + if [[ ! -x "${venv}/bin/python" ]]; then + python3 -m venv "${venv}" + fi + "${venv}/bin/python" -m pip install -e "${ac_root}[test]" +fi + +llvm_config="${LLVM_CONFIG:-}" +if [[ -z "${llvm_config}" ]]; then + for candidate in llvm-config-22 llvm-config; do + if command -v "${candidate}" >/dev/null 2>&1; then + llvm_config="$(command -v "${candidate}")" + break + fi + done +fi +if [[ -z "${llvm_config}" ]]; then + for candidate in \ + /opt/homebrew/opt/llvm/bin/llvm-config \ + /usr/local/opt/llvm/bin/llvm-config; do + if [[ -x "${candidate}" ]]; then + llvm_config="${candidate}" + break + fi + done +fi +[[ -n "${llvm_config}" ]] || pyc_die "LLVM 22 llvm-config is required" +[[ "$("${llvm_config}" --version | cut -d. -f1)" == "22" ]] || \ + pyc_die "Agentic Circuit requires LLVM 22" +export LLVM_DIR="${LLVM_DIR:-$("${llvm_config}" --cmakedir)}" +export MLIR_DIR="${MLIR_DIR:-$(dirname "${LLVM_DIR}")/mlir}" + +if [[ "${resume_from}" == "g0" ]]; then + pyc_log "AC G0/G1: configure standalone AC component" + PATH="${venv}/bin:${PATH}" cmake --preset dev-llvm22 \ + -S "${ac_root}" -DACIR_BUILD_TESTING=ON + cmake --build "${ac_root}/build/dev-llvm22" -j "${PYC_BUILD_JOBS:-6}" + + site_packages="$("${venv}/bin/python" -c \ + 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" + ( + cd "${ac_root}" + PYTHONPATH="src:build/dev-llvm22/python" \ + "${venv}/bin/python" -m unittest discover \ + -s tests/python_frontend -p 'test_*.py' + PYTHONPATH="src:build/dev-llvm22/python" \ + "${venv}/bin/python" -m unittest discover \ + -s tests/cli -p 'test_*.py' + PYTHONPATH="${site_packages}" \ + cmake --build build/dev-llvm22 --target check-acir -j "${PYC_BUILD_JOBS:-6}" + ctest --test-dir build/dev-llvm22 --output-on-failure \ + -j "${PYC_TEST_JOBS:-6}" + ) +fi + +pyc_log "AC G2: build and install the repo-local pyc6 + AC toolchain" +gate_toolchain="${gate_out_dir}/toolchain" +PYC_BUILD_DIR="${gate_toolchain}/build" \ +PYC_INSTALL_PREFIX="${gate_toolchain}/install" \ +PYC_BUILD_AGENTIC_CIRCUIT=ON \ + bash "${PYC_ROOT_DIR}/flows/scripts/pyc" build +toolchain="${gate_toolchain}/install" +pycgen="${toolchain}/bin/acir-queue-pycgen" +pycc="${toolchain}/bin/pycc" +metadata="${toolchain}/share/pycircuit/toolchain-metadata.json" +for required in "${pycgen}" "${pycc}" "${metadata}"; do + [[ -f "${required}" ]] || pyc_die "missing integrated toolchain artifact: ${required}" +done + +cxx="$(command -v c++ || true)" +verilator="$(command -v verilator || true)" +[[ -n "${cxx}" ]] || pyc_die "C++ compiler is required for AC G2" +[[ -n "${verilator}" ]] || pyc_die "Verilator is required for AC G2" + +for case_name in arbiter atomic-transform popcount; do + case_dir="${gate_out_dir}/${case_name}" + if [[ -e "${case_dir}" ]]; then + pyc_die "AC G2 output already exists: ${case_dir}" + fi + "${ac_root}/tools/ac-queue-pyc-build.py" \ + "${ac_root}/test/CodeGen/${case_name}.mlir" \ + --pycgen-tool "${pycgen}" \ + --pycc "${pycc}" \ + --toolchain-lock "${ac_root}/toolchains/pyc.lock.json" \ + --toolchain-metadata "${metadata}" \ + --cxx "${cxx}" \ + --verilator "${verilator}" \ + --pyc-output "${case_dir}/model.pyc" \ + --cpp-output-dir "${case_dir}/cpp" \ + --verilog-output-dir "${case_dir}/verilog" \ + --manifest "${case_dir}/manifest.json" +done + +cat > "${docs_gate_dir}/agentic_circuit_summary.json" < tuple[str, str, str]: description="A Python-based hardware description framework that compiles Python to RTL through MLIR", long_description=(ROOT / "README.md").read_text(encoding="utf-8"), long_description_content_type="text/markdown", - license="MIT", + license="BSD-3-Clause", author="PTO-ISA Contributors", url="https://github.com/PTO-ISA/pyCircuit", project_urls={ diff --git a/pyproject.toml b/pyproject.toml index d449df92..29a79211 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ keywords = ["hardware", "rtl", "verilog", "mlir", "hdl", "fpga", "asic"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", diff --git a/tests/unit/test_agentic_circuit_component.py b/tests/unit/test_agentic_circuit_component.py new file mode 100644 index 00000000..a0bb12a3 --- /dev/null +++ b/tests/unit/test_agentic_circuit_component.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import importlib +import sys +from pathlib import Path + +import tomllib + +REPOSITORY = Path(__file__).resolve().parents[2] +AC_ROOT = REPOSITORY / "components" / "agentic-circuit" + + +def test_two_distributions_use_distinct_namespaces_and_bsd_license() -> None: + pyc_metadata = tomllib.loads( + (REPOSITORY / "pyproject.toml").read_text(encoding="utf-8") + )["project"] + ac_metadata = tomllib.loads( + (AC_ROOT / "pyproject.toml").read_text(encoding="utf-8") + )["project"] + + assert pyc_metadata["name"] == "pycircuit-hisi" + assert ac_metadata["name"] == "agentic-circuit" + assert "License :: OSI Approved :: BSD License" in pyc_metadata["classifiers"] + assert ac_metadata["license"] == "BSD-3-Clause" + + sys.path.insert(0, str(AC_ROOT / "src")) + sys.path.insert(0, str(REPOSITORY / "compiler" / "frontend")) + try: + pycircuit = importlib.import_module("pycircuit") + agentic_circuit = importlib.import_module("agentic_circuit") + finally: + del sys.path[:2] + + assert pycircuit.__name__ == "pycircuit" + assert agentic_circuit.__name__ == "agentic_circuit" + assert not hasattr(pycircuit, "agentic_circuit") + assert pycircuit.module is not agentic_circuit.module + + +def test_acpy_contract_epoch_remains_0_3() -> None: + metadata = tomllib.loads((AC_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + assert metadata["tool"]["agentic-circuit"]["contract-epoch"] == "0.3" + + source = (AC_ROOT / "src" / "agentic_circuit" / "_acpy.py").read_text( + encoding="utf-8" + ) + assert 'schema: str = "agentic-circuit-acpy"' in source + assert 'version: str = "0.1"' in source + assert 'contract_epoch: str = "0.3"' in source