diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5ace4600a..c00c619c4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,8 @@ -version: 2 updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" +- directory: / + ignore: + - dependency-name: "github/gh-aw-actions/**" # Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump. + package-ecosystem: github-actions + schedule: + interval: weekly +version: 2 diff --git a/.github/workflows/academic-citation-tracker.md b/.github/workflows/academic-citation-tracker.md index ef6fc7efa..f8a87499c 100644 --- a/.github/workflows/academic-citation-tracker.md +++ b/.github/workflows/academic-citation-tracker.md @@ -36,7 +36,7 @@ safe-outputs: title-prefix: "[Research Trends] " category: "Agentic Workflows" close-older-discussions: true - expires: 60 + expires: 60d missing-tool: create-issue: true noop: @@ -295,4 +295,4 @@ Store for next run: - DO NOT reproduce copyrighted paper text beyond short fair-use quotes. - DO close older Research Trends discussions automatically (configured). - DO always cite sources (arXiv ID, DOI, GitHub URL) so maintainers can verify. -- DO use cache memory to track longitudinal trends across months. +- DO use cache memory to track longitudinal trends across months. \ No newline at end of file diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index f315e8384..5f63c77b8 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -33,7 +33,7 @@ jobs: tar -cvf z3-build-${{ matrix.android-abi }}.tar *.jar *.so - name: Archive production artifacts - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: android-build-${{ matrix.android-abi }} path: build/z3-build-${{ matrix.android-abi }}.tar diff --git a/.github/workflows/api-coherence-checker.md b/.github/workflows/api-coherence-checker.md index 06ea22c3a..7e2ca894c 100644 --- a/.github/workflows/api-coherence-checker.md +++ b/.github/workflows/api-coherence-checker.md @@ -13,12 +13,10 @@ network: defaults tools: cache-memory: true - serena: ["java", "python", "typescript", "csharp"] github: toolsets: [default] bash: [":*"] edit: {} - glob: {} web-search: {} safe-outputs: diff --git a/.github/workflows/build-warning-fixer.md b/.github/workflows/build-warning-fixer.md index b4c785ad4..8caf35cd8 100644 --- a/.github/workflows/build-warning-fixer.md +++ b/.github/workflows/build-warning-fixer.md @@ -5,8 +5,6 @@ on: workflow_dispatch: permissions: read-all tools: - view: {} - glob: {} edit: bash: true safe-outputs: diff --git a/.github/workflows/build-z3-cache.yml b/.github/workflows/build-z3-cache.yml index 428f12af4..a13822ddd 100644 --- a/.github/workflows/build-z3-cache.yml +++ b/.github/workflows/build-z3-cache.yml @@ -45,7 +45,7 @@ jobs: - name: Restore or create cache id: cache-z3 - uses: actions/cache@v5.0.4 + uses: actions/cache@v5.0.5 with: path: | build/z3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf2eeb246..c0b4070a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,8 +83,16 @@ jobs: - name: Checkout code uses: actions/checkout@v6.0.2 + - name: Select Python + run: | + # Use the first available manylinux interpreter for deterministic selection. + PYTHON=$(printf '%s\n' /opt/python/*/bin/python | sort -V | head -n1) + test -x "$PYTHON" || { echo "Error: no interpreter found under /opt/python/*/bin/python"; exit 1; } + echo "PYTHON=$PYTHON" >> "$GITHUB_ENV" + "$PYTHON" --version + - name: Setup Python virtual environment - run: "/opt/python/cp38-cp38/bin/python -m venv $PWD/env" + run: "$PYTHON -m venv $PWD/env" - name: Install build dependencies run: | @@ -123,8 +131,16 @@ jobs: mkdir -p /tmp/arm-toolchain/ tar xf /tmp/arm-toolchain.tar.xz -C /tmp/arm-toolchain/ --strip-components=1 + - name: Select Python + run: | + # Use the first available manylinux interpreter for deterministic selection. + PYTHON=$(printf '%s\n' /opt/python/*/bin/python | sort -V | head -n1) + test -x "$PYTHON" || { echo "Error: no interpreter found under /opt/python/*/bin/python"; exit 1; } + echo "PYTHON=$PYTHON" >> "$GITHUB_ENV" + "$PYTHON" --version + - name: Setup Python virtual environment - run: "/opt/python/cp38-cp38/bin/python -m venv $PWD/env" + run: "$PYTHON -m venv $PWD/env" - name: Install build dependencies run: | @@ -315,7 +331,7 @@ jobs: - name: Setup Julia (if needed) if: matrix.name == 'debugClang' - uses: julia-actions/setup-julia@v2 + uses: julia-actions/setup-julia@v3 with: version: '1' diff --git a/.github/workflows/code-conventions-analyzer.md b/.github/workflows/code-conventions-analyzer.md index 8264296c3..5ec27678e 100644 --- a/.github/workflows/code-conventions-analyzer.md +++ b/.github/workflows/code-conventions-analyzer.md @@ -8,8 +8,6 @@ tools: cache-memory: true github: toolsets: [default] - view: {} - glob: {} edit: {} bash: - "clang-format --version" diff --git a/.github/workflows/code-simplifier.lock.yml b/.github/workflows/code-simplifier.lock.yml index 3bc57a0a0..3c67f2c46 100644 --- a/.github/workflows/code-simplifier.lock.yml +++ b/.github/workflows/code-simplifier.lock.yml @@ -1,4 +1,5 @@ -# +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"a50e58358dfcf5d6e6e0b92707925040a7b1ad4f29c08cfce11cfa8c9756f876","compiler_version":"v0.71.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.71.5","version":"v0.71.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40","digest":"sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40","digest":"sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40","digest":"sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -13,9 +14,9 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.57.2). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.71.5). DO NOT EDIT. # -# To update this file, edit github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b and run: +# To update this file, edit github/gh-aw/.github/workflows/code-simplifier.md@6762bfba6ae426a03aac46e8f68701461c667404 and run: # gh aw compile # Not all edits will cause changes to this file. # @@ -23,17 +24,45 @@ # # Analyzes recently modified code and creates pull requests with simplifications that improve clarity, consistency, and maintainability while preserving functionality # -# Source: github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b +# Source: github/gh-aw/.github/workflows/code-simplifier.md@6762bfba6ae426a03aac46e8f68701461c667404 # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"6f3bad47dff7f3f86460672a86abd84130d8a7dee19358ef3391e3faf65f4857","compiler_version":"v0.57.2","strict":true} +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@v0.77.0 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 +# - ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c +# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f name: "Code Simplifier" "on": schedule: - - cron: "27 13 * * *" + - cron: "10 4 * * *" # Friendly format: daily (scattered) # skip-if-match: is:pr is:open in:title "[code-simplifier]" # Skip-if-match processed as search check in pre-activation job workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string permissions: {} @@ -48,44 +77,58 @@ jobs: if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: + actions: read contents: read outputs: comment_id: "" comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5 + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 with: - destination: /opt/gh-aw/actions + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Code Simplifier" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/code-simplifier.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} - GH_AW_INFO_VERSION: "" - GH_AW_INFO_AGENT_VERSION: "latest" - GH_AW_INFO_CLI_VERSION: "v0.57.2" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_AGENT_VERSION: "1.0.40" + GH_AW_INFO_CLI_VERSION: "v0.71.5" GH_AW_INFO_WORKFLOW_NAME: "Code Simplifier" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_ALLOWED_DOMAINS: '["go"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.23.0" + GH_AW_INFO_AWF_VERSION: "v0.25.40" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { main } = require('/opt/gh-aw/actions/generate_aw_info.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); await main(core, context); - name: Validate COPILOT_GITHUB_TOKEN secret id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Checkout .github and .agents folders @@ -95,22 +138,46 @@ jobs: sparse-checkout: | .github .agents + .claude + .codex + .crush + .gemini + .opencode + .pi sparse-checkout-cone-mode: true fetch-depth: 1 - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_FILE: "code-simplifier.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.71.5" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); await main(); - name: Create prompt with built-in context env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} @@ -119,20 +186,27 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec run: | - bash /opt/gh-aw/actions/create_prompt_first.sh + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_EOF' + cat << 'GH_AW_PROMPT_b6435d392c5b00fb_EOF' - GH_AW_PROMPT_EOF - cat "/opt/gh-aw/prompts/xpia.md" - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" - cat "/opt/gh-aw/prompts/markdown.md" - cat "/opt/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_EOF' + GH_AW_PROMPT_b6435d392c5b00fb_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_b6435d392c5b00fb_EOF' - Tools: create_issue, missing_tool, missing_data, noop + Tools: create_pull_request, missing_tool, missing_data, noop + GH_AW_PROMPT_b6435d392c5b00fb_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat << 'GH_AW_PROMPT_b6435d392c5b00fb_EOF' + GH_AW_PROMPT_b6435d392c5b00fb_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_b6435d392c5b00fb_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -161,28 +235,28 @@ jobs: {{/if}} - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' + GH_AW_PROMPT_b6435d392c5b00fb_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_b6435d392c5b00fb_EOF' - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' {{#runtime-import .github/workflows/code-simplifier.md}} - GH_AW_PROMPT_EOF + GH_AW_PROMPT_b6435d392c5b00fb_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); await main(); - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_GITHUB_ACTOR: ${{ github.actor }} @@ -193,13 +267,14 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); // Call the substitution function return await substitutePlaceholders({ @@ -213,25 +288,32 @@ jobs: GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED } }); - name: Validate prompt placeholders env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - name: Upload activation artifact if: success() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation + include-hidden-files: true path: | /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + if-no-files-found: ignore retention-days: 1 agent: @@ -249,320 +331,247 @@ jobs: GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json GH_AW_WORKFLOW_ID_SANITIZED: codesimplifier outputs: + agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} - detection_success: ${{ steps.detection_conclusion.outputs.success }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} + inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5 + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 with: - destination: /opt/gh-aw/actions + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Code Simplifier" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/code-simplifier.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" git config --global am.keepcr true # Re-authenticate git with GitHub token SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - name: Checkout PR branch id: checkout-pr if: | - (github.event.pull_request) || (github.event.issue.pull_request) - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI - run: /opt/gh-aw/actions/install_copilot_cli.sh latest - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.23.0 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | - const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.23.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.23.0 ghcr.io/github/gh-aw-firewall/squid:0.23.0 ghcr.io/github/gh-aw-mcpg:v0.1.8 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine - - name: Write Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config run: | - mkdir -p /opt/gh-aw/safeoutputs + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' - {"create_issue":{"max":1},"missing_data":{},"missing_tool":{},"noop":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_EOF - cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' - [ + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_9974579f3008b6e7_EOF' + {"create_pull_request":{"expires":24,"labels":["refactoring","code-quality","automation"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"reviewers":["copilot"],"title_prefix":"[code-simplifier] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_9974579f3008b6e7_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | { - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead. CONSTRAINTS: Maximum 1 issue(s) can be created. Title will be prefixed with \"[code-simplifier] \". Labels [\"refactoring\" \"code-quality\" \"automation\"] will be automatically added.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate.", - "type": "string" + "description_suffixes": { + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[code-simplifier] \". Labels [\"refactoring\" \"code-quality\" \"automation\"] will be automatically added. Reviewers [\"copilot\"] will be assigned." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "base": { + "type": "string", + "sanitize": true, + "maxLength": 128 }, - "integrity": { - "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", - "type": "string" + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" }, "labels": { - "description": "Labels to categorize the issue (e.g., 'bug', 'enhancement'). Labels must exist in the repository.", - "items": { - "type": "string" - }, - "type": "array" + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 }, - "parent": { - "description": "Parent issue number for creating sub-issues. This is the numeric ID from the GitHub URL (e.g., 42 in github.com/owner/repo/issues/42). Can also be a temporary_id (e.g., 'aw_abc123', 'aw_Test123') from a previously created issue in the same workflow run.", - "type": [ - "number", - "string" - ] - }, - "secrecy": { - "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", - "type": "string" - }, - "temporary_id": { - "description": "Unique temporary identifier for referencing this issue before it's created. Format: 'aw_' followed by 3 to 12 alphanumeric characters (e.g., 'aw_abc1', 'aw_Test123'). Use '#aw_ID' in body text to reference other issues by their temporary_id; these are replaced with actual issue numbers after creation.", - "pattern": "^aw_[A-Za-z0-9]{3,12}$", - "type": "string" + "repo": { + "type": "string", + "maxLength": 256 }, "title": { - "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive.", - "type": "string" + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 } - }, - "required": [ - "title", - "body" - ], - "type": "object" + } }, - "name": "create_issue" - }, - { - "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", - "inputSchema": { - "additionalProperties": false, - "properties": { + "missing_data": { + "defaultMax": 20, + "fields": { "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "integrity": { - "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", - "type": "string" - }, - "reason": { - "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", - "type": "string" - }, - "secrecy": { - "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", - "type": "string" - }, - "tool": { - "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", - "type": "string" - } - }, - "required": [ - "reason" - ], - "type": "object" - }, - "name": "missing_tool" - }, - { - "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "integrity": { - "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", - "type": "string" - }, - "message": { - "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", - "type": "string" - }, - "secrecy": { - "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "name": "noop" - }, - { - "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" + "type": "string", + "sanitize": true, + "maxLength": 256 }, "context": { - "description": "Additional context about the missing data or where it should come from (max 256 characters).", - "type": "string" + "type": "string", + "sanitize": true, + "maxLength": 256 }, "data_type": { - "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", - "type": "string" - }, - "integrity": { - "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").", - "type": "string" + "type": "string", + "sanitize": true, + "maxLength": 128 }, "reason": { - "description": "Explanation of why this data is needed to complete the task (max 256 characters).", - "type": "string" - }, - "secrecy": { - "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").", - "type": "string" + "type": "string", + "sanitize": true, + "maxLength": 256 } - }, - "required": [], - "type": "object" + } }, - "name": "missing_data" - } - ] - GH_AW_SAFE_OUTPUTS_TOOLS_EOF - cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' - { - "create_issue": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "parent": { - "issueOrPRNumber": true - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "temporary_id": { - "type": "string" - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } } - } - }, - "missing_data": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "context": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "data_type": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "reason": { - "type": "string", - "sanitize": true, - "maxLength": 256 + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } } - } - }, - "missing_tool": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 512 - }, - "reason": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "tool": { - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "noop": { - "defaultMax": 1, - "fields": { - "message": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } } } } - } - GH_AW_SAFE_OUTPUTS_VALIDATION_EOF + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); - name: Generate Safe Outputs MCP Server Config id: safe-outputs-config run: | @@ -585,37 +594,41 @@ jobs: id: safe-outputs-start env: DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs run: | # Environment variables are set above to prevent template injection export DEBUG + export GH_AW_SAFE_OUTPUTS export GH_AW_SAFE_OUTPUTS_PORT export GH_AW_SAFE_OUTPUTS_API_KEY export GH_AW_SAFE_OUTPUTS_TOOLS_PATH export GH_AW_SAFE_OUTPUTS_CONFIG_PATH export GH_AW_MCP_LOG_DIR - bash /opt/gh-aw/actions/start_safe_outputs_server.sh + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - name: Start MCP Gateway id: start-mcp-gateway env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY @@ -625,20 +638,30 @@ jobs: export DEBUG="*" export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.8' + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_14204ee419df0c42_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "container": "ghcr.io/github/github-mcp-server:v1.0.3", "env": { - "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } } }, "safeoutputs": { @@ -646,6 +669,13 @@ jobs: "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", "headers": { "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } } } }, @@ -656,14 +686,28 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + GH_AW_MCP_CONFIG_14204ee419df0c42_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials - run: bash /opt/gh-aw/actions/clean_git_credentials.sh + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): @@ -671,20 +715,26 @@ jobs: run: | set -o pipefail touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","go.dev","golang.org","goproxy.io","host.docker.internal","pkg.go.dev","proxy.golang.org","raw.githubusercontent.com","registry.npmjs.org","storage.googleapis.com","sum.golang.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","google/deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 - sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --skip-pull --enable-api-proxy \ - -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_VERSION: v0.57.2 + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.71.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_REF_NAME: ${{ github.ref_name }} @@ -696,40 +746,28 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] XDG_CONFIG_HOME: /home/runner - - name: Detect inference access error - id: detect-inference-error + - name: Detect Copilot errors + id: detect-copilot-errors if: always() continue-on-error: true - run: bash /opt/gh-aw/actions/detect_inference_access_error.sh + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} run: | git config --global user.email "github-actions[bot]@users.noreply.github.com" git config --global user.name "github-actions[bot]" git config --global am.keepcr true # Re-authenticate git with GitHub token SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - name: Copy Copilot session state files to logs if: always() continue-on-error: true - run: | - # Copy Copilot session state files to logs folder for artifact collection - # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them - SESSION_STATE_DIR="$HOME/.copilot/session-state" - LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs" - - if [ -d "$SESSION_STATE_DIR" ]; then - echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR" - mkdir -p "$LOGS_DIR" - cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true - echo "Session state files copied successfully" - else - echo "No session-state directory found at $SESSION_STATE_DIR" - fi + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" - name: Stop MCP Gateway if: always() continue-on-error: true @@ -738,15 +776,15 @@ jobs: MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | - bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - name: Redact secrets in logs if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -756,63 +794,49 @@ jobs: SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Append agent step summary if: always() - run: bash /opt/gh-aw/actions/append_agent_step_summary.sh - - name: Upload Safe Outputs + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: safe-output - path: ${{ env.GH_AW_SAFE_OUTPUTS }} - if-no-files-found: warn + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true - name: Ingest agent output id: collect_output if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,go.dev,golang.org,goproxy.io,host.docker.internal,pkg.go.dev,proxy.golang.org,raw.githubusercontent.com,registry.npmjs.org,storage.googleapis.com,sum.golang.org,telemetry.enterprise.githubcopilot.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); await main(); - - name: Upload sanitized agent output - if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: agent-output - path: ${{ env.GH_AW_AGENT_OUTPUT }} - if-no-files-found: warn - - name: Upload engine output files - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: agent_outputs - path: | - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - if-no-files-found: ignore - name: Parse agent logs for step summary if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); await main(); - name: Parse MCP Gateway logs for step summary if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); await main(); - name: Print firewall logs if: always() @@ -820,35 +844,284 @@ jobs: env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall 2>/dev/null || true # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) if command -v awf &> /dev/null; then awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" else echo 'AWF binary not installed, skipping firewall log summary' fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: agent-artifacts + name: agent path: | /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json if-no-files-found: ignore - # --- Threat Detection (inline) --- + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-code-simplifier" + cancel-in-progress: false + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Code Simplifier" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/code-simplifier.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@6762bfba6ae426a03aac46e8f68701461c667404" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/6762bfba6ae426a03aac46e8f68701461c667404/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@6762bfba6ae426a03aac46e8f68701461c667404" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/6762bfba6ae426a03aac46e8f68701461c667404/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@6762bfba6ae426a03aac46e8f68701461c667404" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/6762bfba6ae426a03aac46e8f68701461c667404/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@6762bfba6ae426a03aac46e8f68701461c667404" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/6762bfba6ae426a03aac46e8f68701461c667404/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Code Simplifier" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@6762bfba6ae426a03aac46e8f68701461c667404" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/6762bfba6ae426a03aac46e8f68701461c667404/.github/workflows/code-simplifier.md" + GH_AW_TRACKER_ID: "code-simplifier" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "code-simplifier" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "30" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Code Simplifier" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/code-simplifier.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 - name: Check if detection needed id: detection_guard if: always() env: - OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }} - HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} run: | if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then echo "run_detection=true" >> "$GITHUB_OUTPUT" @@ -857,10 +1130,10 @@ jobs: echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi - - name: Clear MCP configuration for detection + - name: Clear MCP Config for detection if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" rm -f /home/runner/.copilot/mcp-config.json rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files @@ -872,53 +1145,67 @@ jobs: for f in /tmp/gh-aw/aw-*.patch; do [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done echo "Prepared threat detection files:" ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Code Simplifier" WORKFLOW_DESCRIPTION: "Analyzes recently modified code and creates pull requests with simplifications that improve clarity, consistency, and maintainability while preserving functionality" - HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true id: detection_agentic_execution # Copilot CLI tool arguments (sorted): - # --allow-tool shell(cat) - # --allow-tool shell(grep) - # --allow-tool shell(head) - # --allow-tool shell(jq) - # --allow-tool shell(ls) - # --allow-tool shell(tail) - # --allow-tool shell(wc) timeout-minutes: 20 run: | set -o pipefail touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 - sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.23.0 --skip-pull --enable-api-proxy \ - -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.57.2 + GH_AW_VERSION: v0.71.5 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} @@ -929,258 +1216,208 @@ jobs: GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] XDG_CONFIG_HOME: /home/runner - - name: Parse threat detection results - id: parse_detection_results - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: threat-detection.log + name: detection path: /tmp/gh-aw/threat-detection/detection.log if-no-files-found: ignore - - name: Set detection conclusion + - name: Parse and conclude threat detection id: detection_conclusion if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} - DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }} - run: | - if [[ "$RUN_DETECTION" != "true" ]]; then - echo "conclusion=skipped" >> "$GITHUB_OUTPUT" - echo "success=true" >> "$GITHUB_OUTPUT" - echo "Detection was not needed, marking as skipped" - elif [[ "$DETECTION_SUCCESS" == "true" ]]; then - echo "conclusion=success" >> "$GITHUB_OUTPUT" - echo "success=true" >> "$GITHUB_OUTPUT" - echo "Detection passed successfully" - else - echo "conclusion=failure" >> "$GITHUB_OUTPUT" - echo "success=false" >> "$GITHUB_OUTPUT" - echo "Detection found issues" - fi - - conclusion: - needs: - - activation - - agent - - safe_outputs - if: (always()) && (needs.agent.result != 'skipped') - runs-on: ubuntu-slim - permissions: - contents: read - issues: write - concurrency: - group: "gh-aw-conclusion-code-simplifier" - cancel-in-progress: false - outputs: - noop_message: ${{ steps.noop.outputs.noop_message }} - tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} - total_count: ${{ steps.missing_tool.outputs.total_count }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5 + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" with: - destination: /opt/gh-aw/actions - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Process No-Op Messages - id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "Code Simplifier" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/76d37d925abd44fee97379206f105b74b91a285b/.github/workflows/code-simplifier.md" - GH_AW_TRACKER_ID: "code-simplifier" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/noop.cjs'); - await main(); - - name: Record Missing Tool - id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Code Simplifier" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/76d37d925abd44fee97379206f105b74b91a285b/.github/workflows/code-simplifier.md" - GH_AW_TRACKER_ID: "code-simplifier" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); - await main(); - - name: Handle Agent Failure - id: handle_agent_failure - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Code Simplifier" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/76d37d925abd44fee97379206f105b74b91a285b/.github/workflows/code-simplifier.md" - GH_AW_TRACKER_ID: "code-simplifier" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_WORKFLOW_ID: "code-simplifier" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} - GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} - GH_AW_GROUP_REPORTS: "false" - GH_AW_FAILURE_REPORT_AS_ISSUE: "true" - GH_AW_TIMEOUT_MINUTES: "30" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); - await main(); - - name: Handle No-Op Message - id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Code Simplifier" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/76d37d925abd44fee97379206f105b74b91a285b/.github/workflows/code-simplifier.md" - GH_AW_TRACKER_ID: "code-simplifier" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} - GH_AW_NOOP_REPORT_AS_ISSUE: "false" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); - await main(); + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } pre_activation: runs-on: ubuntu-slim outputs: - activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_skip_if_match.outputs.skip_check_ok == 'true') }} + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' && steps.check_skip_if_match.outputs.skip_check_ok == 'true' }} matched_command: '' + setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5 + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 with: - destination: /opt/gh-aw/actions + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Code Simplifier" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/code-simplifier.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Check team membership for workflow id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_REQUIRED_ROLES: admin,maintainer,write + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); await main(); - name: Check skip-if-match query id: check_skip_if_match - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SKIP_QUERY: "is:pr is:open in:title \"[code-simplifier]\"" GH_AW_WORKFLOW_NAME: "Code Simplifier" GH_AW_SKIP_MAX_MATCHES: "1" with: script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_skip_if_match.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_skip_if_match.cjs'); await main(); safe_outputs: - needs: agent - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true') + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read + contents: write issues: write + pull-requests: write timeout-minutes: 15 env: GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/code-simplifier" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.40" GH_AW_TRACKER_ID: "code-simplifier" GH_AW_WORKFLOW_ID: "code-simplifier" GH_AW_WORKFLOW_NAME: "Code Simplifier" - GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/tree/76d37d925abd44fee97379206f105b74b91a285b/.github/workflows/code-simplifier.md" + GH_AW_WORKFLOW_SOURCE: "github/gh-aw/.github/workflows/code-simplifier.md@6762bfba6ae426a03aac46e8f68701461c667404" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/github/gh-aw/blob/6762bfba6ae426a03aac46e8f68701461c667404/.github/workflows/code-simplifier.md" outputs: code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} - created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5 + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 with: - destination: /opt/gh-aw/actions + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Code Simplifier" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/code-simplifier.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" - name: Download agent output artifact id: download-agent-output continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ + name: agent + path: /tmp/gh-aw/ - name: Setup agent output environment variable + id: setup-agent-output-env if: steps.download-agent-output.outcome == 'success' run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,go.dev,golang.org,goproxy.io,host.docker.internal,pkg.go.dev,proxy.golang.org,raw.githubusercontent.com,registry.npmjs.org,storage.googleapis.com,sum.golang.org,telemetry.enterprise.githubcopilot.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"labels\":[\"refactoring\",\"code-quality\",\"automation\"],\"max\":1,\"title_prefix\":\"[code-simplifier] \"},\"missing_data\":{},\"missing_tool\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"expires\":24,\"labels\":[\"refactoring\",\"code-quality\",\"automation\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"reviewers\":[\"copilot\"],\"title_prefix\":\"[code-simplifier] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); await main(); - - name: Upload safe output items manifest + - name: Upload Safe Outputs Items if: always() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: safe-output-items - path: /tmp/safe-output-items.jsonl - if-no-files-found: warn + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore diff --git a/.github/workflows/code-simplifier.md b/.github/workflows/code-simplifier.md index f0d512276..ac4481869 100644 --- a/.github/workflows/code-simplifier.md +++ b/.github/workflows/code-simplifier.md @@ -1,434 +1,3 @@ -<<<<<<< current (local changes) ---- -on: - schedule: daily - skip-if-match: is:pr is:open in:title "[code-simplifier]" -permissions: - contents: read - issues: read - pull-requests: read -safe-outputs: - create-issue: - labels: - - refactoring - - code-quality - - automation - title-prefix: "[code-simplifier] " - noop: - report-as-issue: false -description: Analyzes recently modified code and creates pull requests with simplifications that improve clarity, consistency, and maintainability while preserving functionality -name: Code Simplifier -source: github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b -strict: true -timeout-minutes: 30 -tools: - github: - toolsets: - - default -tracker-id: code-simplifier ---- - - - -# Code Simplifier Agent - -You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer. - -## Your Mission - -Analyze recently modified code from the last 24 hours and apply refinements that improve code quality while preserving all functionality. Create a GitHub issue with a properly formatted diff if improvements are found. - -## Current Context - -- **Repository**: ${{ github.repository }} -- **Workspace**: ${{ github.workspace }} - -## Phase 1: Identify Recently Modified Code - -### 1.1 Find Recent Changes - -Search for merged pull requests and commits from the last 24 hours: - -```bash -# Get yesterday's date in ISO format -YESTERDAY=$(date -d '1 day ago' '+%Y-%m-%d' 2>/dev/null || date -v-1d '+%Y-%m-%d') - -# List recent commits -git log --since="24 hours ago" --pretty=format:"%H %s" --no-merges -``` - -Use GitHub tools to: -- Search for pull requests merged in the last 24 hours: `repo:${{ github.repository }} is:pr is:merged merged:>=${YESTERDAY}` -- Get details of merged PRs to understand what files were changed -- List commits from the last 24 hours to identify modified files - -### 1.2 Extract Changed Files - -For each merged PR or recent commit: -- Use `pull_request_read` with `method: get_files` to list changed files -- Use `get_commit` to see file changes in recent commits -- Focus on source code files (`.go`, `.js`, `.ts`, `.tsx`, `.cjs`, `.py`, etc.) -- Exclude test files, lock files, and generated files - -### 1.3 Determine Scope - -If **no files were changed in the last 24 hours**, exit gracefully without creating a PR: - -``` -✅ No code changes detected in the last 24 hours. -Code simplifier has nothing to process today. -``` - -If **files were changed**, proceed to Phase 2. - -## Phase 2: Analyze and Simplify Code - -### 2.1 Review Project Standards - -Before simplifying, review the project's coding standards from relevant documentation: - -- For Go projects: Check `AGENTS.md`, `DEVGUIDE.md`, or similar files -- For JavaScript/TypeScript: Look for `CLAUDE.md`, style guides, or coding conventions -- For Python: Check for style guides, PEP 8 adherence, or project-specific conventions - -**Key Standards to Apply:** - -For **JavaScript/TypeScript** projects: -- Use ES modules with proper import sorting and extensions -- Prefer `function` keyword over arrow functions for top-level functions -- Use explicit return type annotations for top-level functions -- Follow proper React component patterns with explicit Props types -- Use proper error handling patterns (avoid try/catch when possible) -- Maintain consistent naming conventions - -For **Go** projects: -- Use `any` instead of `interface{}` -- Follow console formatting for CLI output -- Use semantic type aliases for domain concepts -- Prefer small, focused files (200-500 lines ideal) -- Use table-driven tests with descriptive names - -For **Python** projects: -- Follow PEP 8 style guide -- Use type hints for function signatures -- Prefer explicit over implicit code -- Use list/dict comprehensions where they improve clarity (not complexity) - -### 2.2 Simplification Principles - -Apply these refinements to the recently modified code: - -#### 1. Preserve Functionality -- **NEVER** change what the code does - only how it does it -- All original features, outputs, and behaviors must remain intact -- Run tests before and after to ensure no behavioral changes - -#### 2. Enhance Clarity -- Reduce unnecessary complexity and nesting -- Eliminate redundant code and abstractions -- Improve readability through clear variable and function names -- Consolidate related logic -- Remove unnecessary comments that describe obvious code -- **IMPORTANT**: Avoid nested ternary operators - prefer switch statements or if/else chains -- Choose clarity over brevity - explicit code is often better than compact code - -#### 3. Apply Project Standards -- Use project-specific conventions and patterns -- Follow established naming conventions -- Apply consistent formatting -- Use appropriate language features (modern syntax where beneficial) - -#### 4. Maintain Balance -Avoid over-simplification that could: -- Reduce code clarity or maintainability -- Create overly clever solutions that are hard to understand -- Combine too many concerns into single functions or components -- Remove helpful abstractions that improve code organization -- Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners) -- Make the code harder to debug or extend - -### 2.3 Perform Code Analysis - -For each changed file: - -1. **Read the file contents** using the edit or view tool -2. **Identify refactoring opportunities**: - - Long functions that could be split - - Duplicate code patterns - - Complex conditionals that could be simplified - - Unclear variable names - - Missing or excessive comments - - Non-standard patterns -3. **Design the simplification**: - - What specific changes will improve clarity? - - How can complexity be reduced? - - What patterns should be applied? - - Will this maintain all functionality? - -### 2.4 Apply Simplifications - -Use the **edit** tool to modify files: - -```bash -# For each file with improvements: -# 1. Read the current content -# 2. Apply targeted edits to simplify code -# 3. Ensure all functionality is preserved -``` - -**Guidelines for edits:** -- Make surgical, targeted changes -- One logical improvement per edit (but batch multiple edits in a single response) -- Preserve all original behavior -- Keep changes focused on recently modified code -- Don't refactor unrelated code unless it improves understanding of the changes - -## Phase 3: Validate Changes - -### 3.1 Run Tests - -After making simplifications, run the project's test suite to ensure no functionality was broken: - -```bash -# For Go projects -make test-unit - -# For JavaScript/TypeScript projects -npm test - -# For Python projects -pytest -``` - -If tests fail: -- Review the failures carefully -- Revert changes that broke functionality -- Adjust simplifications to preserve behavior -- Re-run tests until they pass - -### 3.2 Run Linters - -Ensure code style is consistent: - -```bash -# For Go projects -make lint - -# For JavaScript/TypeScript projects -npm run lint - -# For Python projects -flake8 . || pylint . -``` - -Fix any linting issues introduced by the simplifications. - -### 3.3 Check Build - -Verify the project still builds successfully: - -```bash -# For Go projects -make build - -# For JavaScript/TypeScript projects -npm run build - -# For Python projects -# (typically no build step, but check imports) -python -m py_compile changed_files.py -``` - -## Phase 4: Create GitHub Issue with Diff - -### 4.1 Determine If Issue Is Needed - -Only create an issue if: -- ✅ You made actual code simplifications -- ✅ All tests pass -- ✅ Linting is clean -- ✅ Build succeeds -- ✅ Changes improve code quality without breaking functionality - -If no improvements were made or changes broke tests, exit gracefully: - -``` -✅ Code analyzed from last 24 hours. -No simplifications needed - code already meets quality standards. -``` - -### 4.2 Generate Git Diff - -Before creating the issue, generate a properly formatted git diff that can be used to create a pull request: - -```bash -# Stage all changes if not already staged -git add . - -# Generate a complete unified diff of all staged changes -git diff --cached > /tmp/code-simplification.diff - -# Read the diff to include in the discussion -cat /tmp/code-simplification.diff -``` - -**Important**: The diff must be in standard unified diff format (git unified diff) that includes: -- File headers with `diff --git a/path b/path` -- Index lines with git hashes -- `---` and `+++` lines showing old and new file paths -- `@@` lines showing line numbers -- Actual code changes with `-` for removed lines and `+` for added lines - -This format is compatible with: -- `git apply` command for direct application -- GitHub's "Create PR from diff" functionality -- GitHub Copilot for suggesting PR creation -- Manual copy-paste into PR creation interface - -### 4.3 Generate Issue Description - -If creating an issue, use this structure: - -```markdown -## Code Simplification - [Date] - -This discussion presents code simplifications that improve clarity, consistency, and maintainability while preserving all functionality. - -### Files Simplified - -- `path/to/file1.go` - [Brief description of improvements] -- `path/to/file2.js` - [Brief description of improvements] - -### Improvements Made - -1. **Reduced Complexity** - - Simplified nested conditionals in `file1.go` - - Extracted helper function for repeated logic - -2. **Enhanced Clarity** - - Renamed variables for better readability - - Removed redundant comments - - Applied consistent naming conventions - -3. **Applied Project Standards** - - Used `function` keyword instead of arrow functions - - Added explicit type annotations - - Followed established patterns - -### Changes Based On - -Recent changes from: -- #[PR_NUMBER] - [PR title] -- Commit [SHORT_SHA] - [Commit message] - -### Testing - -- ✅ All tests pass -- ✅ Linting passes -- ✅ Build succeeds -- ✅ No functional changes - behavior is identical - -### Git Diff - -Below is the complete diff that can be used to create a pull request. You can copy this diff and: -- Use it with GitHub Copilot to create a PR -- Apply it directly with `git apply` -- Create a PR manually by copying the changes - -```diff -[PASTE THE COMPLETE GIT DIFF HERE] -``` - -To apply this diff: - -```bash -# Save the diff to a file -cat > /tmp/code-simplification.diff << 'EOF' -[PASTE DIFF CONTENT] -EOF - -# Apply the diff -git apply /tmp/code-simplification.diff - -# Or create a PR from the current branch -gh pr create --title "[code-simplifier] Code Simplification" --body "See discussion #[NUMBER]" -``` - -### Review Focus - -Please verify: -- Functionality is preserved -- Simplifications improve code quality -- Changes align with project conventions -- No unintended side effects - ---- - -*Automated by Code Simplifier Agent - analyzing code from the last 24 hours* -``` - -### 4.4 Use Safe Outputs - -Create the issue using the safe-outputs configuration: - -- Title will be prefixed with `[code-simplifier]` -- Labeled with `refactoring`, `code-quality`, `automation` -- Contains complete git diff for easy PR creation - -## Important Guidelines - -### Scope Control -- **Focus on recent changes**: Only refine code modified in the last 24 hours -- **Don't over-refactor**: Avoid touching unrelated code -- **Preserve interfaces**: Don't change public APIs or exported functions -- **Incremental improvements**: Make targeted, surgical changes - -### Quality Standards -- **Test first**: Always run tests after simplifications -- **Preserve behavior**: Functionality must remain identical -- **Follow conventions**: Apply project-specific patterns consistently -- **Clear over clever**: Prioritize readability and maintainability - -### Exit Conditions -Exit gracefully without creating an issue if: -- No code was changed in the last 24 hours -- No simplifications are beneficial -- Tests fail after changes -- Build fails after changes -- Changes are too risky or complex - -### Success Metrics -A successful simplification: -- ✅ Improves code clarity without changing behavior -- ✅ Passes all tests and linting -- ✅ Applies project-specific conventions -- ✅ Makes code easier to understand and maintain -- ✅ Focuses on recently modified code -- ✅ Provides clear documentation of changes - -## Output Requirements - -Your output MUST either: - -1. **If no changes in last 24 hours**: - ``` - ✅ No code changes detected in the last 24 hours. - Code simplifier has nothing to process today. - ``` - -2. **If no simplifications beneficial**: - ``` - ✅ Code analyzed from last 24 hours. - No simplifications needed - code already meets quality standards. - ``` - -3. **If simplifications made**: Create an issue with the changes using safe-outputs, including: - - Clear description of improvements - - Complete git diff in proper format - - Instructions for applying the diff or creating a PR - -Begin your code simplification analysis now. Find recently modified code, assess simplification opportunities, apply improvements while preserving functionality, validate changes, and create an issue with a git diff if beneficial. -||||||| base (original) --- name: Code Simplifier description: Analyzes recently modified code and creates pull requests with simplifications that improve clarity, consistency, and maintainability while preserving functionality @@ -443,389 +12,6 @@ permissions: tracker-id: code-simplifier -imports: - - shared/reporting.md - -safe-outputs: - create-pull-request: - title-prefix: "[code-simplifier] " - labels: [refactoring, code-quality, automation] - reviewers: [copilot] - expires: 7d - -tools: - github: - toolsets: [default] - -timeout-minutes: 30 -strict: true -source: github/gh-aw/.github/workflows/code-simplifier.md@76d37d925abd44fee97379206f105b74b91a285b ---- - - - - -# Code Simplifier Agent - -You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer. - -## Your Mission - -Analyze recently modified code from the last 24 hours and apply refinements that improve code quality while preserving all functionality. Create a pull request with the simplified code if improvements are found. - -## Current Context - -- **Repository**: ${{ github.repository }} -- **Analysis Date**: $(date +%Y-%m-%d) -- **Workspace**: ${{ github.workspace }} - -## Phase 1: Identify Recently Modified Code - -### 1.1 Find Recent Changes - -Search for merged pull requests and commits from the last 24 hours: - -```bash -# Get yesterday's date in ISO format -YESTERDAY=$(date -d '1 day ago' '+%Y-%m-%d' 2>/dev/null || date -v-1d '+%Y-%m-%d') - -# List recent commits -git log --since="24 hours ago" --pretty=format:"%H %s" --no-merges -``` - -Use GitHub tools to: -- Search for pull requests merged in the last 24 hours: `repo:${{ github.repository }} is:pr is:merged merged:>=${YESTERDAY}` -- Get details of merged PRs to understand what files were changed -- List commits from the last 24 hours to identify modified files - -### 1.2 Extract Changed Files - -For each merged PR or recent commit: -- Use `pull_request_read` with `method: get_files` to list changed files -- Use `get_commit` to see file changes in recent commits -- Focus on source code files (`.go`, `.js`, `.ts`, `.tsx`, `.cjs`, `.py`, etc.) -- Exclude test files, lock files, and generated files - -### 1.3 Determine Scope - -If **no files were changed in the last 24 hours**, exit gracefully without creating a PR: - -``` -✅ No code changes detected in the last 24 hours. -Code simplifier has nothing to process today. -``` - -If **files were changed**, proceed to Phase 2. - -## Phase 2: Analyze and Simplify Code - -### 2.1 Review Project Standards - -Before simplifying, review the project's coding standards from relevant documentation: - -- For Go projects: Check `AGENTS.md`, `DEVGUIDE.md`, or similar files -- For JavaScript/TypeScript: Look for `CLAUDE.md`, style guides, or coding conventions -- For Python: Check for style guides, PEP 8 adherence, or project-specific conventions - -**Key Standards to Apply:** - -For **JavaScript/TypeScript** projects: -- Use ES modules with proper import sorting and extensions -- Prefer `function` keyword over arrow functions for top-level functions -- Use explicit return type annotations for top-level functions -- Follow proper React component patterns with explicit Props types -- Use proper error handling patterns (avoid try/catch when possible) -- Maintain consistent naming conventions - -For **Go** projects: -- Use `any` instead of `interface{}` -- Follow console formatting for CLI output -- Use semantic type aliases for domain concepts -- Prefer small, focused files (200-500 lines ideal) -- Use table-driven tests with descriptive names - -For **Python** projects: -- Follow PEP 8 style guide -- Use type hints for function signatures -- Prefer explicit over implicit code -- Use list/dict comprehensions where they improve clarity (not complexity) - -### 2.2 Simplification Principles - -Apply these refinements to the recently modified code: - -#### 1. Preserve Functionality -- **NEVER** change what the code does - only how it does it -- All original features, outputs, and behaviors must remain intact -- Run tests before and after to ensure no behavioral changes - -#### 2. Enhance Clarity -- Reduce unnecessary complexity and nesting -- Eliminate redundant code and abstractions -- Improve readability through clear variable and function names -- Consolidate related logic -- Remove unnecessary comments that describe obvious code -- **IMPORTANT**: Avoid nested ternary operators - prefer switch statements or if/else chains -- Choose clarity over brevity - explicit code is often better than compact code - -#### 3. Apply Project Standards -- Use project-specific conventions and patterns -- Follow established naming conventions -- Apply consistent formatting -- Use appropriate language features (modern syntax where beneficial) - -#### 4. Maintain Balance -Avoid over-simplification that could: -- Reduce code clarity or maintainability -- Create overly clever solutions that are hard to understand -- Combine too many concerns into single functions or components -- Remove helpful abstractions that improve code organization -- Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners) -- Make the code harder to debug or extend - -### 2.3 Perform Code Analysis - -For each changed file: - -1. **Read the file contents** using the edit or view tool -2. **Identify refactoring opportunities**: - - Long functions that could be split - - Duplicate code patterns - - Complex conditionals that could be simplified - - Unclear variable names - - Missing or excessive comments - - Non-standard patterns -3. **Design the simplification**: - - What specific changes will improve clarity? - - How can complexity be reduced? - - What patterns should be applied? - - Will this maintain all functionality? - -### 2.4 Apply Simplifications - -Use the **edit** tool to modify files: - -```bash -# For each file with improvements: -# 1. Read the current content -# 2. Apply targeted edits to simplify code -# 3. Ensure all functionality is preserved -``` - -**Guidelines for edits:** -- Make surgical, targeted changes -- One logical improvement per edit (but batch multiple edits in a single response) -- Preserve all original behavior -- Keep changes focused on recently modified code -- Don't refactor unrelated code unless it improves understanding of the changes - -## Phase 3: Validate Changes - -### 3.1 Run Tests - -After making simplifications, run the project's test suite to ensure no functionality was broken: - -```bash -# For Go projects -make test-unit - -# For JavaScript/TypeScript projects -npm test - -# For Python projects -pytest -``` - -If tests fail: -- Review the failures carefully -- Revert changes that broke functionality -- Adjust simplifications to preserve behavior -- Re-run tests until they pass - -### 3.2 Run Linters - -Ensure code style is consistent: - -```bash -# For Go projects -make lint - -# For JavaScript/TypeScript projects -npm run lint - -# For Python projects -flake8 . || pylint . -``` - -Fix any linting issues introduced by the simplifications. - -### 3.3 Check Build - -Verify the project still builds successfully: - -```bash -# For Go projects -make build - -# For JavaScript/TypeScript projects -npm run build - -# For Python projects -# (typically no build step, but check imports) -python -m py_compile changed_files.py -``` - -## Phase 4: Create Pull Request - -### 4.1 Determine If PR Is Needed - -Only create a PR if: -- ✅ You made actual code simplifications -- ✅ All tests pass -- ✅ Linting is clean -- ✅ Build succeeds -- ✅ Changes improve code quality without breaking functionality - -If no improvements were made or changes broke tests, exit gracefully: - -``` -✅ Code analyzed from last 24 hours. -No simplifications needed - code already meets quality standards. -``` - -### 4.2 Generate PR Description - -If creating a PR, use this structure: - -```markdown -## Code Simplification - [Date] - -This PR simplifies recently modified code to improve clarity, consistency, and maintainability while preserving all functionality. - -### Files Simplified - -- `path/to/file1.go` - [Brief description of improvements] -- `path/to/file2.js` - [Brief description of improvements] - -### Improvements Made - -1. **Reduced Complexity** - - Simplified nested conditionals in `file1.go` - - Extracted helper function for repeated logic - -2. **Enhanced Clarity** - - Renamed variables for better readability - - Removed redundant comments - - Applied consistent naming conventions - -3. **Applied Project Standards** - - Used `function` keyword instead of arrow functions - - Added explicit type annotations - - Followed established patterns - -### Changes Based On - -Recent changes from: -- #[PR_NUMBER] - [PR title] -- Commit [SHORT_SHA] - [Commit message] - -### Testing - -- ✅ All tests pass (`make test-unit`) -- ✅ Linting passes (`make lint`) -- ✅ Build succeeds (`make build`) -- ✅ No functional changes - behavior is identical - -### Review Focus - -Please verify: -- Functionality is preserved -- Simplifications improve code quality -- Changes align with project conventions -- No unintended side effects - ---- - -*Automated by Code Simplifier Agent - analyzing code from the last 24 hours* -``` - -### 4.3 Use Safe Outputs - -Create the pull request using the safe-outputs configuration: - -- Title will be prefixed with `[code-simplifier]` -- Labeled with `refactoring`, `code-quality`, `automation` -- Assigned to `copilot` for review -- Set as ready for review (not draft) - -## Important Guidelines - -### Scope Control -- **Focus on recent changes**: Only refine code modified in the last 24 hours -- **Don't over-refactor**: Avoid touching unrelated code -- **Preserve interfaces**: Don't change public APIs or exported functions -- **Incremental improvements**: Make targeted, surgical changes - -### Quality Standards -- **Test first**: Always run tests after simplifications -- **Preserve behavior**: Functionality must remain identical -- **Follow conventions**: Apply project-specific patterns consistently -- **Clear over clever**: Prioritize readability and maintainability - -### Exit Conditions -Exit gracefully without creating a PR if: -- No code was changed in the last 24 hours -- No simplifications are beneficial -- Tests fail after changes -- Build fails after changes -- Changes are too risky or complex - -### Success Metrics -A successful simplification: -- ✅ Improves code clarity without changing behavior -- ✅ Passes all tests and linting -- ✅ Applies project-specific conventions -- ✅ Makes code easier to understand and maintain -- ✅ Focuses on recently modified code -- ✅ Provides clear documentation of changes - -## Output Requirements - -Your output MUST either: - -1. **If no changes in last 24 hours**: - ``` - ✅ No code changes detected in the last 24 hours. - Code simplifier has nothing to process today. - ``` - -2. **If no simplifications beneficial**: - ``` - ✅ Code analyzed from last 24 hours. - No simplifications needed - code already meets quality standards. - ``` - -3. **If simplifications made**: Create a PR with the changes using safe-outputs - -Begin your code simplification analysis now. Find recently modified code, assess simplification opportunities, apply improvements while preserving functionality, validate changes, and create a PR if beneficial. -======= ---- -name: Code Simplifier -description: Analyzes recently modified code and creates pull requests with simplifications that improve clarity, consistency, and maintainability while preserving functionality -on: - schedule: daily - skip-if-match: 'is:pr is:open in:title "[code-simplifier]"' - -permissions: - contents: read - issues: read - pull-requests: read - -tracker-id: code-simplifier - -imports: - - shared/activation-app.md - - shared/reporting.md safe-outputs: create-pull-request: @@ -833,6 +19,8 @@ safe-outputs: labels: [refactoring, code-quality, automation] reviewers: [copilot] expires: 1d + noop: + report-as-issue: false network: allowed: @@ -1216,5 +404,4 @@ Begin your code simplification analysis now. Find recently modified code, assess ```json {"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}} -``` ->>>>>>> new (upstream) +``` \ No newline at end of file diff --git a/.github/workflows/compare-stats-anomaly-reporter.lock.yml b/.github/workflows/compare-stats-anomaly-reporter.lock.yml new file mode 100644 index 000000000..54c1f7b18 --- /dev/null +++ b/.github/workflows/compare-stats-anomaly-reporter.lock.yml @@ -0,0 +1,1373 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"ba7cc0883e671716a71c8e76bcbdff5a80ec7bea87f96a19f5419c019ab96fdd","compiler_version":"v0.76.1","agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.76.1","version":"v0.76.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.55"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.19"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4","digest":"sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.76.1). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Analyze benchmark statistics from the latest 30 hours and publish bug/crash/anomaly summary as a GitHub Discussion +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@v0.77.0 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.55 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.55 +# - ghcr.io/github/gh-aw-mcpg:v0.3.19 +# - ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + +name: "Compare Stats Bug/Crash/Anomaly Reporter" +on: + schedule: + - cron: "0 */12 * * *" + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Compare Stats Bug/Crash/Anomaly Reporter" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/compare-stats-anomaly-reporter.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AGENT_VERSION: "1.0.52" + GH_AW_INFO_CLI_VERSION: "v0.76.1" + GH_AW_INFO_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","mtzguido.tplinkdns.com"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "false" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .antigravity + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "compare-stats-anomaly-reporter.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.76.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_fa9bfce3ee233090_EOF' + + GH_AW_PROMPT_fa9bfce3ee233090_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_fa9bfce3ee233090_EOF' + + Tools: create_discussion, missing_tool, missing_data, noop + + GH_AW_PROMPT_fa9bfce3ee233090_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_fa9bfce3ee233090_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_fa9bfce3ee233090_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_fa9bfce3ee233090_EOF' + + {{#runtime-import .github/workflows/compare-stats-anomaly-reporter.md}} + GH_AW_PROMPT_fa9bfce3ee233090_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKFLOW: process.env.GH_AW_GITHUB_WORKFLOW, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + /tmp/gh-aw/.github/skills + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: read-all + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: comparestatsanomalyreporter + outputs: + agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/compare-stats-anomaly-reporter.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Restore inline skills from activation artifact + env: + GH_AW_SKILL_DIR: ".github/skills" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 ghcr.io/github/gh-aw-mcpg:v0.3.19 ghcr.io/github/github-mcp-server:v1.0.4@sha256:e3816a476a977cfb836e7d221510011436c654d11861db66ecfd826601aba6a4 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_4d0c9ad86b64b83a_EOF' + {"create_discussion":{"category":"agentic workflows","close_older_discussions":true,"expires":168,"fallback_to_issue":true,"max":1,"title_prefix":"[Compare Stats] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_4d0c9ad86b64b83a_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Title will be prefixed with \"[Compare Stats] \". Discussions will be created in category \"agentic workflows\"." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_discussion": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000, + "minLength": 64 + }, + "category": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.19' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_1d647de00ba486c1_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_1d647de00ba486c1_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 45 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","mtzguido.tplinkdns.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect agent errors + if: always() + id: detect-agent-errors + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mtzguido.tplinkdns.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + concurrency: + group: "gh-aw-conclusion-compare-stats-anomaly-reporter" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/compare-stats-anomaly-reporter.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/compare-stats-anomaly-reporter.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/compare-stats-anomaly-reporter.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_MISSING_TOOL_TITLE_PREFIX: "[missing tool]" + GH_AW_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/compare-stats-anomaly-reporter.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/compare-stats-anomaly-reporter.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/compare-stats-anomaly-reporter.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "compare-stats-anomaly-reporter" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CREATE_DISCUSSION_ERRORS: ${{ needs.safe_outputs.outputs.create_discussion_errors }} + GH_AW_CREATE_DISCUSSION_ERROR_COUNT: ${{ needs.safe_outputs.outputs.create_discussion_error_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "45" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/compare-stats-anomaly-reporter.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.55 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.55 ghcr.io/github/gh-aw-firewall/squid:0.25.55 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + WORKFLOW_DESCRIPTION: "Analyze benchmark statistics from the latest 30 hours and publish bug/crash/anomaly summary as a GitHub Discussion" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.52 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.55 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.55/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.55"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.76.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/compare-stats-anomaly-reporter" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.52" + GH_AW_WORKFLOW_ID: "compare-stats-anomaly-reporter" + GH_AW_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/compare-stats-anomaly-reporter.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Compare Stats Bug/Crash/Anomaly Reporter" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/compare-stats-anomaly-reporter.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.52" + GH_AW_INFO_AWF_VERSION: "v0.25.55" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,mtzguido.tplinkdns.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"agentic workflows\",\"close_older_discussions\":true,\"expires\":168,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"[Compare Stats] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + diff --git a/.github/workflows/compare-stats-anomaly-reporter.md b/.github/workflows/compare-stats-anomaly-reporter.md new file mode 100644 index 000000000..8f450991b --- /dev/null +++ b/.github/workflows/compare-stats-anomaly-reporter.md @@ -0,0 +1,191 @@ +--- +description: Analyze benchmark statistics from the latest 30 hours and publish bug/crash/anomaly summary as a GitHub Discussion + +on: + schedule: + - cron: "0 */12 * * *" + workflow_dispatch: + +permissions: read-all + +strict: false +timeout-minutes: 45 + +network: + allowed: + - defaults + - mtzguido.tplinkdns.com + +tools: + bash: [":*"] + github: + toolsets: [default] + +safe-outputs: + create-discussion: + title-prefix: "[Compare Stats] " + category: "agentic workflows" + close-older-discussions: true + missing-tool: + create-issue: true + noop: + report-as-issue: false +--- + +# Compare Stats Bug/Crash/Anomaly Reporter + +Your name is ${{ github.workflow }}. You are a Z3 benchmarking analysis agent for `${{ github.repository }}`. + +Analyze the benchmark statistics page below, focusing on results from the last 30 hours, then create a GitHub Discussion with a concise but actionable summary of: + +- Bugs +- Crashes +- Anomalies + +Source URL: +`http://mtzguido.tplinkdns.com:8081/z3/` + +Note: this endpoint is currently HTTP-only. Treat fetched data as non-sensitive benchmark telemetry and do not include secrets in requests or reports. +Note: the workflow runs every 12 hours but analyzes 30 hours intentionally to provide overlap and avoid missing transient failures between runs. +Overlapping windows are expected; `close-older-discussions: true` keeps only the latest report thread active. + +## Requirements + +### 1) Fetch and save the source page + +Use bash to fetch the page into `/tmp/gh-aw/agent/benchmark_stats.html`. + +Try this first: +```bash +curl -fsSL --max-time 60 "http://mtzguido.tplinkdns.com:8081/z3/" -o /tmp/gh-aw/agent/benchmark_stats.html +``` + +If that fails, retry once with: +```bash +wget -q -T 60 -O /tmp/gh-aw/agent/benchmark_stats.html "http://mtzguido.tplinkdns.com:8081/z3/" +``` + +If both fail, still create a discussion that explains the fetch failure, includes stderr output, and marks the report as incomplete. +After a successful fetch, perform basic integrity checks before parsing: +- file is non-empty +- content includes `= 4`, `unknown_count / total_rows <= 0.4`, and `(sat_count + unsat_count + timeout_count) / total_rows >= 0.6`. + - If set/suite/group columns are missing, fallback grouping order is: directory prefix of benchmark path/name, then benchmark name prefix before first separator (`/`, `:`, `::`), then a single global group. + +2. **Status divergence anomaly**: + - Same benchmark name appears multiple times with conflicting non-timeout statuses (for example `sat` vs `unsat`). + - Ignore timeout-only disagreements here; timeout behavior is covered under the repeated hard-failure anomaly section to reduce noise from transient runtime variance. + +3. **Repeated hard-failure anomaly**: + - Same benchmark appears repeatedly with crash/error-like status in the time window. + +### 5) Generate discussion report + +Create a GitHub Discussion using `create-discussion` safe output. + +Use this structure: + +```markdown +### Compare Stats Analysis Report + +**Source**: [benchmark statistics](http://mtzguido.tplinkdns.com:8081/z3/) +**Workflow Run**: [#${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) +**Analysis Time (UTC)**: +**Window**: last 30 hours (or fallback mode) + +### Executive Summary + +- Rows analyzed: N +- Rows in 30h window: M (or "timestamp unavailable") +- Bugs/crashes: B +- Anomalies: A + +### Bugs and Crashes + +| Benchmark Set | Benchmark | Status | Details | Timestamp | +|---|---|---|---|---| +| ... | + +### Anomalies + +#### Unknown-Outlier Cases +| Benchmark Set | Benchmark | Status | Peer Status Distribution | Timestamp | +|---|---|---|---|---| +| ... | + +#### Status Divergences +| Benchmark | Observed Statuses | Benchmark Set(s) | Timestamp(s) | +|---|---|---|---| +| ... | + +#### Repeated Hard Failures +| Benchmark | Failure Count | Representative Status/Details | Benchmark Set(s) | +|---|---|---|---| +| ... | + +### Notes and Limitations +- Mention parsing assumptions +- Mention missing columns/timestamps if any + +
+Raw Extraction Summary + +- Table count +- Candidate columns used +- Top status distribution +- Up to 30 representative raw rows (sanitized) + +
+``` + +## Reporting Rules + +- Be factual and concise. +- Do not claim certainty when column mapping is heuristic. +- If no bugs/crashes/anomalies are found, still create the discussion and explicitly state "No issues detected in analyzed window." +- Do not open PRs or modify repository files. diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 08ae99656..5c965e1cb 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -89,13 +89,13 @@ jobs: id: date run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7.0.1 with: name: coverage-${{steps.date.outputs.date}} path: ${{github.workspace}}/coverage.html retention-days: 4 - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v7.0.1 with: name: coverage-details-${{steps.date.outputs.date}} path: ${{env.COV_DETAILS_PATH}} diff --git a/.github/workflows/csa-analysis.md b/.github/workflows/csa-analysis.md index a4e0fb236..dc5abe0fa 100644 --- a/.github/workflows/csa-analysis.md +++ b/.github/workflows/csa-analysis.md @@ -16,8 +16,6 @@ tools: github: toolsets: [default] bash: [":*"] - glob: {} - view: {} safe-outputs: create-discussion: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2b4fa1769..a043a47b0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,7 +34,7 @@ jobs: python3 mk_go_doc.py --output-dir=api/html/go --go-api-path=../src/api/go - name: Upload Go Documentation - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: go-docs path: doc/api/html/go/ @@ -94,7 +94,7 @@ jobs: ocamldoc -html -d api/html/ml -sort -hide Z3 -I $( ocamlfind query zarith ) -I ../build-x64/api/ml ../build-x64/api/ml/z3enums.mli ../build-x64/api/ml/z3.mli - name: Setup emscripten - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@v16 with: no-install: true version: ${{env.EM_VERSION}} diff --git a/.github/workflows/issue-backlog-processor.md b/.github/workflows/issue-backlog-processor.md index c7831cd62..0feeeaf6b 100644 --- a/.github/workflows/issue-backlog-processor.md +++ b/.github/workflows/issue-backlog-processor.md @@ -32,6 +32,8 @@ timeout-minutes: 60 Your name is ${{ github.workflow }}. You are an expert AI agent tasked with processing the backlog of open issues in the Z3 theorem prover repository `${{ github.repository }}`. Your mission is to analyze open issues systematically and help maintainers manage the backlog effectively by surfacing actionable insights and providing helpful comments. +> **CRITICAL**: You MUST call either `create-discussion` or `noop` before finishing, under all circumstances. Even if you only analyzed a small number of issues, always produce output. Never exit without calling one of these tools. + ## Your Task ### 1. Initialize or Resume Progress (Cache Memory) @@ -40,25 +42,28 @@ Check your cache memory for: - List of issue numbers already processed and commented on in previous runs - Issues previously flagged for closure, duplication, or merge - Date of last run +- The batch cursor: the last issue number processed (used for pagination across runs) If cache data exists: - Skip re-commenting on issues already commented in a recent run (within the last 4 days) - Re-evaluate previously flagged issues to see if their status has changed - Note any new issues that opened since the last run +- Resume from where the previous run left off (use the stored batch cursor) If this is the first run or memory is empty, initialize a fresh tracking structure. -### 2. Fetch Open Issues +### 2. Fetch Open Issues (Batched) -Use the GitHub API to list all open issues in the repository: -- Retrieve all open issues (paginate through all pages to get the full list) +Use the GitHub API to list open issues in the repository. **Process at most 30 issues per run** to stay within context limits (this limit is based on the average size of Z3 issues including body text and inline code snippets; larger issues may require processing fewer): +- Retrieve one page (30 issues) of open issues - Exclude pull requests (filter where `pull_request` is not present) - Sort by last updated date (most recently updated first) +- If cache has a batch cursor from the last run, fetch the next page after that cursor; otherwise start from the most recently updated issues - For each issue, collect: - Issue number, title, body, labels, author - Date created and last updated - Number of comments - - All comments (for issues with comments) + - **Do NOT fetch comments for every issue up front.** Only fetch comments for a specific issue when at least one of the following is true: the body mentions a version number (potential closure), the title contains words like "duplicate", "same as", or "related to" (potential duplicate), or the issue has labels such as "question", "help wanted", or "wontfix" (potential closure/status change). Fetch comments lazily, one issue at a time, only when one of these criteria is met. - Any referenced pull requests, commits, or other issues ### 3. Analyze Each Issue @@ -110,6 +115,8 @@ Add a comment to an issue if you have **genuinely useful and specific informatio ### 4. Create a Discussion with Findings +**MANDATORY**: You MUST call `create-discussion` now, even if you only analyzed a few issues or found nothing actionable. If there is genuinely nothing to report, call `noop` instead. Do not skip this step. + Create a GitHub Discussion summarizing the analysis results. **Title:** "[Issue Backlog] Backlog Analysis - [Date]" @@ -224,9 +231,13 @@ After completing the analysis, update cache memory with: - Issues flagged for closure, duplication, or merge - Date and timestamp of this run - Count of total issues analyzed +- Batch cursor: the issue number of the last issue processed in this run, so the next run can continue from where this one left off ## Guidelines +- **Always produce output**: You MUST call `create-discussion` or `noop` before finishing — never exit silently. If in doubt about whether there is enough to report, call `create-discussion` with a brief summary. +- **Batch processing**: Only analyze up to 30 issues per run. Store a cursor in cache memory so subsequent runs pick up where you left off. +- **Lazy comment fetching**: Do NOT bulk-fetch all comments for all issues. Only fetch comments for a specific issue when one of these criteria is met: the body mentions a version number, the title contains duplicate/related keywords, or the issue has status-relevant labels (e.g., "question", "help wanted", "wontfix"). - **Prioritize accuracy over coverage**: It is better to analyze 20 issues well than 200 issues poorly - **Be conservative on closures**: Incorrectly closing a valid issue is harmful; when in doubt, keep it open - **Respect the community**: Z3 is used by researchers, security engineers, and developers — treat all issues respectfully diff --git a/.github/workflows/mark-prs-ready-for-review.yml b/.github/workflows/mark-prs-ready-for-review.yml index 4e19bdafa..90b0d668c 100644 --- a/.github/workflows/mark-prs-ready-for-review.yml +++ b/.github/workflows/mark-prs-ready-for-review.yml @@ -16,7 +16,7 @@ jobs: pull-requests: write steps: - name: Mark all draft pull requests ready for review - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/memory-safety-report.md b/.github/workflows/memory-safety-report.md index a2b245f3a..56ffe514f 100644 --- a/.github/workflows/memory-safety-report.md +++ b/.github/workflows/memory-safety-report.md @@ -30,8 +30,6 @@ tools: github: toolsets: [default, actions] bash: [":*"] - glob: {} - view: {} safe-outputs: mentions: false @@ -41,7 +39,7 @@ safe-outputs: title-prefix: "[Memory Safety] " category: "Agentic Workflows" close-older-discussions: true - expires: 7 + expires: 7d missing-tool: create-issue: true noop: diff --git a/.github/workflows/memory-safety.yml b/.github/workflows/memory-safety.yml index 7c1fd16f0..1c7fc7f0b 100644 --- a/.github/workflows/memory-safety.yml +++ b/.github/workflows/memory-safety.yml @@ -104,7 +104,7 @@ jobs: - name: Upload ASan reports if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: asan-reports path: /tmp/asan-reports/ @@ -194,7 +194,7 @@ jobs: - name: Upload UBSan reports if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: ubsan-reports path: /tmp/ubsan-reports/ diff --git a/.github/workflows/nightly-validation.yml b/.github/workflows/nightly-validation.yml index 3f29e1024..5d560677b 100644 --- a/.github/workflows/nightly-validation.yml +++ b/.github/workflows/nightly-validation.yml @@ -665,6 +665,57 @@ jobs: pip install $wheel.FullName python -c "import z3; x = z3.Int('x'); s = z3.Solver(); s.add(x > 0); print('Result:', s.check()); print('Model:', s.model())" + validate-python-wheel-riscv64: + name: "Validate Python wheel for RISC-V 64" + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + + - name: Download RISC-V 64 Python wheel from release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${{ github.event.inputs.release_tag }}" + if [ -z "$TAG" ]; then + TAG="Nightly" + fi + gh release download $TAG --pattern "*riscv64.whl" --dir wheels + + - name: Verify wheel platform tag and contents + run: | + pip install wheel + WHEEL_FILE=$(ls wheels/*.whl | head -n 1) + echo "Wheel file: $WHEEL_FILE" + + # Check that the wheel has a riscv64 platform tag + WHEEL_NAME=$(basename $WHEEL_FILE) + echo "Wheel name: $WHEEL_NAME" + if echo "$WHEEL_NAME" | grep -q "riscv64"; then + echo "riscv64 platform tag found" + else + echo "ERROR: riscv64 platform tag not found in wheel name" + exit 1 + fi + + # Inspect wheel contents + python -m zipfile -l $WHEEL_FILE + + # Verify wheel contains z3 library + if python -m zipfile -l $WHEEL_FILE | grep -q "libz3"; then + echo "libz3 found in wheel" + else + echo "ERROR: libz3 not found in wheel" + exit 1 + fi + # ============================================================================ # MACOS DYLIB HEADERPAD VALIDATION # ============================================================================ diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0decaffad..548fca2eb 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,8 +45,21 @@ jobs: - name: Build run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk --arch=x64 + - name: Validate libz3.dylib and z3 architecture (must be x86_64) + run: | + set -e + for f in build-dist/libz3.dylib build-dist/z3; do + ARCH=$(lipo -archs "$f") + echo "$f architecture: $ARCH" + if [ "$ARCH" != "x86_64" ]; then + echo "ERROR: $f has arch '$ARCH', expected 'x86_64' (see issue #9662)" + exit 1 + fi + done + echo "OK: macOS x64 artifacts are x86_64" + - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: macOsBuild path: dist/*.zip @@ -68,8 +81,21 @@ jobs: - name: Build run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk --arch=arm64 + - name: Validate libz3.dylib and z3 architecture (must be arm64) + run: | + set -e + for f in build-dist/libz3.dylib build-dist/z3; do + ARCH=$(lipo -archs "$f") + echo "$f architecture: $ARCH" + if [ "$ARCH" != "arm64" ]; then + echo "ERROR: $f has arch '$ARCH', expected 'arm64' (see issue #9662)" + exit 1 + fi + done + echo "OK: macOS arm64 artifacts are arm64" + - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: MacArm64 path: dist/*.zip @@ -101,6 +127,17 @@ jobs: Z3_DIR=$(find . -maxdepth 1 -type d -name "z3-*" | head -n 1) echo "Z3_DIR=$Z3_DIR" >> $GITHUB_ENV + - name: Validate shipped libz3.dylib architecture (must be x86_64) + run: | + set -e + DYLIB="artifacts/$Z3_DIR/bin/libz3.dylib" + ARCH=$(lipo -archs "$DYLIB") + echo "Shipped $DYLIB architecture: $ARCH" + if [ "$ARCH" != "x86_64" ]; then + echo "ERROR: x64 nightly zip contains '$ARCH' libz3.dylib (see issue #9662)" + exit 1 + fi + - name: Test install_name_tool with headerpad run: | cd artifacts/$Z3_DIR/bin @@ -149,6 +186,17 @@ jobs: Z3_DIR=$(find . -maxdepth 1 -type d -name "z3-*" | head -n 1) echo "Z3_DIR=$Z3_DIR" >> $GITHUB_ENV + - name: Validate shipped libz3.dylib architecture (must be arm64) + run: | + set -e + DYLIB="artifacts/$Z3_DIR/bin/libz3.dylib" + ARCH=$(lipo -archs "$DYLIB") + echo "Shipped $DYLIB architecture: $ARCH" + if [ "$ARCH" != "arm64" ]; then + echo "ERROR: arm64 nightly zip contains '$ARCH' libz3.dylib (see issue #9662)" + exit 1 + fi + - name: Test install_name_tool with headerpad run: | cd artifacts/$Z3_DIR/bin @@ -198,7 +246,7 @@ jobs: run: python z3test/scripts/test_benchmarks.py build-dist/z3 z3test/regressions/smt2 - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: UbuntuBuild path: dist/*.zip @@ -233,7 +281,7 @@ jobs: python scripts/mk_unix_dist.py --nodotnet --arch=arm64 - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: UbuntuArm64 path: dist/*.zip @@ -288,7 +336,7 @@ jobs: run: zip -r z3doc.zip doc/api - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: UbuntuDoc path: z3doc.zip @@ -303,9 +351,17 @@ jobs: - name: Checkout code uses: actions/checkout@v6.0.2 + - name: Select Python + run: | + # Use the first available manylinux interpreter for deterministic selection. + PYTHON=$(printf '%s\n' /opt/python/*/bin/python | sort -V | head -n1) + test -x "$PYTHON" || { echo "Error: no interpreter found under /opt/python/*/bin/python"; exit 1; } + echo "PYTHON=$PYTHON" >> "$GITHUB_ENV" + "$PYTHON" --version + - name: Setup Python environment run: | - /opt/python/cp38-cp38/bin/python -m venv $PWD/env + "$PYTHON" -m venv $PWD/env echo "$PWD/env/bin" >> $GITHUB_PATH - name: Install build tools @@ -318,7 +374,7 @@ jobs: run: pip install ./src/api/python/wheelhouse/*.whl && python - > "$GITHUB_ENV" + "$PYTHON" --version + - name: Setup Python environment run: | - /opt/python/cp38-cp38/bin/python -m venv $PWD/env + "$PYTHON" -m venv $PWD/env echo "$PWD/env/bin" >> $GITHUB_PATH echo "/tmp/arm-toolchain/bin" >> $GITHUB_PATH echo "/tmp/arm-toolchain/aarch64-none-linux-gnu/libc/usr/bin" >> $GITHUB_PATH @@ -358,12 +422,120 @@ jobs: run: cd src/api/python && CC=aarch64-none-linux-gnu-gcc CXX=aarch64-none-linux-gnu-g++ AR=aarch64-none-linux-gnu-ar LD=aarch64-none-linux-gnu-ld Z3_CROSS_COMPILING=aarch64 python -m build && AUDITWHEEL_PLAT= auditwheel repair --best-plat dist/*.whl && cd ../../.. - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: ManyLinuxPythonBuildArm64 path: src/api/python/wheelhouse/*.whl retention-days: 2 + manylinux-python-riscv64: + name: "Python bindings (manylinux RISC-V 64 cross)" + runs-on: ubuntu-latest + timeout-minutes: 90 + container: quay.io/pypa/manylinux_2_28_x86_64:latest + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + + - name: Download RISC-V toolchain + run: curl -L -o /tmp/riscv-toolchain.tar.gz 'https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/2024.09.03/riscv64-glibc-ubuntu-20.04-gcc-nightly-2024.09.03-nightly.tar.gz' + + - name: Extract RISC-V toolchain + run: | + mkdir -p /tmp/riscv-toolchain/ + tar xf /tmp/riscv-toolchain.tar.gz -C /tmp/riscv-toolchain/ --strip-components=1 + + - name: Install MPFR 4 (required by RISC-V toolchain host binaries) + run: | + dnf install -y gmp-devel + curl -L -o /tmp/mpfr.tar.xz https://ftp.gnu.org/gnu/mpfr/mpfr-4.2.1.tar.xz + tar xf /tmp/mpfr.tar.xz -C /tmp/ + cd /tmp/mpfr-4.2.1 && ./configure --prefix=/usr/local --disable-static && make -j$(nproc) && make install + ldconfig + + - name: Select Python + run: | + # Use the first available manylinux interpreter for deterministic selection. + PYTHON=$(printf '%s\n' /opt/python/*/bin/python | sort -V | head -n1) + test -x "$PYTHON" || { echo "Error: no interpreter found under /opt/python/*/bin/python"; exit 1; } + echo "PYTHON=$PYTHON" >> "$GITHUB_ENV" + "$PYTHON" --version + + - name: Setup Python environment + run: | + "$PYTHON" -m venv $PWD/env + echo "$PWD/env/bin" >> $GITHUB_PATH + echo "/tmp/riscv-toolchain/bin" >> $GITHUB_PATH + + - name: Install build tools + run: | + echo $PATH + stat $(which riscv64-unknown-linux-gnu-gcc) + pip install build git+https://github.com/rhelmot/auditwheel + + - name: Build wheels + run: cd src/api/python && CC=riscv64-unknown-linux-gnu-gcc CXX=riscv64-unknown-linux-gnu-g++ AR=riscv64-unknown-linux-gnu-ar LD=riscv64-unknown-linux-gnu-ld Z3_CROSS_COMPILING=riscv64 python -m build && AUDITWHEEL_PLAT= auditwheel repair --best-plat dist/*.whl && cd ../../.. + + - name: Upload artifact + uses: actions/upload-artifact@v7.0.1 + with: + name: ManyLinuxPythonBuildRiscv64 + path: src/api/python/wheelhouse/*.whl + retention-days: 2 + + pyodide-python: + name: "Python bindings (Pyodide)" + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + + - name: Setup packages + run: sudo apt-get update && sudo apt-get install -y python3-dev python3-pip python3-venv + + - name: Create venv + run: python3 -m venv ~/env + + - name: Install pyodide + run: ~/env/bin/pip install pyodide-build pyodide-cli + + - name: Configure Emscripten + run: | + git clone https://github.com/emscripten-core/emsdk.git ~/emsdk + cd ~/emsdk + PYODIDE_EMSCRIPTEN_VERSION=$(~/env/bin/pyodide config get emscripten_version) + ./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION} + ./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION} + + - name: Build wheel + run: | + source ~/emsdk/emsdk_env.sh + cd src/api/python + CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" CXXFLAGS="${CXXFLAGS}" ~/env/bin/pyodide build --exports whole_archive + env: + CFLAGS: "-fexceptions -s DISABLE_EXCEPTION_CATCHING=0 -g2" + LDFLAGS: "-fexceptions -s WASM_BIGINT" + CXXFLAGS: "-fexceptions -s DISABLE_EXCEPTION_CATCHING=0" + + - name: Setup env-pyodide + run: | + source ~/env/bin/activate + source ~/emsdk/emsdk_env.sh + pyodide venv ~/env-pyodide + + - name: Test wheel + run: | + ~/env-pyodide/bin/pip install src/api/python/dist/*.whl + ~/env-pyodide/bin/python src/api/python/z3test.py z3 + + - name: Upload artifact + uses: actions/upload-artifact@v7.0.1 + with: + name: PyodidePythonBuild + path: src/api/python/dist/*.whl + retention-days: 2 + windows-build-x64: name: "Windows x64 build" runs-on: windows-latest @@ -384,7 +556,7 @@ jobs: python scripts\mk_win_dist.py --x64-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --zip - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: WindowsBuild-x64 path: dist/*.zip @@ -410,7 +582,7 @@ jobs: python scripts\mk_win_dist.py --x86-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --zip - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: WindowsBuild-x86 path: dist/*.zip @@ -436,7 +608,7 @@ jobs: python scripts\mk_win_dist_cmake.py --arm64-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --assembly-version=${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.PATCH }} --zip - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: WindowsBuild-arm64 path: dist/arm64/*.zip @@ -496,7 +668,7 @@ jobs: path: package - name: Setup NuGet - uses: nuget/setup-nuget@v2 + uses: nuget/setup-nuget@v4 with: nuget-version: 'latest' @@ -513,7 +685,7 @@ jobs: nuget pack out\Microsoft.Z3.sym.nuspec -Version ${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.PATCH }}.${{ github.run_number }} -OutputDirectory . -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -BasePath out - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: NuGet path: | @@ -541,7 +713,7 @@ jobs: path: package - name: Setup NuGet - uses: nuget/setup-nuget@v2 + uses: nuget/setup-nuget@v4 with: nuget-version: 'latest' @@ -558,7 +730,7 @@ jobs: nuget pack out\Microsoft.Z3.x86.sym.nuspec -Version ${{ env.MAJOR }}.${{ env.MINOR }}.${{ env.PATCH }}.${{ github.run_number }} -OutputDirectory . -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -BasePath out - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: NuGet32 path: | @@ -568,7 +740,7 @@ jobs: python-package: name: "Python packaging" - needs: [mac-build-x64, mac-build-arm64, windows-build-x64, windows-build-x86, windows-build-arm64, manylinux-python-amd64, manylinux-python-arm64] + needs: [mac-build-x64, mac-build-arm64, windows-build-x64, windows-build-x86, windows-build-arm64, manylinux-python-amd64, manylinux-python-arm64, manylinux-python-riscv64, pyodide-python] runs-on: ubuntu-24.04 steps: - name: Checkout code @@ -621,6 +793,18 @@ jobs: name: ManyLinuxPythonBuildArm64 path: artifacts + - name: Download ManyLinux RISC-V 64 Build + uses: actions/download-artifact@v8.0.1 + with: + name: ManyLinuxPythonBuildRiscv64 + path: artifacts + + - name: Download Pyodide Build + uses: actions/download-artifact@v8.0.1 + with: + name: PyodidePythonBuild + path: artifacts + - name: Extract builds run: | cd artifacts @@ -651,7 +835,7 @@ jobs: cp artifacts/*.whl src/api/python/dist/. - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: PythonPackages path: src/api/python/dist/* @@ -759,4 +943,3 @@ jobs: with: packages-dir: dist repository-url: https://test.pypi.org/legacy/ - diff --git a/.github/workflows/nuget-build.yml b/.github/workflows/nuget-build.yml index 06e1af741..bc4c8c5cb 100644 --- a/.github/workflows/nuget-build.yml +++ b/.github/workflows/nuget-build.yml @@ -34,7 +34,7 @@ jobs: python scripts\mk_win_dist.py --x64-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --assembly-version=${{ github.event.inputs.version || '4.17.0' }} --zip - name: Upload Windows x64 artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: windows-x64 path: dist/*.zip @@ -58,7 +58,7 @@ jobs: python scripts\mk_win_dist.py --x86-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --assembly-version=${{ github.event.inputs.version || '4.17.0' }} --zip - name: Upload Windows x86 artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: windows-x86 path: dist/*.zip @@ -82,7 +82,7 @@ jobs: python scripts\mk_win_dist_cmake.py --arm64-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --assembly-version=${{ github.event.inputs.version || '4.17.0' }} --zip - name: Upload Windows ARM64 artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: windows-arm64 path: build-dist\arm64\dist\*.zip @@ -103,7 +103,7 @@ jobs: run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk - name: Upload Ubuntu artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: ubuntu path: dist/*.zip @@ -124,7 +124,7 @@ jobs: run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk - name: Upload macOS x64 artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: macos-x64 path: dist/*.zip @@ -145,7 +145,7 @@ jobs: run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk --arch=arm64 - name: Upload macOS ARM64 artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: macos-arm64 path: dist/*.zip @@ -181,7 +181,7 @@ jobs: ls -la package-files/ - name: Setup NuGet - uses: nuget/setup-nuget@v2 + uses: nuget/setup-nuget@v4 with: nuget-version: 'latest' @@ -198,7 +198,7 @@ jobs: nuget pack out\Microsoft.Z3.sym.nuspec -OutputDirectory . -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -BasePath out - name: Upload NuGet package - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: nuget-x64 path: | @@ -230,7 +230,7 @@ jobs: run: find packages -type f - name: Setup NuGet - uses: nuget/setup-nuget@v2 + uses: nuget/setup-nuget@v4 with: nuget-version: 'latest' @@ -247,7 +247,7 @@ jobs: nuget pack out\Microsoft.Z3.x86.sym.nuspec -OutputDirectory . -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -BasePath out - name: Upload NuGet package - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: nuget-x86 path: | diff --git a/.github/workflows/ocaml.yaml b/.github/workflows/ocaml.yaml index 7ed972b65..e2ed53d54 100644 --- a/.github/workflows/ocaml.yaml +++ b/.github/workflows/ocaml.yaml @@ -21,7 +21,7 @@ jobs: # Cache ccache (shared across runs) - name: Cache ccache - uses: actions/cache@v5.0.4 + uses: actions/cache@v5.0.5 with: path: ~/.ccache key: ${{ runner.os }}-ccache-${{ github.sha }} @@ -30,7 +30,7 @@ jobs: # Cache opam (compiler + packages) - name: Cache opam - uses: actions/cache@v5.0.4 + uses: actions/cache@v5.0.5 with: path: ~/.opam key: ${{ runner.os }}-opam-${{ matrix.ocaml-version }}-${{ github.sha }} diff --git a/.github/workflows/ostrich-benchmark.md b/.github/workflows/ostrich-benchmark.md index 6e7c450e4..ef298d065 100644 --- a/.github/workflows/ostrich-benchmark.md +++ b/.github/workflows/ostrich-benchmark.md @@ -8,7 +8,10 @@ on: permissions: read-all -network: defaults +network: + allowed: + - defaults + - api.nuget.org tools: bash: true @@ -85,6 +88,8 @@ echo "Found Microsoft.Z3.dll at: $Z3_DOTNET_DLL" If the build fails, report the error clearly and exit without proceeding. +Once the binary is confirmed working, call the `noop` safe-output tool with the message `"Z3 built successfully from the c3 branch. Starting ZIPT build and benchmark — results will be posted as a GitHub Discussion once complete."` This keepalive call refreshes the safe-output MCP session before the long build and benchmark phases begin, preventing a session timeout. + ## Phase 2a: Clone and Build ZIPT Clone the ZIPT solver from the `parikh` branch and compile it against the Z3 .NET bindings built in Phase 1. @@ -150,6 +155,8 @@ if [ "$TOTAL_FILES" -eq 0 ]; then fi ``` +Once the benchmark files are confirmed, call the `noop` safe-output tool with the message `"Benchmark files ready: Ostrich .smt2 files extracted. Starting benchmark run — this may take over an hour."` This second keepalive refreshes the safe-output MCP session immediately before the long per-file benchmark loop begins. + ## Phase 3: Run Benchmarks Run every file from `/tmp/all_ostrich_files.txt` with both Z3 string solvers and ZIPT. Use a **5-second timeout** per run. @@ -402,3 +409,14 @@ Post the Markdown report as a new GitHub Discussion using the `create-discussion - **Handle build failures gracefully**: If Z3 fails to build, report the error and create a brief discussion noting the build failure. If ZIPT fails to build, continue with only the seq/nseq columns and note `n/a` for ZIPT results. - **Large report**: Always put the per-file table in a `
` collapsible section since there may be many files. - **Progress logging**: Print a line per file as you run it (e.g., `[N] [filename] seq=...`) so the workflow log shows progress even for large benchmark sets. + +## Safe Output Guarantee + +You **MUST** call either `create_discussion` or `noop` before the workflow ends, regardless of what happened during execution: + +- **Build succeeded, benchmarks ran**: Call `create_discussion` with the full report. +- **Build succeeded, benchmarks partially ran**: Call `create_discussion` with whatever results were collected and a note about what could not be completed. +- **Z3 build failed**: Call `noop` with a brief message describing the build error. +- **No benchmarks could be run**: Call `noop` with a summary of what failed and why. + +Failing to produce any safe output triggers an automatic workflow-failure issue that clutters the repository. diff --git a/.github/workflows/qf-s-benchmark.md b/.github/workflows/qf-s-benchmark.md index fce83f8a9..653380fd6 100644 --- a/.github/workflows/qf-s-benchmark.md +++ b/.github/workflows/qf-s-benchmark.md @@ -1,5 +1,5 @@ --- -description: Run Z3 string solver benchmarks (seq vs nseq) on QF_S test suite from the c3 branch and post results as a GitHub discussion +description: Benchmark Z3 seq vs nseq string solvers on QF_S test suite from the c3 branch and post results as a GitHub discussion on: schedule: @@ -17,7 +17,7 @@ tools: safe-outputs: create-discussion: - title-prefix: "[ZIPT Benchmark] " + title-prefix: "[QF_S Benchmark] " category: "Agentic Workflows" close-older-discussions: true missing-tool: @@ -25,7 +25,7 @@ safe-outputs: noop: report-as-issue: false -timeout-minutes: 90 +timeout-minutes: 120 steps: - name: Checkout c3 branch @@ -37,437 +37,368 @@ steps: --- +# QF_S String Solver Benchmark -# ZIPT String Solver Benchmark +## Job Description -You are an AI agent that benchmarks Z3 string solvers (`seq` and `nseq`) and the standalone ZIPT solver on QF_S SMT-LIB2 benchmarks from the `c3` branch, and publishes a summary report as a GitHub discussion. +Your name is ${{ github.workflow }}. You are an expert performance analyst for the Z3 theorem prover, specializing in the string/sequence theory. Your task is to benchmark the `seq` solver (classical string theory) against the `nseq` solver (ZIPT-based string theory) on the QF_S test suite from the `c3` branch, and post a structured report as a GitHub Discussion. -## Context +The workspace already contains the `c3` branch (checked out by the preceding workflow step). -- **Repository**: ${{ github.repository }} -- **Workspace**: ${{ github.workspace }} -- **Branch**: c3 (already checked out by the workflow setup step) +## Phase 1: Set Up the Build Environment -## Phase 1: Build Z3 - -Build Z3 from the checked-out `c3` branch using CMake + Ninja, including the .NET bindings required by ZIPT. +Install required build tools: ```bash -cd ${{ github.workspace }} - -# Install build dependencies if missing -sudo apt-get install -y ninja-build cmake python3 zstd dotnet-sdk-8.0 2>/dev/null || true - -# Configure the build in Debug mode to enable assertions and tracing -# (Debug mode is required for -tr: trace flags to produce meaningful output) -mkdir -p build -cd build -cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Debug -DZ3_BUILD_DOTNET_BINDINGS=ON 2>&1 | tail -20 - -# Build z3 binary and .NET bindings (this takes ~15-17 minutes) -ninja z3 2>&1 | tail -30 -ninja build_z3_dotnet_bindings 2>&1 | tail -20 - -# Verify the build succeeded -./z3 --version - -# Locate the Microsoft.Z3.dll produced by the build -Z3_DOTNET_DLL=$(find . -name "Microsoft.Z3.dll" -not -path "*/obj/*" | head -1) -if [ -z "$Z3_DOTNET_DLL" ]; then - echo "ERROR: Microsoft.Z3.dll not found after build" - exit 1 -fi -echo "Found Microsoft.Z3.dll at: $Z3_DOTNET_DLL" +sudo apt-get update -y +sudo apt-get install -y cmake ninja-build python3 python3-pip time ``` -If the build fails, report the error clearly and exit without proceeding. - -## Phase 2a: Clone and Build ZIPT - -Clone the ZIPT solver from the `parikh` branch and compile it against the Z3 .NET bindings built in Phase 1. +Verify tools: ```bash -cd ${{ github.workspace }} +cmake --version +ninja --version +python3 --version +``` -# Re-locate the Microsoft.Z3.dll if needed -Z3_DOTNET_DLL=$(find build -name "Microsoft.Z3.dll" -not -path "*/obj/*" | head -1) -Z3_LIB_DIR=${{ github.workspace }}/build +## Phase 2: Build Z3 in Release Mode -# Clone ZIPT (parikh branch) -git clone --depth=1 --branch parikh https://github.com/CEisenhofer/ZIPT.git /tmp/zipt +Build Z3 in Release mode for accurate benchmark performance numbers and lower memory usage. Running `ninja` in the background with `&` is not allowed — concurrent C++ compilation and LLM inference can exhaust available RAM and kill the agent process. -# Patch ZIPT.csproj to point at the freshly built Microsoft.Z3.dll -# (the repo has a Windows-relative hardcoded path that won't exist here) -sed -i "s|.*|$Z3_DOTNET_DLL|" /tmp/zipt/ZIPT/ZIPT.csproj +```bash +mkdir -p /tmp/z3-build +cd /tmp/z3-build +cmake "$GITHUB_WORKSPACE" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DZ3_BUILD_TEST_EXECUTABLES=OFF \ + 2>&1 | tee /tmp/z3-cmake.log +ninja -j2 z3 2>&1 | tee /tmp/z3-build.log +``` -# Build ZIPT in Release mode -cd /tmp/zipt/ZIPT -dotnet build --configuration Release 2>&1 | tail -20 +Verify the binary was built: -# Locate the built ZIPT.dll -ZIPT_DLL=$(find /tmp/zipt/ZIPT/bin/Release -name "ZIPT.dll" | head -1) -if [ -z "$ZIPT_DLL" ]; then - echo "ERROR: ZIPT.dll not found after build" - exit 1 +```bash +/tmp/z3-build/z3 --version +``` + +If the build fails, report it immediately and stop. + +Once the binary is confirmed working, call the `noop` safe-output tool with the message `"Z3 built successfully from the c3 branch. Benchmark starting — results will be posted as a GitHub Discussion once complete."` This keepalive call refreshes the safe-output MCP session before the long benchmark run begins, preventing a session timeout. + +## Phase 3: Discover QF_S Benchmark Files + +Find all `.smt2` benchmark files in the workspace that belong to the QF_S logic: + +```bash +# Search for explicit QF_S logic declarations +grep -rl 'QF_S' "$GITHUB_WORKSPACE" --include='*.smt2' 2>/dev/null > /tmp/qf_s_files.txt + +# Also look in dedicated benchmark directories +find "$GITHUB_WORKSPACE" \ + \( -path "*/QF_S/*" -o -path "*/qf_s/*" -o -path "*/benchmarks/*" \) \ + -name '*.smt2' 2>/dev/null >> /tmp/qf_s_files.txt + +# Deduplicate +sort -u /tmp/qf_s_files.txt -o /tmp/qf_s_files.txt + +TOTAL=$(wc -l < /tmp/qf_s_files.txt) +echo "Found $TOTAL QF_S benchmark files" +head -20 /tmp/qf_s_files.txt +``` + +If fewer than 5 files are found, also scan the entire workspace for any `.smt2` file that exercises string constraints: + +```bash +if [ "$TOTAL" -lt 5 ]; then + grep -rl 'declare.*String\|str\.\|seq\.' "$GITHUB_WORKSPACE" \ + --include='*.smt2' 2>/dev/null >> /tmp/qf_s_files.txt + sort -u /tmp/qf_s_files.txt -o /tmp/qf_s_files.txt + TOTAL=$(wc -l < /tmp/qf_s_files.txt) + echo "After extended search: $TOTAL files" fi -echo "ZIPT binary: $ZIPT_DLL" +``` -# Make libz3.so visible to the .NET runtime at ZIPT startup -ZIPT_OUT_DIR=$(dirname "$ZIPT_DLL") -if cp "$Z3_LIB_DIR/libz3.so" "$ZIPT_OUT_DIR/" 2>/dev/null; then - echo "Copied libz3.so to $ZIPT_OUT_DIR" +Cap the benchmark set to keep total runtime under 60 minutes: + +```bash +# Use at most 300 files; take a random sample if more are available +if [ "$TOTAL" -gt 300 ]; then + shuf -n 300 /tmp/qf_s_files.txt > /tmp/qf_s_sample.txt else - echo "WARNING: could not copy libz3.so to $ZIPT_OUT_DIR — setting LD_LIBRARY_PATH fallback" + cp /tmp/qf_s_files.txt /tmp/qf_s_sample.txt fi -export LD_LIBRARY_PATH="$Z3_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" -echo "ZIPT build complete." +SAMPLE=$(wc -l < /tmp/qf_s_sample.txt) +echo "Running benchmarks on $SAMPLE files" ``` -If the ZIPT build fails, note the error in the report but continue with the Z3-only benchmark columns. +## Phase 4: Run Benchmarks — seq vs nseq -## Phase 2b: Extract and Select Benchmark Files - -Extract the QF_S benchmark archive and randomly select 50 files. +Run each benchmark with both solvers. Use a per-file timeout of 5 seconds. Set Z3's internal timeout to 4 seconds so it exits cleanly before the shell timeout fires. ```bash -cd ${{ github.workspace }} +Z3=/tmp/z3-build/z3 +TIMEOUT_SEC=5 +Z3_TIMEOUT_SEC=4 +RESULTS=/tmp/benchmark-results.csv -# Extract the archive -mkdir -p /tmp/qfs_benchmarks -tar --zstd -xf tests/QF_S.tar.zst -C /tmp/qfs_benchmarks +echo "file,seq_result,seq_time_ms,nseq_result,nseq_time_ms" > "$RESULTS" -# List all .smt2 files -find /tmp/qfs_benchmarks -name "*.smt2" -type f > /tmp/all_qfs_files.txt -TOTAL_FILES=$(wc -l < /tmp/all_qfs_files.txt) -echo "Total QF_S files: $TOTAL_FILES" +total=0 +done_count=0 +while IFS= read -r smt_file; do + total=$((total + 1)) -# Randomly select 200 files -shuf -n 200 /tmp/all_qfs_files.txt > /tmp/selected_files.txt -echo "Selected 200 files for benchmarking" -cat /tmp/selected_files.txt + # Run with seq solver; capture both stdout (z3 output) and stderr (time output) + SEQ_OUT=$({ time timeout "$TIMEOUT_SEC" "$Z3" \ + smt.string_solver=seq \ + -T:"$Z3_TIMEOUT_SEC" \ + "$smt_file" 2>/dev/null; } 2>&1) + SEQ_RESULT=$(echo "$SEQ_OUT" | grep -E '^(sat|unsat|unknown)' | head -1) + SEQ_MS=$(echo "$SEQ_OUT" | grep real | awk '{split($2,a,"m"); split(a[2],b,"s"); printf "%d", (a[1]*60+b[1])*1000}') + [ -z "$SEQ_RESULT" ] && SEQ_RESULT="timeout" + [ -z "$SEQ_MS" ] && SEQ_MS=$((TIMEOUT_SEC * 1000)) + + # Run with nseq solver; same structure + NSEQ_OUT=$({ time timeout "$TIMEOUT_SEC" "$Z3" \ + smt.string_solver=nseq \ + -T:"$Z3_TIMEOUT_SEC" \ + "$smt_file" 2>/dev/null; } 2>&1) + NSEQ_RESULT=$(echo "$NSEQ_OUT" | grep -E '^(sat|unsat|unknown)' | head -1) + NSEQ_MS=$(echo "$NSEQ_OUT" | grep real | awk '{split($2,a,"m"); split(a[2],b,"s"); printf "%d", (a[1]*60+b[1])*1000}') + [ -z "$NSEQ_RESULT" ] && NSEQ_RESULT="timeout" + [ -z "$NSEQ_MS" ] && NSEQ_MS=$((TIMEOUT_SEC * 1000)) + + SHORT=$(basename "$smt_file") + echo "$SHORT,$SEQ_RESULT,$SEQ_MS,$NSEQ_RESULT,$NSEQ_MS" >> "$RESULTS" + + done_count=$((done_count + 1)) + if [ $((done_count % 50)) -eq 0 ]; then + echo "Progress: $done_count / $SAMPLE files completed" + fi +done < /tmp/qf_s_sample.txt + +echo "Benchmark run complete: $done_count files" ``` -## Phase 3: Run Benchmarks +## Phase 5: Collect Seq Traces for Interesting Cases -Run each of the 200 selected files with both Z3 string solvers and ZIPT. Use a 5-second timeout for seq and a 10-second timeout for nseq and ZIPT. - -For each file, run: -1. `z3 smt.string_solver=seq -tr:seq -T:5 ` — seq solver with sequence-solver tracing enabled; rename the `.z3-trace` output after each run so it is not overwritten. Use `-T:5` when tracing to cap trace size. -2. `z3 smt.string_solver=nseq -T:5 ` — nseq solver without tracing (timing only). -3. `dotnet -t:5000 ` — ZIPT solver (milliseconds). - -Capture: -- **Verdict**: `sat`, `unsat`, `unknown`, `timeout` (if exit code indicates timeout or process is killed), or `bug` (if a solver crashes / produces a non-standard result) -- **Time** (seconds): wall-clock time for the run -- A row is flagged `SOUNDNESS_DISAGREEMENT` when any two solvers that both produced a definitive answer (sat/unsat) disagree - -Use a bash script to automate this: +For benchmarks where `seq` solves in under 2 s but `nseq` times out (seq-fast/nseq-slow cases), collect a brief `seq` trace to understand what algorithm is used: ```bash -#!/usr/bin/env bash -set -euo pipefail +Z3=/tmp/z3-build/z3 +mkdir -p /tmp/traces -Z3=${{ github.workspace }}/build/z3 -ZIPT_DLL=$(find /tmp/zipt/ZIPT/bin/Release -name "ZIPT.dll" 2>/dev/null | head -1) -ZIPT_AVAILABLE=false -[ -n "$ZIPT_DLL" ] && ZIPT_AVAILABLE=true +# Find seq-fast / nseq-slow files: seq solved (sat/unsat) in <2000ms AND nseq timed out +awk -F, 'NR>1 && ($2=="sat"||$2=="unsat") && $3<2000 && $4=="timeout" {print $1}' \ + /tmp/benchmark-results.csv > /tmp/seq_fast_nseq_slow.txt +echo "seq-fast / nseq-slow files: $(wc -l < /tmp/seq_fast_nseq_slow.txt)" -# Ensure libz3.so is on the dynamic-linker path for the .NET runtime -export LD_LIBRARY_PATH=${{ github.workspace }}/build${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} - -RESULTS=/tmp/benchmark_results.tsv -TRACES_DIR=/tmp/seq_traces -mkdir -p "$TRACES_DIR" - -echo -e "file\tseq_verdict\tseq_time\tnseq_verdict\tnseq_time\tzipt_verdict\tzipt_time\tnotes" > "$RESULTS" - -run_z3_seq_traced() { - # Run seq solver with -tr:seq tracing. Cap at 5 s so trace files stay manageable. - local file="$1" - local trace_dest="$2" - local start end elapsed verdict output exit_code - - # Remove any leftover trace from a prior run so we can detect whether one was produced. - rm -f .z3-trace - - start=$(date +%s%3N) - output=$(timeout 7 "$Z3" "smt.string_solver=seq" -tr:seq -T:5 "$file" 2>&1) - exit_code=$? - end=$(date +%s%3N) - elapsed=$(echo "scale=3; ($end - $start) / 1000" | bc) - - # Rename the trace file immediately so the next run does not overwrite it. - if [ -f .z3-trace ]; then - mv .z3-trace "$trace_dest" - else - # Write a sentinel so Phase 4 can detect the absence of a trace. - echo "(no trace produced)" > "$trace_dest" - fi - - if echo "$output" | grep -q "^unsat"; then - verdict="unsat" - elif echo "$output" | grep -q "^sat"; then - verdict="sat" - elif echo "$output" | grep -q "^unknown"; then - verdict="unknown" - elif [ "$exit_code" -eq 124 ]; then - verdict="timeout" - elif echo "$output" | grep -qi "error\|assertion\|segfault\|SIGABRT\|exception"; then - verdict="bug" - else - verdict="unknown" - fi - - echo "$verdict $elapsed" -} - -run_z3_nseq() { - local file="$1" - local start end elapsed verdict output exit_code - - start=$(date +%s%3N) - output=$(timeout 12 "$Z3" "smt.string_solver=nseq" -T:5 "$file" 2>&1) - exit_code=$? - end=$(date +%s%3N) - elapsed=$(echo "scale=3; ($end - $start) / 1000" | bc) - - if echo "$output" | grep -q "^unsat"; then - verdict="unsat" - elif echo "$output" | grep -q "^sat"; then - verdict="sat" - elif echo "$output" | grep -q "^unknown"; then - verdict="unknown" - elif [ "$exit_code" -eq 124 ]; then - verdict="timeout" - elif echo "$output" | grep -qi "error\|assertion\|segfault\|SIGABRT\|exception"; then - verdict="bug" - else - verdict="unknown" - fi - - echo "$verdict $elapsed" -} - -run_zipt() { - local file="$1" - local start end elapsed verdict output exit_code - - if [ "$ZIPT_AVAILABLE" != "true" ]; then - echo "n/a 0.000" - return - fi - - start=$(date +%s%3N) - # ZIPT prints the filename on the first line, then SAT/UNSAT/UNKNOWN on subsequent lines - output=$(timeout 12 dotnet "$ZIPT_DLL" -t:5000 "$file" 2>&1) - exit_code=$? - end=$(date +%s%3N) - elapsed=$(echo "scale=3; ($end - $start) / 1000" | bc) - - if echo "$output" | grep -qi "^UNSAT$"; then - verdict="unsat" - elif echo "$output" | grep -qi "^SAT$"; then - verdict="sat" - elif echo "$output" | grep -qi "^UNKNOWN$"; then - verdict="unknown" - elif [ "$exit_code" -eq 124 ]; then - verdict="timeout" - elif echo "$output" | grep -qi "error\|crash\|exception\|Unsupported"; then - verdict="bug" - else - verdict="unknown" - fi - - echo "$verdict $elapsed" -} - -while IFS= read -r file; do - fname=$(basename "$file") - # Use a sanitised filename (replace non-alphanumeric with _) for the trace path. - safe_name=$(echo "$fname" | tr -cs 'A-Za-z0-9._-' '_') - trace_path="$TRACES_DIR/${safe_name}.z3-trace" - - seq_result=$(run_z3_seq_traced "$file" "$trace_path") - nseq_result=$(run_z3_nseq "$file") - zipt_result=$(run_zipt "$file") - - seq_verdict=$(echo "$seq_result" | cut -d' ' -f1) - seq_time=$(echo "$seq_result" | cut -d' ' -f2) - nseq_verdict=$(echo "$nseq_result" | cut -d' ' -f1) - nseq_time=$(echo "$nseq_result" | cut -d' ' -f2) - zipt_verdict=$(echo "$zipt_result" | cut -d' ' -f1) - zipt_time=$(echo "$zipt_result" | cut -d' ' -f2) - - # Flag soundness disagreement when any two definitive verdicts disagree - notes="" - # Build list of (solver, verdict) pairs for definitive answers only - declare -A definitive_map - [ "$seq_verdict" = "sat" ] || [ "$seq_verdict" = "unsat" ] && definitive_map[seq]="$seq_verdict" - [ "$nseq_verdict" = "sat" ] || [ "$nseq_verdict" = "unsat" ] && definitive_map[nseq]="$nseq_verdict" - [ "$zipt_verdict" = "sat" ] || [ "$zipt_verdict" = "unsat" ] && definitive_map[zipt]="$zipt_verdict" - # Check every pair for conflict - has_sat=false; has_unsat=false - for v in "${definitive_map[@]}"; do - [ "$v" = "sat" ] && has_sat=true - [ "$v" = "unsat" ] && has_unsat=true - done - if $has_sat && $has_unsat; then - notes="SOUNDNESS_DISAGREEMENT" - fi - - echo -e "$fname\t$seq_verdict\t$seq_time\t$nseq_verdict\t$nseq_time\t$zipt_verdict\t$zipt_time\t$notes" >> "$RESULTS" - echo "[$fname] seq=$seq_verdict(${seq_time}s) nseq=$nseq_verdict(${nseq_time}s) zipt=$zipt_verdict(${zipt_time}s) $notes" -done < /tmp/selected_files.txt - -echo "Benchmark run complete. Results saved to $RESULTS" -echo "Trace files saved to $TRACES_DIR" -``` - -Save this script to `/tmp/run_benchmarks.sh`, make it executable, and run it. - -## Phase 3.5: Identify seq-fast / nseq-slow Cases and Analyse Traces - -After the benchmark loop completes, identify files where seq solved the instance quickly but nseq was significantly slower (or timed out). For each such file, read its saved seq trace and produce a hypothesis for why nseq is slower. - -**Definition of "seq-fast / nseq-slow"**: seq_time < 1.0 s AND nseq_time > 3 × seq_time (and nseq_time > 0.5 s). - -For each matching file: -1. Read the corresponding trace file from `/tmp/seq_traces/`. -2. Look for the sequence of lemmas, reductions, or decisions that led seq to a fast conclusion. -3. Identify patterns absent or less exploited in nseq: e.g., length-based propagation early in the trace, Parikh constraints eliminating possibilities, Nielsen graph pruning, equation splitting, or overlap resolution. -4. Write a 3–5 sentence hypothesis explaining the likely reason for the nseq slowdown, referencing specific trace entries where possible. - -Use a script to collect the candidates: - -```bash -#!/usr/bin/env bash -RESULTS=/tmp/benchmark_results.tsv -TRACES_DIR=/tmp/seq_traces -ANALYSIS=/tmp/trace_analysis.md - -echo "# Trace Analysis: seq-fast / nseq-slow Candidates" > "$ANALYSIS" -echo "" >> "$ANALYSIS" - -# Skip header line; columns: file seq_verdict seq_time nseq_verdict nseq_time ... -tail -n +2 "$RESULTS" | while IFS=$'\t' read -r fname seq_verdict seq_time nseq_verdict nseq_time _rest; do - # Use bc for floating-point comparison; bc does not support && so split into separate tests. - is_fast=$(echo "$seq_time < 1.0" | bc -l 2>/dev/null || echo 0) - threshold=$(echo "$seq_time * 3" | bc -l 2>/dev/null || echo 99999) - is_slow_threshold=$(echo "$nseq_time > $threshold" | bc -l 2>/dev/null || echo 0) - # Extra guard: exclude trivially fast seq cases where 3× is still < 0.5 s - is_over_half=$(echo "$nseq_time > 0.5" | bc -l 2>/dev/null || echo 0) - - if [ "$is_fast" = "1" ] && [ "$is_slow_threshold" = "1" ] && [ "$is_over_half" = "1" ]; then - safe_name=$(echo "$fname" | tr -cs 'A-Za-z0-9._-' '_') - trace_path="$TRACES_DIR/${safe_name}.z3-trace" - echo "## $fname" >> "$ANALYSIS" - echo "" >> "$ANALYSIS" - echo "seq: ${seq_time}s (${seq_verdict}), nseq: ${nseq_time}s (${nseq_verdict})" >> "$ANALYSIS" - echo "" >> "$ANALYSIS" - echo "### Trace excerpt (first 200 lines)" >> "$ANALYSIS" - echo '```' >> "$ANALYSIS" - head -200 "$trace_path" 2>/dev/null >> "$ANALYSIS" || echo "(trace file not found on disk)" >> "$ANALYSIS" - echo '```' >> "$ANALYSIS" - echo "" >> "$ANALYSIS" - echo "---" >> "$ANALYSIS" - echo "" >> "$ANALYSIS" - fi +# Collect traces for at most 5 such cases +head -5 /tmp/seq_fast_nseq_slow.txt | while IFS= read -r short; do + # Find the full path + full=$(grep "/$short$" /tmp/qf_s_sample.txt | head -1) + [ -z "$full" ] && continue + timeout 5 "$Z3" \ + smt.string_solver=seq \ + -tr:seq \ + -T:5 \ + "$full" > "/tmp/traces/${short%.smt2}.seq.trace" 2>&1 || true done - -echo "Candidate list written to $ANALYSIS" -cat "$ANALYSIS" ``` -Save this to `/tmp/analyse_traces.sh`, make it executable, and run it. Then read the trace excerpts collected in `/tmp/trace_analysis.md` and — for each candidate — write your hypothesis in the Phase 4 summary report under a **"Trace Analysis"** section. +## Phase 6: Analyze Results -## Phase 4: Generate Summary Report +Compute summary statistics from the CSV. Save the analysis script to a file and run it: -Read `/tmp/benchmark_results.tsv` and compute statistics. Then generate a Markdown report. +```bash +cat > /tmp/analyze_benchmark.py << 'PYEOF' +import csv, sys -Compute: -- **Total benchmarks**: 200 -- **Per solver (seq, nseq, and ZIPT)**: count of sat / unsat / unknown / timeout / bug verdicts -- **Total time used**: sum of all times for each solver -- **Average time per benchmark**: total_time / 200 -- **Soundness disagreements**: files where any two solvers that both returned a definitive answer disagree (these are the most critical bugs) -- **Bugs / crashes**: files with error/crash verdicts +results = [] +with open('/tmp/benchmark-results.csv') as f: + reader = csv.DictReader(f) + for row in reader: + results.append(row) -Format the report as a GitHub Discussion post (GitHub-flavored Markdown): +total = len(results) +if total == 0: + print("No results found.") + sys.exit(0) + +def is_correct(r, solver): + prefix = 'seq' if solver == 'seq' else 'nseq' + return r[f'{prefix}_result'] in ('sat', 'unsat') + +def timed_out(r, solver): + prefix = 'seq' if solver == 'seq' else 'nseq' + return r[f'{prefix}_result'] == 'timeout' + +seq_solved = sum(1 for r in results if is_correct(r, 'seq')) +nseq_solved = sum(1 for r in results if is_correct(r, 'nseq')) +seq_to = sum(1 for r in results if timed_out(r, 'seq')) +nseq_to = sum(1 for r in results if timed_out(r, 'nseq')) + +seq_times = [int(r['seq_time_ms']) for r in results if is_correct(r, 'seq')] +nseq_times = [int(r['nseq_time_ms']) for r in results if is_correct(r, 'nseq')] + +def median(lst): + s = sorted(lst) + n = len(s) + return s[n//2] if n else 0 + +def mean(lst): + return sum(lst)//len(lst) if lst else 0 + +# Disagreements (sat vs unsat or vice-versa) +disagreements = [ + r for r in results + if r['seq_result'] in ('sat','unsat') + and r['nseq_result'] in ('sat','unsat') + and r['seq_result'] != r['nseq_result'] +] + +# seq-fast / nseq-slow: seq solved in <2s, nseq timed out +seq_fast_nseq_slow = [ + r for r in results + if is_correct(r, 'seq') and int(r['seq_time_ms']) < 2000 and timed_out(r, 'nseq') +] +# nseq-fast / seq-slow: nseq solved in <2s, seq timed out +nseq_fast_seq_slow = [ + r for r in results + if is_correct(r, 'nseq') and int(r['nseq_time_ms']) < 2000 and timed_out(r, 'seq') +] + +print(f"TOTAL={total}") +print(f"SEQ_SOLVED={seq_solved}") +print(f"NSEQ_SOLVED={nseq_solved}") +print(f"SEQ_TIMEOUTS={seq_to}") +print(f"NSEQ_TIMEOUTS={nseq_to}") +print(f"SEQ_MEDIAN_MS={median(seq_times)}") +print(f"NSEQ_MEDIAN_MS={median(nseq_times)}") +print(f"SEQ_MEAN_MS={mean(seq_times)}") +print(f"NSEQ_MEAN_MS={mean(nseq_times)}") +print(f"DISAGREEMENTS={len(disagreements)}") +print(f"SEQ_FAST_NSEQ_SLOW={len(seq_fast_nseq_slow)}") +print(f"NSEQ_FAST_SEQ_SLOW={len(nseq_fast_seq_slow)}") + +# Print top-10 slowest for nseq that seq handles fast +print("\nTOP_SEQ_FAST_NSEQ_SLOW:") +for r in sorted(seq_fast_nseq_slow, key=lambda x: -int(x['nseq_time_ms']))[:10]: + print(f" {r['file']} seq={r['seq_time_ms']}ms nseq={r['nseq_time_ms']}ms seq_result={r['seq_result']} nseq_result={r['nseq_result']}") + +print("\nTOP_NSEQ_FAST_SEQ_SLOW:") +for r in sorted(nseq_fast_seq_slow, key=lambda x: -int(x['seq_time_ms']))[:10]: + print(f" {r['file']} seq={r['seq_time_ms']}ms nseq={r['nseq_time_ms']}ms seq_result={r['seq_result']} nseq_result={r['nseq_result']}") + +if disagreements: + print(f"\nDISAGREEMENTS ({len(disagreements)}):") + for r in disagreements[:10]: + print(f" {r['file']} seq={r['seq_result']} nseq={r['nseq_result']}") +PYEOF + +python3 /tmp/analyze_benchmark.py +``` + +## Phase 7: Create GitHub Discussion + +Use the `create_discussion` safe-output tool to post a structured benchmark report. + +The discussion body should be formatted as follows (fill in real numbers from Phase 6): ```markdown -### ZIPT Benchmark Report — Z3 c3 branch +# QF_S Benchmark: seq vs nseq -**Date**: +**Date**: YYYY-MM-DD **Branch**: c3 -**Benchmark set**: QF_S (200 randomly selected files from tests/QF_S.tar.zst) -**Timeout**: 5 seconds for seq (`-T:5`); 5 seconds for nseq (`-T:5`) and ZIPT (`-t:5000`) +**Commit**: `` +**Workflow Run**: [#](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) +**Files benchmarked**: N (capped at 300, timeout 5 s per file) --- -### Summary +## Summary -| Metric | seq solver | nseq solver | ZIPT solver | -|--------|-----------|-------------|-------------| -| sat | X | X | X | -| unsat | X | X | X | -| unknown | X | X | X | -| timeout | X | X | X | -| bug/crash | X | X | X | -| **Total time (s)** | X.XXX | X.XXX | X.XXX | -| **Avg time/benchmark (s)** | X.XXX | X.XXX | X.XXX | - -**Soundness disagreements** (any two solvers return conflicting sat/unsat): N +| Metric | seq | nseq | +|--------|-----|------| +| Files solved (sat/unsat) | SEQ_SOLVED | NSEQ_SOLVED | +| Timeouts | SEQ_TO | NSEQ_TO | +| Median solve time (solved files) | X ms | Y ms | +| Mean solve time (solved files) | X ms | Y ms | +| **Disagreements (sat≠unsat)** | — | N | --- -### Per-File Results +## Performance Comparison -| # | File | seq verdict | seq time (s) | nseq verdict | nseq time (s) | ZIPT verdict | ZIPT time (s) | Notes | -|---|------|-------------|-------------|--------------|--------------|--------------|--------------|-------| -| 1 | benchmark_0001.smt2 | sat | 0.123 | sat | 0.456 | sat | 0.789 | | -| ... | ... | ... | ... | ... | ... | ... | ... | ... | +### seq-fast / nseq-slow (seq < 2 s, nseq timed out) + +These are benchmarks where the classical `seq` solver is significantly faster. These represent regression risk for `nseq`. + +| File | seq (ms) | nseq (ms) | seq result | nseq result | +|------|----------|-----------|------------|-------------| +[TOP 10 ENTRIES] + +### nseq-fast / seq-slow (nseq < 2 s, seq timed out) + +These are benchmarks where `nseq` shows a performance advantage. + +| File | seq (ms) | nseq (ms) | seq result | nseq result | +|------|----------|-----------|------------|-------------| +[TOP 10 ENTRIES] --- -### Notable Issues +## Correctness -#### Soundness Disagreements (Critical) - +**Disagreements** (files where seq says `sat` but nseq says `unsat` or vice versa): N -#### Crashes / Bugs - - -#### Slow Benchmarks (> 8s) - - -#### Trace Analysis: seq-fast / nseq-slow Hypotheses - 3× longer, write a 3–5 sentence hypothesis based on the trace excerpt, referencing specific trace entries where possible. If no such files were found, state "No seq-fast / nseq-slow cases were observed in this run."> +[If disagreements exist, list all of them here with file paths and both results] --- -*Generated automatically by the ZIPT Benchmark workflow on the c3 branch.* +## seq Trace Analysis (seq-fast / nseq-slow cases) + +
+Click to expand trace snippets for top seq-fast/nseq-slow cases + +[Insert trace snippet for each traced file, or "No traces collected" if section was skipped] + +
+ +--- + +## Raw Data + +
+Full results CSV (click to expand) + +```csv +[PASTE FIRST 200 LINES OF /tmp/benchmark-results.csv] ``` -## Phase 5: Post to GitHub Discussion +
-Post the Markdown report as a new GitHub Discussion using the `create-discussion` safe output. +--- -- **Category**: "Agentic Workflows" -- **Title**: `[ZIPT Benchmark] Z3 c3 branch — ` -- Close older discussions with the same title prefix to avoid clutter. +*Generated by the QF_S Benchmark workflow. To reproduce: build Z3 from the `c3` branch and run `z3 smt.string_solver=seq|nseq -T:10 `.* +``` -## Guidelines +## Edge Cases -- **Always build from c3 branch**: The workspace is already checked out on c3; don't change branches. -- **Debug build required**: The build must use `CMAKE_BUILD_TYPE=Debug` so that Z3's internal assertions and trace infrastructure are active; `-tr:` trace flags have no effect in Release builds. -- **Tracing time cap**: Always pass `-T:5` when running with `-tr:seq` to limit solver runtime and keep trace files a manageable size. The nseq and ZIPT runs use `-T:5` / `-t:5000` as before. -- **Rename trace files immediately**: After each seq run, rename `.z3-trace` to a per-benchmark path before starting the next run, or the next invocation will overwrite it. -- **Handle build failures gracefully**: If Z3 fails to build, report the error and create a brief discussion noting the build failure. If ZIPT fails to build, continue with only the seq/nseq columns and note `n/a` for ZIPT results. -- **Handle missing zstd**: If `tar --zstd` fails, try `zstd -d tests/QF_S.tar.zst --stdout | tar -x -C /tmp/qfs_benchmarks`. -- **Be precise with timing**: Use millisecond-precision timestamps and report times in seconds with 3 decimal places. -- **Distinguish timeout from unknown**: A timeout (process killed after 7s outer / 5s Z3-internal for seq, or 12s/10s for nseq) is different from `(unknown)` returned by a solver. -- **ZIPT timeout unit**: ZIPT's `-t` flag takes **milliseconds**, so pass `-t:5000` for a 5-second limit. -- **ZIPT output format**: ZIPT prints the input filename on the first line, then `SAT`, `UNSAT`, or `UNKNOWN` on subsequent lines. Parse accordingly. -- **Report soundness bugs prominently**: If any benchmark shows a conflict between any two solvers that both returned a definitive sat/unsat answer, highlight it as a critical finding and name which pair disagrees. -- **Don't skip any file**: Run all 200 files even if some fail. -- **Large report**: If the per-file table is very long, put it in a `
` collapsible section. +- If the build fails, call `missing_data` explaining the build error and stop. +- If no benchmark files are found at all, call `missing_data` explaining that no QF_S `.smt2` files were found in the `c3` branch. +- If Z3 crashes (segfault) on a file with either solver, record the result as `crash` and continue. +- If the total benchmark set is very small (< 5 files), note this prominently in the discussion and suggest adding more QF_S benchmarks to the `c3` branch. +- If zero disagreements and both solvers time out on the same files, note that the solvers are in agreement. +- If `create_discussion` fails (e.g., MCP session error), call `report_incomplete` with the reason and include the top-line statistics (files solved, timeouts, disagreement count) in the `details` field. + +## Important Notes + +- **DO NOT** modify any source files or create pull requests. +- **DO NOT** run `ninja` or any build command in the background with `&` — concurrent C++ compilation and LLM inference can exhaust available RAM and kill the agent process. Always wait for build commands to complete before proceeding. +- **DO NOT** run benchmarks for longer than 100 minutes total (leave buffer for posting). +- **DO** always report the commit SHA so results can be correlated with specific code versions. +- **DO** close older QF_S Benchmark discussions automatically (configured via `close-older-discussions: true`). +- **DO** highlight disagreements prominently — these are potential correctness bugs. diff --git a/.github/workflows/release-notes-updater.md b/.github/workflows/release-notes-updater.md index b77f47099..b0c6860ad 100644 --- a/.github/workflows/release-notes-updater.md +++ b/.github/workflows/release-notes-updater.md @@ -16,8 +16,6 @@ tools: toolsets: [default] bash: [":*"] edit: {} - glob: {} - view: {} safe-outputs: create-discussion: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 712490003..275207b28 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,19 @@ jobs: - name: Build run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk --arch=x64 + - name: Validate libz3.dylib and z3 architecture (must be x86_64) + run: | + set -e + for f in build-dist/libz3.dylib build-dist/z3; do + ARCH=$(lipo -archs "$f") + echo "$f architecture: $ARCH" + if [ "$ARCH" != "x86_64" ]; then + echo "ERROR: $f has arch '$ARCH', expected 'x86_64' (see issue #9662)" + exit 1 + fi + done + echo "OK: macOS x64 artifacts are x86_64" + - name: Clone z3test run: git clone https://github.com/z3prover/z3test z3test @@ -53,7 +66,7 @@ jobs: run: python z3test/scripts/test_benchmarks.py build-dist/z3 z3test/regressions/smt2 - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: macOsBuild path: dist/*.zip @@ -75,11 +88,24 @@ jobs: - name: Build run: python scripts/mk_unix_dist.py --dotnet-key=$GITHUB_WORKSPACE/resources/z3.snk --arch=arm64 + - name: Validate libz3.dylib and z3 architecture (must be arm64) + run: | + set -e + for f in build-dist/libz3.dylib build-dist/z3; do + ARCH=$(lipo -archs "$f") + echo "$f architecture: $ARCH" + if [ "$ARCH" != "arm64" ]; then + echo "ERROR: $f has arch '$ARCH', expected 'arm64' (see issue #9662)" + exit 1 + fi + done + echo "OK: macOS arm64 artifacts are arm64" + - name: Clone z3test run: git clone https://github.com/z3prover/z3test z3test - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: MacArm64 path: dist/*.zip @@ -111,6 +137,17 @@ jobs: Z3_DIR=$(find . -maxdepth 1 -type d -name "z3-*" | head -n 1) echo "Z3_DIR=$Z3_DIR" >> $GITHUB_ENV + - name: Validate shipped libz3.dylib architecture (must be x86_64) + run: | + set -e + DYLIB="artifacts/$Z3_DIR/bin/libz3.dylib" + ARCH=$(lipo -archs "$DYLIB") + echo "Shipped $DYLIB architecture: $ARCH" + if [ "$ARCH" != "x86_64" ]; then + echo "ERROR: x64 release zip contains '$ARCH' libz3.dylib (see issue #9662)" + exit 1 + fi + - name: Test install_name_tool with headerpad run: | cd artifacts/$Z3_DIR/bin @@ -159,6 +196,17 @@ jobs: Z3_DIR=$(find . -maxdepth 1 -type d -name "z3-*" | head -n 1) echo "Z3_DIR=$Z3_DIR" >> $GITHUB_ENV + - name: Validate shipped libz3.dylib architecture (must be arm64) + run: | + set -e + DYLIB="artifacts/$Z3_DIR/bin/libz3.dylib" + ARCH=$(lipo -archs "$DYLIB") + echo "Shipped $DYLIB architecture: $ARCH" + if [ "$ARCH" != "arm64" ]; then + echo "ERROR: arm64 release zip contains '$ARCH' libz3.dylib (see issue #9662)" + exit 1 + fi + - name: Test install_name_tool with headerpad run: | cd artifacts/$Z3_DIR/bin @@ -208,7 +256,7 @@ jobs: run: python z3test/scripts/test_benchmarks.py build-dist/z3 z3test/regressions/smt2 - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: UbuntuBuild path: dist/*.zip @@ -243,7 +291,7 @@ jobs: python scripts/mk_unix_dist.py --nodotnet --arch=arm64 - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: UbuntuArm64 path: dist/*.zip @@ -298,7 +346,7 @@ jobs: run: zip -r z3doc.zip doc/api - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: UbuntuDoc path: z3doc.zip @@ -313,9 +361,17 @@ jobs: - name: Checkout code uses: actions/checkout@v6.0.2 + - name: Select Python + run: | + # Use the first available manylinux interpreter for deterministic selection. + PYTHON=$(printf '%s\n' /opt/python/*/bin/python | sort -V | head -n1) + test -x "$PYTHON" || { echo "Error: no interpreter found under /opt/python/*/bin/python"; exit 1; } + echo "PYTHON=$PYTHON" >> "$GITHUB_ENV" + "$PYTHON" --version + - name: Setup Python environment run: | - /opt/python/cp38-cp38/bin/python -m venv $PWD/env + "$PYTHON" -m venv $PWD/env echo "$PWD/env/bin" >> $GITHUB_PATH - name: Install build tools @@ -328,7 +384,7 @@ jobs: run: pip install ./src/api/python/wheelhouse/*.whl && python - > "$GITHUB_ENV" + "$PYTHON" --version + - name: Setup Python environment run: | - /opt/python/cp38-cp38/bin/python -m venv $PWD/env + "$PYTHON" -m venv $PWD/env echo "$PWD/env/bin" >> $GITHUB_PATH echo "/tmp/arm-toolchain/bin" >> $GITHUB_PATH echo "/tmp/arm-toolchain/aarch64-none-linux-gnu/libc/usr/bin" >> $GITHUB_PATH @@ -368,12 +432,120 @@ jobs: run: cd src/api/python && CC=aarch64-none-linux-gnu-gcc CXX=aarch64-none-linux-gnu-g++ AR=aarch64-none-linux-gnu-ar LD=aarch64-none-linux-gnu-ld Z3_CROSS_COMPILING=aarch64 python -m build && AUDITWHEEL_PLAT= auditwheel repair --best-plat dist/*.whl && cd ../../.. - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: ManyLinuxPythonBuildArm64 path: src/api/python/wheelhouse/*.whl retention-days: 7 + manylinux-python-riscv64: + name: "Python bindings (manylinux RISC-V 64 cross)" + runs-on: ubuntu-latest + timeout-minutes: 90 + container: quay.io/pypa/manylinux_2_28_x86_64:latest + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + + - name: Download RISC-V toolchain + run: curl -L -o /tmp/riscv-toolchain.tar.gz 'https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/2024.09.03/riscv64-glibc-ubuntu-20.04-gcc-nightly-2024.09.03-nightly.tar.gz' + + - name: Extract RISC-V toolchain + run: | + mkdir -p /tmp/riscv-toolchain/ + tar xf /tmp/riscv-toolchain.tar.gz -C /tmp/riscv-toolchain/ --strip-components=1 + + - name: Install MPFR 4 (required by RISC-V toolchain host binaries) + run: | + dnf install -y gmp-devel + curl -L -o /tmp/mpfr.tar.xz https://ftp.gnu.org/gnu/mpfr/mpfr-4.2.1.tar.xz + tar xf /tmp/mpfr.tar.xz -C /tmp/ + cd /tmp/mpfr-4.2.1 && ./configure --prefix=/usr/local --disable-static && make -j$(nproc) && make install + ldconfig + + - name: Select Python + run: | + # Use the first available manylinux interpreter for deterministic selection. + PYTHON=$(printf '%s\n' /opt/python/*/bin/python | sort -V | head -n1) + test -x "$PYTHON" || { echo "Error: no interpreter found under /opt/python/*/bin/python"; exit 1; } + echo "PYTHON=$PYTHON" >> "$GITHUB_ENV" + "$PYTHON" --version + + - name: Setup Python environment + run: | + "$PYTHON" -m venv $PWD/env + echo "$PWD/env/bin" >> $GITHUB_PATH + echo "/tmp/riscv-toolchain/bin" >> $GITHUB_PATH + + - name: Install build tools + run: | + echo $PATH + stat $(which riscv64-unknown-linux-gnu-gcc) + pip install build git+https://github.com/rhelmot/auditwheel + + - name: Build wheels + run: cd src/api/python && CC=riscv64-unknown-linux-gnu-gcc CXX=riscv64-unknown-linux-gnu-g++ AR=riscv64-unknown-linux-gnu-ar LD=riscv64-unknown-linux-gnu-ld Z3_CROSS_COMPILING=riscv64 python -m build && AUDITWHEEL_PLAT= auditwheel repair --best-plat dist/*.whl && cd ../../.. + + - name: Upload artifact + uses: actions/upload-artifact@v7.0.1 + with: + name: ManyLinuxPythonBuildRiscv64 + path: src/api/python/wheelhouse/*.whl + retention-days: 7 + + pyodide-python: + name: "Python bindings (Pyodide)" + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Checkout code + uses: actions/checkout@v6.0.2 + + - name: Setup packages + run: sudo apt-get update && sudo apt-get install -y python3-dev python3-pip python3-venv + + - name: Create venv + run: python3 -m venv ~/env + + - name: Install pyodide + run: ~/env/bin/pip install pyodide-build pyodide-cli + + - name: Configure Emscripten + run: | + git clone https://github.com/emscripten-core/emsdk.git ~/emsdk + cd ~/emsdk + PYODIDE_EMSCRIPTEN_VERSION=$(~/env/bin/pyodide config get emscripten_version) + ./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION} + ./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION} + + - name: Build wheel + run: | + source ~/emsdk/emsdk_env.sh + cd src/api/python + CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" CXXFLAGS="${CXXFLAGS}" ~/env/bin/pyodide build --exports whole_archive + env: + CFLAGS: "-fexceptions -s DISABLE_EXCEPTION_CATCHING=0 -g2" + LDFLAGS: "-fexceptions -s WASM_BIGINT" + CXXFLAGS: "-fexceptions -s DISABLE_EXCEPTION_CATCHING=0" + + - name: Setup env-pyodide + run: | + source ~/env/bin/activate + source ~/emsdk/emsdk_env.sh + pyodide venv ~/env-pyodide + + - name: Test wheel + run: | + ~/env-pyodide/bin/pip install src/api/python/dist/*.whl + ~/env-pyodide/bin/python src/api/python/z3test.py z3 + + - name: Upload artifact + uses: actions/upload-artifact@v7.0.1 + with: + name: PyodidePythonBuild + path: src/api/python/dist/*.whl + retention-days: 7 + windows-build-x64: name: "Windows x64 build" runs-on: windows-latest @@ -394,7 +566,7 @@ jobs: python scripts\mk_win_dist.py --x64-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --zip - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: WindowsBuild-x64 path: dist/*.zip @@ -420,7 +592,7 @@ jobs: python scripts\mk_win_dist.py --x86-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --zip - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: WindowsBuild-x86 path: dist/*.zip @@ -446,7 +618,7 @@ jobs: python scripts\mk_win_dist_cmake.py --arm64-only --dotnet-key=%GITHUB_WORKSPACE%\resources\z3.snk --assembly-version=${{ env.RELEASE_VERSION }} --zip - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: WindowsBuild-arm64 path: dist/arm64/*.zip @@ -506,7 +678,7 @@ jobs: path: package - name: Setup NuGet - uses: nuget/setup-nuget@v2 + uses: nuget/setup-nuget@v4 with: nuget-version: 'latest' @@ -523,7 +695,7 @@ jobs: nuget pack out\Microsoft.Z3.sym.nuspec -OutputDirectory . -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -BasePath out - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: NuGet path: | @@ -551,7 +723,7 @@ jobs: path: package - name: Setup NuGet - uses: nuget/setup-nuget@v2 + uses: nuget/setup-nuget@v4 with: nuget-version: 'latest' @@ -568,7 +740,7 @@ jobs: nuget pack out\Microsoft.Z3.x86.sym.nuspec -OutputDirectory . -Verbosity detailed -Symbols -SymbolPackageFormat snupkg -BasePath out - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: NuGet32 path: | @@ -578,7 +750,7 @@ jobs: python-package: name: "Python packaging" - needs: [mac-build-x64, mac-build-arm64, windows-build-x64, windows-build-x86, windows-build-arm64, manylinux-python-amd64, manylinux-python-arm64] + needs: [mac-build-x64, mac-build-arm64, windows-build-x64, windows-build-x86, windows-build-arm64, manylinux-python-amd64, manylinux-python-arm64, manylinux-python-riscv64, pyodide-python] runs-on: ubuntu-24.04 steps: - name: Checkout code @@ -631,6 +803,18 @@ jobs: name: ManyLinuxPythonBuildArm64 path: artifacts + - name: Download ManyLinux RISC-V 64 Build + uses: actions/download-artifact@v8.0.1 + with: + name: ManyLinuxPythonBuildRiscv64 + path: artifacts + + - name: Download Pyodide Build + uses: actions/download-artifact@v8.0.1 + with: + name: PyodidePythonBuild + path: artifacts + - name: Extract builds run: | cd artifacts @@ -658,7 +842,7 @@ jobs: cp artifacts/*.whl src/api/python/dist/. - name: Upload artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v7.0.1 with: name: PythonPackage path: src/api/python/dist/* @@ -760,7 +944,7 @@ jobs: path: packages - name: Setup NuGet - uses: nuget/setup-nuget@v2 + uses: nuget/setup-nuget@v4 with: nuget-version: 'latest' diff --git a/.github/workflows/smtlib-benchmark-finder.lock.yml b/.github/workflows/smtlib-benchmark-finder.lock.yml new file mode 100644 index 000000000..2b397404b --- /dev/null +++ b/.github/workflows/smtlib-benchmark-finder.lock.yml @@ -0,0 +1,1442 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7fe62b76e1ec94b221dbb20fe5cd392a7a47a296f9da44c7405c343a2ca3c3e9","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.74.8","version":"v0.74.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.74.8). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Monthly SMTLIB Benchmark Finder. Searches GitHub for repositories containing SMT-LIB benchmarks (.smt2 files), excludes repositories that belong to the official SMT-LIB benchmark sets (linked from smtlib.org and hosted on Zenodo), and posts a curated summary of community-contributed benchmark links as a GitHub Discussion. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@v0.77.0 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.49 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.49 +# - ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388 +# - ghcr.io/github/github-mcp-server:v1.0.4 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + +name: "SMTLIB Benchmark Finder" +on: + schedule: + - cron: "0 8 1 * *" + workflow_dispatch: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "SMTLIB Benchmark Finder" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smtlib-benchmark-finder.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.5' }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_AGENT_VERSION: "1.0.48" + GH_AW_INFO_CLI_VERSION: "v0.74.8" + GH_AW_INFO_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github","smtlib.cs.uiowa.edu","zenodo.org"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.49" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "smtlib-benchmark-finder.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.74.8" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_04598545a7d250b1_EOF' + + GH_AW_PROMPT_04598545a7d250b1_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_04598545a7d250b1_EOF' + + Tools: create_discussion, missing_tool, missing_data, noop + + GH_AW_PROMPT_04598545a7d250b1_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_04598545a7d250b1_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_04598545a7d250b1_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_04598545a7d250b1_EOF' + + {{#runtime-import .github/workflows/smtlib-benchmark-finder.md}} + GH_AW_PROMPT_04598545a7d250b1_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' + GH_AW_CACHE_DESCRIPTION: '' + GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, + GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, + GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKFLOW: process.env.GH_AW_GITHUB_WORKFLOW, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: read-all + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: smtlibbenchmarkfinder + outputs: + agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smtlib-benchmark-finder.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + # Cache memory file share configuration from frontmatter processed below + - name: Create cache-memory directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_cache_memory_dir.sh" + - name: Restore cache-memory file share data + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + restore-keys: | + memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + - name: Setup cache-memory git repository + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + GH_AW_MIN_INTEGRITY: none + run: bash "${RUNNER_TEMP}/gh-aw/actions/setup_cache_memory_git.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.48 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.49 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.49 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49 ghcr.io/github/gh-aw-firewall/squid:0.25.49 ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388 ghcr.io/github/github-mcp-server:v1.0.4 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_1c2429a4603946af_EOF' + {"create_discussion":{"category":"agentic workflows","close_older_discussions":true,"expires":2160,"fallback_to_issue":true,"max":1,"title_prefix":"[SMT-LIB Benchmarks] "},"create_report_incomplete_issue":{},"max_bot_mentions":1,"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_1c2429a4603946af_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Title will be prefixed with \"[SMT-LIB Benchmarks] \". Discussions will be created in category \"agentic workflows\"." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_discussion": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "category": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.9' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_07d3b8ec96f0806f_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_07d3b8ec96f0806f_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 60 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.49/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","codeload.github.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","docs.github.com","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.blog","github.com","github.githubassets.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","lfs.github.com","objects.githubusercontent.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","patch-diff.githubusercontent.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","smtlib.cs.uiowa.edu","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com","zenodo.org"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5","gemini-pro","haiku","any"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"auto":["large"],"claude":["agent","sonnet-6x","haiku","any"],"codex":["agent","gpt-5-codex","gpt-5","any"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"copilot":["agent","gpt-5.4","sonnet","gpt-5","any"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent","gemini-pro","gemini-flash","any"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite","copilot/raptor*mini*"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4-5*","anthropic/*sonnet-4.5*","anthropic/*sonnet-4-5*","copilot/*sonnet-3.7*","copilot/*sonnet-3-7*","anthropic/*sonnet-3.7*","anthropic/*sonnet-3-7*","copilot/*sonnet-3.5*","copilot/*sonnet-3-5*","anthropic/*sonnet-3.5*","anthropic/*sonnet-3-5*"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.49"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.5' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.74.8 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect Copilot errors + id: detect-copilot-errors + if: always() + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,smtlib.cs.uiowa.edu,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,zenodo.org" + GH_AW_ALLOWED_GITHUB_REFS: "" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Commit cache-memory changes + if: always() + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + run: bash "${RUNNER_TEMP}/gh-aw/actions/commit_cache_memory_git.sh" + - name: Upload cache-memory data as artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: cache-memory + include-hidden-files: true + path: /tmp/gh-aw/cache-memory + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + - update_cache_memory + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + concurrency: + group: "gh-aw-conclusion-smtlib-benchmark-finder" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smtlib-benchmark-finder.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_MISSING_TOOL_TITLE_PREFIX: "[missing tool]" + GH_AW_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "smtlib-benchmark-finder" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CREATE_DISCUSSION_ERRORS: ${{ needs.safe_outputs.outputs.create_discussion_errors }} + GH_AW_CREATE_DISCUSSION_ERROR_COUNT: ${{ needs.safe_outputs.outputs.create_discussion_error_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "60" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + GH_AW_CACHE_MEMORY_ENABLED: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smtlib-benchmark-finder.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.49 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49 ghcr.io/github/gh-aw-firewall/squid:0.25.49 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "SMTLIB Benchmark Finder" + WORKFLOW_DESCRIPTION: "Monthly SMTLIB Benchmark Finder. Searches GitHub for repositories containing SMT-LIB benchmarks (.smt2 files), excludes repositories that belong to the official SMT-LIB benchmark sets (linked from smtlib.org and hosted on Zenodo), and posts a curated summary of community-contributed benchmark links as a GitHub Discussion." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.48 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.49 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.49/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.49"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.5' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.74.8 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/smtlib-benchmark-finder" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.48" + GH_AW_WORKFLOW_ID: "smtlib-benchmark-finder" + GH_AW_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smtlib-benchmark-finder.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,smtlib.cs.uiowa.edu,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,zenodo.org" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"agentic workflows\",\"close_older_discussions\":true,\"expires\":2160,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"[SMT-LIB Benchmarks] \"},\"create_report_incomplete_issue\":{},\"mentions\":{\"enabled\":false},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + + update_cache_memory: + needs: + - activation + - agent + - detection + if: > + always() && (needs.detection.result == 'success' || needs.detection.result == 'skipped') && + needs.agent.result == 'success' + runs-on: ubuntu-slim + permissions: {} + env: + GH_AW_WORKFLOW_ID_SANITIZED: smtlibbenchmarkfinder + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "SMTLIB Benchmark Finder" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smtlib-benchmark-finder.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download cache-memory artifact (default) + id: download_cache_default + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Check if cache-memory folder has content (default) + id: check_cache_default + shell: bash + run: | + if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then + echo "has_content=true" >> "$GITHUB_OUTPUT" + else + echo "has_content=false" >> "$GITHUB_OUTPUT" + fi + - name: Save cache-memory to cache (default) + if: steps.check_cache_default.outputs.has_content == 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + diff --git a/.github/workflows/smtlib-benchmark-finder.md b/.github/workflows/smtlib-benchmark-finder.md new file mode 100644 index 000000000..19c615493 --- /dev/null +++ b/.github/workflows/smtlib-benchmark-finder.md @@ -0,0 +1,342 @@ +--- +description: > + Monthly SMTLIB Benchmark Finder. + Searches GitHub for repositories containing SMT-LIB benchmarks (.smt2 files), + excludes repositories that belong to the official SMT-LIB benchmark sets + (linked from smtlib.org and hosted on Zenodo), and posts a curated summary + of community-contributed benchmark links as a GitHub Discussion. + +on: + schedule: + - cron: "0 8 1 * *" + workflow_dispatch: + +timeout-minutes: 60 + +permissions: read-all + +network: + allowed: + - defaults + - github + - smtlib.cs.uiowa.edu + - zenodo.org + +tools: + cache-memory: true + web-fetch: {} + github: + toolsets: [default, repos] + bash: [":*"] + +safe-outputs: + mentions: false + allowed-github-references: [] + max-bot-mentions: 1 + create-discussion: + title-prefix: "[SMT-LIB Benchmarks] " + category: "Agentic Workflows" + close-older-discussions: true + expires: 90d + missing-tool: + create-issue: true + noop: + report-as-issue: false + +--- + +# SMTLIB Benchmark Finder + +## Job Description + +Your name is ${{ github.workflow }}. You are a research analyst for the Z3 theorem +prover repository `${{ github.repository }}`. Your mission is to discover GitHub +repositories that host SMT-LIB benchmarks, exclude the ones that are already part +of the official SMT-LIB benchmark distribution (linked from smtlib.org and published +on Zenodo), and post a curated summary of community-contributed benchmark links as a +GitHub Discussion. + +## Step 1: Load Cache and Determine Run Mode + +Check cache memory for: +- `official_repos`: set of GitHub repository full names (`owner/repo`) and Zenodo + record IDs already identified as official SMT-LIB benchmark sets +- `known_community_repos`: set of repo full names already listed in a previous report +- `last_run_date`: ISO-8601 date string of the previous run + +Use the cache to skip repos already classified. On the very first run (no cache), +perform a full discovery pass. On subsequent runs focus on repos pushed or created +since `last_run_date`. + +## Step 2: Collect Official SMT-LIB Benchmark Sets to Exclude + +### 2.1 Scrape smtlib.org + +Fetch the SMT-LIB benchmarks page and extract all linked Zenodo DOIs and GitHub URLs: + +```bash +curl -s "https://smtlib.cs.uiowa.edu/benchmarks.shtml" -o /tmp/smtlib-benchmarks.html +# Also try the main page and any mirror +curl -s "https://smtlib.cs.uiowa.edu/" -o /tmp/smtlib-home.html +``` + +Parse both files for: +- Zenodo DOI links (`doi.org/10.5281/zenodo.*` or `zenodo.org/record/*`) +- GitHub repository URLs (`github.com/...`) +- Any other hosted benchmark archive links + +### 2.2 Enumerate Zenodo SMT-LIB Community Records + +Query the Zenodo API for all records in the SMT-LIB community: + +```bash +curl -s "https://zenodo.org/api/records?communities=smt-lib&size=100&page=1" \ + -o /tmp/zenodo-smtlib-page1.json + +# Check if there are more pages (paginate until empty) +curl -s "https://zenodo.org/api/records?communities=smt-lib&size=100&page=2" \ + -o /tmp/zenodo-smtlib-page2.json 2>/dev/null || true + +curl -s "https://zenodo.org/api/records?communities=smt-lib&size=100&page=3" \ + -o /tmp/zenodo-smtlib-page3.json 2>/dev/null || true +``` + +For each Zenodo record extract: +- Record ID (e.g. `5827900`) +- Title +- Any GitHub repository URLs listed in the description or related identifiers + +```bash +python3 - <<'PYEOF' +import json, re + +def extract_github_repos(text): + pattern = r'github\.com/([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)' + return set(re.findall(pattern, text or '')) + +official_repos = set() +official_zenodo_ids = set() + +for fname in ['/tmp/zenodo-smtlib-page1.json', '/tmp/zenodo-smtlib-page2.json', + '/tmp/zenodo-smtlib-page3.json']: + try: + data = json.load(open(fname)) + except Exception: + continue + for hit in data.get('hits', {}).get('hits', []): + rid = str(hit.get('id', '')) + official_zenodo_ids.add(rid) + metadata = hit.get('metadata', {}) + description = metadata.get('description', '') + related = ' '.join( + r.get('identifier', '') + for r in metadata.get('related_identifiers', []) + ) + title = metadata.get('title', '') + for repo in extract_github_repos(description + ' ' + related + ' ' + title): + official_repos.add(repo.lower().rstrip('.')) + +with open('/tmp/smtlib-benchmarks.html') as f: + html = f.read() +for repo in extract_github_repos(html): + official_repos.add(repo.lower().rstrip('.')) + +print("OFFICIAL_ZENODO_IDS:", ','.join(sorted(official_zenodo_ids)) or '(none)') +print("OFFICIAL_GITHUB_REPOS:", ','.join(sorted(official_repos)) or '(none)') +PYEOF +``` + +### 2.3 Well-Known Official Repository Patterns + +Regardless of the above scrape, always exclude: +- Any repo under the `SMT-LIB` GitHub organization (`SMT-LIB/*`) +- Any repo whose name matches `smt-comp-*` that is under `SMT-Competition` org +- The Z3 repo itself (`Z3Prover/z3`) and its forks + +Combine all official sources into a single exclusion set stored at +`/tmp/official_exclusions.txt` (one `owner/repo` pattern per line, lowercase). + +## Step 3: Search GitHub for Community SMT-LIB Benchmark Repositories + +Use multiple GitHub search strategies to find repos containing `.smt2` benchmark +files that are NOT part of the official distribution. Search for repos updated +since the last run date (or the last 90 days for the initial run). + +Compute the cutoff date first: +```bash +CUTOFF=$(date -d "90 days ago" +%Y-%m-%d 2>/dev/null || date -v-90d +%Y-%m-%d) +echo "Using cutoff date: $CUTOFF" +``` + +### Search Strategies + +Use the GitHub MCP server tools to run these searches: + +1. **Topic search**: `topic:smtlib pushed:>$CUTOFF` +2. **Topic search variant**: `topic:smt-lib pushed:>$CUTOFF` +3. **Topic search variant**: `topic:smt2 pushed:>$CUTOFF` +4. **Benchmark filename pattern**: `filename:*.smt2 pushed:>$CUTOFF` (limit to top results) +5. **Benchmarks directory pattern**: `path:benchmarks *.smt2 in:path pushed:>$CUTOFF` +6. **README mention**: `SMT-LIB benchmarks in:readme stars:>2 pushed:>$CUTOFF` +7. **Organization-level search**: repos under `SMT-Competition` org, if any are not already excluded + +For each search, collect: `full_name`, `html_url`, `description`, `stargazers_count`, +`updated_at`, `pushed_at`, `default_branch`, `topics`. + +Deduplicate by `full_name`. Limit to 200 total candidates before filtering. + +## Step 4: Filter Out Official Benchmark Sets + +For each candidate repo: + +1. Check if `full_name.lower()` is in the exclusion set from Step 2. +2. Check if the repo is owned by `SMT-LIB` or `SMT-Competition` org (case-insensitive). +3. Check if the repo is a fork of a known official repo (if `fork: true` and parent is + in the exclusion set). + +Discard repos that match any of the above. Keep the rest as community benchmarks. + +Also apply quality filters to reduce noise — skip repos that: +- Have 0 stars and fewer than 3 `.smt2` files (likely a student homework or test repo + with minimal public value); use your judgement — if the repo description clearly + describes a research benchmark, keep it regardless of star count. +- Were created but never pushed after creation (empty repos). +- Have names that are clearly course assignment repositories + (e.g. contain `homework`, `assignment`, `hw[0-9]`, `cs[0-9]{3}`). + +## Step 5: Classify Remaining Repos + +For each repo that survives filtering, classify it into one of these categories: + +| Category | Description | +|----------|-------------| +| **Solver evaluation** | Benchmarks used to evaluate or compare SMT solvers | +| **Verification** | Benchmarks from program verification or model checking | +| **Security / CTF** | Benchmarks from security research or CTF challenges | +| **Theory / logic** | Benchmarks exploring specific SMT theories or logics | +| **Tool output** | Benchmarks generated by another tool (e.g. a compiler, fuzzer) | +| **Education** | Course materials or tutorials with benchmark examples | +| **Other / unknown** | Does not fit another category | + +Base the classification on: repo description, README (fetch if web-fetch is available), +topics, and directory structure. A single brief `web-fetch` of the repo's README is +sufficient; do not fetch individual `.smt2` files. + +Note the dominant SMT logic(s) present, if discernible from the description or topics +(e.g. QF_BV, QF_LIA, QF_S, NIA, …). + +## Step 6: Generate the Discussion Report + +Create a GitHub Discussion. Use heading level 3 or deeper (`###`, `####`, …) for all +section headers; never use `##` or `#` in the body. +Wrap long tables in `
` tags to keep the report scannable. + +Title: `[SMT-LIB Benchmarks] Community Benchmark Repository Survey — [Month YYYY]` + +Structure the report as follows: + +```markdown +**Period covered**: [cutoff date] – [today's date] +**Repositories found**: N community repos (after excluding M official sets) +**New this run**: N (not listed in previous report) + +### Overview + +1–2 sentences summarising the breadth of community SMT-LIB benchmarks found. + +### Community Benchmark Repositories + +Use `###` for category headers. Within each category, list repos as a markdown table. +For each repo include: +- Repo link (`[owner/repo](html_url)`) +- Stars +- Last pushed date +- Dominant logic(s) (if known) +- Brief description (from repo description or README, max 120 chars) + +#### Solver Evaluation + +| Repository | ⭐ | Last pushed | Logic(s) | Description | +|------------|-----|------------|---------|-------------| +| [owner/repo](url) | N | YYYY-MM-DD | QF_BV, QF_LIA | … | + +#### Verification + +[same table structure] + +#### Security / CTF + +[same table structure] + +#### Theory / Logic + +[same table structure] + +#### Tool Output + +[same table structure] + +#### Education + +[same table structure] + +#### Other / Unknown + +[same table structure] + +--- + +### Exclusions Applied + +
+Official SMT-LIB sets excluded from this report + +List Zenodo record IDs and GitHub repos identified as official distributions. + +| Source | Identifier | Notes | +|--------|-----------|-------| +| Zenodo | [10.5281/zenodo.XXXXXXX](https://zenodo.org/record/XXXXXXX) | Official QF_BV benchmark set | +| GitHub | [SMT-LIB/benchmarks-non-incremental](https://github.com/SMT-LIB/benchmarks-non-incremental) | | + +
+ +--- + +### Methodology + +Brief note on search queries used, cutoff date, and any quality filters applied. +``` + +## Step 7: Update Cache Memory + +After posting the discussion, update cache memory with: +- `official_repos`: updated exclusion set (union of previous + newly found) +- `known_community_repos`: union of previous + repos listed in this report +- `last_run_date`: today's ISO-8601 date +- `report_url`: URL of the GitHub Discussion created + +## Guidelines + +- **Be conservative with exclusions**: when in doubt whether a repo is "official", + keep it in the community list rather than silently dropping it. +- **Be accurate**: only include repos that genuinely contain SMT-LIB `.smt2` files + or clearly describe themselves as SMT-LIB benchmark collections. +- **Avoid noise**: student homework repos and trivially small repos add clutter; + apply the quality filters from Step 4 judiciously. +- **No source code changes**: DO NOT create pull requests or modify any source files. +- **No copyrighted content**: DO NOT reproduce benchmark file contents; only post + links and metadata. +- **Always cite sources**: include the full GitHub URL for every listed repository. +- **Use cache**: skip repos already classified in a previous run to keep runtime short. +- **Fail gracefully**: if GitHub search rate-limits the workflow, post whatever was + collected so far with a note that the search was incomplete. + +## Important Notes + +- DO NOT create pull requests or modify source files. +- DO close older SMT-LIB Benchmarks discussions automatically (configured). +- DO always call `create_discussion` or `noop` before the workflow ends. + Failing to produce any safe output triggers an automatic failure issue. +- DO use cache memory to avoid re-processing repos already surveyed. +- DO limit individual `web-fetch` calls (README fetches) to repos where the + description alone is insufficient for classification. diff --git a/.github/workflows/specbot-crash-analyzer.lock.yml b/.github/workflows/specbot-crash-analyzer.lock.yml new file mode 100644 index 000000000..58fa451d5 --- /dev/null +++ b/.github/workflows/specbot-crash-analyzer.lock.yml @@ -0,0 +1,1429 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6ca984b05852b2ec7d6d294a8f22caa4fb6bba8af84d8c39be110bbd512d2db0","compiler_version":"v0.71.5","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.71.5","version":"v0.71.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40","digest":"sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40","digest":"sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40","digest":"sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.71.5). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Build Z3 in debug mode from the c3 branch, compile and run the specbot tests, identify root causes for any crashes, and post findings as a GitHub Discussion. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@v0.77.0 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 +# - ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c +# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + +name: "Specbot Crash Analyzer" +"on": + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "Specbot Crash Analyzer" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/specbot-crash-analyzer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_AGENT_VERSION: "1.0.40" + GH_AW_INFO_CLI_VERSION: "v0.71.5" + GH_AW_INFO_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.40" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "specbot-crash-analyzer.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.71.5" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_3c664e92078139cb_EOF' + + GH_AW_PROMPT_3c664e92078139cb_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_3c664e92078139cb_EOF' + + Tools: create_discussion, missing_tool, missing_data, noop + + GH_AW_PROMPT_3c664e92078139cb_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_3c664e92078139cb_EOF' + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_3c664e92078139cb_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_3c664e92078139cb_EOF' + + {{#runtime-import .github/workflows/specbot-crash-analyzer.md}} + GH_AW_PROMPT_3c664e92078139cb_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ALLOWED_EXTENSIONS: '' + GH_AW_CACHE_DESCRIPTION: '' + GH_AW_CACHE_DIR: '/tmp/gh-aw/cache-memory/' + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_ALLOWED_EXTENSIONS: process.env.GH_AW_ALLOWED_EXTENSIONS, + GH_AW_CACHE_DESCRIPTION: process.env.GH_AW_CACHE_DESCRIPTION, + GH_AW_CACHE_DIR: process.env.GH_AW_CACHE_DIR, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKFLOW: process.env.GH_AW_GITHUB_WORKFLOW, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: read-all + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: specbotcrashanalyzer + outputs: + agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/specbot-crash-analyzer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Checkout c3 branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + ref: c3 + - name: Install build dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y cmake ninja-build python3 gcc g++ 2>&1 | tail -5 + - continue-on-error: true + id: build-z3 + name: Build Z3 in debug mode + run: | + mkdir -p build/debug specbot-results + cd build/debug + cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ../.. 2>&1 | tee ../../specbot-results/cmake.log + ninja 2>&1 | tee ../../specbot-results/build.log + BUILD_EXIT=$? + cd ../.. + echo "build_exit=${BUILD_EXIT}" >> specbot-results/build-status.txt + ls -la build/debug/libz3* build/debug/*.so* 2>/dev/null >> specbot-results/build-status.txt || echo "Library not found" >> specbot-results/build-status.txt + exit $BUILD_EXIT + - continue-on-error: true + name: Compile specbot tests + run: "mkdir -p specbot-results\ngcc -g -O0 \\\n -I src/api \\\n specbot/test_specbot_seq.c \\\n -L build/debug \\\n -lz3 \\\n -Wl,-rpath,\"${GITHUB_WORKSPACE}/build/debug\" \\\n -o specbot-results/test_specbot_seq \\\n 2>&1 | tee specbot-results/compile_specbot_seq.log\necho \"compile_specbot_seq_exit=$?\" >> specbot-results/compile-status.txt\n\ngcc -g -O0 \\\n -I src/api \\\n specbot/test_deeptest_seq.c \\\n -L build/debug \\\n -lz3 \\\n -Wl,-rpath,\"${GITHUB_WORKSPACE}/build/debug\" \\\n -o specbot-results/test_deeptest_seq \\\n 2>&1 | tee specbot-results/compile_deeptest_seq.log\necho \"compile_deeptest_seq_exit=$?\" >> specbot-results/compile-status.txt\n" + - continue-on-error: true + name: Run specbot tests + run: |- + mkdir -p specbot-results + if [ -f specbot-results/test_specbot_seq ]; then + LD_LIBRARY_PATH="${GITHUB_WORKSPACE}/build/debug" timeout 120 specbot-results/test_specbot_seq > specbot-results/test_specbot_seq.log 2>&1 + SPECBOT_EXIT=$? + echo "specbot_seq_exit=${SPECBOT_EXIT}" >> specbot-results/test-status.txt + else + echo "Binary not compiled" > specbot-results/test_specbot_seq.log + echo "specbot_seq_exit=127" >> specbot-results/test-status.txt + fi + + if [ -f specbot-results/test_deeptest_seq ]; then + LD_LIBRARY_PATH="${GITHUB_WORKSPACE}/build/debug" timeout 120 specbot-results/test_deeptest_seq > specbot-results/test_deeptest_seq.log 2>&1 + DEEPTEST_EXIT=$? + echo "deeptest_seq_exit=${DEEPTEST_EXIT}" >> specbot-results/test-status.txt + else + echo "Binary not compiled" > specbot-results/test_deeptest_seq.log + echo "deeptest_seq_exit=127" >> specbot-results/test-status.txt + fi + + # Cache memory file share configuration from frontmatter processed below + - name: Create cache-memory directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_cache_memory_dir.sh" + - name: Restore cache-memory file share data + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + restore-keys: | + memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}- + - name: Setup cache-memory git repository + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + GH_AW_MIN_INTEGRITY: none + run: bash "${RUNNER_TEMP}/gh-aw/actions/setup_cache_memory_git.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_466e88200fc203e0_EOF' + {"create_discussion":{"category":"agentic workflows","close_older_discussions":true,"expires":168,"fallback_to_issue":true,"max":1,"title_prefix":"[Specbot] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_466e88200fc203e0_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Title will be prefixed with \"[Specbot] \". Discussions will be created in category \"agentic workflows\"." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_discussion": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "category": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_310ecb016ae7c835_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.3", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests,discussions" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_310ecb016ae7c835_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 120 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","google/deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.71.5 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect Copilot errors + id: detect-copilot-errors + if: always() + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Commit cache-memory changes + if: always() + env: + GH_AW_CACHE_DIR: /tmp/gh-aw/cache-memory + run: bash "${RUNNER_TEMP}/gh-aw/actions/commit_cache_memory_git.sh" + - name: Upload cache-memory data as artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: always() + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + - update_cache_memory + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + concurrency: + group: "gh-aw-conclusion-specbot-crash-analyzer" + cancel-in-progress: false + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/specbot-crash-analyzer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_MISSING_TOOL_TITLE_PREFIX: "[missing tool]" + GH_AW_WORKFLOW_NAME: "Specbot Crash Analyzer" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Specbot Crash Analyzer" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "specbot-crash-analyzer" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CREATE_DISCUSSION_ERRORS: ${{ needs.safe_outputs.outputs.create_discussion_errors }} + GH_AW_CREATE_DISCUSSION_ERROR_COUNT: ${{ needs.safe_outputs.outputs.create_discussion_error_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "120" + GH_AW_CACHE_MEMORY_ENABLED: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/specbot-crash-analyzer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.40@sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.40@sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280 ghcr.io/github/gh-aw-firewall/squid:0.25.40@sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Specbot Crash Analyzer" + WORKFLOW_DESCRIPTION: "Build Z3 in debug mode from the c3 branch, compile and run the specbot tests, identify root causes for any crashes, and post findings as a GitHub Discussion." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.40 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.40/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.40,squid=sha256:b084f4a2c771f584ee68084ced52fa6b3245197a1889645d817462d307d3ac51,agent=sha256:14ff567e8d9d4c2fbc5e55c973488381c71d7e0fdbe72d30ee7b8a738fd86504,api-proxy=sha256:2883ca3e5ae9f330cafdd9345bfd4ae17fc8da36c96d4c9a1f76e922b4c45280,cli-proxy=sha256:3e7152911d4b4b7b97beef9d3d7d924ff7902227e86001ef3838fb728d5d514c"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.71.5 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/specbot-crash-analyzer" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.40" + GH_AW_WORKFLOW_ID: "specbot-crash-analyzer" + GH_AW_WORKFLOW_NAME: "Specbot Crash Analyzer" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/specbot-crash-analyzer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"agentic workflows\",\"close_older_discussions\":true,\"expires\":168,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"[Specbot] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + + update_cache_memory: + needs: + - activation + - agent + - detection + if: > + always() && (needs.detection.result == 'success' || needs.detection.result == 'skipped') && + needs.agent.result == 'success' + runs-on: ubuntu-slim + permissions: {} + env: + GH_AW_WORKFLOW_ID_SANITIZED: specbotcrashanalyzer + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Specbot Crash Analyzer" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/specbot-crash-analyzer.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download cache-memory artifact (default) + id: download_cache_default + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: cache-memory + path: /tmp/gh-aw/cache-memory + - name: Check if cache-memory folder has content (default) + id: check_cache_default + shell: bash + run: | + if [ -d "/tmp/gh-aw/cache-memory" ] && [ "$(ls -A /tmp/gh-aw/cache-memory 2>/dev/null)" ]; then + echo "has_content=true" >> "$GITHUB_OUTPUT" + else + echo "has_content=false" >> "$GITHUB_OUTPUT" + fi + - name: Save cache-memory to cache (default) + if: steps.check_cache_default.outputs.has_content == 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + key: memory-none-nopolicy-${{ env.GH_AW_WORKFLOW_ID_SANITIZED }}-${{ github.run_id }} + path: /tmp/gh-aw/cache-memory + diff --git a/.github/workflows/specbot-crash-analyzer.md b/.github/workflows/specbot-crash-analyzer.md new file mode 100644 index 000000000..38d9af7ad --- /dev/null +++ b/.github/workflows/specbot-crash-analyzer.md @@ -0,0 +1,247 @@ +--- +description: > + Build Z3 in debug mode from the c3 branch, compile and run the specbot tests, + identify root causes for any crashes, and post findings as a GitHub Discussion. + +on: + workflow_dispatch: + +timeout-minutes: 120 + +permissions: read-all + +network: defaults + +tools: + cache-memory: true + github: + toolsets: [default, discussions] + bash: [":*"] + edit: {} + +safe-outputs: + create-discussion: + title-prefix: "[Specbot] " + category: "Agentic Workflows" + close-older-discussions: true + missing-tool: + create-issue: true + noop: + report-as-issue: false + +steps: + - name: Checkout c3 branch + uses: actions/checkout@v6.0.2 + with: + ref: c3 + persist-credentials: false + + - name: Install build dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y cmake ninja-build python3 gcc g++ 2>&1 | tail -5 + + - name: Build Z3 in debug mode + id: build-z3 + continue-on-error: true + run: | + mkdir -p build/debug specbot-results + cd build/debug + cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug ../.. 2>&1 | tee ../../specbot-results/cmake.log + ninja 2>&1 | tee ../../specbot-results/build.log + BUILD_EXIT=$? + cd ../.. + echo "build_exit=${BUILD_EXIT}" >> specbot-results/build-status.txt + ls -la build/debug/libz3* build/debug/*.so* 2>/dev/null >> specbot-results/build-status.txt || echo "Library not found" >> specbot-results/build-status.txt + exit $BUILD_EXIT + + - name: Compile specbot tests + continue-on-error: true + run: | + mkdir -p specbot-results + gcc -g -O0 \ + -I src/api \ + specbot/test_specbot_seq.c \ + -L build/debug \ + -lz3 \ + -Wl,-rpath,"${GITHUB_WORKSPACE}/build/debug" \ + -o specbot-results/test_specbot_seq \ + 2>&1 | tee specbot-results/compile_specbot_seq.log + echo "compile_specbot_seq_exit=$?" >> specbot-results/compile-status.txt + + gcc -g -O0 \ + -I src/api \ + specbot/test_deeptest_seq.c \ + -L build/debug \ + -lz3 \ + -Wl,-rpath,"${GITHUB_WORKSPACE}/build/debug" \ + -o specbot-results/test_deeptest_seq \ + 2>&1 | tee specbot-results/compile_deeptest_seq.log + echo "compile_deeptest_seq_exit=$?" >> specbot-results/compile-status.txt + + - name: Run specbot tests + continue-on-error: true + run: | + mkdir -p specbot-results + if [ -f specbot-results/test_specbot_seq ]; then + LD_LIBRARY_PATH="${GITHUB_WORKSPACE}/build/debug" timeout 120 specbot-results/test_specbot_seq > specbot-results/test_specbot_seq.log 2>&1 + SPECBOT_EXIT=$? + echo "specbot_seq_exit=${SPECBOT_EXIT}" >> specbot-results/test-status.txt + else + echo "Binary not compiled" > specbot-results/test_specbot_seq.log + echo "specbot_seq_exit=127" >> specbot-results/test-status.txt + fi + + if [ -f specbot-results/test_deeptest_seq ]; then + LD_LIBRARY_PATH="${GITHUB_WORKSPACE}/build/debug" timeout 120 specbot-results/test_deeptest_seq > specbot-results/test_deeptest_seq.log 2>&1 + DEEPTEST_EXIT=$? + echo "deeptest_seq_exit=${DEEPTEST_EXIT}" >> specbot-results/test-status.txt + else + echo "Binary not compiled" > specbot-results/test_deeptest_seq.log + echo "deeptest_seq_exit=127" >> specbot-results/test-status.txt + fi + +--- + +# Specbot Crash Analyzer + +## Job Description + +Your name is ${{ github.workflow }}. You are an expert C/C++ and SMT solver analyst for the Z3 theorem prover +repository `${{ github.repository }}`. The pre-steps above have already built Z3 in debug mode from the `c3` +branch, compiled and run the specbot test suite, and saved all output to the `specbot-results/` directory in +the workspace (`${{ github.workspace }}/specbot-results/`). Your task is to analyze those results, diagnose +any crash root causes by reading the relevant source files, and publish a structured findings report as a +GitHub Discussion. + +**Do not try to build Z3 or run tests yourself.** All build and test output is already in `specbot-results/`. + +## Your Task + +### 1. Read the Pre-Generated Results + +All build and test outputs are in `specbot-results/` (relative to the workspace root). Read each file: + +```bash +# Build status +cat specbot-results/build-status.txt 2>/dev/null || echo "No build status" + +# Compile status +cat specbot-results/compile-status.txt 2>/dev/null || echo "No compile status" + +# Test status +cat specbot-results/test-status.txt 2>/dev/null || echo "No test status" + +# Test output from test_specbot_seq +cat specbot-results/test_specbot_seq.log 2>/dev/null || echo "No test_specbot_seq output" + +# Test output from test_deeptest_seq +cat specbot-results/test_deeptest_seq.log 2>/dev/null || echo "No test_deeptest_seq output" + +# Last 30 lines of the build log +tail -30 specbot-results/build.log 2>/dev/null || echo "No build log" +``` + +If `specbot-results/build-status.txt` shows `build_exit=0`, the build succeeded. +If it shows a non-zero exit, include the last 50 lines of `specbot-results/build.log` in the report +under a "Build Failure" section. + +If `specbot-results/compile-status.txt` shows a non-zero exit for a test, include the compile error +from `specbot-results/compile_specbot_seq.log` or `specbot-results/compile_deeptest_seq.log`. + +Collect every line containing `CRASH` or `ABORT` from the test log files — these are the crashes to analyze. + +### 2. Diagnose Each Crash + +For each crashed test function, perform the following analysis: + +1. **Identify the test body**: read `specbot/test_specbot_seq.c` or `specbot/test_deeptest_seq.c` + to understand what Z3 API calls the test makes and what invariants it exercises. + +2. **Find the likely crash site**: the test exercises the Z3 Nielsen/nseq string solver. Relevant source files are: + - `src/smt/seq_solver.h` and `src/smt/seq_solver.cpp` (or nearby files) + - `src/smt/seq_axioms.cpp`, `src/smt/seq_eq_solver.cpp`, `src/smt/seq_regex.cpp` + - `src/math/lp/` for length-arithmetic paths + - `src/api/z3_api.h` for the public API entry points + + Use `grep` and `view` to locate assertion macros, `UNREACHABLE()`, `SASSERT`, or `throw` statements + in the code paths exercised by the failing test. Example: + ```bash + grep -rn "SASSERT\|UNREACHABLE\|Z3_CATCH" src/smt/seq_solver.cpp 2>/dev/null | head -30 + ``` + +3. **Hypothesize root cause**: based on the Z3 API calls in the test and the assertion/throw sites in + the solver source, state the most likely root cause. Common categories include: + - Violated invariant (SASSERT/UNREACHABLE hit due to unexpected solver state) + - Use-after-free or dangling reference during push/pop + - Unhandled edge case in Nielsen graph construction + - Missing theory-combination lemma between string length and integer arithmetic + +4. **Suggest a fix**: propose a minimal, concrete fix — e.g., a guard condition, an additional lemma, + a missing reference-count increment, or a missing case in a switch/match. + +### 3. Generate the Report + +After analyzing all crashes, produce a structured GitHub Discussion in the "Agentic Workflows" category +using `create-discussion`. + +The discussion body must follow this structure (use `###` and lower for headers): + +``` +### Summary + +- Build: Debug (CMake + Ninja, c3 branch) +- Tests compiled: N +- Tests run: N +- Tests passed: N +- Tests crashed: N +- Tests timed out: N + +### Crash Findings + +For each crash, one subsection: + +#### + +**Test file**: `specbot/test_specbot_seq.c` or `specbot/test_deeptest_seq.c` + +**Observed failure**: ABORT/CRASH — one-line description of what was caught + +**Root cause hypothesis**: explanation of which assertion or code path was hit and why + +**Suggested fix**: concrete proposed change (file, function, what to add/change) + +--- + +### Tests Passed + +List of test names that passed. + +
+Full Test Output + +Raw stdout/stderr from both test binaries. + +
+ +
+Build Log + +Last 30 lines of the ninja build output. + +
+``` + +If there are no crashes at all, write a "No Crashes Found" summary celebrating that all tests passed, +and include the full test output in a collapsible section. + +Use `mentions: false` behavior — do not mention any GitHub usernames in the report. + +Format workflow run references as: `[§${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`. + +## Usage + +Trigger via **Actions → Specbot Crash Analyzer → Run workflow** on any branch. The pre-steps +always check out the `c3` branch where `specbot/test_specbot_seq.c` and +`specbot/test_deeptest_seq.c` live, build Z3, run the tests, and save results to `specbot-results/`. +The agent then analyzes the results and posts a discussion to the "Agentic Workflows" category. diff --git a/.github/workflows/tactic-to-simplifier.md b/.github/workflows/tactic-to-simplifier.md index 56f698a0b..9c6faf8aa 100644 --- a/.github/workflows/tactic-to-simplifier.md +++ b/.github/workflows/tactic-to-simplifier.md @@ -19,8 +19,6 @@ tools: github: toolsets: [default] bash: [":*"] - glob: {} - view: {} safe-outputs: create-issue: diff --git a/.github/workflows/tptp-benchmark.lock.yml b/.github/workflows/tptp-benchmark.lock.yml new file mode 100644 index 000000000..3b8a2c903 --- /dev/null +++ b/.github/workflows/tptp-benchmark.lock.yml @@ -0,0 +1,1323 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f76e98e3954f6892f78728b22721e87d255ca21f122ab7076306b1ed8b5679b8","compiler_version":"v0.72.1","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.72.1","version":"v0.72.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.72.1). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Weekly benchmark of Z3's TPTP front-end against 500 random TPTP problems. Downloads TPTP benchmarks from tptp.org, resolves axiom dependencies, skips large problems, runs each with a 5-second timeout, and posts a discrepancy/crash report as a GitHub discussion. +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@v0.77.0 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.41 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.41 +# - ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c +# - ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + +name: "TPTP Front-End Benchmark" +"on": + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}" + +run-name: "TPTP Front-End Benchmark" + +jobs: + activation: + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + outputs: + comment_id: "" + comment_repo: "" + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/tptp-benchmark.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.40" + GH_AW_INFO_AGENT_VERSION: "1.0.40" + GH_AW_INFO_CLI_VERSION: "v0.72.1" + GH_AW_INFO_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","tptp.org"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.41" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "tptp-benchmark.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.72.1" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_0f8b33477e7f4791_EOF' + + GH_AW_PROMPT_0f8b33477e7f4791_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_0f8b33477e7f4791_EOF' + + Tools: create_discussion, missing_tool, missing_data, noop + + GH_AW_PROMPT_0f8b33477e7f4791_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_0f8b33477e7f4791_EOF' + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_0f8b33477e7f4791_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_0f8b33477e7f4791_EOF' + + {{#runtime-import .github/workflows/tptp-benchmark.md}} + GH_AW_PROMPT_0f8b33477e7f4791_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKFLOW: ${{ github.workflow }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKFLOW: process.env.GH_AW_GITHUB_WORKFLOW, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: read-all + concurrency: + group: "gh-aw-copilot-${{ github.workflow }}" + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: tptpbenchmark + outputs: + agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/tptp-benchmark.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install build dependencies + run: | + sudo apt-get update -y -q + sudo apt-get install -y cmake ninja-build python3 wget curl bc + - name: Build Z3 + run: "mkdir -p /tmp/z3-build\ncd /tmp/z3-build\ncmake \"$GITHUB_WORKSPACE\" \\\n -G Ninja \\\n -DCMAKE_BUILD_TYPE=Release \\\n -DZ3_BUILD_TEST_EXECUTABLES=OFF\nninja -j$(nproc) z3\n./z3 --version\n" + + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.41 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 ghcr.io/github/gh-aw-firewall/squid:0.25.41 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_6d18c7040963ed61_EOF' + {"create_discussion":{"category":"agentic workflows","close_older_discussions":true,"expires":336,"fallback_to_issue":true,"max":1,"title_prefix":"[TPTP Benchmark] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_6d18c7040963ed61_EOF + - name: Generate Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "create_discussion": " CONSTRAINTS: Maximum 1 discussion(s) can be created. Title will be prefixed with \"[TPTP Benchmark] \". Discussions will be created in category \"agentic workflows\"." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "create_discussion": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "category": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_9ec215538233c65c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.3", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_9ec215538233c65c_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 300 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.41/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","tptp.org","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.41"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.72.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect Copilot errors + id: detect-copilot-errors + if: always() + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,tptp.org,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + concurrency: + group: "gh-aw-conclusion-tptp-benchmark" + cancel-in-progress: false + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/tptp-benchmark.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "false" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_MISSING_TOOL_TITLE_PREFIX: "[missing tool]" + GH_AW_WORKFLOW_NAME: "TPTP Front-End Benchmark" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "TPTP Front-End Benchmark" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "tptp-benchmark" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CREATE_DISCUSSION_ERRORS: ${{ needs.safe_outputs.outputs.create_discussion_errors }} + GH_AW_CREATE_DISCUSSION_ERROR_COUNT: ${{ needs.safe_outputs.outputs.create_discussion_error_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "300" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/tptp-benchmark.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.41 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41 ghcr.io/github/gh-aw-firewall/squid:0.25.41 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "TPTP Front-End Benchmark" + WORKFLOW_DESCRIPTION: "Weekly benchmark of Z3's TPTP front-end against 500 random TPTP problems. Downloads TPTP benchmarks from tptp.org, resolves axiom dependencies, skips large problems, runs each with a 5-second timeout, and posts a discrepancy/crash report as a GitHub discussion." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.40 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.41 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.41/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true},"container":{"imageTag":"0.25.41"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.72.1 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: read + discussions: write + issues: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/tptp-benchmark" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.40" + GH_AW_WORKFLOW_ID: "tptp-benchmark" + GH_AW_WORKFLOW_NAME: "TPTP Front-End Benchmark" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@v0.77.0 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "TPTP Front-End Benchmark" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/tptp-benchmark.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.40" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,tptp.org,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"agentic workflows\",\"close_older_discussions\":true,\"expires\":336,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"[TPTP Benchmark] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + diff --git a/.github/workflows/tptp-benchmark.md b/.github/workflows/tptp-benchmark.md new file mode 100644 index 000000000..fe3f7d5b2 --- /dev/null +++ b/.github/workflows/tptp-benchmark.md @@ -0,0 +1,546 @@ +--- +description: > + Weekly benchmark of Z3's TPTP front-end against 500 random TPTP problems. + Downloads TPTP benchmarks from tptp.org, resolves axiom dependencies, + skips large problems, runs each with a 5-second timeout, and posts a + discrepancy/crash report as a GitHub discussion. + +on: + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: read-all + +network: + allowed: + - defaults + - tptp.org + +tools: + bash: true + github: + toolsets: [default] + +safe-outputs: + create-discussion: + title-prefix: "[TPTP Benchmark] " + category: "Agentic Workflows" + close-older-discussions: true + expires: 14d + missing-tool: + create-issue: true + noop: + report-as-issue: false + +timeout-minutes: 300 + +steps: + - name: Checkout repository + uses: actions/checkout@v6.0.2 + with: + persist-credentials: false + + - name: Install build dependencies + run: | + sudo apt-get update -y -q + sudo apt-get install -y cmake ninja-build python3 wget curl bc + + - name: Build Z3 + run: | + mkdir -p /tmp/z3-build + cd /tmp/z3-build + cmake "$GITHUB_WORKSPACE" \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DZ3_BUILD_TEST_EXECUTABLES=OFF + ninja -j$(nproc) z3 + ./z3 --version + +--- + +# TPTP Front-End Benchmark + +## Job Description + +Your name is ${{ github.workflow }}. You are an expert testing engineer for the Z3 theorem prover. Your task is to: + +1. Verify the Z3 binary built by the pre-flight step is available +2. Download the TPTP benchmark library from tptp.org +3. Select 500 random small-to-medium problems (with their axiom dependencies) +4. Run each problem through Z3's TPTP front-end with a 5-second timeout +5. Compare Z3's output against the expected SZS status declared in each problem file +6. Post a detailed report as a GitHub Discussion summarising discrepancies and crashes + +**Repository**: ${{ github.repository }} +**Workspace**: ${{ github.workspace }} + +## Phase 1: Verify Z3 Binary + +Z3 was built by the workflow pre-flight step and is available at `/tmp/z3-build/z3`. +Confirm the binary is present and functional: + +```bash +/tmp/z3-build/z3 --version +``` + +If the binary is missing or returns an error, call the `noop` safe-output with a message describing the problem and stop. + +Once confirmed, call `noop` with `"Z3 binary verified. Downloading TPTP benchmark library — this may take a few minutes."` to keep the safe-output session alive. + +## Phase 2: Download the TPTP Problem Library + +Find the latest TPTP release and download the full archive. + +```bash +# Find the latest TPTP distribution version by fetching the directory listing +TPTP_DIST_URL="https://tptp.org/TPTP/Distribution/" +LATEST_TGZ=$(curl -sL "$TPTP_DIST_URL" \ + | grep -oP 'TPTP-v[0-9]+\.[0-9]+\.[0-9]+\.tgz' \ + | sort -V | tail -1) + +if [ -z "$LATEST_TGZ" ]; then + echo "ERROR: Could not determine latest TPTP version from $TPTP_DIST_URL" + # Fall back to a known stable version + LATEST_TGZ="TPTP-v9.0.0.tgz" +fi + +echo "Downloading $LATEST_TGZ ..." +mkdir -p /tmp/tptp_download +wget -q --show-progress \ + "${TPTP_DIST_URL}${LATEST_TGZ}" \ + -O /tmp/tptp_download/tptp.tgz + +echo "Extracting TPTP library..." +mkdir -p /tmp/tptp +tar -xzf /tmp/tptp_download/tptp.tgz -C /tmp/tptp --strip-components=1 2>&1 | tail -5 + +# Verify extraction +if [ ! -d /tmp/tptp/Problems ] || [ ! -d /tmp/tptp/Axioms ]; then + echo "ERROR: TPTP extraction failed — Problems/ or Axioms/ directory not found" + ls /tmp/tptp/ + exit 1 +fi + +TPTP_ROOT=/tmp/tptp +echo "TPTP library extracted to $TPTP_ROOT" +echo "Problem domains available:" +ls "$TPTP_ROOT/Problems/" | wc -l +echo "Axiom files available:" +ls "$TPTP_ROOT/Axioms/" | wc -l +``` + +If the download or extraction fails, call `noop` with the error details and stop. + +Call `noop` with `"TPTP library downloaded and extracted. Selecting 500 benchmark problems — filtering by size."` to keep the session alive. + +## Phase 3: Select 500 Benchmark Problems + +Filter out large problems and problems that depend on large axiom files, then take a random sample of 500. + +Save this script to `/tmp/select_benchmarks.py` and run it: + +```python +#!/usr/bin/env python3 +""" +Select 500 random TPTP problems that: + - Have a known, conclusive expected status (Theorem, Unsatisfiable, + CounterSatisfiable, Satisfiable) OR Unknown/Open status. + - Are not "large" (problem file <= 50 KB). + - Do not include any axiom file larger than 100 KB. +""" +import os +import re +import random +import sys + +TPTP_ROOT = "/tmp/tptp" +PROBLEMS_DIR = os.path.join(TPTP_ROOT, "Problems") +AXIOMS_DIR = os.path.join(TPTP_ROOT, "Axioms") +MAX_PROBLEM_SIZE = 50 * 1024 # 50 KB +MAX_AXIOM_SIZE = 100 * 1024 # 100 KB +SAMPLE_SIZE = 500 +OUTPUT_FILE = "/tmp/selected_benchmarks.txt" + +include_re = re.compile(r"include\s*\(\s*['\"]([^'\"]+)['\"]", re.IGNORECASE) +status_re = re.compile(r"%\s*Status\s*:\s*(\S+)", re.IGNORECASE) + +def axiom_sizes_ok(problem_path): + """Return True if all included axiom files exist and are <= MAX_AXIOM_SIZE.""" + try: + with open(problem_path, encoding="utf-8", errors="replace") as f: + content = f.read(4096) # header is in first few KB + except OSError: + return False + for m in include_re.finditer(content): + axiom_rel = m.group(1) # e.g. "Axioms/AGT001+0.ax" + axiom_path = os.path.join(TPTP_ROOT, axiom_rel) + if not os.path.exists(axiom_path): + return False # axiom missing — skip + if os.path.getsize(axiom_path) > MAX_AXIOM_SIZE: + return False # axiom too large — skip + return True + +candidates = [] +skipped_size = 0 +skipped_axiom = 0 + +for domain in sorted(os.listdir(PROBLEMS_DIR)): + domain_dir = os.path.join(PROBLEMS_DIR, domain) + if not os.path.isdir(domain_dir): + continue + for fname in os.listdir(domain_dir): + if not fname.endswith(".p"): + continue + fpath = os.path.join(domain_dir, fname) + size = os.path.getsize(fpath) + if size > MAX_PROBLEM_SIZE: + skipped_size += 1 + continue + if not axiom_sizes_ok(fpath): + skipped_axiom += 1 + continue + candidates.append(fpath) + +print(f"Total candidates (after filtering): {len(candidates)}", flush=True) +print(f" Skipped — problem too large : {skipped_size}", flush=True) +print(f" Skipped — axiom too large : {skipped_axiom}", flush=True) + +if len(candidates) == 0: + print("ERROR: No suitable benchmark problems found.", file=sys.stderr) + sys.exit(1) + +if len(candidates) > SAMPLE_SIZE: + random.seed(42) + selected = random.sample(candidates, SAMPLE_SIZE) +else: + selected = candidates + +selected.sort() +with open(OUTPUT_FILE, "w") as f: + f.write("\n".join(selected) + "\n") + +print(f"Selected {len(selected)} problems → {OUTPUT_FILE}", flush=True) +``` + +Run the script: + +```bash +python3 /tmp/select_benchmarks.py +SELECTED=$(wc -l < /tmp/selected_benchmarks.txt) +echo "Benchmark set: $SELECTED problems" +``` + +If no problems are found, call `noop` with an error message and stop. + +Call `noop` with `"$SELECTED problems selected. Starting benchmark run with 5-second timeout per problem — this will take approximately $(( SELECTED * 7 / 60 )) minutes."` to keep the session alive. + +## Phase 4: Run Benchmarks + +Save the following script to `/tmp/run_tptp_benchmarks.sh`, make it executable, and run it. + +```bash +#!/usr/bin/env bash +set -euo pipefail + +Z3=/tmp/z3-build/z3 +TPTP_ROOT=/tmp/tptp +TIMEOUT_HARD=8 # outer OS-level guard (seconds; 3 s beyond Z3's -T:5) +Z3_TIMEOUT=5 # Z3 internal timeout: -T:N sets N-second limit (uppercase -T is seconds) + +RESULTS=/tmp/tptp_results.tsv +PROBLEM_LIST=/tmp/selected_benchmarks.txt + +echo -e "file\texpected\tactual\ttime_s\tnotes" > "$RESULTS" + +# Helper: extract the expected SZS status from the TPTP problem header. +get_expected_status() { + local file="$1" + # Look for lines like: "% Status : Theorem" + grep -m1 -iP '%\s*Status\s*:\s*\K\S+' "$file" 2>/dev/null || echo "Unknown" +} + +# Helper: run z3 on a single TPTP problem with timeout. +run_benchmark() { + local file="$1" + local start end elapsed output exit_code verdict + + start=$(date +%s%3N) # milliseconds since epoch + output=$(TPTP="$TPTP_ROOT" timeout "$TIMEOUT_HARD" \ + "$Z3" -tptp -T:"$Z3_TIMEOUT" "$file" 2>&1) || exit_code=$? + exit_code=${exit_code:-0} + end=$(date +%s%3N) + elapsed=$(echo "scale=3; ($end - $start) / 1000" | bc) + + # Extract SZS status line from output + szs_line=$(echo "$output" | grep -m1 "% SZS status" || true) + + if [ -n "$szs_line" ]; then + # Parse the status keyword (e.g. "Theorem", "CounterSatisfiable", "GaveUp") + verdict=$(echo "$szs_line" | grep -oP '% SZS status \K\S+' || echo "Unknown") + elif [ "$exit_code" -eq 124 ]; then + verdict="Timeout" + elif [ "$exit_code" -ne 0 ]; then + verdict="Crash" + else + verdict="NoOutput" + fi + + echo "$verdict $elapsed" +} + +COUNTER=0 +TOTAL=$(wc -l < "$PROBLEM_LIST") + +while IFS= read -r problem_file; do + COUNTER=$((COUNTER + 1)) + + expected=$(get_expected_status "$problem_file") + result_line=$(run_benchmark "$problem_file") + actual=$(echo "$result_line" | cut -d' ' -f1) + elapsed=$(echo "$result_line" | cut -d' ' -f2) + fname=$(basename "$problem_file") + + # Classify notes + notes="" + # Soundness discrepancy: both answers are conclusive but conflict + conclusive_expected=false + conclusive_actual=false + case "$expected" in + Theorem|Unsatisfiable) conclusive_expected=true ;; + Satisfiable|CounterSatisfiable) conclusive_expected=true ;; + esac + case "$actual" in + Theorem|Unsatisfiable) conclusive_actual=true ;; + Satisfiable|CounterSatisfiable) conclusive_actual=true ;; + esac + + if $conclusive_expected && $conclusive_actual; then + # Map expected to the Z3 output equivalents for comparison + # Theorem (has-conjecture unsat) matches "Theorem" + # Unsatisfiable (no-conjecture unsat) matches "Unsatisfiable" + # Satisfiable (no-conjecture sat) matches "Satisfiable" + # CounterSatisfiable (has-conjecture sat) matches "CounterSatisfiable" + if [ "$expected" != "$actual" ]; then + # Check for sat/unsat polarity conflict + sat_expected=false; sat_actual=false + case "$expected" in Satisfiable|CounterSatisfiable) sat_expected=true ;; esac + case "$actual" in Satisfiable|CounterSatisfiable) sat_actual=true ;; esac + if [ "$sat_expected" != "$sat_actual" ]; then + notes="SOUNDNESS_ERROR" + else + notes="STATUS_MISMATCH" + fi + fi + fi + + if [ "$actual" = "Crash" ]; then + notes="CRASH" + fi + + echo -e "$fname\t$expected\t$actual\t$elapsed\t$notes" >> "$RESULTS" + + if [ -n "$notes" ]; then + echo "[$COUNTER/$TOTAL] $fname expected=$expected actual=$actual time=${elapsed}s *** $notes ***" + elif [ $((COUNTER % 50)) -eq 0 ]; then + echo "[$COUNTER/$TOTAL] Progress checkpoint last=$fname actual=$actual time=${elapsed}s" + fi + +done < "$PROBLEM_LIST" + +echo "Benchmark run complete: $COUNTER problems processed. Results → $RESULTS" +``` + +Run it: + +```bash +chmod +x /tmp/run_tptp_benchmarks.sh +/tmp/run_tptp_benchmarks.sh +``` + +Do not skip any file in the list. + +## Phase 5: Analyze Results + +Save the following script to `/tmp/analyze_tptp.py` and run it: + +```python +#!/usr/bin/env python3 +"""Compute summary statistics from the TPTP benchmark TSV.""" +import csv + +RESULTS_FILE = "/tmp/tptp_results.tsv" + +rows = [] +with open(RESULTS_FILE, newline="") as f: + reader = csv.DictReader(f, delimiter="\t") + for row in reader: + rows.append(row) + +total = len(rows) + +# Verdict counts +from collections import Counter, defaultdict +actual_counts = Counter(r["actual"] for r in rows) +expected_counts = Counter(r["expected"] for r in rows) + +# Flagged rows +soundness_errors = [r for r in rows if r["notes"] == "SOUNDNESS_ERROR"] +status_mismatches = [r for r in rows if r["notes"] == "STATUS_MISMATCH"] +crashes = [r for r in rows if r["notes"] == "CRASH"] +timeouts = [r for r in rows if r["actual"] == "Timeout"] +gave_up = [r for r in rows if r["actual"] == "GaveUp"] + +# Solved correctly (expected matches actual for conclusive verdicts) +conclusive_expected = {"Theorem", "Unsatisfiable", "Satisfiable", "CounterSatisfiable"} +correct = [r for r in rows + if r["expected"] in conclusive_expected + and r["actual"] == r["expected"]] + +print(f"TOTAL={total}") +print(f"CORRECT={len(correct)}") +print(f"TIMEOUTS={len(timeouts)}") +print(f"GAVE_UP={len(gave_up)}") +print(f"CRASHES={len(crashes)}") +print(f"SOUNDNESS_ERRORS={len(soundness_errors)}") +print(f"STATUS_MISMATCHES={len(status_mismatches)}") + +print("\n--- Actual verdict breakdown ---") +for v, c in sorted(actual_counts.items()): + print(f" {v}: {c}") + +print("\n--- Expected status breakdown ---") +for v, c in sorted(expected_counts.items()): + print(f" {v}: {c}") + +if soundness_errors: + print(f"\n--- SOUNDNESS ERRORS ({len(soundness_errors)}) ---") + for r in soundness_errors: + print(f" {r['file']} expected={r['expected']} actual={r['actual']}") + +if crashes: + print(f"\n--- CRASHES ({len(crashes)}) ---") + for r in crashes: + print(f" {r['file']} expected={r['expected']}") + +if status_mismatches: + print(f"\n--- STATUS MISMATCHES ({len(status_mismatches)}) ---") + for r in status_mismatches[:20]: + print(f" {r['file']} expected={r['expected']} actual={r['actual']}") +``` + +Run the analysis: + +```bash +python3 /tmp/analyze_tptp.py +``` + +## Phase 6: Generate and Post the Discussion Report + +Read the TSV at `/tmp/tptp_results.tsv` and the analysis output, then compose a Markdown report and call `create_discussion`. + +The report should use `###` or lower for all headers (never `#` or `##`). Use collapsible `
` sections for large tables. + +Use this structure: + +```markdown +**Date**: +**Branch**: master +**Commit**: `` (run `git rev-parse --short HEAD` in ${{ github.workspace }} to get the SHA) +**Workflow Run**: [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) +**TPTP version**: +**Problems benchmarked**: (random sample, timeout 5 s per problem) + +--- + +### Summary + +| Metric | Count | +|--------|-------| +| Total problems run | N | +| Correct (expected = actual) | N | +| Timeouts | N | +| GaveUp (within time budget) | N | +| Crashes / errors | N | +| Soundness errors (sat↔unsat conflict) | N | +| Status mismatches (Theorem vs Unsatisfiable etc.) | N | + +### Expected Status Distribution + +| Expected Status | Count | +|----------------|-------| +| Theorem | N | +| Unsatisfiable | N | +| Satisfiable | N | +| CounterSatisfiable | N | +| Unknown / Open | N | + +--- + +### ⚠️ Critical: Soundness Errors + +[List ALL files where Z3 returned a conclusive answer that contradicts the expected answer +(e.g., expected Theorem but got CounterSatisfiable). If none, write "None detected."] + +### 💥 Crashes + +[List ALL files where Z3 crashed (non-zero exit, no SZS output, not a timeout). +Include filename and expected status. If none, write "None detected."] + +### Status Mismatches + +[Files where both answers are conclusive but differ in Theorem vs Unsatisfiable polarity +(e.g., expected Theorem but actual Unsatisfiable). These may indicate conjecture-handling +differences rather than soundness bugs. If none, write "None detected."] + +--- + +
+View all Timeouts (problems where Z3 exceeded the 5-second limit) + +| # | File | Expected Status | +|---|------|----------------| +[First 100 timeout rows] + +
+ +
+View full per-problem results table + +| # | File | Expected | Actual | Time (s) | Notes | +|---|------|----------|--------|----------|-------| +[All rows, or first 500 if over limit] + +
+ +--- + +### Recommendations + +[Based on the findings, list actionable items. E.g.: investigate soundness errors, +file crash bugs, note domains where Z3 consistently times out.] +``` + +Post the discussion using the `create_discussion` safe output. The title should be +`[TPTP Benchmark] master — `. + +## Safe Output Guarantee + +You **MUST** call either `create_discussion` or `noop` before the workflow ends: + +- **Full success**: Call `create_discussion` with the complete report. +- **Partial results** (some problems ran): Call `create_discussion` with whatever results are available and a note about incomplete execution. +- **Download failure**: Call `noop` with the download error details. +- **No problems selected**: Call `noop` explaining why no problems were found. +- **Binary missing**: If `/tmp/z3-build/z3` is unexpectedly absent, call `noop` with that detail and stop. + +## Important Notes + +- **Build failure handling**: Z3 was built before the agent loaded. If the binary is missing or non-functional, call `noop` with the error and stop. +- **TPTP environment variable**: Set `TPTP=/tmp/tptp` when invoking `z3 -tptp` so that `include()` directives in problem files resolve correctly against the downloaded Axioms directory. +- **Timeout detection**: Use `timeout 8` as the outer OS-level guard (3 seconds beyond Z3's `-T:5`) to allow Z3 to exit cleanly before the shell kills it. If the exit code from `timeout` is 124, record the verdict as `Timeout`. +- **Crash detection**: A crash is a non-zero exit code with no `% SZS status` line in the output and no timeout. Record it separately from `GaveUp`. +- **SZS status semantics**: Z3 outputs `Theorem` (not `Unsatisfiable`) when it proves a conjecture; `CounterSatisfiable` (not `Satisfiable`) when it finds a counterexample to a conjecture. A status mismatch between `Theorem` and `Unsatisfiable` for the same problem may be innocuous and depends on whether the problem file uses a conjecture formula. +- **Report soundness bugs prominently**: Any case where the polarity of the answer conflicts (expected Theorem/Unsatisfiable but got CounterSatisfiable/Satisfiable, or vice versa) is a potential soundness bug and must be highlighted as critical. +- **Keep progress log**: Print a line for every flagged result and every 50th problem so the workflow log shows progress. +- **Close older discussions**: Configured via `close-older-discussions: true`. Only the latest weekly report remains open. diff --git a/.github/workflows/wasm-release.yml b/.github/workflows/wasm-release.yml index 2fb04d49f..624f80502 100644 --- a/.github/workflows/wasm-release.yml +++ b/.github/workflows/wasm-release.yml @@ -36,7 +36,7 @@ jobs: cp ../../../LICENSE.txt . - name: Setup emscripten - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@v16 with: no-install: true version: ${{env.EM_VERSION}} diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 0eaa8f863..c6e118ad5 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -29,7 +29,7 @@ jobs: node-version: "lts/*" - name: Setup emscripten - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@v16 with: no-install: true version: ${{env.EM_VERSION}} diff --git a/.github/workflows/workflow-suggestion-agent.md b/.github/workflows/workflow-suggestion-agent.md index 948f01079..6886f6505 100644 --- a/.github/workflows/workflow-suggestion-agent.md +++ b/.github/workflows/workflow-suggestion-agent.md @@ -12,11 +12,9 @@ network: defaults tools: cache-memory: true - serena: ["python", "java", "csharp"] github: toolsets: [default] bash: [":*"] - glob: {} safe-outputs: create-discussion: diff --git a/.github/workflows/zipt-code-reviewer.md b/.github/workflows/zipt-code-reviewer.md index bc1e7285c..e09d7fb12 100644 --- a/.github/workflows/zipt-code-reviewer.md +++ b/.github/workflows/zipt-code-reviewer.md @@ -17,8 +17,6 @@ tools: cache-memory: true github: toolsets: [default] - view: {} - glob: {} edit: {} web-fetch: {} bash: @@ -169,12 +167,10 @@ git diff > /tmp/zipt-improvements.diff cat /tmp/zipt-improvements.diff ``` -If no changes were made because no improvements were found or all were too risky, exit gracefully: +If no changes were made because no improvements were found or all were too risky, call the `noop` safe-output tool: ``` -✅ ZIPT code review complete. No concrete improvements found in this run. -Files examined: [list files] -ZIPT files compared: [list files] +noop: "ZIPT code review complete. No concrete improvements found in this run. Files examined: [list files]. ZIPT files compared: [list files]." ``` ## Phase 6: Create GitHub Issue @@ -235,7 +231,12 @@ make test-z3 *Generated by ZIPT Code Reviewer agent — comparing Z3 implementation with CEisenhofer/ZIPT@parikh* ``` -## Important Guidelines +## Important: Always Call a Safe Output Tool + +**You MUST always call at least one safe-output tool before finishing.** Failing to do so is reported as a workflow failure. + +- If you found and applied improvements → call `create_issue` +- If ZIPT is unreachable, no improvements were found, or all improvements are out of scope → call `noop` with a brief explanation ### Scope - **Only** examine the files listed in Phase 1 @@ -249,7 +250,12 @@ make test-z3 - Prefer small, surgical changes over large refactors ### Exit Conditions -Exit without creating an issue if: +Call `noop` (instead of creating an issue) if: - ZIPT repository is unreachable - No concrete, safe improvements can be identified - All identified improvements require architectural changes beyond the scope of a single diff + +Example noop call: +``` +noop: "ZIPT code review complete. No improvements applied: [brief reason, e.g. ZIPT unreachable / no safe changes identified]. Files reviewed: [list]." +``` diff --git a/.gitignore b/.gitignore index cfa4ed939..9cad945c2 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,9 @@ bazel-* .beads build/ .z3-agent/ +.playwright*/ +.atomic/ +.deepscan/ +.deeptest/ +tptp_test/ +tptp_benchmarks/ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d5d487c77..4544fec27 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -60,6 +60,77 @@ Version 4.17.0 https://github.com/Z3Prover/z3/pull/8983 - Fix deep API bugs in Z3 C API (null pointer handling, error propagation). https://github.com/Z3Prover/z3/pull/8972 +- Implement multivariate polynomial factorization via Hensel lifting. Replaces the prior stub + implementation (factor_n_sqf_pp) with a working algorithm: evaluate away extra variables to + reduce to bivariate, factor the univariate specialization, lift via linear Hensel lifting in + Zp[x], and verify the result over Z[x,y]. For more than two variables, bivariate factors are + checked against the original polynomial. Thanks to Lev Nachmanson. +- Add riscv64 Python wheel builds to nightly and release PyPI publishing. + https://github.com/Z3Prover/z3/pull/9153 +- Fix nlsat clear() crash: reset polynomial cache and root-atom assignments during solver + destruction to prevent use-after-free heap corruption. Also fix scoped_numeral_vector copy + constructor to read from the source operand instead of uninitialized self. + https://github.com/Z3Prover/z3/pull/9150 +- Fix #9030: in box mode optimization (opt.priority=box), each objective is now optimized + independently using push/pop scopes, so adding or removing one objective no longer changes + the optimal values of others. +- Fix assertion violation in isolate_roots for nested nlsat calls. Fixes #6871. +- Fix #9036: expand bounded integer quantifiers in qe-light when Fourier-Motzkin elimination + fails due to non-unit coefficients. When all remaining quantified integers have explicit + finite bounds and the product of domain sizes is at most 10000, the quantifier is unrolled + into an explicit disjunction. +- Fix #8023: only skip adding an axiom clause when its satisfying literal is assigned at base + level (scope 0). The previous optimization was unsound: literals can be retracted by + backtracking, causing the string solver to miss propagations such as indexof(a,s) = -1 when + contains(a,s) becomes false after backtracking. +- Fix lock contention in theory_diff_logic and theory_dense_diff_logic when using multi-threaded + solving (smt.threads > 1). A diagnostic IF_VERBOSE(0,...) call was always acquiring the global + verbose mutex, causing catastrophic contention when multiple threads internalized atoms. + Fixes #8019. +- Fix string solver: move m_fixed insertion after check_long_strings guard to prevent premature + marking of string variables with length > 20 as processed. +- Fix documentation for Z3_solver_to_dimacs_string: corrected the function name in the API + comment. Thanks to Mark DenHoed, https://github.com/Z3Prover/z3/pull/9053 +- Add global backbones to parallel architecture for smt.threads > 1. Backbone literals learned + by any worker thread are broadcast to all others, improving search pruning in the shared search + tree. Thanks to Ilana Shapiro. + https://github.com/Z3Prover/z3/pull/9343 +- Terminate on Demand and algorithmic bugfixes in the parallel search tree, including improved + worker termination signaling and fixes to node-state management. Thanks to Ilana Shapiro. + https://github.com/Z3Prover/z3/pull/9336 +- Add adaptive growth knobs for Gröbner basis computation under arith.nl.grobner_adaptive. + Allows tuning of Gröbner basis expansion rate for better NLA performance. Thanks to Arie. + https://github.com/Z3Prover/z3/pull/9390 +- Improvements to NLA lemmas for better nonlinear arithmetic solving. Thanks to Arie. + https://github.com/Z3Prover/z3/pull/9391 +- Throttle lia2card tactic in QF_NIA preamble to avoid combinatorial explosion on large instances. + Thanks to Arie, https://github.com/Z3Prover/z3/pull/9362 +- Fix smt: reset give-up state when escalating final_check level to prevent solver from + incorrectly abandoning solvable instances. Thanks to Lev Nachmanson. + https://github.com/Z3Prover/z3/pull/9408 +- Fix double-free crash in anum by giving anum move semantics to prevent sort-triggered + double-free. Thanks to Arie, https://github.com/Z3Prover/z3/pull/9320 +- Fix lar_term equality operator to correctly compare terms. Thanks to Arie. + https://github.com/Z3Prover/z3/pull/9284 +- Prevent unsound solve-eqs elimination across recursive-function definitions. + https://github.com/Z3Prover/z3/pull/9358 +- Fix inverted logic of is-linear check in solve-eqs, #9311. +- Fix #9293: disable elim-uncnstr simplification under quantifiers to prevent unsound + eliminations. Also fix #9234, #9309. +- Add exception protection for nlsat_tactic and try_for tactic to correctly handle cancellation + and ensure robust exception propagation. +- Add smt.solve_eqs.linear parameter (default false). When set to true, restricts variable + eliminations in solve-eqs to only use linear substitutions, avoiding cross-multiplication + of nested substitutions. +- Fix null dereference in linearise_multi_pattern: reorder null check before side effect. + https://github.com/Z3Prover/z3/pull/9427 +- Add Go and OCaml API coverage: substitution, AST introspection, Spacer, and Goal completion + APIs. https://github.com/Z3Prover/z3/pull/9277 +- Fix two bugs in Python examples. Thanks to Guangyu (Gary) HU. + https://github.com/Z3Prover/z3/pull/9303 +- Add fold-unfold tactic as an alternative to solve-eqs for variable elimination using + fold-unfold transformations. Also exposed as a simplifier. +- Handle SIGXCPU (OS timeout) like a regular `-T` timeout. Users should make sure to set the soft limit below the hard one, as in `ulimit -S -t 30 -H -t 31` for a 30s soft limit, so SIGXCPU is delivered before SIGKILL. Version 4.16.0 ============== diff --git a/cmake/modules/FindDotnet.cmake b/cmake/modules/FindDotnet.cmake index c73cbd605..ccaf24eb8 100644 --- a/cmake/modules/FindDotnet.cmake +++ b/cmake/modules/FindDotnet.cmake @@ -262,8 +262,8 @@ FUNCTION(DOTNET_GET_DEPS _DN_PROJECT arguments) ENDIF() IF(_DN_NETCOREAPP) - SET(_DN_BUILD_OPTIONS -f netcoreapp2.0) - SET(_DN_PACK_OPTIONS /p:TargetFrameworks=netcoreapp2.0) + SET(_DN_BUILD_OPTIONS -f net8.0) + SET(_DN_PACK_OPTIONS /p:TargetFrameworks=net8.0) ELSEIF(UNIX) # Unix builds default to netstandard2.0 SET(_DN_BUILD_OPTIONS -f netstandard2.0) @@ -384,7 +384,7 @@ FUNCTION(RUN_DOTNET DOTNET_PROJECT) COMMAND ${DOTNET_EXE} clean ${DOTNET_PROJPATH} ${DOTNET_BUILD_PROPERTIES} COMMAND ${DOTNET_EXE} build --no-restore ${DOTNET_PROJPATH} -c ${DOTNET_CONFIG} ${DOTNET_BUILD_PROPERTIES} ${DOTNET_BUILD_OPTIONS} # XXX tfm - COMMAND ${DOTNET_EXE} ${DOTNET_OUTPUT_PATH}/netcoreapp2.0/${DOTNET_PROJNAME}.dll ${DOTNET_ARGUMENTS} + COMMAND ${DOTNET_EXE} ${DOTNET_OUTPUT_PATH}/net8.0/${DOTNET_PROJNAME}.dll ${DOTNET_ARGUMENTS} COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/${DOTNET_PROJNAME}.runtimestamp WORKING_DIRECTORY ${DOTNET_OUTPUT_PATH}) ADD_CUSTOM_TARGET( @@ -399,7 +399,7 @@ FUNCTION(TEST_DOTNET DOTNET_PROJECT) IF(WIN32) SET(test_framework_args "") ELSE() - SET(test_framework_args -f netcoreapp2.0) + SET(test_framework_args -f net8.0) ENDIF() ADD_TEST(NAME ${DOTNET_PROJNAME} diff --git a/examples/dotnet/CMakeLists.txt b/examples/dotnet/CMakeLists.txt index b07ae4219..98398f945 100644 --- a/examples/dotnet/CMakeLists.txt +++ b/examples/dotnet/CMakeLists.txt @@ -24,9 +24,9 @@ if(UNIX AND NOT APPLE) add_custom_target( z3_dotnet_test_manual_copy_assembly_hack ALL - COMMAND ${CMAKE_COMMAND} -E copy ${z3_dotnet_test_manual_copy_deps} ${PROJECT_BINARY_DIR}/dotnet/netcoreapp2.0/ + COMMAND ${CMAKE_COMMAND} -E copy ${z3_dotnet_test_manual_copy_deps} ${PROJECT_BINARY_DIR}/dotnet/net8.0/ # hack the libz3 entry in deps so it's easy enough for dotnet to reach it... - COMMAND sed \"s/runtimes\\/.*libz3\\.so/libz3.so/\" -i ${PROJECT_BINARY_DIR}/dotnet/netcoreapp2.0/dotnet.deps.json + COMMAND sed \"s/runtimes\\/.*libz3\\.so/libz3.so/\" -i ${PROJECT_BINARY_DIR}/dotnet/net8.0/dotnet.deps.json ) add_dependencies(z3_dotnet_test_manual_copy_assembly_hack BUILD_dotnet) diff --git a/examples/dotnet/dotnet.csproj b/examples/dotnet/dotnet.csproj index 7776259ea..2084e3678 100644 --- a/examples/dotnet/dotnet.csproj +++ b/examples/dotnet/dotnet.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + net8.0 diff --git a/examples/python/bincover.py b/examples/python/bincover.py index d8a81c25a..72b769982 100644 --- a/examples/python/bincover.py +++ b/examples/python/bincover.py @@ -195,7 +195,7 @@ class BinCoverSolver(UserPropagateBase): assert isinstance(value, BitVecNumRef) bin_index = value.as_long() if bin_index >= len(self.bins): - return NOne + return None return self.bins[bin_index] def _add_item2bin(self, item, bin): diff --git a/examples/python/complex/complex.py b/examples/python/complex/complex.py index aa9adeef8..051641808 100644 --- a/examples/python/complex/complex.py +++ b/examples/python/complex/complex.py @@ -81,7 +81,7 @@ class ComplexExpr: other = _to_complex(other) return And(self.r == other.r, self.i == other.i) - def __neq__(self, other): + def __ne__(self, other): return Not(self.__eq__(other)) def simplify(self): diff --git a/examples/python/mini_ic3.py b/examples/python/mini_ic3.py index 31d3c595b..13056d385 100644 --- a/examples/python/mini_ic3.py +++ b/examples/python/mini_ic3.py @@ -74,6 +74,8 @@ class Horn2Transitions: pred, inv0 = self.is_body(body) if pred is None: return False + if inv0 is None: + return False inv1 = self.is_inv(head) if inv1 is None: return False @@ -335,7 +337,7 @@ class MiniIC3: s = self.states[f - 1].solver if unsat == s.check(cube): core = s.unsat_core() - if not check_disjoint(self.init, self.prev(And(core))): + if len(core) > 0 and check_disjoint(self.init, self.prev(And(core))): return core, f return cube, f diff --git a/examples/python/mini_quip.py b/examples/python/mini_quip.py index a10d5a334..d25abe9af 100644 --- a/examples/python/mini_quip.py +++ b/examples/python/mini_quip.py @@ -3,6 +3,8 @@ import heapq import numpy import time import random +import sys +import copy verbose = True @@ -78,6 +80,8 @@ class Horn2Transitions: pred, inv0 = self.is_body(body) if pred is None: return False + if inv0 is None: + return False inv1 = self.is_inv(head) if inv1 is None: return False @@ -349,12 +353,12 @@ class Quip: def next(self, f): if is_seq(f): return [self.next(f1) for f1 in f] - return substitute(f, zip(self.x0, self.xn)) + return substitute(f, list(zip(self.x0, self.xn))) def prev(self, f): if is_seq(f): return [self.prev(f1) for f1 in f] - return substitute(f, zip(self.xn, self.x0)) + return substitute(f, list(zip(self.xn, self.x0))) def add_solver(self): s = fd_solver() @@ -423,7 +427,8 @@ class Quip: s.push() r = self.reachable.state2cube(state) s.add(And(self.prev(r))) - s.add(self.prev(cube)) + if len(cube) > 0: + s.add(And(self.prev(list(cube)))) is_sat = s.check() s.pop() if is_sat == sat: @@ -441,7 +446,7 @@ class Quip: s = self.states[f - 1].solver if unsat == s.check(cube): core = s.unsat_core() - if self.check_reachable(core): + if len(core) > 0 and self.check_reachable(core): return core, f return cube, f @@ -454,8 +459,8 @@ class Quip: for state in self.reachable.states: s.push() s.add(And(self.next(self.reachable.state2cube(state)))) - print self.reachable.state2cube(state) - print s.check() + print(self.reachable.state2cube(state)) + print(s.check()) s.pop() def lemmas(self, level): @@ -553,7 +558,7 @@ class Quip: s.add(self.init) s.add(self.prev(g.cube)) # since init is a complete assignment, so g.cube must equal to init in sat solver - assert is_sat == s.check() + assert sat == s.check() if verbose: print("") return g @@ -564,7 +569,7 @@ class Quip: if r0 is not None: if g.must: if verbose: - print "" + print("") s = fd_solver() s.add(self.trans) # make it as a concrete reachable state @@ -573,9 +578,16 @@ class Quip: while True: is_sat = s.check(self.next(g.cube)) assert is_sat == sat - r = self.next(self.project0(s.model())) + m = s.model() + r = self.next(self.project0(m)) r = self.reachable.intersect(self.prev(r)) - child = QGoal(self.next(r.children()), g, 0, g.must, 0) + if r is None: + # reachable intersect failed: fall back to the raw + # model projection so we still get a concrete + # predecessor and avoid crashing on r.children() + child = QGoal(self.next(self.project0(m)), g, 0, g.must, 0) + else: + child = QGoal(self.next(r.children()), g, 0, g.must, 0) g = child if not check_disjoint(self.init, self.prev(g.cube)): # g is init, break the loop @@ -596,7 +608,7 @@ class Quip: for l in self.frames[f_1]: if not l.bad and len(l.cube) > 0 and set(l.cube).issubset(g.cube): cube = l.cube - is_sat == unsat + is_sat = unsat break f_1 -= 1 if cube is None: @@ -707,7 +719,7 @@ def test(file): h2t = Horn2Transitions() h2t.parse(file) if verbose: - print("Test file: %s") % file + print("Test file: %s" % file) mp = Quip(h2t.init, h2t.trans, h2t.goal, h2t.xs, h2t.inputs, h2t.xns) start_time = time.time() result = mp.run() @@ -744,7 +756,7 @@ def validate(var, result, trans): s.pop() g = g.parent if verbose: - print "--- validation succeed ----" + print("--- validation succeed ----") return if isinstance(result, ExprRef): inv = result @@ -762,7 +774,7 @@ def validate(var, result, trans): # too many steps to reach invariant if step > 1000: if verbose: - print "--- validation failed --" + print("--- validation failed --") return if not check_disjoint(var.prev(cube), var.prev(inv)): # reach invariant @@ -773,7 +785,7 @@ def validate(var, result, trans): cube = var.projectN(s.model()) s.pop() if verbose: - print "--- validation succeed ----" + print("--- validation succeed ----") return diff --git a/scripts/mk_project.py b/scripts/mk_project.py index fef34d036..e62b2e159 100644 --- a/scripts/mk_project.py +++ b/scripts/mk_project.py @@ -126,5 +126,3 @@ def init_project_def(): add_ml_example('ml_example', 'ml') add_z3py_example('py_example', 'python') return API_files - - diff --git a/scripts/mk_util.py b/scripts/mk_util.py index 67a2fca63..8b862fec2 100644 --- a/scripts/mk_util.py +++ b/scripts/mk_util.py @@ -2314,7 +2314,7 @@ class DotNetExampleComponent(ExampleComponent): dotnet_proj_str = r""" Exe - netcoreapp2.0 + net8.0 %s diff --git a/src/api/api_ast.cpp b/src/api/api_ast.cpp index c0dd3c837..d7ea3d3c8 100644 --- a/src/api/api_ast.cpp +++ b/src/api/api_ast.cpp @@ -129,7 +129,6 @@ extern "C" { Z3_TRY; LOG_Z3_mk_rec_func_decl(c, s, domain_size, domain, range); RESET_ERROR_CODE(); - // recfun::promise_def def = mk_c(c)->recfun().get_plugin().mk_def( to_symbol(s), domain_size, to_sorts(domain), to_sort(range), false); @@ -1088,407 +1087,425 @@ extern "C" { Z3_CATCH_RETURN(""); } + // Helper functions to reduce instruction cache pressure in Z3_get_decl_kind. + // Each theory gets its own function to avoid loading the entire switch table. + + static Z3_decl_kind get_decl_kind_basic(decl_kind k) { + switch(k) { + case OP_TRUE: return Z3_OP_TRUE; + case OP_FALSE: return Z3_OP_FALSE; + case OP_EQ: return Z3_OP_EQ; + case OP_DISTINCT: return Z3_OP_DISTINCT; + case OP_ITE: return Z3_OP_ITE; + case OP_AND: return Z3_OP_AND; + case OP_OR: return Z3_OP_OR; + case OP_XOR: return Z3_OP_XOR; + case OP_NOT: return Z3_OP_NOT; + case OP_IMPLIES: return Z3_OP_IMPLIES; + case OP_OEQ: return Z3_OP_OEQ; + case PR_UNDEF: return Z3_OP_PR_UNDEF; + case PR_TRUE: return Z3_OP_PR_TRUE; + case PR_ASSERTED: return Z3_OP_PR_ASSERTED; + case PR_GOAL: return Z3_OP_PR_GOAL; + case PR_MODUS_PONENS: return Z3_OP_PR_MODUS_PONENS; + case PR_REFLEXIVITY: return Z3_OP_PR_REFLEXIVITY; + case PR_SYMMETRY: return Z3_OP_PR_SYMMETRY; + case PR_TRANSITIVITY: return Z3_OP_PR_TRANSITIVITY; + case PR_TRANSITIVITY_STAR: return Z3_OP_PR_TRANSITIVITY_STAR; + case PR_MONOTONICITY: return Z3_OP_PR_MONOTONICITY; + case PR_QUANT_INTRO: return Z3_OP_PR_QUANT_INTRO; + case PR_BIND: return Z3_OP_PR_BIND; + case PR_DISTRIBUTIVITY: return Z3_OP_PR_DISTRIBUTIVITY; + case PR_AND_ELIM: return Z3_OP_PR_AND_ELIM; + case PR_NOT_OR_ELIM: return Z3_OP_PR_NOT_OR_ELIM; + case PR_REWRITE: return Z3_OP_PR_REWRITE; + case PR_REWRITE_STAR: return Z3_OP_PR_REWRITE_STAR; + case PR_PULL_QUANT: return Z3_OP_PR_PULL_QUANT; + case PR_PUSH_QUANT: return Z3_OP_PR_PUSH_QUANT; + case PR_ELIM_UNUSED_VARS: return Z3_OP_PR_ELIM_UNUSED_VARS; + case PR_DER: return Z3_OP_PR_DER; + case PR_QUANT_INST: return Z3_OP_PR_QUANT_INST; + case PR_HYPOTHESIS: return Z3_OP_PR_HYPOTHESIS; + case PR_LEMMA: return Z3_OP_PR_LEMMA; + case PR_UNIT_RESOLUTION: return Z3_OP_PR_UNIT_RESOLUTION; + case PR_IFF_TRUE: return Z3_OP_PR_IFF_TRUE; + case PR_IFF_FALSE: return Z3_OP_PR_IFF_FALSE; + case PR_COMMUTATIVITY: return Z3_OP_PR_COMMUTATIVITY; + case PR_DEF_AXIOM: return Z3_OP_PR_DEF_AXIOM; + case PR_ASSUMPTION_ADD: return Z3_OP_PR_ASSUMPTION_ADD; + case PR_LEMMA_ADD: return Z3_OP_PR_LEMMA_ADD; + case PR_REDUNDANT_DEL: return Z3_OP_PR_REDUNDANT_DEL; + case PR_CLAUSE_TRAIL: return Z3_OP_PR_CLAUSE_TRAIL; + case PR_DEF_INTRO: return Z3_OP_PR_DEF_INTRO; + case PR_APPLY_DEF: return Z3_OP_PR_APPLY_DEF; + case PR_IFF_OEQ: return Z3_OP_PR_IFF_OEQ; + case PR_NNF_POS: return Z3_OP_PR_NNF_POS; + case PR_NNF_NEG: return Z3_OP_PR_NNF_NEG; + case PR_SKOLEMIZE: return Z3_OP_PR_SKOLEMIZE; + case PR_MODUS_PONENS_OEQ: return Z3_OP_PR_MODUS_PONENS_OEQ; + case PR_TH_LEMMA: return Z3_OP_PR_TH_LEMMA; + case PR_HYPER_RESOLVE: return Z3_OP_PR_HYPER_RESOLVE; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_arith(decl_kind k) { + switch(k) { + case OP_NUM: return Z3_OP_ANUM; + case OP_IRRATIONAL_ALGEBRAIC_NUM: return Z3_OP_AGNUM; + case OP_LE: return Z3_OP_LE; + case OP_GE: return Z3_OP_GE; + case OP_LT: return Z3_OP_LT; + case OP_GT: return Z3_OP_GT; + case OP_ADD: return Z3_OP_ADD; + case OP_SUB: return Z3_OP_SUB; + case OP_UMINUS: return Z3_OP_UMINUS; + case OP_MUL: return Z3_OP_MUL; + case OP_DIV: return Z3_OP_DIV; + case OP_IDIV: return Z3_OP_IDIV; + case OP_REM: return Z3_OP_REM; + case OP_MOD: return Z3_OP_MOD; + case OP_POWER: return Z3_OP_POWER; + case OP_ABS: return Z3_OP_ABS; + case OP_TO_REAL: return Z3_OP_TO_REAL; + case OP_TO_INT: return Z3_OP_TO_INT; + case OP_IS_INT: return Z3_OP_IS_INT; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_array(decl_kind k) { + switch(k) { + case OP_STORE: return Z3_OP_STORE; + case OP_SELECT: return Z3_OP_SELECT; + case OP_CONST_ARRAY: return Z3_OP_CONST_ARRAY; + case OP_ARRAY_DEFAULT: return Z3_OP_ARRAY_DEFAULT; + case OP_ARRAY_MAP: return Z3_OP_ARRAY_MAP; + case OP_SET_UNION: return Z3_OP_SET_UNION; + case OP_SET_INTERSECT: return Z3_OP_SET_INTERSECT; + case OP_SET_DIFFERENCE: return Z3_OP_SET_DIFFERENCE; + case OP_SET_COMPLEMENT: return Z3_OP_SET_COMPLEMENT; + case OP_SET_SUBSET: return Z3_OP_SET_SUBSET; + case OP_AS_ARRAY: return Z3_OP_AS_ARRAY; + case OP_ARRAY_EXT: return Z3_OP_ARRAY_EXT; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_special_relations(decl_kind k) { + switch(k) { + case OP_SPECIAL_RELATION_LO : return Z3_OP_SPECIAL_RELATION_LO; + case OP_SPECIAL_RELATION_PO : return Z3_OP_SPECIAL_RELATION_PO; + case OP_SPECIAL_RELATION_PLO: return Z3_OP_SPECIAL_RELATION_PLO; + case OP_SPECIAL_RELATION_TO : return Z3_OP_SPECIAL_RELATION_TO; + case OP_SPECIAL_RELATION_TC : return Z3_OP_SPECIAL_RELATION_TC; + default: UNREACHABLE(); return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_bv(decl_kind k) { + switch(k) { + case OP_BV_NUM: return Z3_OP_BNUM; + case OP_BIT1: return Z3_OP_BIT1; + case OP_BIT0: return Z3_OP_BIT0; + case OP_BNEG: return Z3_OP_BNEG; + case OP_BADD: return Z3_OP_BADD; + case OP_BSUB: return Z3_OP_BSUB; + case OP_BMUL: return Z3_OP_BMUL; + case OP_BSDIV: return Z3_OP_BSDIV; + case OP_BUDIV: return Z3_OP_BUDIV; + case OP_BSREM: return Z3_OP_BSREM; + case OP_BUREM: return Z3_OP_BUREM; + case OP_BSMOD: return Z3_OP_BSMOD; + case OP_BSDIV0: return Z3_OP_BSDIV0; + case OP_BUDIV0: return Z3_OP_BUDIV0; + case OP_BSREM0: return Z3_OP_BSREM0; + case OP_BUREM0: return Z3_OP_BUREM0; + case OP_BSMOD0: return Z3_OP_BSMOD0; + case OP_ULEQ: return Z3_OP_ULEQ; + case OP_SLEQ: return Z3_OP_SLEQ; + case OP_UGEQ: return Z3_OP_UGEQ; + case OP_SGEQ: return Z3_OP_SGEQ; + case OP_ULT: return Z3_OP_ULT; + case OP_SLT: return Z3_OP_SLT; + case OP_UGT: return Z3_OP_UGT; + case OP_SGT: return Z3_OP_SGT; + case OP_BAND: return Z3_OP_BAND; + case OP_BOR: return Z3_OP_BOR; + case OP_BNOT: return Z3_OP_BNOT; + case OP_BXOR: return Z3_OP_BXOR; + case OP_BNAND: return Z3_OP_BNAND; + case OP_BNOR: return Z3_OP_BNOR; + case OP_BXNOR: return Z3_OP_BXNOR; + case OP_CONCAT: return Z3_OP_CONCAT; + case OP_SIGN_EXT: return Z3_OP_SIGN_EXT; + case OP_ZERO_EXT: return Z3_OP_ZERO_EXT; + case OP_EXTRACT: return Z3_OP_EXTRACT; + case OP_REPEAT: return Z3_OP_REPEAT; + case OP_BREDOR: return Z3_OP_BREDOR; + case OP_BREDAND: return Z3_OP_BREDAND; + case OP_BCOMP: return Z3_OP_BCOMP; + case OP_BSHL: return Z3_OP_BSHL; + case OP_BLSHR: return Z3_OP_BLSHR; + case OP_BASHR: return Z3_OP_BASHR; + case OP_ROTATE_LEFT: return Z3_OP_ROTATE_LEFT; + case OP_ROTATE_RIGHT: return Z3_OP_ROTATE_RIGHT; + case OP_EXT_ROTATE_LEFT: return Z3_OP_EXT_ROTATE_LEFT; + case OP_EXT_ROTATE_RIGHT: return Z3_OP_EXT_ROTATE_RIGHT; + case OP_INT2BV: return Z3_OP_INT2BV; + case OP_UBV2INT: return Z3_OP_BV2INT; + case OP_SBV2INT: return Z3_OP_SBV2INT; + case OP_CARRY: return Z3_OP_CARRY; + case OP_XOR3: return Z3_OP_XOR3; + case OP_BIT2BOOL: return Z3_OP_BIT2BOOL; + case OP_BSMUL_NO_OVFL: return Z3_OP_BSMUL_NO_OVFL; + case OP_BUMUL_NO_OVFL: return Z3_OP_BUMUL_NO_OVFL; + case OP_BSMUL_NO_UDFL: return Z3_OP_BSMUL_NO_UDFL; + case OP_BSDIV_I: return Z3_OP_BSDIV_I; + case OP_BUDIV_I: return Z3_OP_BUDIV_I; + case OP_BSREM_I: return Z3_OP_BSREM_I; + case OP_BUREM_I: return Z3_OP_BUREM_I; + case OP_BSMOD_I: return Z3_OP_BSMOD_I; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_dt(decl_kind k) { + switch(k) { + case OP_DT_CONSTRUCTOR: return Z3_OP_DT_CONSTRUCTOR; + case OP_DT_RECOGNISER: return Z3_OP_DT_RECOGNISER; + case OP_DT_IS: return Z3_OP_DT_IS; + case OP_DT_ACCESSOR: return Z3_OP_DT_ACCESSOR; + case OP_DT_UPDATE_FIELD: return Z3_OP_DT_UPDATE_FIELD; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_datalog(decl_kind k) { + switch(k) { + case datalog::OP_RA_STORE: return Z3_OP_RA_STORE; + case datalog::OP_RA_EMPTY: return Z3_OP_RA_EMPTY; + case datalog::OP_RA_IS_EMPTY: return Z3_OP_RA_IS_EMPTY; + case datalog::OP_RA_JOIN: return Z3_OP_RA_JOIN; + case datalog::OP_RA_UNION: return Z3_OP_RA_UNION; + case datalog::OP_RA_WIDEN: return Z3_OP_RA_WIDEN; + case datalog::OP_RA_PROJECT: return Z3_OP_RA_PROJECT; + case datalog::OP_RA_FILTER: return Z3_OP_RA_FILTER; + case datalog::OP_RA_NEGATION_FILTER: return Z3_OP_RA_NEGATION_FILTER; + case datalog::OP_RA_RENAME: return Z3_OP_RA_RENAME; + case datalog::OP_RA_COMPLEMENT: return Z3_OP_RA_COMPLEMENT; + case datalog::OP_RA_SELECT: return Z3_OP_RA_SELECT; + case datalog::OP_RA_CLONE: return Z3_OP_RA_CLONE; + case datalog::OP_DL_CONSTANT: return Z3_OP_FD_CONSTANT; + case datalog::OP_DL_LT: return Z3_OP_FD_LT; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_seq(decl_kind k) { + switch (k) { + case OP_SEQ_UNIT: return Z3_OP_SEQ_UNIT; + case OP_SEQ_EMPTY: return Z3_OP_SEQ_EMPTY; + case OP_SEQ_CONCAT: return Z3_OP_SEQ_CONCAT; + case OP_SEQ_PREFIX: return Z3_OP_SEQ_PREFIX; + case OP_SEQ_SUFFIX: return Z3_OP_SEQ_SUFFIX; + case OP_SEQ_CONTAINS: return Z3_OP_SEQ_CONTAINS; + case OP_SEQ_EXTRACT: return Z3_OP_SEQ_EXTRACT; + case OP_SEQ_REPLACE: return Z3_OP_SEQ_REPLACE; + case OP_SEQ_REPLACE_RE: return Z3_OP_SEQ_REPLACE_RE; + case OP_SEQ_REPLACE_RE_ALL: return Z3_OP_SEQ_REPLACE_RE_ALL; + case OP_SEQ_REPLACE_ALL: return Z3_OP_SEQ_REPLACE_ALL; + case OP_SEQ_AT: return Z3_OP_SEQ_AT; + case OP_SEQ_NTH: return Z3_OP_SEQ_NTH; + case OP_SEQ_LENGTH: return Z3_OP_SEQ_LENGTH; + case OP_SEQ_INDEX: return Z3_OP_SEQ_INDEX; + case OP_SEQ_TO_RE: return Z3_OP_SEQ_TO_RE; + case OP_SEQ_IN_RE: return Z3_OP_SEQ_IN_RE; + case OP_SEQ_MAP: return Z3_OP_SEQ_MAP; + case OP_SEQ_MAPI: return Z3_OP_SEQ_MAPI; + case OP_SEQ_FOLDL: return Z3_OP_SEQ_FOLDL; + case OP_SEQ_FOLDLI: return Z3_OP_SEQ_FOLDLI; + case _OP_STRING_STRREPL: return Z3_OP_SEQ_REPLACE; + case _OP_STRING_CONCAT: return Z3_OP_SEQ_CONCAT; + case _OP_STRING_LENGTH: return Z3_OP_SEQ_LENGTH; + case _OP_STRING_STRCTN: return Z3_OP_SEQ_CONTAINS; + case _OP_STRING_PREFIX: return Z3_OP_SEQ_PREFIX; + case _OP_STRING_SUFFIX: return Z3_OP_SEQ_SUFFIX; + case _OP_STRING_IN_REGEXP: return Z3_OP_SEQ_IN_RE; + case _OP_STRING_TO_REGEXP: return Z3_OP_SEQ_TO_RE; + case _OP_STRING_CHARAT: return Z3_OP_SEQ_AT; + case _OP_STRING_SUBSTR: return Z3_OP_SEQ_EXTRACT; + case _OP_STRING_STRIDOF: return Z3_OP_SEQ_INDEX; + case _OP_REGEXP_EMPTY: return Z3_OP_RE_EMPTY_SET; + case _OP_REGEXP_FULL_CHAR: return Z3_OP_RE_FULL_SET; + case OP_STRING_STOI: return Z3_OP_STR_TO_INT; + case OP_STRING_ITOS: return Z3_OP_INT_TO_STR; + case OP_STRING_TO_CODE: return Z3_OP_STR_TO_CODE; + case OP_STRING_FROM_CODE: return Z3_OP_STR_FROM_CODE; + case OP_STRING_UBVTOS: return Z3_OP_UBV_TO_STR; + case OP_STRING_SBVTOS: return Z3_OP_SBV_TO_STR; + case OP_STRING_LT: return Z3_OP_STRING_LT; + case OP_STRING_LE: return Z3_OP_STRING_LE; + case OP_RE_PLUS: return Z3_OP_RE_PLUS; + case OP_RE_STAR: return Z3_OP_RE_STAR; + case OP_RE_OPTION: return Z3_OP_RE_OPTION; + case OP_RE_RANGE: return Z3_OP_RE_RANGE; + case OP_RE_CONCAT: return Z3_OP_RE_CONCAT; + case OP_RE_UNION: return Z3_OP_RE_UNION; + case OP_RE_DIFF: return Z3_OP_RE_DIFF; + case OP_RE_INTERSECT: return Z3_OP_RE_INTERSECT; + case OP_RE_LOOP: return Z3_OP_RE_LOOP; + case OP_RE_POWER: return Z3_OP_RE_POWER; + case OP_RE_COMPLEMENT: return Z3_OP_RE_COMPLEMENT; + case OP_RE_EMPTY_SET: return Z3_OP_RE_EMPTY_SET; + case OP_RE_FULL_SEQ_SET: return Z3_OP_RE_FULL_SET; + case OP_RE_FULL_CHAR_SET: return Z3_OP_RE_FULL_CHAR_SET; + case OP_RE_OF_PRED: return Z3_OP_RE_OF_PRED; + case OP_RE_REVERSE: return Z3_OP_RE_REVERSE; + case OP_RE_DERIVATIVE: return Z3_OP_RE_DERIVATIVE; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_char(decl_kind k) { + switch (k) { + case OP_CHAR_CONST: return Z3_OP_CHAR_CONST; + case OP_CHAR_LE: return Z3_OP_CHAR_LE; + case OP_CHAR_TO_INT: return Z3_OP_CHAR_TO_INT; + case OP_CHAR_TO_BV: return Z3_OP_CHAR_TO_BV; + case OP_CHAR_FROM_BV: return Z3_OP_CHAR_FROM_BV; + case OP_CHAR_IS_DIGIT: return Z3_OP_CHAR_IS_DIGIT; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_fpa(decl_kind k) { + switch (k) { + case OP_FPA_RM_NEAREST_TIES_TO_EVEN: return Z3_OP_FPA_RM_NEAREST_TIES_TO_EVEN; + case OP_FPA_RM_NEAREST_TIES_TO_AWAY: return Z3_OP_FPA_RM_NEAREST_TIES_TO_AWAY; + case OP_FPA_RM_TOWARD_POSITIVE: return Z3_OP_FPA_RM_TOWARD_POSITIVE; + case OP_FPA_RM_TOWARD_NEGATIVE: return Z3_OP_FPA_RM_TOWARD_NEGATIVE; + case OP_FPA_RM_TOWARD_ZERO: return Z3_OP_FPA_RM_TOWARD_ZERO; + case OP_FPA_NUM: return Z3_OP_FPA_NUM; + case OP_FPA_PLUS_INF: return Z3_OP_FPA_PLUS_INF; + case OP_FPA_MINUS_INF: return Z3_OP_FPA_MINUS_INF; + case OP_FPA_NAN: return Z3_OP_FPA_NAN; + case OP_FPA_MINUS_ZERO: return Z3_OP_FPA_MINUS_ZERO; + case OP_FPA_PLUS_ZERO: return Z3_OP_FPA_PLUS_ZERO; + case OP_FPA_ADD: return Z3_OP_FPA_ADD; + case OP_FPA_SUB: return Z3_OP_FPA_SUB; + case OP_FPA_NEG: return Z3_OP_FPA_NEG; + case OP_FPA_MUL: return Z3_OP_FPA_MUL; + case OP_FPA_DIV: return Z3_OP_FPA_DIV; + case OP_FPA_REM: return Z3_OP_FPA_REM; + case OP_FPA_ABS: return Z3_OP_FPA_ABS; + case OP_FPA_MIN: return Z3_OP_FPA_MIN; + case OP_FPA_MAX: return Z3_OP_FPA_MAX; + case OP_FPA_FMA: return Z3_OP_FPA_FMA; + case OP_FPA_SQRT: return Z3_OP_FPA_SQRT; + case OP_FPA_EQ: return Z3_OP_FPA_EQ; + case OP_FPA_ROUND_TO_INTEGRAL: return Z3_OP_FPA_ROUND_TO_INTEGRAL; + case OP_FPA_LT: return Z3_OP_FPA_LT; + case OP_FPA_GT: return Z3_OP_FPA_GT; + case OP_FPA_LE: return Z3_OP_FPA_LE; + case OP_FPA_GE: return Z3_OP_FPA_GE; + case OP_FPA_IS_NAN: return Z3_OP_FPA_IS_NAN; + case OP_FPA_IS_INF: return Z3_OP_FPA_IS_INF; + case OP_FPA_IS_ZERO: return Z3_OP_FPA_IS_ZERO; + case OP_FPA_IS_NORMAL: return Z3_OP_FPA_IS_NORMAL; + case OP_FPA_IS_SUBNORMAL: return Z3_OP_FPA_IS_SUBNORMAL; + case OP_FPA_IS_NEGATIVE: return Z3_OP_FPA_IS_NEGATIVE; + case OP_FPA_IS_POSITIVE: return Z3_OP_FPA_IS_POSITIVE; + case OP_FPA_FP: return Z3_OP_FPA_FP; + case OP_FPA_TO_FP: return Z3_OP_FPA_TO_FP; + case OP_FPA_TO_FP_UNSIGNED: return Z3_OP_FPA_TO_FP_UNSIGNED; + case OP_FPA_TO_UBV: return Z3_OP_FPA_TO_UBV; + case OP_FPA_TO_SBV: return Z3_OP_FPA_TO_SBV; + case OP_FPA_TO_REAL: return Z3_OP_FPA_TO_REAL; + case OP_FPA_TO_IEEE_BV: return Z3_OP_FPA_TO_IEEE_BV; + case OP_FPA_BVWRAP: return Z3_OP_FPA_BVWRAP; + case OP_FPA_BV2RM: return Z3_OP_FPA_BV2RM; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_label(decl_kind k) { + switch(k) { + case OP_LABEL: return Z3_OP_LABEL; + case OP_LABEL_LIT: return Z3_OP_LABEL_LIT; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_pb(decl_kind k) { + switch(k) { + case OP_PB_LE: return Z3_OP_PB_LE; + case OP_PB_GE: return Z3_OP_PB_GE; + case OP_PB_EQ: return Z3_OP_PB_EQ; + case OP_AT_MOST_K: return Z3_OP_PB_AT_MOST; + case OP_AT_LEAST_K: return Z3_OP_PB_AT_LEAST; + default: return Z3_OP_INTERNAL; + } + } + + static Z3_decl_kind get_decl_kind_finite_set(decl_kind k) { + switch(k) { + case OP_FINITE_SET_EMPTY: return Z3_OP_FINITE_SET_EMPTY; + case OP_FINITE_SET_SINGLETON: return Z3_OP_FINITE_SET_SINGLETON; + case OP_FINITE_SET_UNION: return Z3_OP_FINITE_SET_UNION; + case OP_FINITE_SET_INTERSECT: return Z3_OP_FINITE_SET_INTERSECT; + case OP_FINITE_SET_DIFFERENCE: return Z3_OP_FINITE_SET_DIFFERENCE; + case OP_FINITE_SET_IN: return Z3_OP_FINITE_SET_IN; + case OP_FINITE_SET_SIZE: return Z3_OP_FINITE_SET_SIZE; + case OP_FINITE_SET_SUBSET: return Z3_OP_FINITE_SET_SUBSET; + case OP_FINITE_SET_MAP: return Z3_OP_FINITE_SET_MAP; + case OP_FINITE_SET_FILTER: return Z3_OP_FINITE_SET_FILTER; + case OP_FINITE_SET_RANGE: return Z3_OP_FINITE_SET_RANGE; + case OP_FINITE_SET_EXT: return Z3_OP_FINITE_SET_EXT; + case OP_FINITE_SET_MAP_INVERSE: return Z3_OP_FINITE_SET_MAP_INVERSE; + default: return Z3_OP_INTERNAL; + } + } + Z3_decl_kind Z3_API Z3_get_decl_kind(Z3_context c, Z3_func_decl d) { Z3_TRY; LOG_Z3_get_decl_kind(c, d); RESET_ERROR_CODE(); func_decl* _d = to_func_decl(d); - if (d == nullptr || null_family_id == _d->get_family_id()) { + if (d == nullptr || null_family_id == _d->get_family_id()) return Z3_OP_UNINTERPRETED; - } - if (mk_c(c)->get_basic_fid() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_TRUE: return Z3_OP_TRUE; - case OP_FALSE: return Z3_OP_FALSE; - case OP_EQ: return Z3_OP_EQ; - case OP_DISTINCT: return Z3_OP_DISTINCT; - case OP_ITE: return Z3_OP_ITE; - case OP_AND: return Z3_OP_AND; - case OP_OR: return Z3_OP_OR; - case OP_XOR: return Z3_OP_XOR; - case OP_NOT: return Z3_OP_NOT; - case OP_IMPLIES: return Z3_OP_IMPLIES; - case OP_OEQ: return Z3_OP_OEQ; - case PR_UNDEF: return Z3_OP_PR_UNDEF; - case PR_TRUE: return Z3_OP_PR_TRUE; - case PR_ASSERTED: return Z3_OP_PR_ASSERTED; - case PR_GOAL: return Z3_OP_PR_GOAL; - case PR_MODUS_PONENS: return Z3_OP_PR_MODUS_PONENS; - case PR_REFLEXIVITY: return Z3_OP_PR_REFLEXIVITY; - case PR_SYMMETRY: return Z3_OP_PR_SYMMETRY; - case PR_TRANSITIVITY: return Z3_OP_PR_TRANSITIVITY; - case PR_TRANSITIVITY_STAR: return Z3_OP_PR_TRANSITIVITY_STAR; - case PR_MONOTONICITY: return Z3_OP_PR_MONOTONICITY; - case PR_QUANT_INTRO: return Z3_OP_PR_QUANT_INTRO; - case PR_BIND: return Z3_OP_PR_BIND; - case PR_DISTRIBUTIVITY: return Z3_OP_PR_DISTRIBUTIVITY; - case PR_AND_ELIM: return Z3_OP_PR_AND_ELIM; - case PR_NOT_OR_ELIM: return Z3_OP_PR_NOT_OR_ELIM; - case PR_REWRITE: return Z3_OP_PR_REWRITE; - case PR_REWRITE_STAR: return Z3_OP_PR_REWRITE_STAR; - case PR_PULL_QUANT: return Z3_OP_PR_PULL_QUANT; - case PR_PUSH_QUANT: return Z3_OP_PR_PUSH_QUANT; - case PR_ELIM_UNUSED_VARS: return Z3_OP_PR_ELIM_UNUSED_VARS; - case PR_DER: return Z3_OP_PR_DER; - case PR_QUANT_INST: return Z3_OP_PR_QUANT_INST; - case PR_HYPOTHESIS: return Z3_OP_PR_HYPOTHESIS; - case PR_LEMMA: return Z3_OP_PR_LEMMA; - case PR_UNIT_RESOLUTION: return Z3_OP_PR_UNIT_RESOLUTION; - case PR_IFF_TRUE: return Z3_OP_PR_IFF_TRUE; - case PR_IFF_FALSE: return Z3_OP_PR_IFF_FALSE; - case PR_COMMUTATIVITY: return Z3_OP_PR_COMMUTATIVITY; - case PR_DEF_AXIOM: return Z3_OP_PR_DEF_AXIOM; - case PR_ASSUMPTION_ADD: return Z3_OP_PR_ASSUMPTION_ADD; - case PR_LEMMA_ADD: return Z3_OP_PR_LEMMA_ADD; - case PR_REDUNDANT_DEL: return Z3_OP_PR_REDUNDANT_DEL; - case PR_CLAUSE_TRAIL: return Z3_OP_PR_CLAUSE_TRAIL; - case PR_DEF_INTRO: return Z3_OP_PR_DEF_INTRO; - case PR_APPLY_DEF: return Z3_OP_PR_APPLY_DEF; - case PR_IFF_OEQ: return Z3_OP_PR_IFF_OEQ; - case PR_NNF_POS: return Z3_OP_PR_NNF_POS; - case PR_NNF_NEG: return Z3_OP_PR_NNF_NEG; - case PR_SKOLEMIZE: return Z3_OP_PR_SKOLEMIZE; - case PR_MODUS_PONENS_OEQ: return Z3_OP_PR_MODUS_PONENS_OEQ; - case PR_TH_LEMMA: return Z3_OP_PR_TH_LEMMA; - case PR_HYPER_RESOLVE: return Z3_OP_PR_HYPER_RESOLVE; - default: - return Z3_OP_INTERNAL; - } - } - if (mk_c(c)->get_arith_fid() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_NUM: return Z3_OP_ANUM; - case OP_IRRATIONAL_ALGEBRAIC_NUM: return Z3_OP_AGNUM; - case OP_LE: return Z3_OP_LE; - case OP_GE: return Z3_OP_GE; - case OP_LT: return Z3_OP_LT; - case OP_GT: return Z3_OP_GT; - case OP_ADD: return Z3_OP_ADD; - case OP_SUB: return Z3_OP_SUB; - case OP_UMINUS: return Z3_OP_UMINUS; - case OP_MUL: return Z3_OP_MUL; - case OP_DIV: return Z3_OP_DIV; - case OP_IDIV: return Z3_OP_IDIV; - case OP_REM: return Z3_OP_REM; - case OP_MOD: return Z3_OP_MOD; - case OP_POWER: return Z3_OP_POWER; - case OP_ABS: return Z3_OP_ABS; - case OP_TO_REAL: return Z3_OP_TO_REAL; - case OP_TO_INT: return Z3_OP_TO_INT; - case OP_IS_INT: return Z3_OP_IS_INT; - default: - return Z3_OP_INTERNAL; - } - } - if (mk_c(c)->get_array_fid() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_STORE: return Z3_OP_STORE; - case OP_SELECT: return Z3_OP_SELECT; - case OP_CONST_ARRAY: return Z3_OP_CONST_ARRAY; - case OP_ARRAY_DEFAULT: return Z3_OP_ARRAY_DEFAULT; - case OP_ARRAY_MAP: return Z3_OP_ARRAY_MAP; - case OP_SET_UNION: return Z3_OP_SET_UNION; - case OP_SET_INTERSECT: return Z3_OP_SET_INTERSECT; - case OP_SET_DIFFERENCE: return Z3_OP_SET_DIFFERENCE; - case OP_SET_COMPLEMENT: return Z3_OP_SET_COMPLEMENT; - case OP_SET_SUBSET: return Z3_OP_SET_SUBSET; - case OP_AS_ARRAY: return Z3_OP_AS_ARRAY; - case OP_ARRAY_EXT: return Z3_OP_ARRAY_EXT; - default: - return Z3_OP_INTERNAL; - } - } + family_id fid = _d->get_family_id(); + decl_kind k = _d->get_decl_kind(); - if (mk_c(c)->get_special_relations_fid() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_SPECIAL_RELATION_LO : return Z3_OP_SPECIAL_RELATION_LO; - case OP_SPECIAL_RELATION_PO : return Z3_OP_SPECIAL_RELATION_PO; - case OP_SPECIAL_RELATION_PLO: return Z3_OP_SPECIAL_RELATION_PLO; - case OP_SPECIAL_RELATION_TO : return Z3_OP_SPECIAL_RELATION_TO; - case OP_SPECIAL_RELATION_TC : return Z3_OP_SPECIAL_RELATION_TC; - default: UNREACHABLE(); - } - } - - - if (mk_c(c)->get_bv_fid() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_BV_NUM: return Z3_OP_BNUM; - case OP_BIT1: return Z3_OP_BIT1; - case OP_BIT0: return Z3_OP_BIT0; - case OP_BNEG: return Z3_OP_BNEG; - case OP_BADD: return Z3_OP_BADD; - case OP_BSUB: return Z3_OP_BSUB; - case OP_BMUL: return Z3_OP_BMUL; - case OP_BSDIV: return Z3_OP_BSDIV; - case OP_BUDIV: return Z3_OP_BUDIV; - case OP_BSREM: return Z3_OP_BSREM; - case OP_BUREM: return Z3_OP_BUREM; - case OP_BSMOD: return Z3_OP_BSMOD; - case OP_BSDIV0: return Z3_OP_BSDIV0; - case OP_BUDIV0: return Z3_OP_BUDIV0; - case OP_BSREM0: return Z3_OP_BSREM0; - case OP_BUREM0: return Z3_OP_BUREM0; - case OP_BSMOD0: return Z3_OP_BSMOD0; - case OP_ULEQ: return Z3_OP_ULEQ; - case OP_SLEQ: return Z3_OP_SLEQ; - case OP_UGEQ: return Z3_OP_UGEQ; - case OP_SGEQ: return Z3_OP_SGEQ; - case OP_ULT: return Z3_OP_ULT; - case OP_SLT: return Z3_OP_SLT; - case OP_UGT: return Z3_OP_UGT; - case OP_SGT: return Z3_OP_SGT; - case OP_BAND: return Z3_OP_BAND; - case OP_BOR: return Z3_OP_BOR; - case OP_BNOT: return Z3_OP_BNOT; - case OP_BXOR: return Z3_OP_BXOR; - case OP_BNAND: return Z3_OP_BNAND; - case OP_BNOR: return Z3_OP_BNOR; - case OP_BXNOR: return Z3_OP_BXNOR; - case OP_CONCAT: return Z3_OP_CONCAT; - case OP_SIGN_EXT: return Z3_OP_SIGN_EXT; - case OP_ZERO_EXT: return Z3_OP_ZERO_EXT; - case OP_EXTRACT: return Z3_OP_EXTRACT; - case OP_REPEAT: return Z3_OP_REPEAT; - case OP_BREDOR: return Z3_OP_BREDOR; - case OP_BREDAND: return Z3_OP_BREDAND; - case OP_BCOMP: return Z3_OP_BCOMP; - case OP_BSHL: return Z3_OP_BSHL; - case OP_BLSHR: return Z3_OP_BLSHR; - case OP_BASHR: return Z3_OP_BASHR; - case OP_ROTATE_LEFT: return Z3_OP_ROTATE_LEFT; - case OP_ROTATE_RIGHT: return Z3_OP_ROTATE_RIGHT; - case OP_EXT_ROTATE_LEFT: return Z3_OP_EXT_ROTATE_LEFT; - case OP_EXT_ROTATE_RIGHT: return Z3_OP_EXT_ROTATE_RIGHT; - case OP_INT2BV: return Z3_OP_INT2BV; - case OP_UBV2INT: return Z3_OP_BV2INT; - case OP_SBV2INT: return Z3_OP_SBV2INT; - case OP_CARRY: return Z3_OP_CARRY; - case OP_XOR3: return Z3_OP_XOR3; - case OP_BIT2BOOL: return Z3_OP_BIT2BOOL; - case OP_BSMUL_NO_OVFL: return Z3_OP_BSMUL_NO_OVFL; - case OP_BUMUL_NO_OVFL: return Z3_OP_BUMUL_NO_OVFL; - case OP_BSMUL_NO_UDFL: return Z3_OP_BSMUL_NO_UDFL; - case OP_BSDIV_I: return Z3_OP_BSDIV_I; - case OP_BUDIV_I: return Z3_OP_BUDIV_I; - case OP_BSREM_I: return Z3_OP_BSREM_I; - case OP_BUREM_I: return Z3_OP_BUREM_I; - case OP_BSMOD_I: return Z3_OP_BSMOD_I; - default: - return Z3_OP_INTERNAL; - } - } - if (mk_c(c)->get_dt_fid() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_DT_CONSTRUCTOR: return Z3_OP_DT_CONSTRUCTOR; - case OP_DT_RECOGNISER: return Z3_OP_DT_RECOGNISER; - case OP_DT_IS: return Z3_OP_DT_IS; - case OP_DT_ACCESSOR: return Z3_OP_DT_ACCESSOR; - case OP_DT_UPDATE_FIELD: return Z3_OP_DT_UPDATE_FIELD; - default: - return Z3_OP_INTERNAL; - } - } - if (mk_c(c)->get_datalog_fid() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case datalog::OP_RA_STORE: return Z3_OP_RA_STORE; - case datalog::OP_RA_EMPTY: return Z3_OP_RA_EMPTY; - case datalog::OP_RA_IS_EMPTY: return Z3_OP_RA_IS_EMPTY; - case datalog::OP_RA_JOIN: return Z3_OP_RA_JOIN; - case datalog::OP_RA_UNION: return Z3_OP_RA_UNION; - case datalog::OP_RA_WIDEN: return Z3_OP_RA_WIDEN; - case datalog::OP_RA_PROJECT: return Z3_OP_RA_PROJECT; - case datalog::OP_RA_FILTER: return Z3_OP_RA_FILTER; - case datalog::OP_RA_NEGATION_FILTER: return Z3_OP_RA_NEGATION_FILTER; - case datalog::OP_RA_RENAME: return Z3_OP_RA_RENAME; - case datalog::OP_RA_COMPLEMENT: return Z3_OP_RA_COMPLEMENT; - case datalog::OP_RA_SELECT: return Z3_OP_RA_SELECT; - case datalog::OP_RA_CLONE: return Z3_OP_RA_CLONE; - case datalog::OP_DL_CONSTANT: return Z3_OP_FD_CONSTANT; - case datalog::OP_DL_LT: return Z3_OP_FD_LT; - default: - return Z3_OP_INTERNAL; - } - } - - if (mk_c(c)->get_seq_fid() == _d->get_family_id()) { - switch (_d->get_decl_kind()) { - case OP_SEQ_UNIT: return Z3_OP_SEQ_UNIT; - case OP_SEQ_EMPTY: return Z3_OP_SEQ_EMPTY; - case OP_SEQ_CONCAT: return Z3_OP_SEQ_CONCAT; - case OP_SEQ_PREFIX: return Z3_OP_SEQ_PREFIX; - case OP_SEQ_SUFFIX: return Z3_OP_SEQ_SUFFIX; - case OP_SEQ_CONTAINS: return Z3_OP_SEQ_CONTAINS; - case OP_SEQ_EXTRACT: return Z3_OP_SEQ_EXTRACT; - case OP_SEQ_REPLACE: return Z3_OP_SEQ_REPLACE; - case OP_SEQ_REPLACE_RE: return Z3_OP_SEQ_REPLACE_RE; - case OP_SEQ_REPLACE_RE_ALL: return Z3_OP_SEQ_REPLACE_RE_ALL; - case OP_SEQ_REPLACE_ALL: return Z3_OP_SEQ_REPLACE_ALL; - case OP_SEQ_AT: return Z3_OP_SEQ_AT; - case OP_SEQ_NTH: return Z3_OP_SEQ_NTH; - case OP_SEQ_LENGTH: return Z3_OP_SEQ_LENGTH; - case OP_SEQ_INDEX: return Z3_OP_SEQ_INDEX; - case OP_SEQ_TO_RE: return Z3_OP_SEQ_TO_RE; - case OP_SEQ_IN_RE: return Z3_OP_SEQ_IN_RE; - case OP_SEQ_MAP: return Z3_OP_SEQ_MAP; - case OP_SEQ_MAPI: return Z3_OP_SEQ_MAPI; - case OP_SEQ_FOLDL: return Z3_OP_SEQ_FOLDL; - case OP_SEQ_FOLDLI: return Z3_OP_SEQ_FOLDLI; - - case _OP_STRING_STRREPL: return Z3_OP_SEQ_REPLACE; - case _OP_STRING_CONCAT: return Z3_OP_SEQ_CONCAT; - case _OP_STRING_LENGTH: return Z3_OP_SEQ_LENGTH; - case _OP_STRING_STRCTN: return Z3_OP_SEQ_CONTAINS; - case _OP_STRING_PREFIX: return Z3_OP_SEQ_PREFIX; - case _OP_STRING_SUFFIX: return Z3_OP_SEQ_SUFFIX; - case _OP_STRING_IN_REGEXP: return Z3_OP_SEQ_IN_RE; - case _OP_STRING_TO_REGEXP: return Z3_OP_SEQ_TO_RE; - case _OP_STRING_CHARAT: return Z3_OP_SEQ_AT; - case _OP_STRING_SUBSTR: return Z3_OP_SEQ_EXTRACT; - case _OP_STRING_STRIDOF: return Z3_OP_SEQ_INDEX; - case _OP_REGEXP_EMPTY: return Z3_OP_RE_EMPTY_SET; - case _OP_REGEXP_FULL_CHAR: return Z3_OP_RE_FULL_SET; - - case OP_STRING_STOI: return Z3_OP_STR_TO_INT; - case OP_STRING_ITOS: return Z3_OP_INT_TO_STR; - case OP_STRING_TO_CODE: return Z3_OP_STR_TO_CODE; - case OP_STRING_FROM_CODE: return Z3_OP_STR_FROM_CODE; - - case OP_STRING_UBVTOS: return Z3_OP_UBV_TO_STR; - case OP_STRING_SBVTOS: return Z3_OP_SBV_TO_STR; - case OP_STRING_LT: return Z3_OP_STRING_LT; - case OP_STRING_LE: return Z3_OP_STRING_LE; - - case OP_RE_PLUS: return Z3_OP_RE_PLUS; - case OP_RE_STAR: return Z3_OP_RE_STAR; - case OP_RE_OPTION: return Z3_OP_RE_OPTION; - case OP_RE_RANGE: return Z3_OP_RE_RANGE; - case OP_RE_CONCAT: return Z3_OP_RE_CONCAT; - case OP_RE_UNION: return Z3_OP_RE_UNION; - case OP_RE_DIFF: return Z3_OP_RE_DIFF; - case OP_RE_INTERSECT: return Z3_OP_RE_INTERSECT; - case OP_RE_LOOP: return Z3_OP_RE_LOOP; - case OP_RE_POWER: return Z3_OP_RE_POWER; - case OP_RE_COMPLEMENT: return Z3_OP_RE_COMPLEMENT; - case OP_RE_EMPTY_SET: return Z3_OP_RE_EMPTY_SET; - - case OP_RE_FULL_SEQ_SET: return Z3_OP_RE_FULL_SET; - case OP_RE_FULL_CHAR_SET: return Z3_OP_RE_FULL_CHAR_SET; - case OP_RE_OF_PRED: return Z3_OP_RE_OF_PRED; - case OP_RE_REVERSE: return Z3_OP_RE_REVERSE; - case OP_RE_DERIVATIVE: return Z3_OP_RE_DERIVATIVE; - default: - return Z3_OP_INTERNAL; - } - } - - if (mk_c(c)->get_char_fid() == _d->get_family_id()) { - switch (_d->get_decl_kind()) { - case OP_CHAR_CONST: return Z3_OP_CHAR_CONST; - case OP_CHAR_LE: return Z3_OP_CHAR_LE; - case OP_CHAR_TO_INT: return Z3_OP_CHAR_TO_INT; - case OP_CHAR_TO_BV: return Z3_OP_CHAR_TO_BV; - case OP_CHAR_FROM_BV: return Z3_OP_CHAR_FROM_BV; - case OP_CHAR_IS_DIGIT: return Z3_OP_CHAR_IS_DIGIT; - default: - return Z3_OP_INTERNAL; - } - } - - if (mk_c(c)->get_fpa_fid() == _d->get_family_id()) { - switch (_d->get_decl_kind()) { - case OP_FPA_RM_NEAREST_TIES_TO_EVEN: return Z3_OP_FPA_RM_NEAREST_TIES_TO_EVEN; - case OP_FPA_RM_NEAREST_TIES_TO_AWAY: return Z3_OP_FPA_RM_NEAREST_TIES_TO_AWAY; - case OP_FPA_RM_TOWARD_POSITIVE: return Z3_OP_FPA_RM_TOWARD_POSITIVE; - case OP_FPA_RM_TOWARD_NEGATIVE: return Z3_OP_FPA_RM_TOWARD_NEGATIVE; - case OP_FPA_RM_TOWARD_ZERO: return Z3_OP_FPA_RM_TOWARD_ZERO; - case OP_FPA_NUM: return Z3_OP_FPA_NUM; - case OP_FPA_PLUS_INF: return Z3_OP_FPA_PLUS_INF; - case OP_FPA_MINUS_INF: return Z3_OP_FPA_MINUS_INF; - case OP_FPA_NAN: return Z3_OP_FPA_NAN; - case OP_FPA_MINUS_ZERO: return Z3_OP_FPA_MINUS_ZERO; - case OP_FPA_PLUS_ZERO: return Z3_OP_FPA_PLUS_ZERO; - case OP_FPA_ADD: return Z3_OP_FPA_ADD; - case OP_FPA_SUB: return Z3_OP_FPA_SUB; - case OP_FPA_NEG: return Z3_OP_FPA_NEG; - case OP_FPA_MUL: return Z3_OP_FPA_MUL; - case OP_FPA_DIV: return Z3_OP_FPA_DIV; - case OP_FPA_REM: return Z3_OP_FPA_REM; - case OP_FPA_ABS: return Z3_OP_FPA_ABS; - case OP_FPA_MIN: return Z3_OP_FPA_MIN; - case OP_FPA_MAX: return Z3_OP_FPA_MAX; - case OP_FPA_FMA: return Z3_OP_FPA_FMA; - case OP_FPA_SQRT: return Z3_OP_FPA_SQRT; - case OP_FPA_EQ: return Z3_OP_FPA_EQ; - case OP_FPA_ROUND_TO_INTEGRAL: return Z3_OP_FPA_ROUND_TO_INTEGRAL; - case OP_FPA_LT: return Z3_OP_FPA_LT; - case OP_FPA_GT: return Z3_OP_FPA_GT; - case OP_FPA_LE: return Z3_OP_FPA_LE; - case OP_FPA_GE: return Z3_OP_FPA_GE; - case OP_FPA_IS_NAN: return Z3_OP_FPA_IS_NAN; - case OP_FPA_IS_INF: return Z3_OP_FPA_IS_INF; - case OP_FPA_IS_ZERO: return Z3_OP_FPA_IS_ZERO; - case OP_FPA_IS_NORMAL: return Z3_OP_FPA_IS_NORMAL; - case OP_FPA_IS_SUBNORMAL: return Z3_OP_FPA_IS_SUBNORMAL; - case OP_FPA_IS_NEGATIVE: return Z3_OP_FPA_IS_NEGATIVE; - case OP_FPA_IS_POSITIVE: return Z3_OP_FPA_IS_POSITIVE; - case OP_FPA_FP: return Z3_OP_FPA_FP; - case OP_FPA_TO_FP: return Z3_OP_FPA_TO_FP; - case OP_FPA_TO_FP_UNSIGNED: return Z3_OP_FPA_TO_FP_UNSIGNED; - case OP_FPA_TO_UBV: return Z3_OP_FPA_TO_UBV; - case OP_FPA_TO_SBV: return Z3_OP_FPA_TO_SBV; - case OP_FPA_TO_REAL: return Z3_OP_FPA_TO_REAL; - case OP_FPA_TO_IEEE_BV: return Z3_OP_FPA_TO_IEEE_BV; - case OP_FPA_BVWRAP: return Z3_OP_FPA_BVWRAP; - case OP_FPA_BV2RM: return Z3_OP_FPA_BV2RM; - return Z3_OP_UNINTERPRETED; - default: - return Z3_OP_INTERNAL; - } - } - - if (mk_c(c)->m().get_label_family_id() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_LABEL: return Z3_OP_LABEL; - case OP_LABEL_LIT: return Z3_OP_LABEL_LIT; - default: - return Z3_OP_INTERNAL; - } - } - - if (mk_c(c)->get_pb_fid() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_PB_LE: return Z3_OP_PB_LE; - case OP_PB_GE: return Z3_OP_PB_GE; - case OP_PB_EQ: return Z3_OP_PB_EQ; - case OP_AT_MOST_K: return Z3_OP_PB_AT_MOST; - case OP_AT_LEAST_K: return Z3_OP_PB_AT_LEAST; - default: return Z3_OP_INTERNAL; - } - } - - if (mk_c(c)->fsutil().get_family_id() == _d->get_family_id()) { - switch(_d->get_decl_kind()) { - case OP_FINITE_SET_EMPTY: return Z3_OP_FINITE_SET_EMPTY; - case OP_FINITE_SET_SINGLETON: return Z3_OP_FINITE_SET_SINGLETON; - case OP_FINITE_SET_UNION: return Z3_OP_FINITE_SET_UNION; - case OP_FINITE_SET_INTERSECT: return Z3_OP_FINITE_SET_INTERSECT; - case OP_FINITE_SET_DIFFERENCE: return Z3_OP_FINITE_SET_DIFFERENCE; - case OP_FINITE_SET_IN: return Z3_OP_FINITE_SET_IN; - case OP_FINITE_SET_SIZE: return Z3_OP_FINITE_SET_SIZE; - case OP_FINITE_SET_SUBSET: return Z3_OP_FINITE_SET_SUBSET; - case OP_FINITE_SET_MAP: return Z3_OP_FINITE_SET_MAP; - case OP_FINITE_SET_FILTER: return Z3_OP_FINITE_SET_FILTER; - case OP_FINITE_SET_RANGE: return Z3_OP_FINITE_SET_RANGE; - case OP_FINITE_SET_EXT: return Z3_OP_FINITE_SET_EXT; - case OP_FINITE_SET_MAP_INVERSE: return Z3_OP_FINITE_SET_MAP_INVERSE; - default: return Z3_OP_INTERNAL; - } - } - - if (mk_c(c)->recfun().get_family_id() == _d->get_family_id()) + if (mk_c(c)->get_basic_fid() == fid) + return get_decl_kind_basic(k); + if (mk_c(c)->get_arith_fid() == fid) + return get_decl_kind_arith(k); + if (mk_c(c)->get_bv_fid() == fid) + return get_decl_kind_bv(k); + if (mk_c(c)->get_array_fid() == fid) + return get_decl_kind_array(k); + if (mk_c(c)->get_dt_fid() == fid) + return get_decl_kind_dt(k); + if (mk_c(c)->get_seq_fid() == fid) + return get_decl_kind_seq(k); + if (mk_c(c)->get_fpa_fid() == fid) + return get_decl_kind_fpa(k); + if (mk_c(c)->get_datalog_fid() == fid) + return get_decl_kind_datalog(k); + if (mk_c(c)->get_pb_fid() == fid) + return get_decl_kind_pb(k); + if (mk_c(c)->get_special_relations_fid() == fid) + return get_decl_kind_special_relations(k); + if (mk_c(c)->get_char_fid() == fid) + return get_decl_kind_char(k); + if (mk_c(c)->m().get_label_family_id() == fid) + return get_decl_kind_label(k); + if (mk_c(c)->fsutil().get_family_id() == fid) + return get_decl_kind_finite_set(k); + if (mk_c(c)->recfun().get_family_id() == fid) return Z3_OP_RECURSIVE; return Z3_OP_UNINTERPRETED; @@ -1505,11 +1522,7 @@ extern "C" { return 0; } var* va = to_var(_a); - if (va) { - return va->get_idx(); - } - SET_ERROR_CODE(Z3_INVALID_ARG, nullptr); - return 0; + return va->get_idx(); Z3_CATCH_RETURN(0); } diff --git a/src/api/dotnet/CMakeLists.txt b/src/api/dotnet/CMakeLists.txt index c309f4027..3d3864139 100644 --- a/src/api/dotnet/CMakeLists.txt +++ b/src/api/dotnet/CMakeLists.txt @@ -147,8 +147,13 @@ endforeach() set(Z3_DOTNET_NUPKG_VERSION "${VER_MAJOR}.${VER_MINOR}.${VER_BUILD}") if(TARGET_ARCHITECTURE STREQUAL "i686") set(Z3_DOTNET_PLATFORM "x86") + set(Z3_DOTNET_WIN_RID "win-x86") +elseif(TARGET_ARCHITECTURE STREQUAL "arm64") + set(Z3_DOTNET_PLATFORM "AnyCPU") + set(Z3_DOTNET_WIN_RID "win-arm64") else() set(Z3_DOTNET_PLATFORM "AnyCPU") + set(Z3_DOTNET_WIN_RID "win-x64") endif() # TODO conditional for signing. we can then enable the ``Release_delaysign`` configuration diff --git a/src/api/dotnet/Context.cs b/src/api/dotnet/Context.cs index 52c9af8f6..cba223f14 100644 --- a/src/api/dotnet/Context.cs +++ b/src/api/dotnet/Context.cs @@ -562,6 +562,63 @@ namespace Microsoft.Z3 } } + /// + /// Create a type variable sort for use as a parameter in polymorphic datatypes. + /// + /// name of the type variable + public Sort MkTypeVariable(Symbol name) + { + Debug.Assert(name != null); + CheckContextMatch(name); + return new Sort(this, Native.Z3_mk_type_variable(nCtx, name.NativeObject)); + } + + /// + /// Create a type variable sort for use as a parameter in polymorphic datatypes. + /// + /// name of the type variable + public Sort MkTypeVariable(string name) + { + using var symbol = MkSymbol(name); + return MkTypeVariable(symbol); + } + + /// + /// Create a polymorphic datatype sort with explicit type parameters. + /// Type parameters should be sorts created with . + /// + /// name of the datatype sort + /// array of type variable sorts + /// array of constructors + public DatatypeSort MkPolymorphicDatatypeSort(Symbol name, Sort[] typeParams, Constructor[] constructors) + { + Debug.Assert(name != null); + Debug.Assert(typeParams != null); + Debug.Assert(constructors != null); + Debug.Assert(constructors.All(c => c != null)); + + CheckContextMatch(name); + CheckContextMatch(typeParams); + CheckContextMatch(constructors); + return new DatatypeSort(this, + Native.Z3_mk_polymorphic_datatype(nCtx, name.NativeObject, + (uint)typeParams.Length, AST.ArrayToNative(typeParams), + (uint)constructors.Length, Z3Object.ArrayToNative(constructors))); + } + + /// + /// Create a polymorphic datatype sort with explicit type parameters. + /// Type parameters should be sorts created with . + /// + /// name of the datatype sort + /// array of type variable sorts + /// array of constructors + public DatatypeSort MkPolymorphicDatatypeSort(string name, Sort[] typeParams, Constructor[] constructors) + { + using var symbol = MkSymbol(name); + return MkPolymorphicDatatypeSort(symbol, typeParams, constructors); + } + /// /// Update a datatype field at expression t with value v. /// The function performs a record update at t. The field diff --git a/src/api/dotnet/Microsoft.Z3.csproj.in b/src/api/dotnet/Microsoft.Z3.csproj.in index ec136809d..cf5aacf46 100644 --- a/src/api/dotnet/Microsoft.Z3.csproj.in +++ b/src/api/dotnet/Microsoft.Z3.csproj.in @@ -84,10 +84,10 @@ ${Z3_DOTNET_COMPILE_ITEMS} - + - - runtimes\win-x64\native + + runtimes\${Z3_DOTNET_WIN_RID}\native runtimes\linux-x64\native @@ -99,7 +99,7 @@ ${Z3_DOTNET_COMPILE_ITEMS} - + runtimes\win-x86\native diff --git a/src/api/dotnet/Microsoft.Z3.props b/src/api/dotnet/Microsoft.Z3.props index a5db71359..4625fdd18 100644 --- a/src/api/dotnet/Microsoft.Z3.props +++ b/src/api/dotnet/Microsoft.Z3.props @@ -9,7 +9,8 @@ $(MSBuildThisFileDirectory)..\ - $(Z3_PACKAGE_PATH)runtimes\win-x64\native\libz3.dll + $(Z3_PACKAGE_PATH)runtimes\win-arm64\native\libz3.dll + $(Z3_PACKAGE_PATH)runtimes\win-x64\native\libz3.dll $(Z3_PACKAGE_PATH)runtimes\win-x86\native\libz3.dll $(Z3_PACKAGE_PATH)runtimes\linux-x64\native\libz3.so diff --git a/src/api/dotnet/Microsoft.Z3.targets b/src/api/dotnet/Microsoft.Z3.targets index 38e56b350..a1436242c 100644 --- a/src/api/dotnet/Microsoft.Z3.targets +++ b/src/api/dotnet/Microsoft.Z3.targets @@ -1,7 +1,7 @@ - + %(RecursiveDir)%(FileName)%(Extension) PreserveNewest diff --git a/src/api/go/char.go b/src/api/go/char.go new file mode 100644 index 000000000..846101f96 --- /dev/null +++ b/src/api/go/char.go @@ -0,0 +1,43 @@ +package z3 + +/* +#include "z3.h" +*/ +import "C" + +// Char operations + +// MkCharSort creates the character sort (Unicode characters). +func (c *Context) MkCharSort() *Sort { + return newSort(c, C.Z3_mk_char_sort(c.ptr)) +} + +// MkChar creates a character literal from a Unicode code point. +func (c *Context) MkChar(ch uint) *Expr { + return newExpr(c, C.Z3_mk_char(c.ptr, C.uint(ch))) +} + +// MkCharLe creates a character less-than-or-equal predicate (ch1 ≤ ch2). +func (c *Context) MkCharLe(ch1, ch2 *Expr) *Expr { + return newExpr(c, C.Z3_mk_char_le(c.ptr, ch1.ptr, ch2.ptr)) +} + +// MkCharToInt converts a character to its integer (Unicode code point) value. +func (c *Context) MkCharToInt(ch *Expr) *Expr { + return newExpr(c, C.Z3_mk_char_to_int(c.ptr, ch.ptr)) +} + +// MkCharToBV converts a character to a bit-vector. +func (c *Context) MkCharToBV(ch *Expr) *Expr { + return newExpr(c, C.Z3_mk_char_to_bv(c.ptr, ch.ptr)) +} + +// MkCharFromBV converts a bit-vector to a character. +func (c *Context) MkCharFromBV(bv *Expr) *Expr { + return newExpr(c, C.Z3_mk_char_from_bv(c.ptr, bv.ptr)) +} + +// MkCharIsDigit creates a predicate that is true if the character is a decimal digit. +func (c *Context) MkCharIsDigit(ch *Expr) *Expr { + return newExpr(c, C.Z3_mk_char_is_digit(c.ptr, ch.ptr)) +} diff --git a/src/api/go/datatype.go b/src/api/go/datatype.go index f4ae8a4d4..fc5e1c187 100644 --- a/src/api/go/datatype.go +++ b/src/api/go/datatype.go @@ -127,6 +127,41 @@ func (c *Context) MkDatatypeSort(name string, constructors []*Constructor) *Sort return newSort(c, C.Z3_mk_datatype(c.ptr, sym.ptr, C.uint(numCons), &cons[0])) } +// MkPolymorphicDatatypeSort creates a polymorphic datatype sort with explicit type parameters. +// typeParams should be sorts created with MkTypeVariable. +// Self-recursive field sorts should be passed as nil; use the fieldSortRefs parameter in +// MkConstructor to indicate the recursive reference by index. +func (c *Context) MkPolymorphicDatatypeSort(name string, typeParams []*Sort, constructors []*Constructor) *Sort { + sym := c.MkStringSymbol(name) + + numParams := len(typeParams) + numCons := len(constructors) + + var paramPtr *C.Z3_sort + if numParams > 0 { + paramPtrs := make([]C.Z3_sort, numParams) + for i, p := range typeParams { + paramPtrs[i] = p.ptr + } + paramPtr = ¶mPtrs[0] + } + + var consPtr *C.Z3_constructor + if numCons > 0 { + consPtrs := make([]C.Z3_constructor, numCons) + for i, cons := range constructors { + consPtrs[i] = cons.ptr + } + consPtr = &consPtrs[0] + } + + return newSort(c, C.Z3_mk_polymorphic_datatype( + c.ptr, sym.ptr, + C.uint(numParams), paramPtr, + C.uint(numCons), consPtr, + )) +} + // MkDatatypeSorts creates multiple mutually recursive datatype sorts. func (c *Context) MkDatatypeSorts(names []string, constructorLists [][]*Constructor) []*Sort { numTypes := uint(len(names)) diff --git a/src/api/go/fixedpoint.go b/src/api/go/fixedpoint.go index ab28569fc..55db062f4 100644 --- a/src/api/go/fixedpoint.go +++ b/src/api/go/fixedpoint.go @@ -218,6 +218,59 @@ func (f *Fixedpoint) FromFile(filename string) { C.Z3_fixedpoint_from_file(f.ctx.ptr, f.ptr, cstr) } +// QueryFromLvl poses a query against the asserted rules at the given level. +// This is a Spacer-specific function. +func (f *Fixedpoint) QueryFromLvl(query *Expr, lvl uint) Status { + result := C.Z3_fixedpoint_query_from_lvl(f.ctx.ptr, f.ptr, query.ptr, C.uint(lvl)) + switch result { + case C.Z3_L_TRUE: + return Satisfiable + case C.Z3_L_FALSE: + return Unsatisfiable + default: + return Unknown + } +} + +// GetGroundSatAnswer retrieves a bottom-up sequence of ground facts. +// The previous call to Query or QueryFromLvl must have returned Satisfiable. +// This is a Spacer-specific function. +func (f *Fixedpoint) GetGroundSatAnswer() *Expr { + ptr := C.Z3_fixedpoint_get_ground_sat_answer(f.ctx.ptr, f.ptr) + if ptr == nil { + return nil + } + return newExpr(f.ctx, ptr) +} + +// GetRulesAlongTrace returns the list of rules along the counterexample trace. +// This is a Spacer-specific function. +func (f *Fixedpoint) GetRulesAlongTrace() *ASTVector { + return newASTVector(f.ctx, C.Z3_fixedpoint_get_rules_along_trace(f.ctx.ptr, f.ptr)) +} + +// GetRuleNamesAlongTrace returns the list of rule names along the counterexample trace. +// This is a Spacer-specific function. +func (f *Fixedpoint) GetRuleNamesAlongTrace() *Symbol { + return newSymbol(f.ctx, C.Z3_fixedpoint_get_rule_names_along_trace(f.ctx.ptr, f.ptr)) +} + +// AddInvariant adds an assumed invariant for the predicate pred. +// This is a Spacer-specific function. +func (f *Fixedpoint) AddInvariant(pred *FuncDecl, property *Expr) { + C.Z3_fixedpoint_add_invariant(f.ctx.ptr, f.ptr, pred.ptr, property.ptr) +} + +// GetReachable retrieves the reachable states of a predicate. +// This is a Spacer-specific function. +func (f *Fixedpoint) GetReachable(pred *FuncDecl) *Expr { + ptr := C.Z3_fixedpoint_get_reachable(f.ctx.ptr, f.ptr, pred.ptr) + if ptr == nil { + return nil + } + return newExpr(f.ctx, ptr) +} + // Statistics represents statistics for Z3 solvers type Statistics struct { ctx *Context diff --git a/src/api/go/relations.go b/src/api/go/relations.go new file mode 100644 index 000000000..637c6ab5b --- /dev/null +++ b/src/api/go/relations.go @@ -0,0 +1,38 @@ +package z3 + +/* +#include "z3.h" +*/ +import "C" + +// Special relation constructors + +// MkLinearOrder creates a linear (total) order relation over the given sort. +// The id parameter distinguishes multiple linear orders over the same sort. +func (c *Context) MkLinearOrder(s *Sort, id uint) *FuncDecl { + return newFuncDecl(c, C.Z3_mk_linear_order(c.ptr, s.ptr, C.uint(id))) +} + +// MkPartialOrder creates a partial order relation over the given sort. +// The id parameter distinguishes multiple partial orders over the same sort. +func (c *Context) MkPartialOrder(s *Sort, id uint) *FuncDecl { + return newFuncDecl(c, C.Z3_mk_partial_order(c.ptr, s.ptr, C.uint(id))) +} + +// MkPiecewiseLinearOrder creates a piecewise linear order relation over the given sort. +// The id parameter distinguishes multiple piecewise linear orders over the same sort. +func (c *Context) MkPiecewiseLinearOrder(s *Sort, id uint) *FuncDecl { + return newFuncDecl(c, C.Z3_mk_piecewise_linear_order(c.ptr, s.ptr, C.uint(id))) +} + +// MkTreeOrder creates a tree order relation over the given sort. +// The id parameter distinguishes multiple tree orders over the same sort. +func (c *Context) MkTreeOrder(s *Sort, id uint) *FuncDecl { + return newFuncDecl(c, C.Z3_mk_tree_order(c.ptr, s.ptr, C.uint(id))) +} + +// MkTransitiveClosure creates the transitive closure of a binary relation. +// The resulting relation is recursive. +func (c *Context) MkTransitiveClosure(f *FuncDecl) *FuncDecl { + return newFuncDecl(c, C.Z3_mk_transitive_closure(c.ptr, f.ptr)) +} diff --git a/src/api/go/set.go b/src/api/go/set.go new file mode 100644 index 000000000..53b1be672 --- /dev/null +++ b/src/api/go/set.go @@ -0,0 +1,77 @@ +package z3 + +/* +#include "z3.h" +*/ +import "C" + +// Regular (array-encoded) Set operations + +// MkSetSort creates a set sort with the given element sort. +func (c *Context) MkSetSort(elemSort *Sort) *Sort { + return newSort(c, C.Z3_mk_set_sort(c.ptr, elemSort.ptr)) +} + +// MkEmptySet creates an empty set of the given element sort. +func (c *Context) MkEmptySet(elemSort *Sort) *Expr { + return newExpr(c, C.Z3_mk_empty_set(c.ptr, elemSort.ptr)) +} + +// MkFullSet creates the full set (universe) of the given element sort. +func (c *Context) MkFullSet(elemSort *Sort) *Expr { + return newExpr(c, C.Z3_mk_full_set(c.ptr, elemSort.ptr)) +} + +// MkSetAdd adds an element to a set. +func (c *Context) MkSetAdd(set, elem *Expr) *Expr { + return newExpr(c, C.Z3_mk_set_add(c.ptr, set.ptr, elem.ptr)) +} + +// MkSetDel removes an element from a set. +func (c *Context) MkSetDel(set, elem *Expr) *Expr { + return newExpr(c, C.Z3_mk_set_del(c.ptr, set.ptr, elem.ptr)) +} + +// MkSetUnion creates the union of two or more sets. +func (c *Context) MkSetUnion(sets ...*Expr) *Expr { + if len(sets) == 0 { + return nil + } + cSets := make([]C.Z3_ast, len(sets)) + for i, s := range sets { + cSets[i] = s.ptr + } + return newExpr(c, C.Z3_mk_set_union(c.ptr, C.uint(len(sets)), &cSets[0])) +} + +// MkSetIntersect creates the intersection of two or more sets. +func (c *Context) MkSetIntersect(sets ...*Expr) *Expr { + if len(sets) == 0 { + return nil + } + cSets := make([]C.Z3_ast, len(sets)) + for i, s := range sets { + cSets[i] = s.ptr + } + return newExpr(c, C.Z3_mk_set_intersect(c.ptr, C.uint(len(sets)), &cSets[0])) +} + +// MkSetDifference creates the set difference (set1 \ set2). +func (c *Context) MkSetDifference(set1, set2 *Expr) *Expr { + return newExpr(c, C.Z3_mk_set_difference(c.ptr, set1.ptr, set2.ptr)) +} + +// MkSetComplement creates the complement of a set. +func (c *Context) MkSetComplement(set *Expr) *Expr { + return newExpr(c, C.Z3_mk_set_complement(c.ptr, set.ptr)) +} + +// MkSetMember creates a membership predicate: elem ∈ set. +func (c *Context) MkSetMember(elem, set *Expr) *Expr { + return newExpr(c, C.Z3_mk_set_member(c.ptr, elem.ptr, set.ptr)) +} + +// MkSetSubset creates a subset predicate: set1 ⊆ set2. +func (c *Context) MkSetSubset(set1, set2 *Expr) *Expr { + return newExpr(c, C.Z3_mk_set_subset(c.ptr, set1.ptr, set2.ptr)) +} diff --git a/src/api/go/spacer.go b/src/api/go/spacer.go new file mode 100644 index 000000000..6b79c8ce6 --- /dev/null +++ b/src/api/go/spacer.go @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation 2025 +// Z3 Go API: Spacer quantifier elimination and model projection functions + +package z3 + +/* +#include "z3.h" +#include +*/ +import "C" +import "runtime" + +// ASTMap represents a mapping from Z3 ASTs to Z3 ASTs. +type ASTMap struct { + ctx *Context + ptr C.Z3_ast_map +} + +// newASTMap creates a new ASTMap and manages its reference count. +func newASTMap(ctx *Context, ptr C.Z3_ast_map) *ASTMap { + m := &ASTMap{ctx: ctx, ptr: ptr} + C.Z3_ast_map_inc_ref(ctx.ptr, ptr) + runtime.SetFinalizer(m, func(am *ASTMap) { + C.Z3_ast_map_dec_ref(am.ctx.ptr, am.ptr) + }) + return m +} + +// MkASTMap creates a new empty AST map. +func (c *Context) MkASTMap() *ASTMap { + return newASTMap(c, C.Z3_mk_ast_map(c.ptr)) +} + +// Contains returns true if the map contains the key k. +func (m *ASTMap) Contains(k *Expr) bool { + return bool(C.Z3_ast_map_contains(m.ctx.ptr, m.ptr, k.ptr)) +} + +// Find returns the value associated with key k. +func (m *ASTMap) Find(k *Expr) *Expr { + return newExpr(m.ctx, C.Z3_ast_map_find(m.ctx.ptr, m.ptr, k.ptr)) +} + +// Insert associates key k with value v in the map. +func (m *ASTMap) Insert(k, v *Expr) { + C.Z3_ast_map_insert(m.ctx.ptr, m.ptr, k.ptr, v.ptr) +} + +// Erase removes the entry with key k from the map. +func (m *ASTMap) Erase(k *Expr) { + C.Z3_ast_map_erase(m.ctx.ptr, m.ptr, k.ptr) +} + +// Reset removes all entries from the map. +func (m *ASTMap) Reset() { + C.Z3_ast_map_reset(m.ctx.ptr, m.ptr) +} + +// Size returns the number of entries in the map. +func (m *ASTMap) Size() uint { + return uint(C.Z3_ast_map_size(m.ctx.ptr, m.ptr)) +} + +// Keys returns all keys in the map as an ASTVector. +func (m *ASTMap) Keys() *ASTVector { + return newASTVector(m.ctx, C.Z3_ast_map_keys(m.ctx.ptr, m.ptr)) +} + +// String returns the string representation of the map. +func (m *ASTMap) String() string { + return C.GoString(C.Z3_ast_map_to_string(m.ctx.ptr, m.ptr)) +} + +// ModelExtrapolate extrapolates a model of a formula. +// Given a model m and formula fml, returns an expression that is implied by fml +// and is consistent with the model. This is a Spacer-specific function. +func (c *Context) ModelExtrapolate(m *Model, fml *Expr) *Expr { + return newExpr(c, C.Z3_model_extrapolate(c.ptr, m.ptr, fml.ptr)) +} + +// QeLite performs best-effort quantifier elimination. +// vars is a vector of variables to eliminate, body is the formula. +func (c *Context) QeLite(vars *ASTVector, body *Expr) *Expr { + return newExpr(c, C.Z3_qe_lite(c.ptr, vars.ptr, body.ptr)) +} + +// QeModelProject projects variables given a model. +// bound is a slice of application expressions representing the variables to project. +func (c *Context) QeModelProject(m *Model, bound []*Expr, body *Expr) *Expr { + n := len(bound) + cBound := make([]C.Z3_app, n) + for i, b := range bound { + cBound[i] = C.Z3_to_app(c.ptr, b.ptr) + } + var boundPtr *C.Z3_app + if n > 0 { + boundPtr = &cBound[0] + } + return newExpr(c, C.Z3_qe_model_project(c.ptr, m.ptr, C.uint(n), boundPtr, body.ptr)) +} + +// QeModelProjectSkolem projects variables given a model, storing the skolem witnesses in map_. +// bound is a slice of application expressions representing the variables to project. +func (c *Context) QeModelProjectSkolem(m *Model, bound []*Expr, body *Expr, map_ *ASTMap) *Expr { + n := len(bound) + cBound := make([]C.Z3_app, n) + for i, b := range bound { + cBound[i] = C.Z3_to_app(c.ptr, b.ptr) + } + var boundPtr *C.Z3_app + if n > 0 { + boundPtr = &cBound[0] + } + return newExpr(c, C.Z3_qe_model_project_skolem(c.ptr, m.ptr, C.uint(n), boundPtr, body.ptr, map_.ptr)) +} + +// QeModelProjectWithWitness projects variables given a model and extracts witnesses. +// The map_ is populated with bindings of projected variables to witness terms. +// bound is a slice of application expressions representing the variables to project. +func (c *Context) QeModelProjectWithWitness(m *Model, bound []*Expr, body *Expr, map_ *ASTMap) *Expr { + n := len(bound) + cBound := make([]C.Z3_app, n) + for i, b := range bound { + cBound[i] = C.Z3_to_app(c.ptr, b.ptr) + } + var boundPtr *C.Z3_app + if n > 0 { + boundPtr = &cBound[0] + } + return newExpr(c, C.Z3_qe_model_project_with_witness(c.ptr, m.ptr, C.uint(n), boundPtr, body.ptr, map_.ptr)) +} diff --git a/src/api/go/tactic.go b/src/api/go/tactic.go index 8961c2df8..e3b37622d 100644 --- a/src/api/go/tactic.go +++ b/src/api/go/tactic.go @@ -228,6 +228,17 @@ func (g *Goal) String() string { return C.GoString(C.Z3_goal_to_string(g.ctx.ptr, g.ptr)) } +// IsInconsistent returns true if the goal contains the formula false. +func (g *Goal) IsInconsistent() bool { + return bool(C.Z3_goal_inconsistent(g.ctx.ptr, g.ptr)) +} + +// ToDimacsString converts the goal to a string in DIMACS format. +// If includeNames is true, formula names are included as comments. +func (g *Goal) ToDimacsString(includeNames bool) string { + return C.GoString(C.Z3_goal_to_dimacs_string(g.ctx.ptr, g.ptr, C.bool(includeNames))) +} + // ApplyResult represents the result of applying a tactic to a goal. type ApplyResult struct { ctx *Context diff --git a/src/api/go/z3.go b/src/api/go/z3.go index 0d1322ea8..4e982111e 100644 --- a/src/api/go/z3.go +++ b/src/api/go/z3.go @@ -240,6 +240,45 @@ func newExpr(ctx *Context, ptr C.Z3_ast) *Expr { return expr } +// intsToCs converts a []int slice to []C.int, returning the slice and +// a pointer to its first element (nil if empty). +func intsToCs(ints []int) ([]C.int, *C.int) { + if len(ints) == 0 { + return nil, nil + } + cInts := make([]C.int, len(ints)) + for i, v := range ints { + cInts[i] = C.int(v) + } + return cInts, &cInts[0] +} + +// exprsToASTs converts a []*Expr slice to []C.Z3_ast, returning the slice and +// a pointer to its first element (nil if empty). +func exprsToASTs(exprs []*Expr) ([]C.Z3_ast, *C.Z3_ast) { + if len(exprs) == 0 { + return nil, nil + } + cExprs := make([]C.Z3_ast, len(exprs)) + for i, e := range exprs { + cExprs[i] = e.ptr + } + return cExprs, &cExprs[0] +} + +// sortsToCSorts converts a []*Sort slice to []C.Z3_sort, returning the slice and +// a pointer to its first element (nil if empty). +func sortsToCSorts(sorts []*Sort) ([]C.Z3_sort, *C.Z3_sort) { + if len(sorts) == 0 { + return nil, nil + } + cSorts := make([]C.Z3_sort, len(sorts)) + for i, s := range sorts { + cSorts[i] = s.ptr + } + return cSorts, &cSorts[0] +} + // String returns the string representation of the expression. func (e *Expr) String() string { return C.GoString(C.Z3_ast_to_string(e.ctx.ptr, e.ptr)) @@ -291,6 +330,21 @@ func newASTVector(ctx *Context, ptr C.Z3_ast_vector) *ASTVector { return v } +// Size returns the number of ASTs in the vector. +func (v *ASTVector) Size() uint { + return uint(C.Z3_ast_vector_size(v.ctx.ptr, v.ptr)) +} + +// Get returns the i-th AST in the vector. +func (v *ASTVector) Get(i uint) *Expr { + return newExpr(v.ctx, C.Z3_ast_vector_get(v.ctx.ptr, v.ptr, C.uint(i))) +} + +// String returns the string representation of the AST vector. +func (v *ASTVector) String() string { + return C.GoString(C.Z3_ast_vector_to_string(v.ctx.ptr, v.ptr)) +} + // ParamDescrs represents parameter descriptions for Z3 objects. type ParamDescrs struct { ctx *Context @@ -353,11 +407,8 @@ func (c *Context) MkAnd(exprs ...*Expr) *Expr { if len(exprs) == 1 { return exprs[0] } - cExprs := make([]C.Z3_ast, len(exprs)) - for i, e := range exprs { - cExprs[i] = e.ptr - } - return newExpr(c, C.Z3_mk_and(c.ptr, C.uint(len(exprs)), &cExprs[0])) + _, cExprsPtr := exprsToASTs(exprs) + return newExpr(c, C.Z3_mk_and(c.ptr, C.uint(len(exprs)), cExprsPtr)) } // MkOr creates a disjunction. @@ -368,11 +419,8 @@ func (c *Context) MkOr(exprs ...*Expr) *Expr { if len(exprs) == 1 { return exprs[0] } - cExprs := make([]C.Z3_ast, len(exprs)) - for i, e := range exprs { - cExprs[i] = e.ptr - } - return newExpr(c, C.Z3_mk_or(c.ptr, C.uint(len(exprs)), &cExprs[0])) + _, cExprsPtr := exprsToASTs(exprs) + return newExpr(c, C.Z3_mk_or(c.ptr, C.uint(len(exprs)), cExprsPtr)) } // MkNot creates a negation. @@ -407,11 +455,52 @@ func (c *Context) MkDistinct(exprs ...*Expr) *Expr { if len(exprs) <= 1 { return c.MkTrue() } - cExprs := make([]C.Z3_ast, len(exprs)) - for i, e := range exprs { - cExprs[i] = e.ptr + _, cExprsPtr := exprsToASTs(exprs) + return newExpr(c, C.Z3_mk_distinct(c.ptr, C.uint(len(exprs)), cExprsPtr)) +} + +// Pseudo-Boolean / cardinality constraints + +// MkAtMost encodes p1 + p2 + ... + pn <= k. +func (c *Context) MkAtMost(args []*Expr, k uint) *Expr { + _, cArgsPtr := exprsToASTs(args) + return newExpr(c, C.Z3_mk_atmost(c.ptr, C.uint(len(args)), cArgsPtr, C.uint(k))) +} + +// MkAtLeast encodes p1 + p2 + ... + pn >= k. +func (c *Context) MkAtLeast(args []*Expr, k uint) *Expr { + _, cArgsPtr := exprsToASTs(args) + return newExpr(c, C.Z3_mk_atleast(c.ptr, C.uint(len(args)), cArgsPtr, C.uint(k))) +} + +// MkPBLe encodes k1*p1 + k2*p2 + ... + kn*pn <= k. +func (c *Context) MkPBLe(args []*Expr, coeffs []int, k int) *Expr { + if len(args) != len(coeffs) { + panic("MkPBLe: args and coeffs must have the same length") } - return newExpr(c, C.Z3_mk_distinct(c.ptr, C.uint(len(exprs)), &cExprs[0])) + _, cArgsPtr := exprsToASTs(args) + _, cCoeffsPtr := intsToCs(coeffs) + return newExpr(c, C.Z3_mk_pble(c.ptr, C.uint(len(args)), cArgsPtr, cCoeffsPtr, C.int(k))) +} + +// MkPBGe encodes k1*p1 + k2*p2 + ... + kn*pn >= k. +func (c *Context) MkPBGe(args []*Expr, coeffs []int, k int) *Expr { + if len(args) != len(coeffs) { + panic("MkPBGe: args and coeffs must have the same length") + } + _, cArgsPtr := exprsToASTs(args) + _, cCoeffsPtr := intsToCs(coeffs) + return newExpr(c, C.Z3_mk_pbge(c.ptr, C.uint(len(args)), cArgsPtr, cCoeffsPtr, C.int(k))) +} + +// MkPBEq encodes k1*p1 + k2*p2 + ... + kn*pn = k. +func (c *Context) MkPBEq(args []*Expr, coeffs []int, k int) *Expr { + if len(args) != len(coeffs) { + panic("MkPBEq: args and coeffs must have the same length") + } + _, cArgsPtr := exprsToASTs(args) + _, cCoeffsPtr := intsToCs(coeffs) + return newExpr(c, C.Z3_mk_pbeq(c.ptr, C.uint(len(args)), cArgsPtr, cCoeffsPtr, C.int(k))) } // FuncDecl represents a function declaration. @@ -460,54 +549,26 @@ func (f *FuncDecl) GetRange() *Sort { // MkFuncDecl creates a function declaration. func (c *Context) MkFuncDecl(name *Symbol, domain []*Sort, range_ *Sort) *FuncDecl { - cDomain := make([]C.Z3_sort, len(domain)) - for i, s := range domain { - cDomain[i] = s.ptr - } - var domainPtr *C.Z3_sort - if len(domain) > 0 { - domainPtr = &cDomain[0] - } + _, domainPtr := sortsToCSorts(domain) return newFuncDecl(c, C.Z3_mk_func_decl(c.ptr, name.ptr, C.uint(len(domain)), domainPtr, range_.ptr)) } // MkRecFuncDecl creates a recursive function declaration. // After creating, use AddRecDef to provide the function body. func (c *Context) MkRecFuncDecl(name *Symbol, domain []*Sort, range_ *Sort) *FuncDecl { - cDomain := make([]C.Z3_sort, len(domain)) - for i, s := range domain { - cDomain[i] = s.ptr - } - var domainPtr *C.Z3_sort - if len(domain) > 0 { - domainPtr = &cDomain[0] - } + _, domainPtr := sortsToCSorts(domain) return newFuncDecl(c, C.Z3_mk_rec_func_decl(c.ptr, name.ptr, C.uint(len(domain)), domainPtr, range_.ptr)) } // AddRecDef adds the definition (body) for a recursive function created with MkRecFuncDecl. func (c *Context) AddRecDef(f *FuncDecl, args []*Expr, body *Expr) { - cArgs := make([]C.Z3_ast, len(args)) - for i, a := range args { - cArgs[i] = a.ptr - } - var argsPtr *C.Z3_ast - if len(args) > 0 { - argsPtr = &cArgs[0] - } + _, argsPtr := exprsToASTs(args) C.Z3_add_rec_def(c.ptr, f.ptr, C.uint(len(args)), argsPtr, body.ptr) } // MkApp creates a function application. func (c *Context) MkApp(decl *FuncDecl, args ...*Expr) *Expr { - cArgs := make([]C.Z3_ast, len(args)) - for i, a := range args { - cArgs[i] = a.ptr - } - var argsPtr *C.Z3_ast - if len(args) > 0 { - argsPtr = &cArgs[0] - } + _, argsPtr := exprsToASTs(args) return newExpr(c, C.Z3_mk_app(c.ptr, decl.ptr, C.uint(len(args)), argsPtr)) } @@ -546,6 +607,66 @@ func (e *Expr) Simplify() *Expr { return newExpr(e.ctx, C.Z3_simplify(e.ctx.ptr, e.ptr)) } +// GetDecl returns the function declaration of an application expression. +func (e *Expr) GetDecl() *FuncDecl { + return newFuncDecl(e.ctx, C.Z3_get_app_decl(e.ctx.ptr, C.Z3_to_app(e.ctx.ptr, e.ptr))) +} + +// NumArgs returns the number of arguments of an application expression. +func (e *Expr) NumArgs() uint { + return uint(C.Z3_get_app_num_args(e.ctx.ptr, C.Z3_to_app(e.ctx.ptr, e.ptr))) +} + +// Arg returns the i-th argument of an application expression. +func (e *Expr) Arg(i uint) *Expr { + return newExpr(e.ctx, C.Z3_get_app_arg(e.ctx.ptr, C.Z3_to_app(e.ctx.ptr, e.ptr), C.uint(i))) +} + +// Substitute replaces every occurrence of from[i] in the expression with to[i]. +// The from and to slices must have the same length. +func (e *Expr) Substitute(from, to []*Expr) *Expr { + n := len(from) + cFrom := make([]C.Z3_ast, n) + cTo := make([]C.Z3_ast, n) + for i := range from { + cFrom[i] = from[i].ptr + cTo[i] = to[i].ptr + } + var fromPtr, toPtr *C.Z3_ast + if n > 0 { + fromPtr = &cFrom[0] + toPtr = &cTo[0] + } + return newExpr(e.ctx, C.Z3_substitute(e.ctx.ptr, e.ptr, C.uint(n), fromPtr, toPtr)) +} + +// SubstituteVars replaces free variables in the expression with the expressions in to. +// Variable with de-Bruijn index i is replaced with to[i]. +func (e *Expr) SubstituteVars(to []*Expr) *Expr { + _, toPtr := exprsToASTs(to) + return newExpr(e.ctx, C.Z3_substitute_vars(e.ctx.ptr, e.ptr, C.uint(len(to)), toPtr)) +} + +// SubstituteFuns replaces every occurrence of from[i] applied to arguments +// with to[i] in the expression. +// The from and to slices must have the same length. +func (e *Expr) SubstituteFuns(from []*FuncDecl, to []*Expr) *Expr { + n := len(from) + cFrom := make([]C.Z3_func_decl, n) + cTo := make([]C.Z3_ast, n) + for i := range from { + cFrom[i] = from[i].ptr + cTo[i] = to[i].ptr + } + var fromPtr *C.Z3_func_decl + var toPtr *C.Z3_ast + if n > 0 { + fromPtr = &cFrom[0] + toPtr = &cTo[0] + } + return newExpr(e.ctx, C.Z3_substitute_funs(e.ctx.ptr, e.ptr, C.uint(n), fromPtr, toPtr)) +} + // MkTypeVariable creates a type variable sort for use in polymorphic functions and datatypes func (c *Context) MkTypeVariable(name *Symbol) *Sort { return newSort(c, C.Z3_mk_type_variable(c.ptr, name.ptr)) @@ -639,12 +760,7 @@ func (q *Quantifier) String() string { // MkQuantifier creates a quantifier with patterns func (c *Context) MkQuantifier(isForall bool, weight int, sorts []*Sort, names []*Symbol, body *Expr, patterns []*Pattern) *Quantifier { - var forallInt C.bool - if isForall { - forallInt = true - } else { - forallInt = false - } + forallInt := C.bool(isForall) numBound := len(sorts) if numBound != len(names) { @@ -687,12 +803,7 @@ func (c *Context) MkQuantifier(isForall bool, weight int, sorts []*Sort, names [ // MkQuantifierConst creates a quantifier using constant bound variables func (c *Context) MkQuantifierConst(isForall bool, weight int, bound []*Expr, body *Expr, patterns []*Pattern) *Quantifier { - var forallInt C.bool - if isForall { - forallInt = true - } else { - forallInt = false - } + forallInt := C.bool(isForall) numBound := len(bound) var cBound []C.Z3_app @@ -815,6 +926,33 @@ func (c *Context) MkLambdaConst(bound []*Expr, body *Expr) *Lambda { return newLambda(c, ptr) } +// SetGlobalParam sets a global Z3 parameter. +func SetGlobalParam(id, value string) { + cID := C.CString(id) + cValue := C.CString(value) + defer C.free(unsafe.Pointer(cID)) + defer C.free(unsafe.Pointer(cValue)) + C.Z3_global_param_set(cID, cValue) +} + +// GetGlobalParam retrieves the value of a global Z3 parameter. +// Returns the value and true if the parameter exists, or empty string and false otherwise. +func GetGlobalParam(id string) (string, bool) { + cID := C.CString(id) + defer C.free(unsafe.Pointer(cID)) + var cValue C.Z3_string + ok := C.Z3_global_param_get(cID, &cValue) + if ok == C.bool(false) { + return "", false + } + return C.GoString(cValue), true +} + +// ResetAllGlobalParams resets all global Z3 parameters to their default values. +func ResetAllGlobalParams() { + C.Z3_global_param_reset_all() +} + // astVectorToExprs converts a Z3_ast_vector to a slice of Expr. // This function properly manages the reference count of the vector by // incrementing it on entry and decrementing it on exit. diff --git a/src/api/java/Context.java b/src/api/java/Context.java index 22887729c..d02f4f287 100644 --- a/src/api/java/Context.java +++ b/src/api/java/Context.java @@ -4616,6 +4616,38 @@ public class Context implements AutoCloseable { ); } + /** + * Creates a piecewise linear order. + * @param index The index of the order. + * @param sort The sort of the order. + */ + public final FuncDecl mkPiecewiseLinearOrder(R sort, int index) { + return (FuncDecl) FuncDecl.create( + this, + Native.mkPiecewiseLinearOrder( + nCtx(), + sort.getNativeObject(), + index + ) + ); + } + + /** + * Creates a tree order. + * @param index The index of the order. + * @param sort The sort of the order. + */ + public final FuncDecl mkTreeOrder(R sort, int index) { + return (FuncDecl) FuncDecl.create( + this, + Native.mkTreeOrder( + nCtx(), + sort.getNativeObject(), + index + ) + ); + } + /** * Return the nonzero subresultants of p and q with respect to the "variable" x. * Note that any subterm that cannot be viewed as a polynomial is assumed to be a variable. diff --git a/src/api/js/PUBLISHED_README.md b/src/api/js/PUBLISHED_README.md index 42d58a916..85987ba7a 100644 --- a/src/api/js/PUBLISHED_README.md +++ b/src/api/js/PUBLISHED_README.md @@ -16,6 +16,17 @@ const { This package has different initialization for browser and node. Your bundler and node should choose good version automatically, but you can import the one you need manually - `const { init } = require('z3-solver/node');` or `const { init } = require('z3-solver/browser');`. +The `init` function also accepts an optional Emscripten module overrides object. This is useful in runtimes such as Deno where you may want to provide a wasm load path explicitly instead of relying on filesystem reads. In Deno 2.1+, `import.meta.resolve(...)` returns a string synchronously, so it can be used directly in `locateFile`. For example: + +```typescript +import { init } from 'npm:z3-solver'; + +const api = await init({ + locateFile: (file, _prefix): string => + import.meta.resolve(`npm:z3-solver/build/${file}`), // _prefix is unused here +}); +``` + ### Limitations The package requires threads, which means you'll need to be running in an environment which supports `SharedArrayBuffer`. In browsers, in addition to ensuring the browser has implemented `SharedArrayBuffer`, you'll need to serve your page with [special headers](https://web.dev/coop-coep/). There's a [neat trick](https://github.com/gzuidhof/coi-serviceworker) for doing that client-side on e.g. Github Pages, though you shouldn't use that trick in more complex applications. diff --git a/src/api/js/package-lock.json b/src/api/js/package-lock.json index a26cb3e27..9917a912c 100644 --- a/src/api/js/package-lock.json +++ b/src/api/js/package-lock.json @@ -5842,10 +5842,11 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, diff --git a/src/api/js/scripts/make-ts-wrapper.ts b/src/api/js/scripts/make-ts-wrapper.ts index d19307243..560ad292e 100644 --- a/src/api/js/scripts/make-ts-wrapper.ts +++ b/src/api/js/scripts/make-ts-wrapper.ts @@ -444,8 +444,8 @@ ${Object.entries(primitiveTypes) .map(e => `type ${e[0]} = ${e[1]};`) .join('\n')} -export async function init(initModule: any) { - let Mod = await initModule(); +export async function init(initModule: any, moduleOverrides: Record = {}) { + let Mod = await initModule(moduleOverrides); // this works for both signed and unsigned, because JS will wrap for you when constructing the Uint32Array function intArrayToByteArr(ints: number[]) { @@ -461,13 +461,13 @@ export async function init(initModule: any) { } let outAddress = Mod._malloc(${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS}); - let outUintArray = (new Uint32Array(Mod.HEAPU32.buffer, outAddress, 4)); + let outUintArray = (new Uint32Array(Mod.HEAPU32.buffer, outAddress, ${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS / 4})); let getOutUint = (i: ${getValidOutArrayIndexes(4)}) => outUintArray[i]; - let outIntArray = (new Int32Array(Mod.HEAPU32.buffer, outAddress, 4)); + let outIntArray = (new Int32Array(Mod.HEAPU32.buffer, outAddress, ${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS / 4})); let getOutInt = (i: ${getValidOutArrayIndexes(4)}) => outIntArray[i]; - let outUint64Array = (new BigUint64Array(Mod.HEAPU32.buffer, outAddress, 2)); + let outUint64Array = (new BigUint64Array(Mod.HEAPU32.buffer, outAddress, ${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS / 8})); let getOutUint64 = (i: ${getValidOutArrayIndexes(8)}) => outUint64Array[i]; - let outInt64Array = (new BigInt64Array(Mod.HEAPU32.buffer, outAddress, 2)); + let outInt64Array = (new BigInt64Array(Mod.HEAPU32.buffer, outAddress, ${BYTES_TO_ALLOCATE_FOR_OUT_PARAMS / 8})); let getOutInt64 = (i: ${getValidOutArrayIndexes(8)}) => outInt64Array[i]; return { diff --git a/src/api/js/src/browser.test.ts b/src/api/js/src/browser.test.ts new file mode 100644 index 000000000..827f7281d --- /dev/null +++ b/src/api/js/src/browser.test.ts @@ -0,0 +1,43 @@ +const mockInitWrapper = jest.fn(); +const mockCreateApi = jest.fn(); + +jest.mock('./low-level', () => ({ + init: mockInitWrapper, + Z3Core: undefined, + Z3LowLevel: undefined, +})); +jest.mock('./high-level', () => ({ + createApi: mockCreateApi, +})); + +import { init } from './browser'; + +describe('browser init', () => { + beforeEach(() => { + delete (global as any).initZ3; + mockInitWrapper.mockReset(); + mockCreateApi.mockReset(); + }); + + it('passes module overrides to the browser initializer', async () => { + const initZ3 = jest.fn(); + const locateFile = jest.fn((file: string) => `https://example.test/${file}`); + const lowLevel = { Z3: { low: true }, em: { module: true } }; + const highLevel = { Context: jest.fn() }; + (global as any).initZ3 = initZ3; + mockInitWrapper.mockResolvedValue(lowLevel); + mockCreateApi.mockReturnValue(highLevel); + + const api = await init({ locateFile }); + + expect(mockInitWrapper).toHaveBeenCalledWith(initZ3, { locateFile }); + expect(mockCreateApi).toHaveBeenCalledWith(lowLevel.Z3, lowLevel.em); + expect(api).toEqual({ ...lowLevel, ...highLevel }); + }); + + it('throws when initZ3 is unavailable', async () => { + await expect(init()).rejects.toThrow( + 'initZ3 was not imported correctly. Please consult documentation on how to load Z3 in browser', + ); + }); +}); diff --git a/src/api/js/src/browser.ts b/src/api/js/src/browser.ts index 1a6e41f39..83acec7f6 100644 --- a/src/api/js/src/browser.ts +++ b/src/api/js/src/browser.ts @@ -1,16 +1,16 @@ import { createApi, Z3HighLevel } from './high-level'; -import { init as initWrapper, Z3LowLevel } from './low-level'; +import { init as initWrapper, Z3LowLevel, Z3ModuleOverrides } from './low-level'; export * from './high-level/types'; export { Z3Core, Z3LowLevel } from './low-level'; export * from './low-level/types.__GENERATED__'; -export async function init(): Promise { +export async function init(moduleOverrides: Z3ModuleOverrides = {}): Promise { const initZ3 = (global as any).initZ3; if (initZ3 === undefined) { throw new Error('initZ3 was not imported correctly. Please consult documentation on how to load Z3 in browser'); } - const lowLevel = await initWrapper(initZ3); + const lowLevel = await initWrapper(initZ3, moduleOverrides); const highLevel = createApi(lowLevel.Z3, lowLevel.em); return { ...lowLevel, ...highLevel }; } diff --git a/src/api/js/src/high-level/high-level.ts b/src/api/js/src/high-level/high-level.ts index 71f156557..f336fe30b 100644 --- a/src/api/js/src/high-level/high-level.ts +++ b/src/api/js/src/high-level/high-level.ts @@ -1172,9 +1172,16 @@ export function createApi(Z3: Z3Core, em?: any): Z3HighLevel { createDatatypes(...datatypes: DatatypeImpl[]): DatatypeSortImpl[] { return createDatatypes(...datatypes); }, + createPolymorphicDatatype(typeParams: Sort[], datatype: DatatypeImpl): DatatypeSortImpl { + return createPolymorphicDatatype(typeParams, datatype); + }, }, ); + function TypeVariable(name: string): Sort { + return new SortImpl(check(Z3.mk_type_variable(contextPtr, Z3.mk_string_symbol(contextPtr, name)))); + } + //////////////// // Operations // //////////////// @@ -1950,10 +1957,46 @@ export function createApi(Z3: Z3Core, em?: any): Z3HighLevel { return new FuncDeclImpl(check(Z3.mk_partial_order(contextPtr, sort.ptr, index))); } + function mkLinearOrder(sort: Sort, index: number): FuncDecl { + return new FuncDeclImpl(check(Z3.mk_linear_order(contextPtr, sort.ptr, index))); + } + + function mkPiecewiseLinearOrder(sort: Sort, index: number): FuncDecl { + return new FuncDeclImpl(check(Z3.mk_piecewise_linear_order(contextPtr, sort.ptr, index))); + } + + function mkTreeOrder(sort: Sort, index: number): FuncDecl { + return new FuncDeclImpl(check(Z3.mk_tree_order(contextPtr, sort.ptr, index))); + } + function mkTransitiveClosure(f: FuncDecl): FuncDecl { return new FuncDeclImpl(check(Z3.mk_transitive_closure(contextPtr, f.ptr))); } + function mkChar(ch: number): Expr { + return new ExprImpl(check(Z3.mk_char(contextPtr, ch))); + } + + function mkCharLe(ch1: Expr, ch2: Expr): Bool { + return new BoolImpl(check(Z3.mk_char_le(contextPtr, ch1.ast, ch2.ast))); + } + + function mkCharToInt(ch: Expr): Arith { + return new ArithImpl(check(Z3.mk_char_to_int(contextPtr, ch.ast))); + } + + function mkCharToBV(ch: Expr): Expr { + return new ExprImpl(check(Z3.mk_char_to_bv(contextPtr, ch.ast))); + } + + function mkCharFromBV(bv: Expr): Expr { + return new ExprImpl(check(Z3.mk_char_from_bv(contextPtr, bv.ast))); + } + + function mkCharIsDigit(ch: Expr): Bool { + return new BoolImpl(check(Z3.mk_char_is_digit(contextPtr, ch.ast))); + } + async function polynomialSubresultants( p: Arith, q: Arith, @@ -4689,6 +4732,10 @@ export function createApi(Z3: Z3Core, em?: any): Z3HighLevel { const datatypes = createDatatypes(this); return datatypes[0]; } + + createPolymorphic(typeParams: Sort[]): DatatypeSort { + return createPolymorphicDatatype(typeParams, this); + } } class DatatypeSortImpl extends SortImpl implements DatatypeSort { @@ -4845,6 +4892,84 @@ export function createApi(Z3: Z3Core, em?: any): Z3HighLevel { } } + function createPolymorphicDatatype(typeParams: Sort[], datatype: DatatypeImpl): DatatypeSortImpl { + if (!(datatype instanceof DatatypeImpl)) { + throw new Error('Datatype instance expected'); + } + + const constructors: Z3_constructor[] = []; + + try { + for (const [constructorName, fields] of datatype.constructors) { + const fieldNames: string[] = []; + const fieldSorts: Z3_sort[] = []; + const fieldRefs: number[] = []; + + for (const [fieldName, fieldSort] of fields) { + fieldNames.push(fieldName); + + if (fieldSort instanceof DatatypeImpl) { + // Self-recursive reference + if (fieldSort !== datatype) { + throw new Error( + `Referenced datatype "${fieldSort.name}" is not the polymorphic datatype being created; mutual recursion is not supported in createPolymorphicDatatype`, + ); + } + fieldSorts.push(null as any); + fieldRefs.push(0); + } else { + fieldSorts.push((fieldSort as Sort).ptr); + fieldRefs.push(0); + } + } + + const constructor = Z3.mk_constructor( + contextPtr, + Z3.mk_string_symbol(contextPtr, constructorName), + Z3.mk_string_symbol(contextPtr, `is_${constructorName}`), + fieldNames.map(name => Z3.mk_string_symbol(contextPtr, name)), + fieldSorts, + fieldRefs, + ); + constructors.push(constructor); + } + + const nameSymbol = Z3.mk_string_symbol(contextPtr, datatype.name); + const paramPtrs = typeParams.map(p => p.ptr); + const resultSort = Z3.mk_polymorphic_datatype(contextPtr, nameSymbol, paramPtrs, constructors); + + const sortImpl = new DatatypeSortImpl(resultSort); + + // Attach constructor, recognizer, and accessor functions dynamically + const numConstructors = sortImpl.numConstructors(); + for (let j = 0; j < numConstructors; j++) { + const constructor = sortImpl.constructorDecl(j); + const recognizer = sortImpl.recognizer(j); + const constructorName = constructor.name().toString(); + + if (constructor.arity() === 0) { + (sortImpl as any)[constructorName] = constructor.call(); + } else { + (sortImpl as any)[constructorName] = constructor; + } + + (sortImpl as any)[`is_${constructorName}`] = recognizer; + + for (let k = 0; k < constructor.arity(); k++) { + const accessor = sortImpl.accessor(j, k); + const accessorName = accessor.name().toString(); + (sortImpl as any)[accessorName] = accessor; + } + } + + return sortImpl; + } finally { + for (const constructor of constructors) { + Z3.del_constructor(contextPtr, constructor); + } + } + } + class QuantifierImpl< QVarSorts extends NonEmptySortArray, QSort extends BoolSort | SMTArraySort, @@ -5292,6 +5417,7 @@ export function createApi(Z3: Z3Core, em?: any): Z3HighLevel { Set, FiniteSet, Datatype, + TypeVariable, //////////////// // Operations // @@ -5400,7 +5526,16 @@ export function createApi(Z3: Z3Core, em?: any): Z3HighLevel { Full, mkPartialOrder, + mkLinearOrder, + mkPiecewiseLinearOrder, + mkTreeOrder, mkTransitiveClosure, + mkChar, + mkCharLe, + mkCharToInt, + mkCharToBV, + mkCharFromBV, + mkCharIsDigit, polynomialSubresultants, }; cleanup.register(ctx, () => Z3.del_context(contextPtr)); diff --git a/src/api/js/src/high-level/types.ts b/src/api/js/src/high-level/types.ts index db28c8d16..6eef81044 100644 --- a/src/api/js/src/high-level/types.ts +++ b/src/api/js/src/high-level/types.ts @@ -479,6 +479,12 @@ export interface Context { /** @category Expressions */ readonly Datatype: DatatypeCreation; + /** + * Create a type variable sort for use as a parameter in polymorphic datatypes. + * @category Sorts + */ + TypeVariable(name: string): Sort; + //////////////// // Operations // //////////////// @@ -927,6 +933,30 @@ export interface Context { */ mkPartialOrder(sort: Sort, index: number): FuncDecl; + /** + * Create a linear (total) order relation over a sort. + * @param sort The sort of the relation + * @param index The index of the relation + * @category Operations + */ + mkLinearOrder(sort: Sort, index: number): FuncDecl; + + /** + * Create a piecewise linear order relation over a sort. + * @param sort The sort of the relation + * @param index The index of the relation + * @category Operations + */ + mkPiecewiseLinearOrder(sort: Sort, index: number): FuncDecl; + + /** + * Create a tree order relation over a sort. + * @param sort The sort of the relation + * @param index The index of the relation + * @category Operations + */ + mkTreeOrder(sort: Sort, index: number): FuncDecl; + /** * Create the transitive closure of a binary relation. * The resulting relation is recursive. @@ -935,6 +965,49 @@ export interface Context { */ mkTransitiveClosure(f: FuncDecl): FuncDecl; + /** + * Create a character literal from a Unicode code point. + * @param ch The Unicode code point + * @category Characters + */ + mkChar(ch: number): Expr; + + /** + * Create a character less-than-or-equal predicate (ch1 ≤ ch2). + * @param ch1 First character + * @param ch2 Second character + * @category Characters + */ + mkCharLe(ch1: Expr, ch2: Expr): Bool; + + /** + * Convert a character to its integer (Unicode code point) value. + * @param ch The character expression + * @category Characters + */ + mkCharToInt(ch: Expr): Arith; + + /** + * Convert a character to a bit-vector. + * @param ch The character expression + * @category Characters + */ + mkCharToBV(ch: Expr): Expr; + + /** + * Convert a bit-vector to a character. + * @param bv The bit-vector expression + * @category Characters + */ + mkCharFromBV(bv: Expr): Expr; + + /** + * Create a predicate that is true if the character is a decimal digit. + * @param ch The character expression + * @category Characters + */ + mkCharIsDigit(ch: Expr): Bool; + /** * Return the nonzero subresultants of p and q with respect to the "variable" x. * Note that any subterm that cannot be viewed as a polynomial is assumed to be a variable. @@ -3136,6 +3209,15 @@ export interface Datatype { * For mutually recursive datatypes, use Context.createDatatypes instead. */ create(): DatatypeSort; + + /** + * Create a polymorphic datatype sort with explicit type parameters. + * Type parameters should be sorts created with Context.TypeVariable. + * Self-recursive fields may reference this Datatype object directly. + * + * @param typeParams Array of type variable sorts + */ + createPolymorphic(typeParams: AnySort[]): DatatypeSort; } /** @@ -3154,6 +3236,17 @@ export interface DatatypeCreation { * @returns Array of created DatatypeSort instances */ createDatatypes(...datatypes: Datatype[]): DatatypeSort[]; + + /** + * Create a single polymorphic datatype sort with explicit type parameters. + * Type parameters should be sorts created with Context.TypeVariable. + * Self-recursive fields in constructors may reference the Datatype object directly. + * + * @param typeParams Array of type variable sorts + * @param datatype Datatype declaration with constructors + * @returns Created DatatypeSort instance + */ + createPolymorphicDatatype(typeParams: AnySort[], datatype: Datatype): DatatypeSort; } /** diff --git a/src/api/js/src/low-level/index.ts b/src/api/js/src/low-level/index.ts index 1791eae27..f4924eee5 100644 --- a/src/api/js/src/low-level/index.ts +++ b/src/api/js/src/low-level/index.ts @@ -1,4 +1,8 @@ export * from './types.__GENERATED__'; export * from './wrapper.__GENERATED__'; +export type Z3ModuleOverrides = { + locateFile?: (path: string, prefix: string) => string; + [key: string]: unknown; +}; export type Z3Core = Awaited>['Z3']; export type Z3LowLevel = Awaited>; diff --git a/src/api/js/src/node.test.ts b/src/api/js/src/node.test.ts new file mode 100644 index 000000000..7ea67cb21 --- /dev/null +++ b/src/api/js/src/node.test.ts @@ -0,0 +1,37 @@ +const mockInitModule = jest.fn(); +const mockInitWrapper = jest.fn(); +const mockCreateApi = jest.fn(); + +jest.mock('./z3-built', () => mockInitModule, { virtual: true }); +jest.mock('./low-level', () => ({ + init: mockInitWrapper, + Z3Core: undefined, + Z3LowLevel: undefined, +})); +jest.mock('./high-level', () => ({ + createApi: mockCreateApi, +})); + +import { init } from './node'; + +describe('node init', () => { + beforeEach(() => { + mockInitModule.mockReset(); + mockInitWrapper.mockReset(); + mockCreateApi.mockReset(); + }); + + it('passes module overrides to the low-level initializer', async () => { + const locateFile = jest.fn((file: string) => `npm:z3-solver/build/${file}`); + const lowLevel = { Z3: { low: true }, em: { module: true } }; + const highLevel = { Context: jest.fn() }; + mockInitWrapper.mockResolvedValue(lowLevel); + mockCreateApi.mockReturnValue(highLevel); + + const api = await init({ locateFile }); + + expect(mockInitWrapper).toHaveBeenCalledWith(mockInitModule, { locateFile }); + expect(mockCreateApi).toHaveBeenCalledWith(lowLevel.Z3, lowLevel.em); + expect(api).toEqual({ ...lowLevel, ...highLevel }); + }); +}); diff --git a/src/api/js/src/node.ts b/src/api/js/src/node.ts index 87be038e5..a2e4f3e0c 100644 --- a/src/api/js/src/node.ts +++ b/src/api/js/src/node.ts @@ -2,7 +2,7 @@ import initModule = require('./z3-built'); import { createApi, Z3HighLevel } from './high-level'; -import { init as initWrapper, Z3LowLevel } from './low-level'; +import { init as initWrapper, Z3LowLevel, Z3ModuleOverrides } from './low-level'; export * from './high-level/types'; export { Z3Core, Z3LowLevel } from './low-level'; export * from './low-level/types.__GENERATED__'; @@ -29,10 +29,17 @@ export * from './low-level/types.__GENERATED__'; * * console.log(`x=${model.get(x)}, y=${model.get(y)}`); * // x=0, y=12 + * + * // Deno users can provide an Emscripten locateFile hook to load the wasm + * // through npm's asset resolution instead of filesystem reads. + * // const api = await init({ + * // locateFile: (file, _prefix): string => + * // import.meta.resolve(`npm:z3-solver/build/${file}`), // _prefix is unused here + * // }); * ``` * @category Global */ -export async function init(): Promise { - const lowLevel = await initWrapper(initModule); +export async function init(moduleOverrides: Z3ModuleOverrides = {}): Promise { + const lowLevel = await initWrapper(initModule, moduleOverrides); const highLevel = createApi(lowLevel.Z3, lowLevel.em); return { ...lowLevel, ...highLevel }; } diff --git a/src/api/julia/z3jl.cpp b/src/api/julia/z3jl.cpp index ec3efa86b..3df98ac01 100644 --- a/src/api/julia/z3jl.cpp +++ b/src/api/julia/z3jl.cpp @@ -320,6 +320,20 @@ JLCXX_MODULE define_julia_module(jlcxx::Module &m) m.method("finite_set_map", &finite_set_map); m.method("finite_set_filter", &finite_set_filter); m.method("finite_set_range", &finite_set_range); + m.method("empty_set", &empty_set); + m.method("full_set", &full_set); + m.method("set_add", &set_add); + m.method("set_del", &set_del); + m.method("set_union", &set_union); + m.method("set_intersect", &set_intersect); + m.method("set_difference", &set_difference); + m.method("set_complement", &set_complement); + m.method("set_member", &set_member); + m.method("set_subset", &set_subset); + m.method("linear_order", &linear_order); + m.method("partial_order", &partial_order); + m.method("piecewise_linear_order", &piecewise_linear_order); + m.method("tree_order", &tree_order); // ------------------------------------------------------------------------- @@ -629,7 +643,13 @@ JLCXX_MODULE define_julia_module(jlcxx::Module &m) .MM(context, string_sort) .MM(context, seq_sort) .MM(context, re_sort) + .MM(context, char_sort) .MM(context, finite_set_sort) + .method("set_sort", [](context &c, sort s) { + Z3_sort r = Z3_mk_set_sort(c, s); + c.check_error(); + return sort(c, r); + }) .method("array_sort", static_cast(&context::array_sort)) .method("array_sort", static_cast(&context::array_sort)) .method("fpa_sort", static_cast(&context::fpa_sort)) diff --git a/src/api/ml/z3.ml b/src/api/ml/z3.ml index 74cf974b2..f64c4ddd8 100644 --- a/src/api/ml/z3.ml +++ b/src/api/ml/z3.ml @@ -475,6 +475,7 @@ sig val substitute : expr -> expr list -> expr list -> expr val substitute_one : expr -> expr -> expr -> expr val substitute_vars : expr -> expr list -> expr + val substitute_funs : expr -> FuncDecl.func_decl list -> expr list -> expr val translate : expr -> context -> expr val to_string : expr -> string val is_numeral : expr -> bool @@ -537,6 +538,13 @@ end = struct let substitute_vars x to_ = Z3native.substitute_vars (gc x) x (List.length to_) to_ + let substitute_funs x from to_ = + let len = List.length from in + if List.length to_ <> len then + raise (Error "Argument sizes do not match") + else + Z3native.substitute_funs (gc x) x len from to_ + let translate (x:expr) to_ctx = if gc x = to_ctx then x @@ -587,6 +595,12 @@ struct let mk_eq = Z3native.mk_eq let mk_distinct ctx args = Z3native.mk_distinct ctx (List.length args) args + let mk_atmost ctx args k = Z3native.mk_atmost ctx (List.length args) args k + let mk_atleast ctx args k = Z3native.mk_atleast ctx (List.length args) args k + let mk_pble ctx args coeffs k = Z3native.mk_pble ctx (List.length args) args coeffs k + let mk_pbge ctx args coeffs k = Z3native.mk_pbge ctx (List.length args) args coeffs k + let mk_pbeq ctx args coeffs k = Z3native.mk_pbeq ctx (List.length args) args coeffs k + let get_bool_value x = lbool_of_int (Z3native.get_bool_value (gc x) x) let is_bool x = @@ -1283,6 +1297,9 @@ struct let mk_seq_contains = Z3native.mk_seq_contains let mk_seq_extract = Z3native.mk_seq_extract let mk_seq_replace = Z3native.mk_seq_replace + let mk_seq_replace_all = Z3native.mk_seq_replace_all + let mk_seq_replace_re = Z3native.mk_seq_replace_re + let mk_seq_replace_re_all = Z3native.mk_seq_replace_re_all let mk_seq_at = Z3native.mk_seq_at let mk_seq_length = Z3native.mk_seq_length let mk_seq_nth = Z3native.mk_seq_nth @@ -1308,6 +1325,7 @@ struct let mk_re_loop = Z3native.mk_re_loop let mk_re_intersect ctx args = Z3native.mk_re_intersect ctx (List.length args) args let mk_re_complement = Z3native.mk_re_complement + let mk_re_diff = Z3native.mk_re_diff let mk_re_empty = Z3native.mk_re_empty let mk_re_full = Z3native.mk_re_full let mk_char = Z3native.mk_char @@ -1336,6 +1354,15 @@ struct let mk_range = Z3native.mk_finite_set_range end +module SpecialRelation = +struct + let mk_linear_order = Z3native.mk_linear_order + let mk_partial_order = Z3native.mk_partial_order + let mk_piecewise_linear_order = Z3native.mk_piecewise_linear_order + let mk_tree_order = Z3native.mk_tree_order + let mk_transitive_closure = Z3native.mk_transitive_closure +end + module FloatingPoint = struct module RoundingMode = diff --git a/src/api/ml/z3.mli b/src/api/ml/z3.mli index 689fa088d..f5e90d845 100644 --- a/src/api/ml/z3.mli +++ b/src/api/ml/z3.mli @@ -531,6 +531,10 @@ sig For every [i] smaller than [num_exprs], the variable with de-Bruijn index [i] is replaced with term [to[i]]. *) val substitute_vars : Expr.expr -> Expr.expr list -> expr + (** Substitute every application of [from[i]] with [to[i]] in the expression. + The [from] and [to] lists must have the same length. *) + val substitute_funs : Expr.expr -> FuncDecl.func_decl list -> Expr.expr list -> expr + (** Translates (copies) the term to another context. @return A copy of the term which is associated with the other context *) val translate : Expr.expr -> context -> expr @@ -632,6 +636,21 @@ sig (** Creates a [distinct] term. *) val mk_distinct : context -> Expr.expr list -> Expr.expr + (** Encodes p1 + p2 + ... + pn <= k. *) + val mk_atmost : context -> Expr.expr list -> int -> Expr.expr + + (** Encodes p1 + p2 + ... + pn >= k. *) + val mk_atleast : context -> Expr.expr list -> int -> Expr.expr + + (** Encodes k1*p1 + k2*p2 + ... + kn*pn <= k. *) + val mk_pble : context -> Expr.expr list -> int list -> int -> Expr.expr + + (** Encodes k1*p1 + k2*p2 + ... + kn*pn >= k. *) + val mk_pbge : context -> Expr.expr list -> int list -> int -> Expr.expr + + (** Encodes k1*p1 + k2*p2 + ... + kn*pn = k. *) + val mk_pbeq : context -> Expr.expr list -> int list -> int -> Expr.expr + (** Indicates whether the expression is the true or false expression or something else (L_UNDEF). *) val get_bool_value : Expr.expr -> Z3enums.lbool @@ -1968,9 +1987,22 @@ sig (** extract sub-sequence starting at index given by second argument and of length provided by third argument *) val mk_seq_extract : context -> Expr.expr -> Expr.expr -> Expr.expr -> Expr.expr - (** replace first occurrence of second argument by third *) + (** [mk_seq_replace ctx seq target replacement] replaces the first occurrence + of [target] within [seq] with [replacement]. *) val mk_seq_replace : context -> Expr.expr -> Expr.expr -> Expr.expr -> Expr.expr + (** [mk_seq_replace_all ctx seq target replacement] replaces all occurrences + of [target] within [seq] with [replacement]. *) + val mk_seq_replace_all : context -> Expr.expr -> Expr.expr -> Expr.expr -> Expr.expr + + (** [mk_seq_replace_re ctx seq re replacement] replaces the first occurrence + matching the regular expression [re] within [seq] with [replacement]. *) + val mk_seq_replace_re : context -> Expr.expr -> Expr.expr -> Expr.expr -> Expr.expr + + (** [mk_seq_replace_re_all ctx seq re replacement] replaces all occurrences + matching the regular expression [re] within [seq] with [replacement]. *) + val mk_seq_replace_re_all : context -> Expr.expr -> Expr.expr -> Expr.expr -> Expr.expr + (** a unit sequence at index provided by second argument *) val mk_seq_at : context -> Expr.expr -> Expr.expr -> Expr.expr @@ -2048,6 +2080,9 @@ sig (** the regular expression complement *) val mk_re_complement : context -> Expr.expr -> Expr.expr + (** the regular expression difference *) + val mk_re_diff : context -> Expr.expr -> Expr.expr -> Expr.expr + (** the regular expression that accepts no sequences *) val mk_re_empty : context -> Sort.sort -> Expr.expr @@ -2121,6 +2156,31 @@ sig end +(** Special relation constructors *) +module SpecialRelation : +sig + (** Create a linear (total) order relation over the given sort. + The [id] parameter distinguishes multiple linear orders over the same sort. *) + val mk_linear_order : context -> Sort.sort -> int -> FuncDecl.func_decl + + (** Create a partial order relation over the given sort. + The [id] parameter distinguishes multiple partial orders over the same sort. *) + val mk_partial_order : context -> Sort.sort -> int -> FuncDecl.func_decl + + (** Create a piecewise linear order relation over the given sort. + The [id] parameter distinguishes multiple piecewise linear orders over the same sort. *) + val mk_piecewise_linear_order : context -> Sort.sort -> int -> FuncDecl.func_decl + + (** Create a tree order relation over the given sort. + The [id] parameter distinguishes multiple tree orders over the same sort. *) + val mk_tree_order : context -> Sort.sort -> int -> FuncDecl.func_decl + + (** Create the transitive closure of a binary relation. + The resulting relation is recursive. *) + val mk_transitive_closure : context -> FuncDecl.func_decl -> FuncDecl.func_decl + +end + (** Floating-Point Arithmetic *) module FloatingPoint : sig diff --git a/src/api/python/setup.py b/src/api/python/setup.py index 8f70b9c6f..116b3b570 100644 --- a/src/api/python/setup.py +++ b/src/api/python/setup.py @@ -313,6 +313,7 @@ class bdist_wheel(_bdist_wheel): ("linux", "x86_64"): "linux_x86_64", ("linux", "aarch64"): "linux_aarch64", ('linux', "riscv64"): "linux_riscv64", + ("linux", "loongarch64"): "linux_loongarch64", # windows arm64 is not supported by pypi yet ("win", "x64"): "win_amd64", ("win", "x86"): "win32", diff --git a/src/api/python/z3/z3.py b/src/api/python/z3/z3.py index 02ed4f166..34eb9f5f8 100644 --- a/src/api/python/z3/z3.py +++ b/src/api/python/z3/z3.py @@ -5612,6 +5612,20 @@ class Datatype: """ return CreateDatatypes([self])[0] + def create_polymorphic(self, type_params): + """Create a polymorphic Z3 datatype with explicit type variables. + + `type_params` is a list of type variables created with `DeclareTypeVar`. + Constructor field sorts may reference these type variables. + Self-recursive fields may reference this datatype directly. + + >>> A = DeclareTypeVar('A') + >>> Pair = Datatype('Pair') + >>> Pair.declare('pair', ('fst', A), ('snd', A)) + >>> Pair = Pair.create_polymorphic([A]) + """ + return CreatePolymorphicDatatype(self, type_params) + class ScopedConstructor: """Auxiliary object used to create Z3 datatypes.""" @@ -5733,6 +5747,76 @@ def CreateDatatypes(*ds): return tuple(result) +def CreatePolymorphicDatatype(d, type_params): + """Create a single polymorphic Z3 datatype with explicit type parameters. + + `d` is a `Datatype` helper object whose constructors have been declared. + `type_params` is a list of type variables created with `DeclareTypeVar`. + Constructor field sorts may reference these type variables, and self-recursive + fields may reference `d` directly. + + >>> A = DeclareTypeVar('A') + >>> Pair = Datatype('Pair') + >>> Pair.declare('pair', ('fst', A), ('snd', A)) + >>> Pair = CreatePolymorphicDatatype(Pair, [A]) + """ + if z3_debug(): + _z3_assert(isinstance(d, Datatype), "Datatype expected") + _z3_assert(d.constructors != [], "Non-empty Datatype expected") + ctx = d.ctx + name = to_symbol(d.name, ctx) + num_params = len(type_params) + params_arr = (Sort * num_params)() + for i, p in enumerate(type_params): + if z3_debug(): + _z3_assert(is_sort(p), "Z3 sort expected for type parameter") + params_arr[i] = p.ast + num_cs = len(d.constructors) + cs = (Constructor * num_cs)() + to_delete = [] + for j in range(num_cs): + c = d.constructors[j] + cname = to_symbol(c[0], ctx) + rname = to_symbol(c[1], ctx) + fs = c[2] + num_fs = len(fs) + fnames = (Symbol * num_fs)() + sorts = (Sort * num_fs)() + refs = (ctypes.c_uint * num_fs)() + for k in range(num_fs): + fname = fs[k][0] + ftype = fs[k][1] + fnames[k] = to_symbol(fname, ctx) + if isinstance(ftype, Datatype): + if z3_debug(): + _z3_assert(ftype is d, "Only self-recursive references are supported in polymorphic datatypes. Use CreateDatatypes for mutually recursive datatypes.") + sorts[k] = None + refs[k] = 0 + else: + if z3_debug(): + _z3_assert(is_sort(ftype), "Z3 sort expected") + sorts[k] = ftype.ast + refs[k] = 0 + cs[j] = Z3_mk_constructor(ctx.ref(), cname, rname, num_fs, fnames, sorts, refs) + to_delete.append(ScopedConstructor(cs[j], ctx)) + out = Z3_mk_polymorphic_datatype(ctx.ref(), name, num_params, params_arr, num_cs, cs) + dref = DatatypeSortRef(out, ctx) + num_cs_actual = dref.num_constructors() + for j in range(num_cs_actual): + cref = dref.constructor(j) + cref_name = cref.name() + cref_arity = cref.arity() + if cref_arity == 0: + cref = cref() + setattr(dref, cref_name, cref) + rref = dref.recognizer(j) + setattr(dref, "is_" + cref_name, rref) + for k in range(cref_arity): + aref = dref.accessor(j, k) + setattr(dref, aref.name(), aref) + return dref + + class DatatypeSortRef(SortRef): """Datatype sorts.""" diff --git a/src/ast/array_decl_plugin.cpp b/src/ast/array_decl_plugin.cpp index 7da470221..b51972934 100644 --- a/src/ast/array_decl_plugin.cpp +++ b/src/ast/array_decl_plugin.cpp @@ -36,7 +36,8 @@ array_decl_plugin::array_decl_plugin(): m_set_complement_sym("complement"), m_set_subset_sym("subset"), m_array_ext_sym("array-ext"), - m_as_array_sym("as-array") { + m_as_array_sym("as-array"), + m_choice_sym("choice") { } #define ARRAY_SORT_STR "Array" @@ -433,6 +434,20 @@ func_decl * array_decl_plugin::mk_as_array(func_decl * f) { return m_manager->mk_const_decl(m_as_array_sym, s, info); } +func_decl* array_decl_plugin::mk_choice(unsigned arity, sort* const* domain) { + if (arity != 1) { + m_manager->raise_exception("choice takes one argument"); + return nullptr; + } + sort* s = domain[0]; + if (!is_array_sort(s) || get_array_arity(s) != 1 || !m_manager->is_bool(get_array_range(s))) { + m_manager->raise_exception("choice expects an argument with sort (Array T Bool)"); + return nullptr; + } + return m_manager->mk_func_decl(m_choice_sym, arity, domain, get_array_domain(s, 0), + func_decl_info(m_family_id, OP_CHOICE)); +} + func_decl * array_decl_plugin::mk_func_decl(decl_kind k, unsigned num_parameters, parameter const * parameters, unsigned arity, sort * const * domain, sort * range) { @@ -501,6 +516,8 @@ func_decl * array_decl_plugin::mk_func_decl(decl_kind k, unsigned num_parameters func_decl * f = to_func_decl(parameters[0].get_ast()); return mk_as_array(f); } + case OP_CHOICE: + return mk_choice(arity, domain); default: return nullptr; } } @@ -529,6 +546,7 @@ void array_decl_plugin::get_op_names(svector& op_names, symbol con op_names.push_back(builtin_name("complement",OP_SET_COMPLEMENT)); op_names.push_back(builtin_name("subset",OP_SET_SUBSET)); op_names.push_back(builtin_name("as-array", OP_AS_ARRAY)); + op_names.push_back(builtin_name("choice", OP_CHOICE)); op_names.push_back(builtin_name("array-ext", OP_ARRAY_EXT)); #if 0 @@ -655,4 +673,3 @@ func_decl* array_util::mk_array_ext(sort *domain, unsigned i) { parameter p(i); return m_manager.mk_func_decl(m_fid, OP_ARRAY_EXT, 1, &p, 2, domains); } - diff --git a/src/ast/array_decl_plugin.h b/src/ast/array_decl_plugin.h index 36403f3ca..0c4983eec 100644 --- a/src/ast/array_decl_plugin.h +++ b/src/ast/array_decl_plugin.h @@ -63,6 +63,7 @@ enum array_op_kind { OP_SET_COMPLEMENT, OP_SET_SUBSET, OP_AS_ARRAY, // used for model construction + OP_CHOICE, LAST_ARRAY_OP }; @@ -79,6 +80,7 @@ class array_decl_plugin : public decl_plugin { symbol m_set_subset_sym; symbol m_array_ext_sym; symbol m_as_array_sym; + symbol m_choice_sym; bool check_set_arguments(unsigned arity, sort * const * domain); @@ -106,6 +108,8 @@ class array_decl_plugin : public decl_plugin { func_decl * mk_as_array(func_decl * f); + func_decl * mk_choice(unsigned arity, sort* const* domain); + bool is_array_sort(sort* s) const; public: array_decl_plugin(); @@ -164,6 +168,7 @@ public: bool is_difference(expr* n) const { return is_app_of(n, m_fid, OP_SET_DIFFERENCE); } bool is_complement(expr* n) const { return is_app_of(n, m_fid, OP_SET_COMPLEMENT); } bool is_as_array(expr * n) const { return is_app_of(n, m_fid, OP_AS_ARRAY); } + bool is_choice(expr* n) const { return is_app_of(n, m_fid, OP_CHOICE); } bool is_as_array(expr * n, func_decl*& f) const { return is_as_array(n) && (f = get_as_array_func_decl(n), true); } bool is_select(func_decl* f) const { return is_decl_of(f, m_fid, OP_SELECT); } bool is_store(func_decl* f) const { return is_decl_of(f, m_fid, OP_STORE); } @@ -172,6 +177,7 @@ public: bool is_union(func_decl* f) const { return is_decl_of(f, m_fid, OP_SET_UNION); } bool is_intersect(func_decl* f) const { return is_decl_of(f, m_fid, OP_SET_INTERSECT); } bool is_as_array(func_decl* f) const { return is_decl_of(f, m_fid, OP_AS_ARRAY); } + bool is_choice(func_decl* f) const { return is_decl_of(f, m_fid, OP_CHOICE); } bool is_default(func_decl* f) const { return is_decl_of(f, m_fid, OP_ARRAY_DEFAULT); } bool is_default(expr* n) const { return is_app_of(n, m_fid, OP_ARRAY_DEFAULT); } bool is_subset(expr const* n) const { return is_app_of(n, m_fid, OP_SET_SUBSET); } @@ -308,6 +314,10 @@ public: return m_manager.mk_app(m_fid, OP_AS_ARRAY, 1, ¶m, 0, nullptr, nullptr); } + app* mk_choice(expr* p) const { + return m_manager.mk_app(m_fid, OP_CHOICE, p); + } + sort* get_array_range_rec(sort* s) { while (is_array(s)) { s = get_array_range(s); @@ -317,5 +327,3 @@ public: }; - - diff --git a/src/ast/ast.cpp b/src/ast/ast.cpp index cf5e7af87..928bebc07 100644 --- a/src/ast/ast.cpp +++ b/src/ast/ast.cpp @@ -242,21 +242,14 @@ func_decl_info::func_decl_info(family_id family_id, decl_kind k, unsigned num_pa m_injective(false), m_idempotent(false), m_skolem(false), - m_lambda(false), m_polymorphic(false) { } bool func_decl_info::operator==(func_decl_info const & info) const { - return decl_info::operator==(info) && - m_left_assoc == info.m_left_assoc && - m_right_assoc == info.m_right_assoc && - m_flat_associative == info.m_flat_associative && - m_commutative == info.m_commutative && - m_chainable == info.m_chainable && - m_pairwise == info.m_pairwise && - m_injective == info.m_injective && - m_skolem == info.m_skolem && - m_lambda == info.m_lambda; + return decl_info::operator==(info) && m_left_assoc == info.m_left_assoc && m_right_assoc == info.m_right_assoc && + m_flat_associative == info.m_flat_associative && m_commutative == info.m_commutative && + m_chainable == info.m_chainable && m_pairwise == info.m_pairwise && m_injective == info.m_injective && + m_skolem == info.m_skolem; } std::ostream & operator<<(std::ostream & out, func_decl_info const & info) { @@ -270,7 +263,6 @@ std::ostream & operator<<(std::ostream & out, func_decl_info const & info) { if (info.is_injective()) out << " :injective "; if (info.is_idempotent()) out << " :idempotent "; if (info.is_skolem()) out << " :skolem "; - if (info.is_lambda()) out << " :lambda "; if (info.is_polymorphic()) out << " :polymorphic "; return out; } @@ -1625,19 +1617,6 @@ bool ast_manager::are_distinct(expr* a, expr* b) const { return false; } -void ast_manager::add_lambda_def(func_decl* f, quantifier* q) { - TRACE(model, tout << "add lambda def " << mk_pp(q, *this) << "\n"); - m_lambda_defs.insert(f, q); - f->get_info()->set_lambda(true); - inc_ref(q); -} - -quantifier* ast_manager::is_lambda_def(func_decl* f) { - if (f->get_info() && f->get_info()->is_lambda()) - return m_lambda_defs[f]; - return nullptr; -} - void ast_manager::register_plugin(family_id id, decl_plugin * plugin) { SASSERT(m_plugins.get(id, 0) == 0); @@ -1670,7 +1649,7 @@ bool ast_manager::slow_not_contains(ast const * n) { } #endif -#if 1 +#if 0 static unsigned s_count = 0; static void track_id(ast_manager& m, ast* n, unsigned id) { @@ -1832,10 +1811,6 @@ void ast_manager::delete_node(ast * n) { m_poly_roots.erase(f); if (f->m_info != nullptr) { func_decl_info * info = f->get_info(); - if (info->is_lambda()) { - push_dec_ref(m_lambda_defs[f]); - m_lambda_defs.remove(f); - } info->del_eh(*this); dealloc(info); } diff --git a/src/ast/ast.h b/src/ast/ast.h index 7fc86070d..03713ee4b 100644 --- a/src/ast/ast.h +++ b/src/ast/ast.h @@ -404,7 +404,6 @@ struct func_decl_info : public decl_info { bool m_injective:1; bool m_idempotent:1; bool m_skolem:1; - bool m_lambda:1; bool m_polymorphic:1; func_decl_info(family_id family_id = null_family_id, decl_kind k = null_decl_kind, unsigned num_parameters = 0, parameter const * parameters = nullptr); @@ -419,7 +418,6 @@ struct func_decl_info : public decl_info { bool is_injective() const { return m_injective; } bool is_idempotent() const { return m_idempotent; } bool is_skolem() const { return m_skolem; } - bool is_lambda() const { return m_lambda; } bool is_polymorphic() const { return m_polymorphic; } void set_associative(bool flag = true) { m_left_assoc = flag; m_right_assoc = flag; } @@ -432,7 +430,6 @@ struct func_decl_info : public decl_info { void set_injective(bool flag = true) { m_injective = flag; } void set_idempotent(bool flag = true) { m_idempotent = flag; } void set_skolem(bool flag = true) { m_skolem = flag; } - void set_lambda(bool flag = true) { m_lambda = flag; } void set_polymorphic(bool flag = true) { m_polymorphic = flag; } bool operator==(func_decl_info const & info) const; @@ -661,7 +658,6 @@ public: bool is_pairwise() const { return get_info() != nullptr && get_info()->is_pairwise(); } bool is_injective() const { return get_info() != nullptr && get_info()->is_injective(); } bool is_skolem() const { return get_info() != nullptr && get_info()->is_skolem(); } - bool is_lambda() const { return get_info() != nullptr && get_info()->is_lambda(); } bool is_idempotent() const { return get_info() != nullptr && get_info()->is_idempotent(); } bool is_polymorphic() const { return get_info() != nullptr && get_info()->is_polymorphic(); } unsigned get_arity() const { return m_arity; } @@ -857,7 +853,8 @@ public: enum quantifier_kind { forall_k, exists_k, - lambda_k + lambda_k, + choice_k }; class quantifier : public expr { @@ -1512,7 +1509,6 @@ protected: proof_gen_mode m_proof_mode; bool m_int_real_coercions; // If true, use hack that automatically introduces to_int/to_real when needed. ast_table m_ast_table; - obj_map m_lambda_defs; id_gen m_expr_id_gen; id_gen m_decl_id_gen; sort * m_bool_sort; @@ -1642,15 +1638,7 @@ public: bool are_distinct(expr * a, expr * b) const; bool contains(ast * a) const { return m_ast_table.contains(a); } - - bool is_lambda_def(quantifier* q) const { return q->get_qid() == m_lambda_def; } - void add_lambda_def(func_decl* f, quantifier* q); - quantifier* is_lambda_def(func_decl* f); - quantifier* is_lambda_def(app* e) { return is_lambda_def(e->get_decl()); } - obj_map const& lambda_defs() const { return m_lambda_defs; } - - symbol const& lambda_def_qid() const { return m_lambda_def; } - + unsigned get_num_asts() const { return m_ast_table.size(); } void debug_ref_count() { m_debug_ref_count = true; } diff --git a/src/ast/ast_lt.cpp b/src/ast/ast_lt.cpp index cab7c5b53..48c60085f 100644 --- a/src/ast/ast_lt.cpp +++ b/src/ast/ast_lt.cpp @@ -18,12 +18,12 @@ Revision History: --*/ #include "ast/ast.h" -#define check_symbol(S1,S2) if (S1 != S2) return lt(S1,S2) -#define check_value(V1,V2) if (V1 != V2) return V1 < V2 -#define check_bool(B1,B2) if (B1 != B2) return !B1 && B2 -#define check_ptr(P1,P2) if (!P1 && P2) return true; if (P1 && !P2) return false -#define check_ast(T1,T2) if (T1 != T2) { n1 = T1; n2 = T2; goto start; } -#define check_zstring(S1, S2) if (S1 != S2) return S1 < S2 +#define check_symbol(S1,S2) if ((S1) != (S2)) return lt((S1),(S2)) +#define check_value(V1,V2) if ((V1) != (V2)) return (V1) < (V2) +#define check_bool(B1,B2) if ((B1) != (B2)) return !(B1) && (B2) +#define check_ptr(P1,P2) if (!(P1) && (P2)) return true; if ((P1) && !(P2)) return false +#define check_ast(T1,T2) if ((T1) != (T2)) { n1 = (T1); n2 = (T2); goto start; } +#define check_zstring(S1, S2) if ((S1) != (S2)) return (S1) < (S2) #define check_parameter(p1, p2) { \ check_value(p1.get_kind(), p2.get_kind()); \ diff --git a/src/ast/ast_pp_dot.cpp b/src/ast/ast_pp_dot.cpp index 3dae0492c..17750a7b9 100644 --- a/src/ast/ast_pp_dot.cpp +++ b/src/ast/ast_pp_dot.cpp @@ -77,7 +77,7 @@ private: void pp_atomic_step(const expr * e) { unsigned id = get_id(e); - m_out << "node_" << id << " [shape=box,color=\"yellow\",style=\"filled\",label=\"" << label_of_expr(e) << "\"] ;" << std::endl; + m_out << "node_" << id << " [shape=box,color=\"yellow\",style=\"filled\",label=\"" << label_of_expr(e) << "\"] ;" << '\n'; } void pp_step(const proof * p) { @@ -91,7 +91,7 @@ private: m_first ? (m_first=false,"color=\"red\"") : num_parents==0 ? "color=\"yellow\"": ""; m_out << "node_" << id << " [shape=box,style=\"filled\",label=\"" << label_of_expr(p_res) << "\"" - << color << "]" << std::endl; + << color << "]" << '\n'; // now print edges to parents (except last one, which is the result) std::string label = p->get_decl()->get_name().str(); for (unsigned i = 0 ; i < num_parents; ++i) { @@ -99,7 +99,7 @@ private: // explore parent, also print a link to it push_term(to_app(parent)); m_out << "node_" << id << " -> " << "node_" << get_id((expr*)parent) - << "[label=\"" << label << "\"];" << std::endl;; + << "[label=\"" << label << "\"];" << '\n'; } } else { pp_atomic_step(p); @@ -120,11 +120,11 @@ private: // main printer std::ostream & ast_pp_dot::pp(std::ostream & out) const { - out << "digraph proof { " << std::endl; + out << "digraph proof { " << '\n'; ast_pp_dot_st pp_st(this, out); pp_st.push_term(m_pr); pp_st.pp_loop(); - out << std::endl << " } " << std::endl << std::flush; + out << '\n' << " } " << '\n' << std::flush; return out; } diff --git a/src/ast/ast_smt_pp.cpp b/src/ast/ast_smt_pp.cpp index 23130e902..db52a583f 100644 --- a/src/ast/ast_smt_pp.cpp +++ b/src/ast/ast_smt_pp.cpp @@ -733,7 +733,8 @@ public: m_AUFLIRA("AUFLIRA"), // It's much easier to read those testcases with that. m_no_lets(no_lets), - m_simplify_implies(simplify_implies) + m_simplify_implies(simplify_implies), + m_top(nullptr) { m_basic_fid = m.get_basic_family_id(); m_label_fid = m.mk_family_id("label"); diff --git a/src/ast/ast_translation.cpp b/src/ast/ast_translation.cpp index affe9d49d..126b52099 100644 --- a/src/ast/ast_translation.cpp +++ b/src/ast/ast_translation.cpp @@ -181,20 +181,12 @@ void ast_translation::mk_func_decl(func_decl * f, frame & fr) { new_fi.set_injective(fi->is_injective()); new_fi.set_skolem(fi->is_skolem()); new_fi.set_idempotent(fi->is_idempotent()); - new_fi.set_lambda(fi->is_lambda()); new_f = m_to_manager.mk_func_decl(f->get_name(), f->get_arity(), new_domain, new_range, new_fi); - - if (new_fi.is_lambda()) { - quantifier* q = from().is_lambda_def(f); - ast_translation tr(from(), to()); - quantifier* new_q = tr(q); - to().add_lambda_def(new_f, new_q); - } } TRACE(ast_translation, tout << f->get_name() << " "; if (fi) tout << *fi; tout << "\n"; diff --git a/src/ast/ast_util.cpp b/src/ast/ast_util.cpp index 5d71ffda9..d29d437f2 100644 --- a/src/ast/ast_util.cpp +++ b/src/ast/ast_util.cpp @@ -168,7 +168,7 @@ expr * mk_and(ast_manager & m, unsigned num_args, expr * const * args) { } app* mk_and(ast_manager & m, unsigned num_args, app * const * args) { - return to_app(mk_and(m, num_args, (expr* const*) args)); + return to_app(mk_and(m, num_args, reinterpret_cast(args))); } expr * mk_or(ast_manager & m, unsigned num_args, expr * const * args) { diff --git a/src/ast/bv_decl_plugin.cpp b/src/ast/bv_decl_plugin.cpp index 76dc8fe07..bb65581cc 100644 --- a/src/ast/bv_decl_plugin.cpp +++ b/src/ast/bv_decl_plugin.cpp @@ -783,6 +783,9 @@ void bv_decl_plugin::get_op_names(svector & op_names, symbol const op_names.push_back(builtin_name("rotate_left",OP_ROTATE_LEFT)); op_names.push_back(builtin_name("rotate_right",OP_ROTATE_RIGHT)); op_names.push_back(builtin_name("bit2bool", OP_BIT2BOOL)); + op_names.push_back(builtin_name("ubv_to_int", OP_UBV2INT)); + op_names.push_back(builtin_name("sbv_to_int", OP_SBV2INT)); + op_names.push_back(builtin_name("int_to_bv", OP_INT2BV)); if (logic == symbol::null || logic == symbol("ALL") || logic == "QF_FD" || logic == "HORN") { op_names.push_back(builtin_name("bvumul_noovfl",OP_BUMUL_NO_OVFL)); @@ -804,11 +807,10 @@ void bv_decl_plugin::get_op_names(svector & op_names, symbol const op_names.push_back(builtin_name("ext_rotate_left",OP_EXT_ROTATE_LEFT)); op_names.push_back(builtin_name("ext_rotate_right",OP_EXT_ROTATE_RIGHT)); op_names.push_back(builtin_name("int2bv",OP_INT2BV)); - op_names.push_back(builtin_name("int_to_bv",OP_INT2BV)); + op_names.push_back(builtin_name("bv2int",OP_UBV2INT)); op_names.push_back(builtin_name("bv2nat",OP_UBV2INT)); - op_names.push_back(builtin_name("ubv_to_int",OP_UBV2INT)); - op_names.push_back(builtin_name("sbv_to_int",OP_SBV2INT)); + op_names.push_back(builtin_name("mkbv",OP_MKBV)); } } diff --git a/src/ast/euf/euf_mam.cpp b/src/ast/euf/euf_mam.cpp index cce838c61..d47eb087b 100644 --- a/src/ast/euf/euf_mam.cpp +++ b/src/ast/euf/euf_mam.cpp @@ -133,7 +133,7 @@ namespace euf { // Instructions // // ------------------------------------ - typedef enum { + typedef enum : uint8_t { INIT1=0, INIT2, INIT3, INIT4, INIT5, INIT6, INITN, INITAC, BIND1, BIND2, BIND3, BIND4, BIND5, BIND6, BINDN, YIELD1, YIELD2, YIELD3, YIELD4, YIELD5, YIELD6, YIELDN, @@ -239,6 +239,7 @@ namespace euf { unsigned short m_num_args; unsigned m_ireg; unsigned m_oreg; + unsigned m_curr_generation; }; struct get_cgr : public instruction { @@ -1926,28 +1927,38 @@ namespace euf { m_max_generation = std::max(m_max_generation, n->generation()); } + void get_f_app(func_decl* lbl, unsigned num_expected_args, enode* curr, enode*& matching_cgr, enode*& min_gen_match) { + if (curr->get_decl() == lbl && curr->num_args() == num_expected_args) { + if (curr->is_cgr() && !matching_cgr) + matching_cgr = curr; + if (!min_gen_match || min_gen_match->generation() > curr->generation()) + min_gen_match = curr; + } + } + // We have to provide the number of expected arguments because we have flat-assoc applications such as +. // Flat-assoc applications may have arbitrary number of arguments. enode * get_first_f_app(func_decl * lbl, unsigned num_expected_args, enode * first) { + enode *matching_cgr = nullptr, *min_gen_match = nullptr; for (enode* curr : euf::enode_class(first)) { - if (curr->get_decl() == lbl && curr->is_cgr() && curr->num_args() == num_expected_args) { - update_max_generation(curr, first); - return curr; - } + get_f_app(lbl, num_expected_args, curr, matching_cgr, min_gen_match); + curr = curr->get_next(); } - return nullptr; + if (matching_cgr) + update_max_generation(min_gen_match, first); + return matching_cgr; } enode * get_next_f_app(func_decl * lbl, unsigned num_expected_args, enode * first, enode * curr) { curr = curr->get_next(); + enode *matching_cgr = nullptr, *min_gen_match = nullptr; while (curr != first) { - if (curr->get_decl() == lbl && curr->is_cgr() && curr->num_args() == num_expected_args) { - update_max_generation(curr, first); - return curr; - } + get_f_app(lbl, num_expected_args, curr, matching_cgr, min_gen_match); curr = curr->get_next(); } - return nullptr; + if (matching_cgr) + update_max_generation(min_gen_match, first); + return matching_cgr; } /** @@ -2563,6 +2574,7 @@ namespace euf { m_backtrack_stack[m_top].m_instr = m_pc; \ m_backtrack_stack[m_top].m_old_max_generation = m_curr_max_generation; \ m_backtrack_stack[m_top].m_curr = m_app; \ + const_cast(static_cast(m_pc))->m_curr_generation = m_max_generation; \ m_top++; BIND_COMMON(); @@ -2829,7 +2841,8 @@ namespace euf { goto backtrack; \ } \ bp.m_curr = m_app; \ - TRACE(mam_int, tout << "bind next candidate:\n" << mk_ll_pp(m_app->get_expr(), m);); \ + m_max_generation = m_b->m_curr_generation; \ + TRACE(mam_int, tout << "bind next candidate:\n" << mk_ll_pp(m_app->get_expr(), m);); \ m_oreg = m_b->m_oreg BBIND_COMMON(); @@ -4059,4 +4072,4 @@ void euf::mam::ground_subterms(expr* e, ptr_vector& ground) { euf::mam* euf::mam::mk(euf::mam_solver& ctx, euf::on_binding_callback& em) { return alloc(mam_impl, ctx, em, true); -} \ No newline at end of file +} diff --git a/src/ast/euf/ho_matcher.cpp b/src/ast/euf/ho_matcher.cpp index f86d82b64..a740167ad 100644 --- a/src/ast/euf/ho_matcher.cpp +++ b/src/ast/euf/ho_matcher.cpp @@ -64,7 +64,7 @@ namespace euf { } void ho_matcher::search() { - IF_VERBOSE(1, display(verbose_stream())); + IF_VERBOSE(10, display(verbose_stream())); while (m.inc()) { // Q, B -> Q', B'. Push work on the backtrack stack and new work items @@ -77,7 +77,7 @@ namespace euf { break; } - IF_VERBOSE(1, display(verbose_stream() << "ho_matcher: done\n")); + IF_VERBOSE(10, display(verbose_stream() << "ho_matcher: done\n")); } void ho_matcher::backtrack() { @@ -92,7 +92,7 @@ namespace euf { while (!m_backtrack.empty()) { auto& wi = *m_backtrack.back(); bool st = consume_work(wi); - IF_VERBOSE(3, display(verbose_stream() << "ho_matcher::consume_work: " << wi.pat << " =?= " << wi.t << " -> " << (st?"true":"false") << "\n");); + TRACE(ho_matching, display(tout << "ho_matcher::consume_work: " << mk_bounded_pp(wi.pat, m) << " =?= " << mk_bounded_pp(wi.t, m) << " -> " << (st?"true":"false") << "\n");); if (st) { if (m_goals.empty()) m_on_match(m_subst); @@ -110,7 +110,11 @@ namespace euf { } lbool ho_matcher::are_equal(unsigned o1, expr* p, unsigned o2, expr* t) const { - SASSERT(p->get_sort() == t->get_sort()); + if (p->get_sort() != t->get_sort()) { + TRACE(ho_matching, tout << "sort mismatch: " << mk_pp(p, m) << " : " << mk_pp(p->get_sort(), m) + << " vs " << mk_pp(t, m) << " : " << mk_pp(t->get_sort(), m) << "\n";); + return l_false; + } if (o1 == o2 && p == t) return l_true; @@ -239,25 +243,19 @@ namespace euf { return r; } - // We assume that m_rewriter should produce - // something amounting to weak-head normal form WHNF + expr_ref ho_matcher::whnf_star(expr *e, unsigned offset) const { + expr_ref r(e, m); + while (true) { + auto q = whnf(r, offset); + if (q == r) + return r; + r = q; + } + } void ho_matcher::reduce(match_goal& wi) { - while (true) { - expr_ref r = whnf(wi.pat, wi.pat_offset()); - if (r == wi.pat) - break; - IF_VERBOSE(3, verbose_stream() << "ho_matcher::reduce: " << wi.pat << " -> " << r << "\n";); - wi.pat = r; - } - - while (true) { - expr_ref r = whnf(wi.t, wi.term_offset()); - if (r == wi.t) - break; - IF_VERBOSE(3, verbose_stream() << "ho_matcher::reduce: " << wi.t << " -> " << r << "\n";); - wi.t = r; - } + wi.pat = whnf_star(wi.pat, wi.pat_offset()); + wi.t = whnf_star(wi.t, wi.term_offset()); } bool ho_matcher::consume_work(match_goal &wi) { @@ -288,7 +286,6 @@ namespace euf { break; } - // v >= offset // v - offset |-> t if (is_meta_var(p, wi.pat_offset()) && is_closed(t, 0, wi.term_offset())) { @@ -299,7 +296,6 @@ namespace euf { return true; } - // N = \ x. T => ((shift1 N) x) = T if (is_lambda(t) && !is_lambda(p)) { auto q = to_quantifier(t); @@ -318,6 +314,43 @@ namespace euf { return true; } + // \x . N = T => N = ((shift1 T) x) + if (is_lambda(p) && !is_lambda(t)) { + auto q = to_quantifier(p); + auto p_body = q->get_expr(); + auto nd = q->get_num_decls(); + var_shifter vs(m); + expr_ref r(m); + vs(t, nd, r); + expr_ref_vector args(m); + args.push_back(r); + for (unsigned i = 0; i < nd; ++i) + args.push_back(m.mk_var(nd - 1 - i, q->get_decl_sort(i))); + r = m_array.mk_select(args); + m_goals.push(wi.level, wi.term_offset() + nd, p_body, r); + wi.set_done(); + return true; + } + + // + // lambda x . p == lambda x . t + // + if (is_quantifier(p) && is_quantifier(t)) { + auto qp = to_quantifier(p); + auto qt = to_quantifier(t); + unsigned pd = qp->get_num_decls(); + unsigned td = qt->get_num_decls(); + if (qp->get_kind() != qt->get_kind()) + return false; + if (pd != td) + return false; + for (unsigned i = 0; i < pd; ++i) + if (qp->get_decl_sort(i) != qt->get_decl_sort(i)) + return false; + m_goals.push(wi.level, wi.term_offset() + td, qp->get_expr(), qt->get_expr()); + return true; + } + // Flex head unitary // H(pat) = t @@ -457,25 +490,7 @@ namespace euf { m_goals.push(wi.level, wi.term_offset(), tp->get_arg(i), ta->get_arg(i)); return true; } - - // - // lambda x . p == lambda x . t - // - if (is_quantifier(p) && is_quantifier(t)) { - auto qp = to_quantifier(p); - auto qt = to_quantifier(t); - unsigned pd = qp->get_num_decls(); - unsigned td = qt->get_num_decls(); - if (qp->get_kind() != qt->get_kind()) - return false; - if (pd != td) - return false; - for (unsigned i = 0; i < pd; ++i) - if (qp->get_decl_sort(i) != qt->get_decl_sort(i)) - return false; - m_goals.push(wi.level, wi.term_offset() + td, qp->get_expr(), qt->get_expr()); - return true; - } + return false; } @@ -488,8 +503,7 @@ namespace euf { uint_set vars; while (m_array.is_select(p)) { auto a = to_app(p); - for (unsigned i = 1; i < a->get_num_args(); ++i) { - auto arg = a->get_arg(i); + for (auto arg : *a) { if (!is_bound_var(arg, offset)) return false; auto idx = to_var(arg)->get_idx(); @@ -549,15 +563,12 @@ namespace euf { } expr_ref_vector pat2bound(m); for (auto a : pats) { - unsigned sz = a->get_num_args(); - for (unsigned i = 1; i < sz; ++i) { - auto arg = a->get_arg(i); + for (auto arg : *a) { SASSERT(is_bound_var(arg, offset)); auto idx = to_var(arg)->get_idx(); pat2bound.reserve(idx + 1); pat2bound[idx] = m.mk_var(--num_bound, arg->get_sort()); - } - p1 = a->get_arg(0); + } } var_subst sub(m, false); expr_ref lam = sub(t, pat2bound); @@ -575,7 +586,7 @@ namespace euf { // // keep track of number of internal scopes and offset to non-capture variables. - // a variable is captured if it's index is in the interval [scopes, offset[. + // a variable is captured if its index is in the interval [scopes, offset[. // bool ho_matcher::is_closed(expr* v, unsigned scopes, unsigned offset) const { if (is_ground(v)) @@ -630,49 +641,59 @@ namespace euf { void ho_matcher::add_binding(var* v, unsigned offset, expr* t) { SASSERT(v->get_idx() >= offset); m_subst.set(v->get_idx() - offset, t); - IF_VERBOSE(1, verbose_stream() << "ho_matcher::add_binding: v" << v->get_idx() - offset << " -> " << mk_pp(t, m) << "\n";); + SASSERT(v->get_sort() == t->get_sort()); + TRACE(ho_matching, tout << "ho_matcher::add_binding: v" << v->get_idx() - offset << " -> " << mk_pp(t, m) << "\n";); m_trail.push(undo_set(m_subst, v->get_idx() - offset)); } std::pair ho_matcher::compile_ho_pattern(quantifier* q, app* p) { app* p1 = nullptr; - if (m_pat2hopat.find(p, p)) { - q = m_q2hoq[q]; - return { q, p }; + quantifier *q1 = nullptr; + if (m_pat2hopat.find(p, p1) && m_q2hoq.find(q, q1)) { + return { q1, p1 }; } - auto is_ho = any_of(subterms::all(expr_ref(p, m)), [&](expr* t) { return m_unitary.is_flex(0, t); }); + auto is_ho = any_of(subterms::all(expr_ref(p, m)), [&](expr* t) { + return m_unitary.is_flex(0, t) || + // m.is_lambda_def(t) || + is_lambda(t); + }); if (!is_ho) return { q, p }; - ptr_vector todo; + vector> todo; ptr_buffer bound; expr_ref_vector cache(m); unsigned nb = q->get_num_decls(); - todo.push_back(p); + bool contains_pat2abs = m_pat2abs.contains(p); + SASSERT(m.is_pattern(p)); + todo.push_back({p, 0}); while (!todo.empty()) { - auto t = todo.back(); + auto [t, lvl] = todo.back(); if (is_var(t)) { cache.setx(t->get_id(), t); todo.pop_back(); continue; } - if (m_unitary.is_flex(0, t)) { - m_pat2abs.insert_if_not_there(p, svector>()).push_back({ nb, t }); + if ((m_unitary.is_flex(0, t) && lvl > 1) || // m.is_lambda_def(t) || + is_lambda(t)) { + if (!contains_pat2abs) + m_pat2abs.insert_if_not_there(p, svector>()).push_back({ nb, t }); auto v = m.mk_var(nb++, t->get_sort()); bound.push_back(v); cache.setx(t->get_id(), v); todo.pop_back(); continue; - } + } if (is_app(t)) { auto a = to_app(t); + unsigned sz = a->get_num_args(); ptr_buffer args; for (auto arg : *a) { cache.reserve(arg->get_id() + 1); expr* arg1 = cache.get(arg->get_id()); if (!arg1) - todo.push_back(arg); + todo.push_back({arg, lvl + 1}); else args.push_back(arg1); } @@ -682,11 +703,15 @@ namespace euf { cache.setx(t->get_id(), m.mk_app(a->get_decl(), args.size(), args.data())); } if (is_quantifier(t)) { - m_pat2abs.remove(p); + if (!contains_pat2abs) + m_pat2abs.remove(p); return { q, p }; } } p1 = to_app(cache.get(p->get_id())); + + if (p1 == p) + return {q, p}; expr_free_vars free_vars; free_vars(p1); app_ref_vector new_ground(m); @@ -713,6 +738,8 @@ namespace euf { auto body = q->get_expr(); if (!new_patterns.empty()) { ptr_vector pats; + CTRACE(ho_matching, !m.is_pattern(p1), + tout << mk_pp(p, m) << "\n" << mk_pp(p1, m) << "\n";); VERIFY(m.is_pattern(p1, pats)); for (auto p : new_patterns) // patterns for variables that are not free in new pattern pats.push_back(p); @@ -721,23 +748,40 @@ namespace euf { p1 = m.mk_pattern(pats.size(), pats.data()); } - quantifier* q1 = m.mk_forall(sorts.size(), sorts.data(), names.data(), body); + q1 = m.mk_forall(sorts.size(), sorts.data(), names.data(), body); - m_pat2hopat.insert(p, p1); - m_hopat2pat.insert(p1, p); - m_q2hoq.insert(q, q1); - m_hoq2q.insert(q1, q); - m_hopat2free_vars.insert(p1, std::move(free_vars)); m_ho_patterns.push_back(p1); m_ho_qs.push_back(q1); trail().push(push_back_vector(m_ho_patterns)); trail().push(push_back_vector(m_ho_qs)); - trail().push(insert_map(m_pat2hopat, p)); - trail().push(insert_map(m_hopat2pat, p1)); - trail().push(insert_map(m_pat2abs, p)); - trail().push(insert_map(m_q2hoq, q)); - trail().push(insert_map(m_hoq2q, q1)); - trail().push(insert_map(m_hopat2free_vars, p1)); + + if (!m_pat2hopat.contains(p)) { + m_pat2hopat.insert(p, p1); + trail().push(insert_map(m_pat2hopat, p)); + } + if (!m_hopat2pat.contains(p1)) { + m_hopat2pat.insert(p1, p); + trail().push(insert_map(m_hopat2pat, p1)); + } + if (!m_q2hoq.contains(q)) { + m_q2hoq.insert(q, q1); + trail().push(insert_map(m_q2hoq, q)); + } + if (!m_hoq2q.contains(q1)) { + m_hoq2q.insert(q1, q); + trail().push(insert_map(m_hoq2q, q1)); + } + if (!m_hopat2free_vars.contains(p1)) { + m_hopat2free_vars.insert(p1, std::move(free_vars)); + trail().push(insert_map(m_hopat2free_vars, p1)); + } + if (!contains_pat2abs) + trail().push(insert_map(m_pat2abs, p)); + + TRACE(ho_matching, tout << mk_pp(q, m) << "\n" + << mk_pp(p, m) << "\n->\n" + << mk_pp(q1, m) << "\n" + << mk_pp(p1, m) << "\n"); return { q1, p1 }; } @@ -745,28 +789,46 @@ namespace euf { return m_hopat2pat.contains(p); } + void ho_matcher::register_ho_pattern(app* alias_p, app* full_p) { + if (alias_p == full_p) return; + auto orig_p = m_hopat2pat[full_p]; + m_hopat2pat.insert(alias_p, orig_p); + m_hopat2free_vars.insert(alias_p, m_hopat2free_vars[full_p]); + m_ho_patterns.push_back(alias_p); + trail().push(push_back_vector(m_ho_patterns)); + trail().push(insert_map(m_hopat2pat, alias_p)); + trail().push(insert_map(m_hopat2free_vars, alias_p)); + } + void ho_matcher::refine_ho_match(app* p, expr_ref_vector& s) { auto fo_pat = m_hopat2pat[p]; + IF_VERBOSE(10, verbose_stream() << "refine_ho_match: p=" << mk_pp(p, m) << "\n fo_pat=" << mk_pp(fo_pat, m) << "\n"; + verbose_stream() << " m_pat2abs has fo_pat: " << m_pat2abs.contains(fo_pat) << "\n"; + auto& abs = m_pat2abs[fo_pat]; + verbose_stream() << " m_pat2abs size: " << abs.size() << "\n"; + for (auto [v, pat] : abs) verbose_stream() << " v=" << v << " pat=" << mk_pp(pat, m) << "\n";); m_trail.push_scope(); m_subst.resize(0); m_subst.resize(s.size()); m_goals.reset(); + // MAM bindings are reversed: s[i] = binding for var idx = s.size()-1-i + // m_subst is indexed by var index directly for (unsigned i = 0; i < s.size(); ++i) { auto idx = s.size() - i - 1; if (!m_hopat2free_vars[p].contains(idx)) s[i] = m.mk_var(idx, s[i]->get_sort()); else if (s.get(i)) - m_subst.set(i, s.get(i)); + m_subst.set(idx, s.get(i)); } - IF_VERBOSE(1, verbose_stream() << "refine " << mk_pp(p, m) << "\n" << s << "\n"); + TRACE(ho_matching, tout << "refine " << mk_pp(p, m) << "\n" << s << "\n"); unsigned num_bound = 0, level = 0; for (auto [v, pat] : m_pat2abs[fo_pat]) { var_subst sub(m, true); auto pat_refined = sub(pat, s); - IF_VERBOSE(1, verbose_stream() << mk_pp(pat, m) << " -> " << pat_refined << "\n"); - m_goals.push(level, num_bound, pat_refined, s.get(s.size() - v - 1)); + TRACE(ho_matching, tout << mk_pp(pat, m) << " -> " << pat_refined << "\n"); + m_goals.push(level, num_bound, pat_refined, m_subst.get(v)); } search(); diff --git a/src/ast/euf/ho_matcher.h b/src/ast/euf/ho_matcher.h index 65477078c..023555926 100644 --- a/src/ast/euf/ho_matcher.h +++ b/src/ast/euf/ho_matcher.h @@ -25,6 +25,7 @@ Author: #include "ast/for_each_expr.h" #include "ast/reg_decl_plugins.h" #include "ast/ast_pp.h" +#include "ast/ast_ll_pp.h" #include "ast/rewriter/array_rewriter.h" #include "ast/rewriter/var_subst.h" @@ -88,13 +89,15 @@ namespace euf { } match_goal(unsigned level, unsigned offset, expr_ref const& pat, expr_ref const& t) noexcept : - base_offset(offset), pat(pat), t(t), level(level) {} + base_offset(offset), pat(pat), t(t), level(level) { + SASSERT(pat->get_sort() == t->get_sort()); + } unsigned term_offset() const { return base_offset + delta_offset; } unsigned pat_offset() const { return base_offset + delta_offset; } std::ostream& display(std::ostream& out) const { - return out << "[" << level << ":" << base_offset + delta_offset << "] " << pat << " ~ " << t << "\n"; + return out << "[" << level << ":" << base_offset + delta_offset << "] " << mk_bounded_pp(pat, pat.m()) << " ~ " << mk_bounded_pp(t, t.m()) << "\n"; } }; @@ -329,6 +332,8 @@ namespace euf { bool consume_work(match_goal& wi); expr_ref whnf(expr* e, unsigned offset) const; + + expr_ref whnf_star(expr *e, unsigned offset) const; bool is_bound_var(expr* v, unsigned offset) const { return is_var(v) && to_var(v)->get_idx() < offset; } @@ -389,11 +394,23 @@ namespace euf { bool is_ho_pattern(app* p); + // Register an alias pattern (e.g., after stripping ground elements) + // that maps to the same original pattern as full_p + void register_ho_pattern(app* alias_p, app* full_p); + void refine_ho_match(app* p, expr_ref_vector& s); bool is_free(app* p, unsigned i) const { return m_hopat2free_vars[p].contains(i); } quantifier* hoq2q(quantifier* q) const { return m_hoq2q[q]; } + + svector> const* get_flex_subterms(app* p) const { + auto orig_p = m_hopat2pat.find_core(p); + if (!orig_p) return nullptr; + auto abs = m_pat2abs.find_core(orig_p->get_data().get_value()); + return abs ? &abs->get_data().get_value() : nullptr; + } + }; } diff --git a/src/ast/fpa/fpa2bv_converter.cpp b/src/ast/fpa/fpa2bv_converter.cpp index 5e02a101e..d2431174e 100644 --- a/src/ast/fpa/fpa2bv_converter.cpp +++ b/src/ast/fpa/fpa2bv_converter.cpp @@ -2976,13 +2976,12 @@ void fpa2bv_converter::mk_to_real(func_decl * f, unsigned num, expr * const * ar prev_bit = bit; } - expr_ref one_div_exp2(m); - one_div_exp2 = m_arith_util.mk_div(one, exp2); - exp2 = m.mk_ite(exp_is_neg, one_div_exp2, exp2); - dbg_decouple("fpa2bv_to_real_exp2", exp2); - - expr_ref res(m), two_exp2(m), minus_res(m), sgn_is_1(m); + expr_ref two_exp2(m), one_div_two_exp2(m); two_exp2 = m_arith_util.mk_power(two, exp2); + one_div_two_exp2 = m_arith_util.mk_div(one, two_exp2); + two_exp2 = m.mk_ite(exp_is_neg, one_div_two_exp2, two_exp2); + dbg_decouple("fpa2bv_to_real_exp2", two_exp2); + expr_ref res(m), minus_res(m), sgn_is_1(m); res = m_arith_util.mk_mul(rsig, two_exp2); minus_res = m_arith_util.mk_uminus(res); sgn_is_1 = m.mk_eq(sgn, bv1); @@ -2990,7 +2989,7 @@ void fpa2bv_converter::mk_to_real(func_decl * f, unsigned num, expr * const * ar dbg_decouple("fpa2bv_to_real_sig_times_exp2", res); TRACE(fpa2bv_to_real, tout << "rsig = " << mk_ismt2_pp(rsig, m) << std::endl; - tout << "exp2 = " << mk_ismt2_pp(exp2, m) << std::endl;); + tout << "two_exp2 = " << mk_ismt2_pp(two_exp2, m) << std::endl;); expr_ref unspec(m); mk_to_real_unspecified(f, num, args, unspec); diff --git a/src/ast/normal_forms/defined_names.cpp b/src/ast/normal_forms/defined_names.cpp index 997022c69..9bb2b89c2 100644 --- a/src/ast/normal_forms/defined_names.cpp +++ b/src/ast/normal_forms/defined_names.cpp @@ -121,9 +121,6 @@ app * defined_names::impl::gen_name(expr * e, sort_ref_buffer & var_sorts, buffe sort * range = e->get_sort(); func_decl * new_skolem_decl = m.mk_fresh_func_decl(m_z3name, symbol::null, domain.size(), domain.data(), range); app * n = m.mk_app(new_skolem_decl, new_args.size(), new_args.data()); - if (is_lambda(e)) { - m.add_lambda_def(new_skolem_decl, to_quantifier(e)); - } return n; } @@ -193,43 +190,7 @@ void defined_names::impl::mk_definition(expr * e, app * n, sort_ref_buffer & var else if (m.is_term_ite(e)) { bound_vars(var_sorts, var_names, MK_OR(MK_NOT(to_app(e)->get_arg(0)), MK_EQ(n, to_app(e)->get_arg(1))), n, defs); bound_vars(var_sorts, var_names, MK_OR(to_app(e)->get_arg(0), MK_EQ(n, to_app(e)->get_arg(2))), n, defs); - } - else if (is_lambda(e)) { - // n(y) = \x . M[x,y] - // => - // n(y)[x] = M, forall x y - // - // NB. The pattern is incomplete. - // consider store(a, i, v) == \lambda j . if i = j then v else a[j] - // the instantiation rules for store(a, i, v) are: - // store(a, i, v)[j] = if i = j then v else a[j] with patterns {a[j], store(a, i, v)} { store(a, i, v)[j] } - // The first pattern is not included. - // TBD use a model-based scheme for extracting instantiations instead of - // using multi-patterns. - // - - quantifier* q = to_quantifier(e); - expr_ref_vector args(m); - expr_ref n2(m), n3(m); - var_shifter vs(m); - vs(n, q->get_num_decls(), n2); - args.push_back(n2); - var_sorts.append(q->get_num_decls(), q->get_decl_sorts()); - var_names.append(q->get_num_decls(), q->get_decl_names()); - for (unsigned i = 0; i < q->get_num_decls(); ++i) { - args.push_back(m.mk_var(q->get_num_decls() - i - 1, q->get_decl_sort(i))); - } - array_util autil(m); - func_decl * f = nullptr; - if (autil.is_as_array(n2, f)) { - n3 = m.mk_app(f, args.size()-1, args.data() + 1); - } - else { - n3 = autil.mk_select(args.size(), args.data()); - } - bound_vars(var_sorts, var_names, MK_EQ(q->get_expr(), n3), to_app(n3), defs, m.lambda_def_qid()); - - } + } else { bound_vars(var_sorts, var_names, MK_EQ(e, n), n, defs); } diff --git a/src/ast/normal_forms/name_exprs.cpp b/src/ast/normal_forms/name_exprs.cpp index 9cf1ab08f..577c3d51a 100644 --- a/src/ast/normal_forms/name_exprs.cpp +++ b/src/ast/normal_forms/name_exprs.cpp @@ -100,7 +100,7 @@ class name_quantifier_labels : public name_exprs_core { public: pred(ast_manager & m):m(m) {} bool operator()(expr * t) override { - return is_quantifier(t) || m.is_label(t); + return (is_quantifier(t) && !is_lambda(t)) || m.is_label(t); } }; @@ -127,7 +127,7 @@ class name_nested_formulas : public name_exprs_core { TRACE(name_exprs, tout << "name_nested_formulas::pred:\n" << mk_ismt2_pp(t, m) << "\n";); if (is_app(t)) return to_app(t)->get_family_id() == m.get_basic_family_id() && to_app(t)->get_num_args() > 0 && t != m_root; - return m.is_label(t) || is_quantifier(t); + return m.is_label(t) || (is_quantifier(t) && !is_lambda(t)); } }; diff --git a/src/ast/normal_forms/pull_quant.cpp b/src/ast/normal_forms/pull_quant.cpp index a37580b86..be84afcc9 100644 --- a/src/ast/normal_forms/pull_quant.cpp +++ b/src/ast/normal_forms/pull_quant.cpp @@ -188,7 +188,7 @@ struct pull_quant::imp { var_names.data(), nested_q->get_expr(), std::min(q->get_weight(), nested_q->get_weight()), - m.is_lambda_def(q) ? symbol("pulled-lambda") : q->get_qid()); + q->get_qid()); } void pull_quant1(quantifier * q, expr * new_expr, expr_ref & result) { diff --git a/src/ast/pattern/pattern_inference.cpp b/src/ast/pattern/pattern_inference.cpp index 6d3518de0..6fd8c684d 100644 --- a/src/ast/pattern/pattern_inference.cpp +++ b/src/ast/pattern/pattern_inference.cpp @@ -254,6 +254,27 @@ void pattern_inference_cfg::collect::save_candidate(expr * n, unsigned delta) { } return; } + case AST_QUANTIFIER: { + quantifier * q = to_quantifier(n); + unsigned num_decls = q->get_num_decls(); + info * body_info = nullptr; + m_cache.find(entry(q->get_expr(), delta + num_decls), body_info); + if (body_info == nullptr) { + save(n, delta, nullptr); + return; + } + // The lambda/quantifier itself is a valid sub-term in a pattern. + // Propagate the free variables from the body (they already refer + // to the outer quantifier's bindings) and keep the node as-is. + expr * new_body = body_info->m_node.get(); + quantifier_ref new_q(m); + if (new_body != q->get_expr()) + new_q = m.update_quantifier(q, new_body); + else + new_q = q; + save(n, delta, alloc(info, m, new_q, body_info->m_free_vars, body_info->m_size + 1)); + return; + } default: save(n, delta, nullptr); return; @@ -363,6 +384,8 @@ bool pattern_inference_cfg::contains_subpattern::operator()(expr * n) { break; case AST_VAR: break; + case AST_QUANTIFIER: + break; default: UNREACHABLE(); } @@ -525,7 +548,7 @@ void pattern_inference_cfg::reset_pre_patterns() { bool pattern_inference_cfg::is_forbidden(app * n) const { - func_decl const * decl = n->get_decl(); + func_decl * decl = n->get_decl(); if (is_ground(n)) return false; // Remark: skolem constants should not be used in patterns, since they do not diff --git a/src/ast/recfun_decl_plugin.cpp b/src/ast/recfun_decl_plugin.cpp index 660dcab28..57c1d4d12 100644 --- a/src/ast/recfun_decl_plugin.cpp +++ b/src/ast/recfun_decl_plugin.cpp @@ -444,7 +444,8 @@ namespace recfun { promise_def plugin::mk_def(symbol const& name, unsigned n, sort *const * params, sort * range, bool is_generated) { def* d = u().decl_fun(name, n, params, range, is_generated); - SASSERT(!m_defs.contains(d->get_decl())); + if (m_defs.contains(d->get_decl())) + throw default_exception(std::string("recursive function ") + name.str() + " already defined"); m_defs.insert(d->get_decl(), d); return promise_def(&u(), d); } diff --git a/src/ast/rewriter/array_rewriter.cpp b/src/ast/rewriter/array_rewriter.cpp index 67f969197..5d3ddf1dd 100644 --- a/src/ast/rewriter/array_rewriter.cpp +++ b/src/ast/rewriter/array_rewriter.cpp @@ -750,7 +750,10 @@ bool array_rewriter::add_store(expr_ref_vector& args, unsigned num_idxs, expr* e } if (is_var(e1) && is_ground(e2)) { unsigned idx = to_var(e1)->get_idx(); - args[num_idxs - idx - 1] = e2; + unsigned nidx = num_idxs - idx - 1; + if (args.get(nidx) && args.get(nidx) != e2) + return false; + args[nidx] = e2; } else { return false; @@ -858,19 +861,45 @@ br_status array_rewriter::mk_eq_core(expr * lhs, expr * rhs, expr_ref & result) return false; }; + auto domain_is_larger_than = [&](sort* s, unsigned num_stores) { + unsigned sz = get_array_arity(s); + rational dsz(1); + for (unsigned i = 0; i < sz; ++i) { + sort* d = get_array_domain(s, i); + if (d->is_infinite()) + return true; + if (d->is_very_big()) + return false; + dsz *= rational(d->get_num_elements().size(), rational::ui64()); + if (dsz > rational(num_stores, rational::ui64())) + return true; + } + return false; + }; + + expr* lhs1 = lhs; + expr* rhs1 = rhs; + unsigned num_lhs = 0, num_rhs = 0; + while (m_util.is_store(lhs1)) { + lhs1 = to_app(lhs1)->get_arg(0); + ++num_lhs; + } + while (m_util.is_store(rhs1)) { + rhs1 = to_app(rhs1)->get_arg(0); + ++num_rhs; + } + + if (m_util.is_const(lhs1, v) && m_util.is_const(rhs1, w) && + domain_is_larger_than(lhs->get_sort(), num_lhs + num_rhs)) { + mk_eq(lhs, lhs, rhs, fmls); + mk_eq(rhs, lhs, rhs, fmls); + fmls.push_back(m().mk_eq(v, w)); + result = m().mk_and(fmls); + return BR_REWRITE_FULL; + } + if (m_expand_store_eq) { - expr* lhs1 = lhs; - expr* rhs1 = rhs; - unsigned num_lhs = 0, num_rhs = 0; - while (m_util.is_store(lhs1)) { - lhs1 = to_app(lhs1)->get_arg(0); - ++num_lhs; - } - while (m_util.is_store(rhs1)) { - rhs1 = to_app(rhs1)->get_arg(0); - ++num_rhs; - } if (lhs1 == rhs1) { mk_eq(lhs, lhs, rhs, fmls); mk_eq(rhs, lhs, rhs, fmls); diff --git a/src/ast/rewriter/enum2bv_rewriter.cpp b/src/ast/rewriter/enum2bv_rewriter.cpp index d2c5fd122..8210cdc93 100644 --- a/src/ast/rewriter/enum2bv_rewriter.cpp +++ b/src/ast/rewriter/enum2bv_rewriter.cpp @@ -225,6 +225,7 @@ struct enum2bv_rewriter::imp { new_body_ref = mk_and(bounds); break; case lambda_k: + case choice_k: UNREACHABLE(); break; } diff --git a/src/ast/rewriter/rewriter_def.h b/src/ast/rewriter/rewriter_def.h index ebfc71482..ad4702de5 100644 --- a/src/ast/rewriter/rewriter_def.h +++ b/src/ast/rewriter/rewriter_def.h @@ -561,9 +561,13 @@ void rewriter_tpl::process_quantifier(quantifier * q, frame & fr) { expr * const * np = it + 1; expr * const * nnp = np + num_pats; unsigned j = 0; - for (unsigned i = 0; i < num_pats; ++i) + for (unsigned i = 0; i < num_pats; ++i) { if (m_manager.is_pattern(np[i])) new_pats[j++] = np[i]; + else { + IF_VERBOSE(10, verbose_stream() << "[rewriter] dropping pattern (is_pattern check failed) for qid=" << q->get_qid() << " pattern[" << i << "]: " << mk_ismt2_pp(np[i], m_manager, 3) << "\n";); + } + } new_pats.shrink(j); num_pats = j; j = 0; @@ -664,7 +668,7 @@ template void rewriter_tpl::display_bindings(std::ostream& out) { for (unsigned i = 0; i < m_bindings.size(); ++i) { if (m_bindings[i]) - out << i << ": " << mk_ismt2_pp(m_bindings[i], m()) << ";\n"; + out << i << ": " << mk_ismt2_pp(m_bindings[i], m()) << " : " << mk_pp(m_bindings[i]->get_sort(), m()) << ";\n"; } } diff --git a/src/ast/simplifiers/CMakeLists.txt b/src/ast/simplifiers/CMakeLists.txt index d43bbe203..aae8227e4 100644 --- a/src/ast/simplifiers/CMakeLists.txt +++ b/src/ast/simplifiers/CMakeLists.txt @@ -17,6 +17,7 @@ z3_add_component(simplifiers euf_completion.cpp extract_eqs.cpp factor_simplifier.cpp + fold_unfold.cpp linear_equation.cpp max_bv_sharing.cpp model_reconstruction_trail.cpp diff --git a/src/ast/simplifiers/bound_propagator.cpp b/src/ast/simplifiers/bound_propagator.cpp index 240ba7a98..a73d81fb8 100644 --- a/src/ast/simplifiers/bound_propagator.cpp +++ b/src/ast/simplifiers/bound_propagator.cpp @@ -381,7 +381,7 @@ bool bound_propagator::relevant_bound(var x, double new_k) const { if (b == nullptr) return true; // variable did not have a bound - double interval_size; + double interval_size = 0.0; bool bounded = get_interval_size(x, interval_size); if (!is_int(x)) { @@ -939,4 +939,3 @@ void bound_propagator::display(std::ostream & out) const { } - diff --git a/src/ast/simplifiers/dependent_expr_state.cpp b/src/ast/simplifiers/dependent_expr_state.cpp index 9cfd8d1a0..1b9315466 100644 --- a/src/ast/simplifiers/dependent_expr_state.cpp +++ b/src/ast/simplifiers/dependent_expr_state.cpp @@ -88,22 +88,6 @@ void dependent_expr_state::freeze_recfun() { m_num_recfun = sz; } -/** -* Freeze all functions used in lambda defined declarations -*/ -void dependent_expr_state::freeze_lambda() { - auto& m = m_frozen_trail.get_manager(); - unsigned sz = m.lambda_defs().size(); - if (m_num_lambdas >= sz) - return; - - ast_mark visited; - for (auto const& [f, body] : m.lambda_defs()) - freeze_terms(body, false, visited); - m_trail.push(value_trail(m_num_lambdas)); - m_num_lambdas = sz; -} - /** * The current qhead is to be updated to qtail. @@ -122,8 +106,7 @@ void dependent_expr_state::freeze_suffix() { if (m_suffix_frozen) return; m_suffix_frozen = true; - freeze_recfun(); - freeze_lambda(); + freeze_recfun(); auto& m = m_frozen_trail.get_manager(); ast_mark visited; ptr_vector es; diff --git a/src/ast/simplifiers/dependent_expr_state.h b/src/ast/simplifiers/dependent_expr_state.h index f30671bef..504f67ad0 100644 --- a/src/ast/simplifiers/dependent_expr_state.h +++ b/src/ast/simplifiers/dependent_expr_state.h @@ -51,7 +51,6 @@ class dependent_expr_state { func_decl_ref_vector m_frozen_trail; void freeze_prefix(); void freeze_recfun(); - void freeze_lambda(); void freeze_terms(expr* term, bool only_as_array, ast_mark& visited); void freeze(func_decl* f); struct thaw : public trail { diff --git a/src/ast/simplifiers/elim_unconstrained.cpp b/src/ast/simplifiers/elim_unconstrained.cpp index 974b37a00..6b53882cd 100644 --- a/src/ast/simplifiers/elim_unconstrained.cpp +++ b/src/ast/simplifiers/elim_unconstrained.cpp @@ -121,7 +121,7 @@ eliminate: elim_unconstrained::elim_unconstrained(ast_manager& m, dependent_expr_state& fmls) : dependent_expr_simplifier(m, fmls), m_inverter(m), m_lt(*this), m_heap(1024, m_lt), m_trail(m), m_args(m) { std::function is_var = [&](expr* e) { - return is_uninterp_const(e) && !m_fmls.frozen(e) && get_node(e).is_root() && get_node(e).num_parents() <= 1; + return is_uninterp_const(e) && !m_fmls.frozen(e) && !m_disabled.is_marked(e) && get_node(e).is_root() && get_node(e).num_parents() <= 1; }; m_inverter.set_is_var(is_var); } @@ -247,10 +247,12 @@ elim_unconstrained::node& elim_unconstrained::get_node(expr* t) { m_heap.increased(arg->get_id()); } } - else if (is_quantifier(t)) { - node& ch = get_node(to_quantifier(t)->get_expr()); + else if (is_quantifier(t)) { + auto body = to_quantifier(t)->get_expr(); + node& ch = get_node(body); SASSERT(ch.is_root()); ch.add_parent(*n); + disable(body); } } return *n; @@ -411,10 +413,9 @@ void elim_unconstrained::update_model_trail(generic_model_converter& mc, vector< case generic_model_converter::instruction::HIDE: break; case generic_model_converter::instruction::ADD: - // new_def = entry.m_def; - // (*rp)(new_def); - new_def = m.mk_const(entry.m_f); - sub->insert(new_def, new_def, nullptr, nullptr); + new_def = entry.m_def; + (*rp)(new_def); + sub->insert(m.mk_const(entry.m_f), new_def, nullptr, nullptr); break; } } @@ -436,6 +437,7 @@ void elim_unconstrained::reduce() { assert_normalized(old_fmls); update_model_trail(*mc, old_fmls); mc->reset(); + m_disabled.reset(); } } @@ -443,3 +445,21 @@ void elim_unconstrained::updt_params(params_ref const& p) { smt_params_helper sp(p); m_config.m_enabled = sp.elim_unconstrained(); } + +void elim_unconstrained::disable(expr* e) { + if (m_disabled.is_marked(e)) + return; + + ptr_buffer todo; + todo.push_back(e); + while (!todo.empty()) { + e = todo.back(); + todo.pop_back(); + if (m_disabled.is_marked(e)) + continue; + m_disabled.mark(e); + if (is_app(e)) + for (auto arg : *to_app(e)) + todo.push_back(arg); + } +} diff --git a/src/ast/simplifiers/elim_unconstrained.h b/src/ast/simplifiers/elim_unconstrained.h index 4a248b44f..4a946c191 100644 --- a/src/ast/simplifiers/elim_unconstrained.h +++ b/src/ast/simplifiers/elim_unconstrained.h @@ -92,6 +92,7 @@ class elim_unconstrained : public dependent_expr_simplifier { stats m_stats; config m_config; bool m_created_compound = false; + expr_mark m_disabled; bool is_var_lt(int v1, int v2) const; node& get_node(unsigned n) const { return *m_nodes[n]; } @@ -108,6 +109,7 @@ class elim_unconstrained : public dependent_expr_simplifier { expr* reconstruct_term(node& n); void assert_normalized(vector& old_fmls); void update_model_trail(generic_model_converter& mc, vector const& old_fmls); + void disable(expr *e); public: diff --git a/src/ast/simplifiers/fold_unfold.cpp b/src/ast/simplifiers/fold_unfold.cpp new file mode 100644 index 000000000..346f96683 --- /dev/null +++ b/src/ast/simplifiers/fold_unfold.cpp @@ -0,0 +1,396 @@ +/*++ +Copyright (c) 2022 Microsoft Corporation + +Module Name: + + fold_unfold.h + +Abstract: + + fold-unfold simplifier + +Author: + + Nikolaj Bjorner (nbjorner) 2025-11-5. + +- remove alias x = y +- remove alias with const x = k +- fold-unfold simplification x = f(y), y = g(z), f(g(z)) = u -> x |-> u + +- assign levels to E-nodes: + - dfs over roots. + - visit children, assign level + - +- remove alias with linear x = f(y) -> x |-> f(y) if level y < level x +--*/ + +#include "ast/ast_pp.h" +#include "ast/simplifiers/fold_unfold.h" +#include "ast/rewriter/expr_replacer.h" +#include "util/union_find.h" +#include "params/smt_params_helper.hpp" + +namespace euf { + + fold_unfold::fold_unfold(ast_manager& m, dependent_expr_state& fmls) + : dependent_expr_simplifier(m, fmls), + m_rewriter(m), + m_egraph(m) { + register_extract_eqs(m, m_extract_plugins); + m_rewriter.set_flat_and_or(false); + // flat sum/prod := false + } + + void fold_unfold::reduce() { + if (!m_config.m_enabled) + return; + + m_fmls.freeze_suffix(); + + for (extract_eq* ex : m_extract_plugins) + ex->pre_process(m_fmls); + + reduce_alias(true); + reduce_linear(); + reduce_alias(false); + } + + void fold_unfold::reduce_alias(bool fuf) { + m_subst = nullptr; + dep_eq_vector eqs; + get_eqs(eqs); + extract_subst(fuf, eqs); + vector old_fmls; + apply_subst(old_fmls); + } + + void fold_unfold::get_eqs(dep_eq_vector& eqs) { + for (extract_eq* ex : m_extract_plugins) + for (unsigned i : indices()) + ex->get_eqs(m_fmls[i], eqs); + } + + void fold_unfold::extract_subst(bool fuf, dep_eq_vector const& eqs) { + m_find.reset(); + for (auto const& [orig, v, t, d] : eqs) { + auto a = mk_enode(v); + auto b = mk_enode(t); + // verbose_stream() << mk_bounded_pp(v, m) << " == " << mk_bounded_pp(t, m) << "\n"; + proof_ref pr(m); + auto j = to_ptr(push_pr_dep(pr, d)); + m_egraph.merge(a, b, j); + } + + // choose uninterpreted or value representative + auto find_rep = [&](enode *a, ptr_buffer& vars) { + enode *rep = nullptr; + for (auto b : euf::enode_class(a)) { + expr *t = b->get_expr(); + if (is_uninterp_const(t)) + vars.push_back(b); + if (m.is_value(t)) + rep = b; + } + if (!rep) { + for (auto v : vars) + if (!rep || v->get_id() < rep->get_id()) + rep = v; + } + return rep; + }; + + for (auto a : m_egraph.nodes()) { + if (!a->is_root()) + continue; + ptr_buffer vars; + enode *rep = find_rep(a, vars); + if (!rep) + continue; + for (auto w : vars) { + if (w != rep) + m_find.setx(w->get_id(), rep, nullptr); + } + } + if (fuf) { + // find new equalities by performing fold-unfold + vector> new_eqs; + for (auto n : m_egraph.nodes()) { + if (!n->is_root()) + continue; + auto ne = n->get_expr(); + unsigned depth = 3; + vector> es; + unfold(depth, n, nullptr, es); + // verbose_stream() << "unfolds " << es.size() << "\n"; + for (auto [e, d] : es) { + expr_ref r(m); + proof_ref pr(m); + fold(e, r, pr); + if (ne == r) + continue; + new_eqs.push_back({n, r, pr, d}); + } + } + for (auto const &[a, t, pr, d] : new_eqs) { + auto b = mk_enode(t); + auto j = to_ptr(push_pr_dep(pr, d)); + m_egraph.merge(a, b, j); + } + } + + for (auto a : m_egraph.nodes()) { + if (!a->is_root()) + continue; + ptr_buffer vars; + enode *rep = find_rep(a, vars); + if (!rep) + continue; + for (auto v : vars) { + if (v == rep) + continue; + m_find.setx(v->get_id(), rep, nullptr); + // verbose_stream() << "insert " << mk_pp(v->get_expr(), m) << " " << mk_pp(rep->get_expr(), m) << "\n"; + insert_subst(v->get_expr(), rep->get_expr(), explain_eq(v, rep)); + m_stats.m_num_elim_vars++; + } + } + } + + expr_dependency *fold_unfold::explain_eq(enode *a, enode *b) { + if (a == b) + return nullptr; + ptr_vector just; + m_egraph.begin_explain(); + m_egraph.explain_eq(just, nullptr, a, b); + m_egraph.end_explain(); + expr_dependency *d = nullptr; + for (size_t *j : just) + d = m.mk_join(d, m_pr_dep[from_ptr(j)].second); + return d; + } + + unsigned fold_unfold::push_pr_dep(proof *pr, expr_dependency *d) { + unsigned sz = m_pr_dep.size(); + SASSERT(!m.proofs_enabled() || pr); + m_pr_dep.push_back({proof_ref(pr, m), d}); + m_trail.push(push_back_vector(m_pr_dep)); + return sz; + } + + enode *fold_unfold::mk_enode(expr *e) { + m_todo.push_back(e); + enode *n; + while (!m_todo.empty()) { + e = m_todo.back(); + if (m_egraph.find(e)) { + m_todo.pop_back(); + continue; + } + if (!is_app(e)) { + m_egraph.mk(e, m_generation, 0, nullptr); + m_todo.pop_back(); + continue; + } + m_args.reset(); + unsigned sz = m_todo.size(); + for (expr *arg : *to_app(e)) { + n = m_egraph.find(arg); + if (n) + m_args.push_back(n); + else + m_todo.push_back(arg); + } + if (sz == m_todo.size()) { + n = m_egraph.mk(e, m_generation, m_args.size(), m_args.data()); + if (m_egraph.get_plugin(e->get_sort()->get_family_id())) + m_egraph.add_th_var(n, m_th_var++, e->get_sort()->get_family_id()); + if (!m.is_eq(e)) { + for (auto ch : m_args) + for (auto idv : euf::enode_th_vars(*ch)) + m_egraph.register_shared(n, idv.get_id()); + } + m_todo.pop_back(); + } + } + return m_egraph.find(e); + } + + + void fold_unfold::fold(expr *e, expr_ref &result, proof_ref &pr) { + m_rewriter(e, result, pr); + } + + void fold_unfold::unfold(unsigned n, enode *e, expr_dependency* d, vector>& es) { + if (n == 0) { + es.push_back({expr_ref(e->get_expr(), m), d}); + return; + } + if (es.size() > 10) + return; + unsigned count = 0; + for (auto sib : euf::enode_class(e)) { + auto sib_e = sib->get_expr(); + if (!is_app(sib_e)) + continue; + if (is_uninterp_const(sib_e)) { + auto f = m_find.get(sib->get_id(), nullptr); + if (f && f != sib) + continue; + } + ++count; + expr_ref_vector args(m); + expr_dependency *d1 = m.mk_join(d, explain_eq(sib, e)); + unfold_arg(n, 0, sib, args, d1, es); + if (count > 2) + break; + } + // verbose_stream() << "count " << count << "\n"; + } + + void fold_unfold::unfold_arg(unsigned n, unsigned i, enode* e, expr_ref_vector& args, expr_dependency* d, + vector>& es) { + if (i == e->num_args()) { + es.push_back({expr_ref(m.mk_app(e->get_decl(), args), m), d}); + return; + } + vector> es_arg; + unfold(n - 1, e->get_arg(i), d, es_arg); + for (auto [arg, dep] : es_arg) { + args.push_back(arg); + unfold_arg(n, i + 1, e, args, dep, es); + args.pop_back(); + if (es.size() > 10) + return; + } + } + + void fold_unfold::insert_subst(expr * v, expr * t, expr_dependency* d) { + if (!m_subst) + m_subst = alloc(expr_substitution, m, true, false); + m_subst->insert(v, t, d); + } + + void fold_unfold::apply_subst(vector &old_fmls) { + if (!m.inc()) + return; + if (!m_subst) + return; + + scoped_ptr rp = mk_default_expr_replacer(m, false); + rp->set_substitution(m_subst.get()); + + for (unsigned i : indices()) { + auto [f, p, d] = m_fmls[i](); + auto [new_f, new_dep] = rp->replace_with_dep(f); + proof_ref new_pr(m); + expr_ref tmp(m); + m_rewriter(new_f, tmp, new_pr); + if (tmp == f) + continue; + new_dep = m.mk_join(d, new_dep); + old_fmls.push_back(m_fmls[i]); + m_fmls.update(i, dependent_expr(m, tmp, mp(p, new_pr), new_dep)); + } + m_fmls.model_trail().push(m_subst.detach(), old_fmls, false); + } + + void fold_unfold::set_levels() { + m_node2level.reset(); + m_level2node.reset(); + m_level_count = 0; + for (auto n : m_egraph.nodes()) + if (n->is_root()) + set_level(n); + for (auto n : m_egraph.nodes()) + if (n->is_root()) + n->unmark1(); + } + + void fold_unfold::set_level(enode* n) { + SASSERT(n->is_root()); + + if (m_node2level.get(n->get_id(), UINT_MAX) != UINT_MAX) + return; + + if (!n->is_marked1()) { + n->mark1(); + for (auto b : enode_class(n)) { + for (auto arg : enode_args(b)) + set_level(arg->get_root()); + } + } + if (m_node2level.get(n->get_id(), UINT_MAX) != UINT_MAX) + return; + for (auto a : enode_class(n)) { + m_node2level.setx(a->get_id(), m_level_count, UINT_MAX); + m_level2node.setx(m_level_count, a, nullptr); + } + ++m_level_count; + } + + void fold_unfold::reduce_linear() { + set_levels(); + m_subst = alloc(expr_substitution, m, true, false); + scoped_ptr rp = mk_default_expr_replacer(m, false); + rp->set_substitution(m_subst.get()); + for (auto n : m_level2node) { + SASSERT(n); + SASSERT(n->is_root()); + // if a is uninterpreted and is not eliminated, + // n is equal to a linear term with lower level argument + // back-substitute the linear term using existing subst. + // update subst with a -> linear term + enode *var = nullptr; + enode *term = nullptr; + for (auto a : enode_class(n)) { + if (m_find.get(a->get_id(), nullptr) != nullptr) // already substituted + continue; + if (is_uninterp_const(a->get_expr())) + var = a; + else if (is_linear_term(a)) + term = a; + } + if (var && term) { + m_find.setx(var->get_id(), term, nullptr); // record that var was replaced + auto dep = explain_eq(var, term); + auto [new_term, new_dep] = rp->replace_with_dep(term->get_expr()); + expr_ref r(m); + proof_ref pr(m); + m_rewriter(new_term, r, pr); + m_subst->insert(var->get_expr(), r, m.mk_join(dep, new_dep)); + } + } + vector old_fmls; + apply_subst(old_fmls); + } + + bool fold_unfold::is_linear_term(enode *n) { + unsigned num_vars = 0; + unsigned level = m_node2level[n->get_root_id()]; + for (auto arg : enode_args(n)) + if (!m.is_value(arg->get_expr())) { + if (m_node2level[arg->get_root_id()] >= level) + return false; + ++num_vars; + } + return num_vars <= 1; + } + + void fold_unfold::updt_params(params_ref const &p) { + m_config.m_enabled = true; + params_ref p1; + p1.set_bool("eliminate_mod", false); + for (auto ex : m_extract_plugins) { + ex->updt_params(p); + ex->updt_params(p1); + } + } + + void fold_unfold::collect_param_descrs(param_descrs &r) {} + + void fold_unfold::collect_statistics(statistics &st) const { + st.update("fold-unfold-steps", m_stats.m_num_steps); + st.update("fold-unfold-elim-vars", m_stats.m_num_elim_vars); + } + +} diff --git a/src/ast/simplifiers/fold_unfold.h b/src/ast/simplifiers/fold_unfold.h new file mode 100644 index 000000000..577801f2d --- /dev/null +++ b/src/ast/simplifiers/fold_unfold.h @@ -0,0 +1,108 @@ + +/*++ +Copyright (c) 2022 Microsoft Corporation + +Module Name: + + fold_unfold.h + +Abstract: + + fold-unfold simplifier + +Author: + + Nikolaj Bjorner (nbjorner) 2025-11-5. + +--*/ + +#pragma once + +#include "util/scoped_ptr_vector.h" +#include "ast/expr_substitution.h" +#include "ast/rewriter/th_rewriter.h" +#include "ast/simplifiers/extract_eqs.h" +#include "ast/euf/euf_egraph.h" + +namespace euf { + + class fold_unfold : public dependent_expr_simplifier { + friend class solve_context_eqs; + + struct stats { + unsigned m_num_steps = 0; + unsigned m_num_elim_vars = 0; + void reset() { + m_num_steps = 0; + m_num_elim_vars = 0; + } + }; + + struct config { + bool m_enabled = true; + }; + + stats m_stats; + config m_config; + th_rewriter m_rewriter; + egraph m_egraph; + scoped_ptr_vector m_extract_plugins; + unsigned_vector m_var2id; // app->get_id() |-> small numeral + scoped_ptr m_subst; // current substitution + vector> m_pr_dep; + + void get_eqs(dep_eq_vector &eqs); + void extract_subst(bool fuf, dep_eq_vector const &eqs); + void insert_subst(expr *v, expr *t, expr_dependency* d); + void apply_subst(vector &old_fmls); + void reduce_alias(bool fuf); + void reduce_linear(); + + size_t *to_ptr(size_t i) const { + return reinterpret_cast(i); + } + unsigned from_ptr(size_t *s) const { + return (unsigned)reinterpret_cast(s); + } + unsigned push_pr_dep(proof *pr, expr_dependency *d); + expr_dependency *explain_eq(enode *a, enode *b); + + ptr_vector m_todo; + enode_vector m_args, m_find; + unsigned_vector m_node2level; + enode_vector m_level2node; + unsigned m_level_count = 0; + + void set_levels(); + void set_level(enode *n); + bool is_linear_term(enode *n); + + unsigned m_generation = 0; + unsigned m_th_var = 0; + enode *mk_enode(expr *e); + + void fold(expr *e, expr_ref &result, proof_ref &pr); + void unfold(unsigned n, enode *e, expr_dependency* d, vector> &es); + void unfold_arg(unsigned n, unsigned i, enode *e, expr_ref_vector &args, expr_dependency *d, + vector> &es); + + public: + fold_unfold(ast_manager &m, dependent_expr_state &fmls); + + char const *name() const override { + return "fold-unfold"; + } + + void reduce() override; + + void updt_params(params_ref const &p) override; + + void collect_param_descrs(param_descrs &r) override; + + void collect_statistics(statistics &st) const override; + + void reset_statistics() override { + m_stats.reset(); + } + }; +} // namespace euf diff --git a/src/ast/simplifiers/solve_eqs.cpp b/src/ast/simplifiers/solve_eqs.cpp index 9022f0c8d..0cdb5de69 100644 --- a/src/ast/simplifiers/solve_eqs.cpp +++ b/src/ast/simplifiers/solve_eqs.cpp @@ -121,7 +121,10 @@ namespace euf { continue; if (!m_config.m_enable_non_ground && has_quantifiers(t)) - continue; + continue; + + if (!m_config.m_enable_non_linear && !is_linear(t)) + continue; bool is_safe = true; unsigned todo_sz = todo.size(); @@ -241,10 +244,12 @@ namespace euf { unsigned count = 0; vector old_fmls; dep_eq_vector eqs; + auto _reset_unsafe = on_scope_exit([&]() { m_unsafe_vars.reset(); }); do { old_fmls.reset(); m_subst_ids.reset(); eqs.reset(); + filter_unsafe_vars(); get_eqs(eqs); extract_dep_graph(eqs); extract_subst(); @@ -262,6 +267,7 @@ namespace euf { old_fmls.reset(); m_subst_ids.reset(); eqs.reset(); + filter_unsafe_vars(); solve_context_eqs context_solve(*this); context_solve.collect_nested_equalities(eqs); extract_dep_graph(eqs); @@ -313,6 +319,15 @@ namespace euf { return num <= m_config.m_max_occs; } + bool solve_eqs::is_linear(expr* t) const { + unsigned num_values = 0; + if (!is_app(t)) + return false; + for (auto arg : *to_app(t)) + num_values += m.is_value(arg) ? 0 : 1; + return num_values <= 1; + } + void solve_eqs::save_subst(vector const& old_fmls) { if (!m_subst->empty()) m_fmls.model_trail().push(m_subst.detach(), old_fmls, false); @@ -322,7 +337,7 @@ namespace euf { m_unsafe_vars.reset(); recfun::util rec(m); for (func_decl* f : rec.get_rec_funs()) - for (expr* term : subterms::all(expr_ref(rec.get_def(f).get_rhs(), m), &m_todo, &m_visited)) + for (expr* term : subterms::all(expr_ref(rec.get_def(f).get_rhs(), m))) m_unsafe_vars.mark(term); } @@ -342,6 +357,7 @@ namespace euf { smt_params_helper sp(p); m_config.m_enabled = sp.solve_eqs(); m_config.m_enable_non_ground = sp.solve_eqs_non_ground(); + m_config.m_enable_non_linear = !sp.solve_eqs_linear(); } void solve_eqs::collect_param_descrs(param_descrs& r) { diff --git a/src/ast/simplifiers/solve_eqs.h b/src/ast/simplifiers/solve_eqs.h index 5f9a993aa..7a2f49111 100644 --- a/src/ast/simplifiers/solve_eqs.h +++ b/src/ast/simplifiers/solve_eqs.h @@ -43,6 +43,7 @@ namespace euf { unsigned m_max_occs = UINT_MAX; bool m_enabled = true; bool m_enable_non_ground = true; + bool m_enable_non_linear = true; }; stats m_stats; @@ -74,6 +75,7 @@ namespace euf { void collect_num_occs(expr * t, expr_fast_mark1 & visited); void collect_num_occs(); bool check_occs(expr* t) const; + bool is_linear(expr *t) const; public: diff --git a/src/cmd_context/CMakeLists.txt b/src/cmd_context/CMakeLists.txt index f3cdb3c03..0b75a8526 100644 --- a/src/cmd_context/CMakeLists.txt +++ b/src/cmd_context/CMakeLists.txt @@ -12,6 +12,7 @@ z3_add_component(cmd_context simplifier_cmds.cpp tactic_cmds.cpp tactic_manager.cpp + tptp_frontend.cpp COMPONENT_DEPENDENCIES rewriter solver diff --git a/src/cmd_context/tptp_frontend.cpp b/src/cmd_context/tptp_frontend.cpp new file mode 100644 index 000000000..c2b2c4fc3 --- /dev/null +++ b/src/cmd_context/tptp_frontend.cpp @@ -0,0 +1,2258 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ast/arith_decl_plugin.h" +#include "ast/array_decl_plugin.h" +#include "ast/expr_abstract.h" +#include "ast/ast_util.h" +#include "ast/rewriter/expr_safe_replace.h" +#include "cmd_context/cmd_context.h" +#include "cmd_context/tptp_frontend.h" +#include "solver/solver.h" +#include "util/error_codes.h" +#include "util/rational.h" +#include "util/timeout.h" +#include "util/z3_exception.h" + +bool g_display_statistics = false; +bool g_display_model = false; + +static void on_timeout() { + std::cout << "% SZS status Timeout\n"; + std::cout.flush(); + _Exit(0); +} + +namespace { + +enum class token_kind { + eof_tok, + id, + str, + lparen, + rparen, + lbrack, + rbrack, + comma, + dot, + colon, + and_tok, + or_tok, + not_tok, + forall_tok, + exists_tok, + type_forall_tok, // !> + type_exists_tok, // ?* + equal_tok, + neq_tok, + iff_tok, + implies_tok, + implied_tok, + xor_tok, + nor_tok, + nand_tok, + gt_tok, + lt_tok, + star_tok, + slash_tok, + minus_tok, + at_tok, + lambda_tok +}; + +struct parse_error : public std::exception { + std::string m_msg; + parse_error(std::string const& msg): m_msg(msg) {} + char const* what() const noexcept override { return m_msg.c_str(); } +}; + +class scoped_regular_stream { + cmd_context& m_ctx; + std::string m_prev; +public: + scoped_regular_stream(cmd_context& ctx, std::ostream& out): m_ctx(ctx), m_prev(ctx.get_regular_stream_name()) { m_ctx.set_regular_stream(out); } + ~scoped_regular_stream() { m_ctx.set_regular_stream(m_prev.c_str()); } +}; + +struct token { + token_kind kind = token_kind::eof_tok; + std::string text; + unsigned line = 1; + unsigned col = 1; +}; + +class lexer { + std::string const& m_input; + size_t m_pos = 0; + unsigned m_line = 1; + unsigned m_col = 1; + + bool eof() const { return m_pos >= m_input.size(); } + char peek(unsigned k = 0) const { return m_pos + k < m_input.size() ? m_input[m_pos + k] : '\0'; } + char get() { + char c = peek(); + if (!eof()) { + ++m_pos; + if (c == '\n') { + ++m_line; + m_col = 1; + } + else { + ++m_col; + } + } + return c; + } + + static bool is_symbol_start(char c) { + return std::isalnum(static_cast(c)) || c == '$' || c == '_'; + } + + static bool is_id_char(char c) { + return std::isalnum(static_cast(c)) || c == '$' || c == '_' || c == '\'' || c == '-'; + } + + void skip_ws_comments() { + while (!eof()) { + if (std::isspace(static_cast(peek()))) { + get(); + continue; + } + if (peek() == '%') { + while (!eof() && get() != '\n') {} + continue; + } + if (peek() == '/' && peek(1) == '*') { + get(); + get(); + while (!eof()) { + if (peek() == '*' && peek(1) == '/') { + get(); + get(); + break; + } + get(); + } + continue; + } + break; + } + } + +public: + lexer(std::string const& input): m_input(input) {} + + token next() { + skip_ws_comments(); + token t; + t.line = m_line; + t.col = m_col; + if (eof()) { + t.kind = token_kind::eof_tok; + return t; + } + + if (peek() == '\'' || peek() == '"') { + char q = get(); + t.kind = token_kind::str; + while (!eof()) { + char c = get(); + if (c == '\\' && !eof()) { + t.text.push_back(c); + t.text.push_back(get()); + continue; + } + if (c == q) return t; + t.text.push_back(c); + } + throw parse_error("unterminated string literal"); + } + + if (peek() == '<' && peek(1) == '=' && peek(2) == '>') { + get(); get(); get(); + t.kind = token_kind::iff_tok; + t.text = "<=>"; + return t; + } + if (peek() == '<' && peek(1) == '~' && peek(2) == '>') { + get(); get(); get(); + t.kind = token_kind::xor_tok; + t.text = "<~>"; + return t; + } + if (peek() == '=' && peek(1) == '>') { + get(); get(); + t.kind = token_kind::implies_tok; + t.text = "=>"; + return t; + } + if (peek() == '<' && peek(1) == '=') { + get(); get(); + t.kind = token_kind::implied_tok; + t.text = "<="; + return t; + } + if (peek() == '~' && peek(1) == '|') { + get(); get(); + t.kind = token_kind::nor_tok; + t.text = "~|"; + return t; + } + if (peek() == '~' && peek(1) == '&') { + get(); get(); + t.kind = token_kind::nand_tok; + t.text = "~&"; + return t; + } + if (peek() == '!' && peek(1) == '=') { + get(); get(); + t.kind = token_kind::neq_tok; + t.text = "!="; + return t; + } + + char c = get(); + switch (c) { + case '(': t.kind = token_kind::lparen; return t; + case ')': t.kind = token_kind::rparen; return t; + case '[': t.kind = token_kind::lbrack; return t; + case ']': t.kind = token_kind::rbrack; return t; + case ',': t.kind = token_kind::comma; return t; + case '.': t.kind = token_kind::dot; return t; + case ':': t.kind = token_kind::colon; return t; + case '&': t.kind = token_kind::and_tok; return t; + case '|': t.kind = token_kind::or_tok; return t; + case '~': t.kind = token_kind::not_tok; return t; + case '!': + if (peek() == '>') { get(); t.kind = token_kind::type_forall_tok; return t; } + if (peek() == '!') { get(); t.kind = token_kind::id; t.text = "!!"; return t; } + t.kind = token_kind::forall_tok; return t; + case '?': + if (peek() == '*') { get(); t.kind = token_kind::type_exists_tok; return t; } + if (peek() == '?') { get(); t.kind = token_kind::id; t.text = "??"; return t; } + t.kind = token_kind::exists_tok; return t; + case '=': t.kind = token_kind::equal_tok; return t; + case '>': t.kind = token_kind::gt_tok; return t; + case '<': t.kind = token_kind::lt_tok; return t; + case '*': t.kind = token_kind::star_tok; return t; + case '/': t.kind = token_kind::slash_tok; return t; + case '-': t.kind = token_kind::minus_tok; return t; + case '@': + if (peek() == '+') { get(); t.kind = token_kind::id; t.text = "@+"; return t; } + if (peek() == '-') { get(); t.kind = token_kind::id; t.text = "@-"; return t; } + t.kind = token_kind::at_tok; return t; + case '^': t.kind = token_kind::lambda_tok; return t; + case '{': + // Modal operators: {$box}, {$dia}, etc. — lex as identifier including braces + t.kind = token_kind::id; + t.text.push_back(c); + while (!eof() && peek() != '}') + t.text.push_back(get()); + if (!eof()) t.text.push_back(get()); // consume '}' + return t; + default: + break; + } + + if (is_symbol_start(c)) { + t.kind = token_kind::id; + t.text.push_back(c); + while (!eof() && is_id_char(peek())) + t.text.push_back(get()); + return t; + } + + std::ostringstream out; + out << "unexpected character '" << c << "' at " << t.line << ":" << t.col; + throw parse_error(out.str()); + } +}; + +struct parsed_type { + std::vector domain; + sort* range = nullptr; + parsed_type(sort* s): range(s) {} + parsed_type(std::vector const& d, sort* r): domain(d), range(r) {} +}; + +class tptp_parser { + cmd_context& m_cmd; + ast_manager& m; + arith_util m_arith; + array_util m_array; + sort* m_univ; + bool m_has_conjecture = false; + bool m_last_name_quoted = false; + std::unordered_map m_sorts; + sort_ref_vector m_pinned_sorts; // prevents cached sorts from being freed + std::unordered_map m_decls; + func_decl_ref_vector m_pinned_decls; // prevents cached func_decls from being freed + expr_ref_vector m_pinned_exprs; // prevents bound variable apps from being freed + std::unordered_map, sort*>> m_typed_decls; + std::vector> m_bound; + bool m_in_at_arg = false; // true when parsing inside @ argument (lambda body stops consuming @) + struct implicit_var_scope { + std::unordered_map vars; + ptr_vector order; + }; + implicit_var_scope* m_implicit_scope = nullptr; + std::unordered_set m_seen_files; + + // Table-driven operator dispatch + using op_builder = std::function; + struct op_entry { + bool is_infix; + unsigned precedence; // only meaningful for infix; higher = tighter binding + bool right_assoc; + op_builder builder; + }; + std::unordered_map m_ops; + + // Infix precedence levels: + static constexpr unsigned PREC_IFF = 1; // <=> <~> + static constexpr unsigned PREC_IMPLIES = 2; // => <= + static constexpr unsigned PREC_OR = 3; // | ~| + static constexpr unsigned PREC_AND = 4; // & ~& + static constexpr unsigned PREC_EQ = 5; // = != + + std::string m_input; + std::unique_ptr m_lex; + token m_curr; + + // Helper: check arity for arithmetic operators + void check_arith_arity(expr_ref_vector const& args, unsigned expected, char const* name) { + if (args.size() != expected) { + std::ostringstream out; + out << "'" << name << "' expects arity " << expected; + throw parse_error(out.str()); + } + } + + // Helper: coerce two arithmetic args to same sort (promote int to real if needed) + std::pair coerce_arith2(expr_ref_vector const& args) { + expr_ref a(args[0], m), b(args[1], m); + // Coerce U-sorted args to Int (from HO encoding / $let bindings) + if (!m_arith.is_int_real(a) && !m_arith.is_int_real(b)) { + a = coerce_arg(a, m_arith.mk_int()); + b = coerce_arg(b, m_arith.mk_int()); + } else if (!m_arith.is_int_real(a)) { + a = coerce_arg(a, b->get_sort()); + } else if (!m_arith.is_int_real(b)) { + b = coerce_arg(b, a->get_sort()); + } + if (m_arith.is_real(a) || m_arith.is_real(b)) { + if (m_arith.is_int(a)) a = expr_ref(m_arith.mk_to_real(a), m); + if (m_arith.is_int(b)) b = expr_ref(m_arith.mk_to_real(b), m); + } + return { a, b }; + } + + // Helper: quotient dispatch (integer division for int/int, real division otherwise) + expr_ref mk_quotient(expr_ref_vector const& args) { + expr_ref a(args[0], m), b(args[1], m); + if (m_arith.is_int(a) && m_arith.is_int(b)) + return expr_ref(m_arith.mk_idiv(a, b), m); + if (m_arith.is_int(a)) a = expr_ref(m_arith.mk_to_real(a), m); + if (m_arith.is_int(b)) b = expr_ref(m_arith.mk_to_real(b), m); + return expr_ref(m_arith.mk_div(a, b), m); + } + + // Map infix token to operator name (returns nullptr if not an infix op token) + char const* token_to_op_name() const { + switch (m_curr.kind) { + case token_kind::iff_tok: return "<=>"; + case token_kind::xor_tok: return "<~>"; + case token_kind::implies_tok: return "=>"; + case token_kind::implied_tok: return "<="; + case token_kind::or_tok: return "|"; + case token_kind::nor_tok: return "~|"; + case token_kind::and_tok: return "&"; + case token_kind::nand_tok: return "~&"; + case token_kind::equal_tok: return "="; + case token_kind::neq_tok: return "!="; + default: return nullptr; + } + } + static std::string to_lower(std::string s) { + for (char& c : s) c = static_cast(std::tolower(static_cast(c))); + return s; + } + + static bool is_var_name(std::string const& s) { + if (s.empty()) return false; + unsigned char c = static_cast(s[0]); + return std::isupper(c) || s[0] == '_'; + } + + std::string loc() const { + std::ostringstream out; + out << m_curr.line << ":" << m_curr.col; + return out.str(); + } + + void next() { m_curr = m_lex->next(); } + + bool is(token_kind k) const { return m_curr.kind == k; } + + bool accept(token_kind k) { + if (is(k)) { + next(); + return true; + } + return false; + } + + void expect(token_kind k, char const* msg) { + if (!accept(k)) { + std::ostringstream out; + out << "expected " << msg << " at " << loc(); + throw parse_error(out.str()); + } + } + + // Grammar: ::= | + // ::= | + // Used universally for parsing identifiers, keywords, and quoted names. + std::string parse_name() { + if (is(token_kind::id) || is(token_kind::str)) { + m_last_name_quoted = is(token_kind::str); + std::string r = m_curr.text; + next(); + return r; + } + std::ostringstream out; + out << "expected identifier at " << loc(); + throw parse_error(out.str()); + } + + sort* get_sort(std::string const& n) { + if (n == "$i") return m_univ; + if (n == "$o") return m.mk_bool_sort(); + if (n == "$int") return m_arith.mk_int(); + if (n == "$rat" || n == "$real") return m_arith.mk_real(); + auto it = m_sorts.find(n); + if (it != m_sorts.end()) return it->second; + sort* s = m.mk_uninterpreted_sort(symbol(n)); + m_sorts.emplace(n, s); + m_pinned_sorts.push_back(s); + return s; + } + + // For higher-order types like ($i > $o), create an uninterpreted sort + // Function type A > B is represented as Array(A, B). + // Multi-argument A * B > C is represented as Array(A, Array(B, C)) (curried). + sort* get_ho_sort(std::vector const& domain, sort* range) { + sort* s = range; + for (int i = (int)domain.size() - 1; i >= 0; --i) + s = m_array.mk_array_sort(domain[i], s); + return s; + } + + static bool is_ttype(sort* s) { + return s->get_name() == symbol("$tType"); + } + + static bool is_nonempty_digit_string(std::string const& s) { + if (s.empty()) return false; + for (char c : s) { + if (!std::isdigit(static_cast(c))) + return false; + } + return true; + } + + // Grammar: ::= | | + // ::= | + // ::= / + // ::= | ... + // Parses integer, rational (N/D), and real (N.D or N.DeE) numeric literals. + expr_ref parse_numeral_from_name(std::string const& n) { + SASSERT(is_nonempty_digit_string(n)); + rational num(n.c_str()); + if (accept(token_kind::dot)) { + std::string frac = parse_name(); + if (!is_nonempty_digit_string(frac)) + throw parse_error("fractional part of decimal literal must be a sequence of digits"); + rational den(1); + for (unsigned i = 0; i < frac.size(); ++i) { + den *= rational(10); + } + rational frac_num(frac.c_str()); + return expr_ref(m_arith.mk_numeral(num + frac_num / den, false), m); + } + if (accept(token_kind::slash_tok)) { + std::string d = parse_name(); + if (!is_nonempty_digit_string(d)) + throw parse_error("denominator of rational literal must be a sequence of digits"); + rational den(d.c_str()); + if (den.is_zero()) + throw parse_error("denominator of rational literal cannot be zero"); + return expr_ref(m_arith.mk_numeral(num / den, false), m); + } + return expr_ref(m_arith.mk_numeral(num, true), m); + } + + static std::string mk_decl_key(std::string const& name, unsigned arity, char tag) { + return std::to_string(name.size()) + ":" + name + "\x1f" + std::to_string(arity) + "\x1f" + tag; + } + + static std::string mk_typed_key(std::string const& name, unsigned arity) { + return mk_decl_key(name, arity, 't'); + } + + func_decl* mk_decl(std::string const& name, unsigned arity, bool pred) { + auto itt = m_typed_decls.find(mk_typed_key(name, arity)); + if (itt != m_typed_decls.end()) { + std::string typed_decl_key = mk_decl_key(name, arity, 'd'); + auto itd = m_decls.find(typed_decl_key); + if (itd != m_decls.end()) return itd->second; + auto const& sig = itt->second; + func_decl* f = m.mk_func_decl(symbol(name), sig.first.size(), sig.first.data(), sig.second); + m_pinned_decls.push_back(f); + m_decls.emplace(typed_decl_key, f); + return f; + } + + std::string key = mk_decl_key(name, arity, pred ? 'p' : 'f'); + auto itd = m_decls.find(key); + if (itd != m_decls.end()) return itd->second; + + std::vector dom(arity, m_univ); + func_decl* f = m.mk_func_decl(symbol(name), arity, dom.data(), pred ? m.mk_bool_sort() : m_univ); + m_pinned_decls.push_back(f); + m_decls.emplace(key, f); + return f; + } + + // Create a modal operator declaration: Bool → Bool + func_decl* mk_modal_op(std::string const& name) { + std::string key = mk_decl_key(name, 1, 'm'); + auto it = m_decls.find(key); + if (it != m_decls.end()) return it->second; + sort* bool_sort = m.mk_bool_sort(); + func_decl* f = m.mk_func_decl(symbol(name), 1, &bool_sort, bool_sort); + m_pinned_decls.push_back(f); + m_decls.emplace(key, f); + return f; + } + + // When a symbol is used with 0 args but has a typed decl with arity > 0, + // create a 0-arity constant with the function type sort (for THF function-as-value). + func_decl* mk_decl_or_ho_const(std::string const& name, unsigned arity, bool pred) { + if (arity == 0) { + // Check if there's a typed decl at any arity > 0 for this name + for (unsigned try_arity = 1; try_arity <= 30; ++try_arity) { + auto itt = m_typed_decls.find(mk_typed_key(name, try_arity)); + if (itt != m_typed_decls.end()) { + auto const& sig = itt->second; + sort* ho = get_ho_sort(sig.first, sig.second); + std::string dkey = mk_decl_key(name, 0, 'h'); + auto itd = m_decls.find(dkey); + if (itd != m_decls.end()) return itd->second; + func_decl* f = m.mk_func_decl(symbol(name), 0, static_cast(nullptr), ho); + m_pinned_decls.push_back(f); + m_decls.emplace(dkey, f); + return f; + } + } + } + return mk_decl(name, arity, pred); + } + + // Coerce an expression to a target sort using boxing/unboxing functions + expr_ref coerce_arg(expr_ref const& e, sort* target) { + sort* actual = e->get_sort(); + if (actual == target) return e; + // Create a boxing function from actual sort to target sort + std::string box_name = std::string("$box_") + actual->get_name().str() + "_to_" + target->get_name().str(); + std::string key = mk_decl_key(box_name, 1, 'f'); + auto it = m_decls.find(key); + func_decl* f; + if (it != m_decls.end()) { + f = it->second; + } else { + f = m.mk_func_decl(symbol(box_name), 1, &actual, target); + m_pinned_decls.push_back(f); + m_decls.emplace(key, f); + } + return expr_ref(m.mk_app(f, e.get()), m); + } + + // Coerce expression to Bool sort — if U-sorted, wrap with an uninterpreted predicate + expr_ref ensure_bool(expr* e) { + if (m.is_bool(e->get_sort())) return expr_ref(e, m); + return coerce_arg(expr_ref(e, m), m.mk_bool_sort()); + } + + // Coerce arguments of a function application to match declared sorts + void coerce_args(func_decl* f, expr_ref_vector& args) { + for (unsigned i = 0; i < args.size() && i < f->get_arity(); ++i) { + sort* expected = f->get_domain(i); + sort* actual = args.get(i)->get_sort(); + if (expected != actual) { + args[i] = coerce_arg(expr_ref(args.get(i), m), expected); + } + } + } + + // Coerce result to expected sort if needed + expr_ref coerce_result(expr_ref const& e, sort* expected) { + if (!expected || e->get_sort() == expected) return e; + return coerce_arg(e, expected); + } + + bool find_bound(std::string const& n, expr_ref& e) const { + for (auto it = m_bound.rbegin(); it != m_bound.rend(); ++it) { + auto jt = it->find(n); + if (jt != it->end()) { + e = jt->second; + return true; + } + } + return false; + } + + bool is_bound_var(app* a) const { + std::string name = a->get_decl()->get_name().str(); + for (auto it = m_bound.rbegin(); it != m_bound.rend(); ++it) { + auto jt = it->find(name); + if (jt != it->end() && jt->second == a) + return true; + } + return false; + } + + bool should_create_implicit_var(std::string const& n) const { + return is_var_name(n) && m_implicit_scope; + } + + app* get_or_create_implicit_var(std::string const& n) { + if (!m_implicit_scope) + throw parse_error("unexpected parser state: missing implicit variable scope"); + auto it = m_implicit_scope->vars.find(n); + if (it != m_implicit_scope->vars.end()) return it->second; + app* c = m.mk_const(symbol(n), m_univ); + m_pinned_exprs.push_back(c); + m_implicit_scope->vars.emplace(n, c); + m_implicit_scope->order.push_back(c); + return c; + } + + class scoped_implicit_vars { + tptp_parser& m_p; + implicit_var_scope* m_prev_scope; + public: + scoped_implicit_vars(tptp_parser& p, implicit_var_scope& scope): + m_p(p), + m_prev_scope(p.m_implicit_scope) { + m_p.m_implicit_scope = &scope; + } + scoped_implicit_vars(scoped_implicit_vars const&) = delete; + scoped_implicit_vars& operator=(scoped_implicit_vars const&) = delete; + scoped_implicit_vars(scoped_implicit_vars&&) = delete; + scoped_implicit_vars& operator=(scoped_implicit_vars&&) = delete; + ~scoped_implicit_vars() { + m_p.m_implicit_scope = m_prev_scope; + } + }; + + expr_ref mk_quantifier(bool is_forall, ptr_vector const& bound, expr_ref const& body) { + SASSERT(body); + if (bound.empty()) return body; + expr_ref b = ensure_bool(body); + return is_forall ? ::mk_forall(m, bound.size(), bound.data(), b.get()) : ::mk_exists(m, bound.size(), bound.data(), b.get()); + } + + // $is_rat(x) ≡ exists a:Int, b:Int. b != 0 && x = a/b + expr_ref mk_is_rat(expr_ref const& x) { + sort* int_sort = m_arith.mk_int(); + app* a = m.mk_fresh_const("a", int_sort); + app* b = m.mk_fresh_const("b", int_sort); + expr_ref ar(m_arith.mk_to_real(a), m); + expr_ref br(m_arith.mk_to_real(b), m); + expr_ref xr(x); + if (m_arith.is_int(x)) + xr = expr_ref(m_arith.mk_to_real(x), m); + expr_ref b_ne_zero(m.mk_not(m.mk_eq(b, m_arith.mk_int(0))), m); + expr_ref x_eq_div(m.mk_eq(xr, m_arith.mk_div(ar, br)), m); + expr_ref body(m.mk_and(b_ne_zero, x_eq_div), m); + ptr_vector bound; + bound.push_back(a); + bound.push_back(b); + return expr_ref(::mk_exists(m, bound.size(), bound.data(), body.get()), m); + } + + // Grammar: ::= | | | + // | () + // ::= | | | + // () + // ::= $oType | $o | $iType | $i | $tType | $real | $rat | $int + parsed_type parse_type_atom() { + if (accept(token_kind::lparen)) { + std::vector prod = parse_type_product_raw(); + if (accept(token_kind::gt_tok)) { + // Full function type inside parens: (A * B > C) or (A > B > C) + parsed_type rhs = parse_type_expr(); + std::vector full_domain = prod; + if (!rhs.domain.empty()) { + // Nested higher-order: (A > B > C) → flatten + full_domain.insert(full_domain.end(), rhs.domain.begin(), rhs.domain.end()); + } + expect(token_kind::rparen, "')'"); + // Return with domain/range preserved for proper flattening + return parsed_type(full_domain, rhs.range); + } + expect(token_kind::rparen, "')'"); + if (prod.size() == 1) + return parsed_type(prod[0]); + // Parenthesized product: (A * B) — used as domain in outer context + return parsed_type(prod, nullptr); + } + std::string n = parse_name(); + // Handle parameterized type constructors: fun(A, B), product_prod(A, B), etc. + if (accept(token_kind::lparen)) { + // Consume type arguments — for monomorphization, we ignore them + // and return the base sort (or m_univ if the constructor result is $tType) + if (!accept(token_kind::rparen)) { + do { parse_type_expr(); } while (accept(token_kind::comma)); + expect(token_kind::rparen, "')'"); + } + // Return m_univ as the monomorphized result of any type constructor application + return parsed_type(m_univ); + } + sort* s = get_sort(n); + // Handle type-level application with @: list @ nat, pair @ A @ B, etc. + // Monomorphize by consuming all @ arguments and returning m_univ. + if (is(token_kind::at_tok)) { + while (accept(token_kind::at_tok)) { + parse_type_atom(); // consume the argument type + } + return parsed_type(m_univ); + } + return parsed_type(s); + } + + // Grammar: ::= * + // | * + // Product types form the domain in mapping types: (A * B) > C + std::vector parse_type_product_raw() { + parsed_type first = parse_type_atom(); + if (!first.domain.empty() && first.range == nullptr) { + // Already a parenthesized product from nested parens + std::vector args = first.domain; + while (accept(token_kind::star_tok)) { + parsed_type t = parse_type_atom(); + if (!t.domain.empty()) { + args.insert(args.end(), t.domain.begin(), t.domain.end()); + } else { + args.push_back(t.range); + } + } + return args; + } + if (!first.domain.empty()) { + // Function type as first element of product — use ho_sort + sort* ho = get_ho_sort(first.domain, first.range); + std::vector args; + args.push_back(ho); + while (accept(token_kind::star_tok)) { + parsed_type t = parse_type_atom(); + if (!t.domain.empty() && t.range != nullptr) { + args.push_back(get_ho_sort(t.domain, t.range)); + } else if (!t.domain.empty()) { + args.insert(args.end(), t.domain.begin(), t.domain.end()); + } else { + args.push_back(t.range); + } + } + return args; + } + std::vector args; + args.push_back(first.range); + while (accept(token_kind::star_tok)) { + parsed_type t = parse_type_atom(); + if (!t.domain.empty() && t.range != nullptr) { + args.push_back(get_ho_sort(t.domain, t.range)); + } else if (!t.domain.empty()) { + args.insert(args.end(), t.domain.begin(), t.domain.end()); + } else { + args.push_back(t.range); + } + } + return args; + } + + // Grammar: ::= | | + // ::= > + // ::= | + // ::= > + parsed_type parse_type_product() { + parsed_type first = parse_type_atom(); + // If atom returned a function type and no '*' follows, return it directly + if (!first.domain.empty() && first.range != nullptr && !is(token_kind::star_tok)) { + return first; + } + // Build product vector + std::vector args; + if (!first.domain.empty() && first.range != nullptr) { + // Function type used as element in a product + args.push_back(get_ho_sort(first.domain, first.range)); + } else if (!first.domain.empty() && first.range == nullptr) { + // Parenthesized product: flatten + args = first.domain; + } else { + args.push_back(first.range); + } + while (accept(token_kind::star_tok)) { + parsed_type t = parse_type_atom(); + if (!t.domain.empty() && t.range != nullptr) { + args.push_back(get_ho_sort(t.domain, t.range)); + } else if (!t.domain.empty()) { + args.insert(args.end(), t.domain.begin(), t.domain.end()); + } else { + args.push_back(t.range); + } + } + return parsed_type(args, nullptr); + } + + // Grammar: ::= | | + // ::= | + // ::= !> [] : + // Parses: atom, atom > atom, (A * B) > C, !>[X:$tType] : T + parsed_type parse_type_expr() { + // Handle type quantification at the expression level for proper domain/range preservation + if (is(token_kind::type_forall_tok) || is(token_kind::type_exists_tok)) { + next(); + expect(token_kind::lbrack, "'['"); + std::vector type_params; + if (!accept(token_kind::rbrack)) { + do { + std::string tv = parse_name(); + if (accept(token_kind::colon)) + parse_type_expr(); // consume $tType annotation + m_sorts.insert_or_assign(tv, m_univ); + type_params.push_back(m_univ); + } while (accept(token_kind::comma)); + expect(token_kind::rbrack, "']'"); + } + expect(token_kind::colon, "':'"); + parsed_type inner = parse_type_expr(); + // Prepend type params to domain + if (!type_params.empty()) { + std::vector full_domain = type_params; + full_domain.insert(full_domain.end(), inner.domain.begin(), inner.domain.end()); + return parsed_type(full_domain, inner.range); + } + return inner; + } + parsed_type prod = parse_type_product(); + if (accept(token_kind::gt_tok)) { + parsed_type rhs = parse_type_expr(); + // prod is either a product (domain non-empty, range==nullptr) or a single sort (domain empty) + std::vector domain; + if (!prod.domain.empty() && prod.range == nullptr) { + domain = prod.domain; + } else if (!prod.domain.empty() && prod.range != nullptr) { + // A function type as domain element — wrap it + domain.push_back(get_ho_sort(prod.domain, prod.range)); + } else { + domain.push_back(prod.range); + } + if (!rhs.domain.empty()) { + // Higher-order result type: A > (B > C) flattened to (A, B) > C + domain.insert(domain.end(), rhs.domain.begin(), rhs.domain.end()); + return parsed_type(domain, rhs.range); + } + return parsed_type(domain, rhs.range); + } + // No '>' follows — must be a single type or a function type from parens + if (!prod.domain.empty() && prod.range != nullptr) { + // Function type from parenthesized expression + return prod; + } + if (!prod.domain.empty() && prod.range == nullptr) { + if (prod.domain.size() != 1) + throw parse_error("type product must be followed by '>'"); + return parsed_type(prod.domain[0]); + } + return parsed_type(prod.range); + } + + void skip_annotations_until_rparen() { + int depth = 0; + while (!is(token_kind::eof_tok)) { + if (accept(token_kind::lparen) || accept(token_kind::lbrack)) { + ++depth; + continue; + } + if (is(token_kind::rparen) || is(token_kind::rbrack)) { + if (depth == 0) return; + --depth; + next(); + continue; + } + next(); + } + } + + void skip_balanced(token_kind open_k, token_kind close_k) { + int depth = 1; + while (depth > 0 && !is(token_kind::eof_tok)) { + if (accept(open_k)) ++depth; + else if (accept(close_k)) --depth; + else next(); + } + } + + // Grammar: ::= | () + // ::= | , + // ::= | () + // ::= $uminus | $sum | $difference | $product | ... + // ::= $ite(,,) + // Handles: numerals, bound variables, let-bound names, defined functors, + // plain function/constant symbols, parenthesized formulas. + expr_ref parse_term(); + + // Grammar: (same as parse_term, primary productions) + expr_ref parse_term_primary() { + if (accept(token_kind::lparen)) { + expr_ref e = parse_formula(); + expect(token_kind::rparen, "')'"); + return e; + } + if (accept(token_kind::lambda_tok)) { + return parse_lambda_expr(); + } + if (accept(token_kind::minus_tok)) { + expr_ref e = parse_term_primary(); + if (!m_arith.is_int_real(e)) + throw parse_error("unary '-' expects arithmetic term"); + return expr_ref(m_arith.mk_uminus(e), m); + } + std::string n = parse_name(); + if (n == "$true") return expr_ref(m.mk_true(), m); + if (n == "$false") return expr_ref(m.mk_false(), m); + + if (is_nonempty_digit_string(n)) { + return parse_numeral_from_name(n); + } + + expr_ref b(m); + // Check bound variables: uppercase (quantifier vars) AND lowercase (let-bound names) + if (!m_last_name_quoted && find_bound(n, b)) { + // For let-bound names followed by '(', apply via array select (function-style let) + if (is(token_kind::lparen)) { + next(); + expr_ref_vector fargs(m); + if (!accept(token_kind::rparen)) { + do { fargs.push_back(parse_term()); } while (accept(token_kind::comma)); + expect(token_kind::rparen, "')'"); + } + expr_ref result = b; + for (unsigned i = 0; i < fargs.size(); ++i) + result = expr_ref(m_array.mk_select(result, fargs.get(i)), m); + return result; + } + return b; + } + if (!m_last_name_quoted && should_create_implicit_var(n)) + return expr_ref(get_or_create_implicit_var(n), m); + + expr_ref_vector args(m); + // $ite needs special parsing: first arg is formula, rest are formulas (branches can be equalities) + if (n == "$ite") { + expect(token_kind::lparen, "'('"); + args.push_back(parse_formula()); + expect(token_kind::comma, "','"); + args.push_back(parse_formula()); + expect(token_kind::comma, "','"); + args.push_back(parse_formula()); + expect(token_kind::rparen, "')'"); + } + else if (n == "$let") { + return parse_let_expr(); + } + else if (accept(token_kind::lparen)) { + if (!accept(token_kind::rparen)) { + do { args.push_back(parse_term()); } while (accept(token_kind::comma)); + expect(token_kind::rparen, "')'"); + } + } + + // Table-driven prefix operator dispatch + auto op_it = m_ops.find(n); + if (op_it != m_ops.end() && !op_it->second.is_infix) { + return op_it->second.builder(args); + } + + func_decl* f = mk_decl_or_ho_const(n, args.size(), false); + if (!args.empty()) coerce_args(f, args); + return expr_ref(args.empty() ? m.mk_const(f) : m.mk_app(f, args.size(), args.data()), m); + } + + // Grammar: ::= | + // ::= | + // Entry point for formula parsing (wraps parse_expr with default precedence). + expr_ref parse_formula(); + + // Grammar: ::= @ + // | @ + // @ is THF function application, encoded via array select. + expr_ref apply_at(expr_ref e) { + if (!is(token_kind::at_tok)) return e; + + // @ corresponds to array select (function application) + while (accept(token_kind::at_tok)) { + expr_ref arg = parse_at_arg(); + sort* e_sort = e->get_sort(); + if (!m_array.is_array(e_sort)) { + sort* arg_sort = arg->get_sort(); + sort* result_sort = m.is_bool(arg_sort) ? m.mk_bool_sort() : m_univ; + sort* arr_sort = m_array.mk_array_sort(arg_sort, result_sort); + e = coerce_arg(e, arr_sort); + } else { + // Array but domain may not match arg sort — coerce arg + sort* dom = get_array_domain(e_sort, 0); + if (dom != arg->get_sort()) + arg = coerce_arg(arg, dom); + } + e = expr_ref(m_array.mk_select(e, arg), m); + } + return e; + } + + // Grammar: Argument to @ (THF application); may be an atom, negation, quantified formula, + // parenthesized formula, or lambda. Handles the right-operand of . + // Parse an argument to @ — can be a term, a formula (negation, quantifier, parens with connectives), or a lambda + expr_ref parse_at_arg() { + if (accept(token_kind::not_tok)) { + expr_ref e = parse_at_arg(); + return expr_ref(m.mk_not(ensure_bool(e)), m); + } + if (accept(token_kind::lambda_tok)) { + return parse_lambda_expr(); + } + if (accept(token_kind::lparen)) { + expr_ref e = parse_formula(); + expect(token_kind::rparen, "')'"); + // Do NOT call apply_at here — outer apply_at owns the remaining @ tokens + return e; + } + if (is(token_kind::forall_tok) || is(token_kind::exists_tok)) { + bool is_forall = is(token_kind::forall_tok); + next(); + expect(token_kind::lbrack, "'['"); + ptr_vector vars; + std::unordered_map scope; + if (!accept(token_kind::rbrack)) { + do { + std::string v = parse_name(); + sort* s = m_univ; + if (accept(token_kind::colon)) { + parsed_type t = parse_type_expr(); + if (!t.domain.empty()) s = get_ho_sort(t.domain, t.range); + else s = t.range; + } + app* c = m.mk_const(symbol(v), s); + m_pinned_exprs.push_back(c); + vars.push_back(c); + scope.emplace(v, c); + } while (accept(token_kind::comma)); + expect(token_kind::rbrack, "']'"); + } + expect(token_kind::colon, "':'"); + m_bound.push_back(scope); + // Quantifier body in @-arg should NOT consume @ — those belong to enclosing application + bool save_in_at_arg = m_in_at_arg; + m_in_at_arg = true; + expr_ref body = parse_formula(); + m_in_at_arg = save_in_at_arg; + m_bound.pop_back(); + return mk_quantifier(is_forall, vars, body); + } + // Simple term (name with optional function args) — no @ consumption here + return parse_term_primary(); + } + + func_decl* mk_zero_arity_decl(symbol const& name, sort* range) { + std::string name_str = name.str(); + if (range == m_univ) + return mk_decl_or_ho_const(name_str, 0, false); + if (m.is_bool(range)) + return mk_decl_or_ho_const(name_str, 0, true); + std::string key = mk_decl_key(name_str, 0, 'c') + "\x1f" + std::to_string(range->get_id()); + auto it = m_decls.find(key); + if (it != m_decls.end()) return it->second; + func_decl* f = m.mk_func_decl(name, 0, static_cast(nullptr), range); + m_pinned_decls.push_back(f); + m_decls.emplace(key, f); + return f; + } + + expr_ref coerce_zero_arity(app* a, sort* range) { + return expr_ref(m.mk_const(mk_zero_arity_decl(a->get_decl()->get_name(), range)), m); + } + + // Coerce an expression from Bool sort to m_univ by rebuilding with a function decl. + // Works for both 0-arity constants and function applications. + expr_ref coerce_to_univ(expr_ref const& e) { + if (!is_app(e) || e->get_sort() == m_univ) + return e; + app* a = to_app(e); + if (a->get_num_args() == 0) + return coerce_zero_arity(a, m_univ); + // Rebuild with a function (non-predicate) declaration + func_decl* f = mk_decl(a->get_decl()->get_name().str(), a->get_num_args(), false); + expr_ref_vector args(m); + for (unsigned i = 0; i < a->get_num_args(); ++i) + args.push_back(a->get_arg(i)); + coerce_args(f, args); + return expr_ref(m.mk_app(f, args.size(), args.data()), m); + } + + // Coerce two expressions to have the same sort for equality. + // In TPTP, = is term equality and m_univ is the default sort. + // If one side has Bool sort (parsed as predicate), coerce it to m_univ. + // If sorts already match and are not Bool, returns lhs unchanged. + expr_ref coerce_eq(expr_ref lhs, expr_ref& rhs) { + // Coerce Bool-sorted operands to m_univ since = is term equality in TPTP + if (m.is_bool(lhs->get_sort()) && is_app(lhs) && !m.is_true(lhs) && !m.is_false(lhs)) + lhs = coerce_to_univ(lhs); + if (m.is_bool(rhs->get_sort()) && is_app(rhs) && !m.is_true(rhs) && !m.is_false(rhs)) + rhs = coerce_to_univ(rhs); + + if (lhs->get_sort() == rhs->get_sort()) return lhs; + + // Coerce 0-arity constants to match the other side's sort + if (is_app(lhs) && to_app(lhs)->get_num_args() == 0 && lhs->get_sort() != rhs->get_sort()) { + return coerce_zero_arity(to_app(lhs), rhs->get_sort()); + } + if (is_app(rhs) && to_app(rhs)->get_num_args() == 0 && lhs->get_sort() != rhs->get_sort()) { + rhs = coerce_zero_arity(to_app(rhs), lhs->get_sort()); + return lhs; + } + // Last resort: coerce both sides to have the same sort + if (lhs->get_sort() != rhs->get_sort()) { + // Prefer coercing to rhs sort, falling back to m_univ + sort* target = rhs->get_sort(); + lhs = coerce_arg(lhs, target); + } + return lhs; + } + + // Grammar: ::= := + // ::= | + // ::= := + // Parse a single let definition: name := value or name(X,Y,...) := value. + // For function-style definitions, wraps value in lambdas over the parameter variables. + std::pair parse_single_let_defn() { + std::string name = parse_name(); + std::vector param_vars; + std::unordered_map param_scope; + if (accept(token_kind::lparen)) { + if (!accept(token_kind::rparen)) { + do { + std::string v = parse_name(); + app* c = m.mk_const(symbol(v), m_univ); + m_pinned_exprs.push_back(c); + param_vars.push_back(c); + param_scope.emplace(v, c); + } while (accept(token_kind::comma)); + expect(token_kind::rparen, "')'"); + } + } + // Parse ':=' + expect(token_kind::colon, "':'"); + expect(token_kind::equal_tok, "'='"); + // Bind parameter variables for parsing the RHS + if (!param_scope.empty()) + m_bound.push_back(param_scope); + expr_ref value = parse_formula(); + if (!param_scope.empty()) + m_bound.pop_back(); + // For function-style definitions, wrap value in lambdas + if (!param_vars.empty()) { + expr_ref result = value; + for (int i = (int)param_vars.size() - 1; i >= 0; --i) { + expr_ref abs_body(m); + expr_abstract(m, 0, 1, (expr* const*)¶m_vars[i], result, abs_body); + sort* s = param_vars[i]->get_sort(); + symbol nm = param_vars[i]->get_decl()->get_name(); + result = expr_ref(m.mk_lambda(1, &s, &nm, abs_body), m); + } + value = result; + } + return {name, std::move(value)}; + } + + // Parse $let(types, defns, body) + // Grammar: + // thf_let ::= $let(thf_let_types, thf_let_defns, thf_logic_formula) + // txf_let ::= $let(txf_let_types, txf_let_defns, tff_term) + // let_types ::= atom_typing | [atom_typing_list] + // let_defns ::= let_defn | [let_defn_list] + // let_defn ::= LHS := RHS + expr_ref parse_let_expr() { + expect(token_kind::lparen, "'('"); + + // --- Part 1: Parse type declarations --- + std::vector let_names; + std::vector let_sorts; + + auto parse_one_typing = [&]() { + std::string name = parse_name(); + if (accept(token_kind::lparen)) { + if (!accept(token_kind::rparen)) { + do { parse_type_expr(); } while (accept(token_kind::comma)); + expect(token_kind::rparen, "')'"); + } + } + expect(token_kind::colon, "':'"); + parsed_type t = parse_type_expr(); + sort* s = t.domain.empty() ? t.range : get_ho_sort(t.domain, t.range); + let_names.push_back(name); + let_sorts.push_back(s); + }; + + if (is(token_kind::lbrack)) { + next(); + if (!accept(token_kind::rbrack)) { + do { parse_one_typing(); } while (accept(token_kind::comma)); + expect(token_kind::rbrack, "']'"); + } + } else { + parse_one_typing(); + } + + expect(token_kind::comma, "','"); + + // --- Create bound constants for all let-bound names --- + std::unordered_map scope; + for (unsigned i = 0; i < let_names.size(); ++i) { + app* c = m.mk_const(symbol(let_names[i]), let_sorts[i]); + m_pinned_exprs.push_back(c); + scope.emplace(let_names[i], c); + } + + // --- Part 2: Parse definitions --- + // Let-bound names are NOT in scope during RHS parsing (non-recursive semantics). + // Each definition has its own ':=' operator. + std::vector> defns; + + if (is(token_kind::lbrack)) { + next(); + if (!accept(token_kind::rbrack)) { + do { + defns.push_back(parse_single_let_defn()); + } while (accept(token_kind::comma)); + expect(token_kind::rbrack, "']'"); + } + } else { + defns.push_back(parse_single_let_defn()); + } + + expect(token_kind::comma, "','"); + + // --- Part 3: Parse body with let-bound names in scope --- + m_bound.push_back(scope); + expr_ref body = parse_formula(); + m_bound.pop_back(); + expect(token_kind::rparen, "')'"); + + // --- Substitute all let bindings in the body --- + expr_safe_replace replacer(m); + for (auto& [defn_name, defn_value] : defns) { + auto it = scope.find(defn_name); + if (it != scope.end()) + replacer.insert(it->second, defn_value.get()); + } + expr_ref result(m); + replacer(body, result); + return result; + } + + // Grammar: ::= | + // | + // ::= + // ::= | + // ::= $true | $false | () + // ::= $less | $lesseq | $greater | $greatereq | $is_int | $is_rat | ... + // ::= = | != + // Also handles: let-bound name resolution, implicit variable creation. + expr_ref parse_atomic_formula() { + if (accept(token_kind::lparen)) { + // Check for parenthesized connective used as higher-order term: (~), (&), (|), etc. + if (is(token_kind::not_tok) || is(token_kind::and_tok) || is(token_kind::or_tok) || + is(token_kind::implies_tok) || is(token_kind::iff_tok) || is(token_kind::xor_tok)) { + std::string op_text; + unsigned arity = 2; + switch (m_curr.kind) { + case token_kind::not_tok: op_text = "~"; arity = 1; break; + case token_kind::and_tok: op_text = "&"; break; + case token_kind::or_tok: op_text = "|"; break; + case token_kind::implies_tok: op_text = "=>"; break; + case token_kind::iff_tok: op_text = "<=>"; break; + case token_kind::xor_tok: op_text = "<~>"; break; + default: break; + } + token saved = m_curr; + next(); + if (accept(token_kind::rparen)) { + // Parenthesized connective: treat as HO constant with array sort + sort* bool_sort = m.mk_bool_sort(); + sort* ho_sort; + if (arity == 1) + ho_sort = m_array.mk_array_sort(bool_sort, bool_sort); + else + ho_sort = m_array.mk_array_sort(bool_sort, m_array.mk_array_sort(bool_sort, bool_sort)); + std::string key = mk_decl_key(op_text, 0, 'h'); + auto it = m_decls.find(key); + func_decl* f; + if (it != m_decls.end()) { + f = it->second; + } else { + f = m.mk_func_decl(symbol(op_text), 0, static_cast(nullptr), ho_sort); + m_pinned_decls.push_back(f); + m_decls.emplace(key, f); + } + return expr_ref(m.mk_const(f), m); + } + // Not a parenthesized connective — lparen was consumed and connective was consumed + // but ')' didn't follow. Parse as formula with the connective already consumed. + expr_ref inner(m); + if (saved.kind == token_kind::not_tok) { + expr_ref e = parse_formula(); + inner = expr_ref(m.mk_not(e), m); + } else { + // Binary connective at start of parens — shouldn't happen in valid TPTP + throw parse_error("unexpected connective after '(' at " + loc()); + } + expect(token_kind::rparen, "')'"); + return inner; + } + // Parentheses create a new scope for @ consumption + bool save_in_at_arg = m_in_at_arg; + m_in_at_arg = false; + expr_ref e = parse_formula(); + expect(token_kind::rparen, "')'"); + m_in_at_arg = save_in_at_arg; + return e; + } + + // Handle negative numerals in formula position: -2 = $uminus(2) + if (accept(token_kind::minus_tok)) { + expr_ref t = parse_term(); + return expr_ref(m_arith.mk_uminus(t), m); + } + + // Tuple/list in formula position: [t1, t2, ...] — return first element for simplicity + if (accept(token_kind::lbrack)) { + if (accept(token_kind::rbrack)) + return expr_ref(m.mk_const(symbol("$nil"), m_univ), m); + expr_ref first = parse_formula(); + while (accept(token_kind::comma)) + parse_formula(); // consume remaining elements + expect(token_kind::rbrack, "']'"); + return first; + } + + std::string n = parse_name(); + if (n == "$true") return expr_ref(m.mk_true(), m); + if (n == "$false") return expr_ref(m.mk_false(), m); + + if (is_nonempty_digit_string(n)) { + return parse_numeral_from_name(n); + } + + // Check if name is let-bound (works for both uppercase vars and lowercase let-bound names) + { + expr_ref b(m); + if (!m_last_name_quoted && find_bound(n, b)) { + // If followed by '(' args, apply via array select (function-style let) + if (is(token_kind::lparen)) { + next(); + expr_ref_vector fargs(m); + if (!accept(token_kind::rparen)) { + do { fargs.push_back(parse_term()); } while (accept(token_kind::comma)); + expect(token_kind::rparen, "')'"); + } + expr_ref result = b; + for (unsigned i = 0; i < fargs.size(); ++i) + result = expr_ref(m_array.mk_select(result, fargs.get(i)), m); + return result; + } + return b; + } + } + + // Choice operators @+ and @- with quantifier-like syntax: @+[X: T] : body + if ((n == "@+" || n == "@-") && is(token_kind::lbrack)) { + expect(token_kind::lbrack, "'['"); + ptr_vector vars; + std::unordered_map scope; + if (!accept(token_kind::rbrack)) { + do { + std::string v = parse_name(); + sort* s = m_univ; + if (accept(token_kind::colon)) { + parsed_type t = parse_type_expr(); + if (!t.domain.empty()) s = get_ho_sort(t.domain, t.range); + else s = t.range; + } + app* c = m.mk_const(symbol(v), s); + m_pinned_exprs.push_back(c); + vars.push_back(c); + scope.emplace(v, c); + } while (accept(token_kind::comma)); + expect(token_kind::rbrack, "']'"); + } + expect(token_kind::colon, "':'"); + m_bound.push_back(scope); + expr_ref body = parse_formula(); + m_bound.pop_back(); + // Approximate choice as existential quantification + return mk_quantifier(false, vars, body); + } + + expr_ref_vector args(m); + // $ite needs special parsing: first arg is formula, rest are formulas (branches can be equalities) + if (n == "$ite") { + expect(token_kind::lparen, "'('"); + args.push_back(parse_formula()); + expect(token_kind::comma, "','"); + args.push_back(parse_formula()); + expect(token_kind::comma, "','"); + args.push_back(parse_formula()); + expect(token_kind::rparen, "')'"); + } + else if (n == "$let") { + return parse_let_expr(); + } + else if (accept(token_kind::lparen)) { + if (!accept(token_kind::rparen)) { + do { args.push_back(parse_term()); } while (accept(token_kind::comma)); + expect(token_kind::rparen, "')'"); + } + } + + // Table-driven prefix operator dispatch + auto op_it = m_ops.find(n); + if (op_it != m_ops.end() && !op_it->second.is_infix) { + return op_it->second.builder(args); + } + + expr_ref lhs(m); + bool has_lhs = false; + if (args.empty()) { + if (!m_last_name_quoted && should_create_implicit_var(n)) { + lhs = expr_ref(get_or_create_implicit_var(n), m); + has_lhs = true; + } + } + + if (has_lhs) + return lhs; + + auto typed = m_typed_decls.find(mk_typed_key(n, args.size())); + if (typed != m_typed_decls.end()) { + func_decl* f = args.empty() ? mk_decl_or_ho_const(n, 0, false) : mk_decl(n, args.size(), false); + if (!args.empty()) coerce_args(f, args); + return expr_ref(args.empty() ? m.mk_const(f) : m.mk_app(f, args.size(), args.data()), m); + } + + if (args.empty() && (is(token_kind::equal_tok) || is(token_kind::neq_tok))) { + func_decl* f = mk_decl_or_ho_const(n, 0, false); + return expr_ref(m.mk_const(f), m); + } + + func_decl* pred = mk_decl_or_ho_const(n, args.size(), true); + if (!args.empty()) coerce_args(pred, args); + return expr_ref(args.empty() ? m.mk_const(pred) : m.mk_app(pred, args.size(), args.data()), m); + } + + // Grammar: ::= ^ [] : + // ::= | , + // ::= | + // Produces Z3 lambda terms (array-valued). + // Parse THF lambda expression: ^ [X: T, ...] : body + // Uses Z3's native lambda construct, which produces array terms. + expr_ref parse_lambda_expr() { + expect(token_kind::lbrack, "'['"); + ptr_vector vars; + std::unordered_map scope; + if (!accept(token_kind::rbrack)) { + do { + std::string v = parse_name(); + sort* s = m_univ; + if (accept(token_kind::colon)) { + parsed_type t = parse_type_expr(); + if (!t.domain.empty()) { + s = get_ho_sort(t.domain, t.range); + } else if (t.range) { + s = t.range; + } + } + app* c = m.mk_const(symbol(v), s); + m_pinned_exprs.push_back(c); + vars.push_back(c); + scope.emplace(v, c); + } while (accept(token_kind::comma)); + expect(token_kind::rbrack, "']'"); + } + expect(token_kind::colon, "':'"); + m_bound.push_back(scope); + // Lambda body does NOT consume @ — @ belongs to the enclosing application + bool save_in_at_arg = m_in_at_arg; + m_in_at_arg = true; + expr_ref body = parse_formula(); + m_in_at_arg = save_in_at_arg; + m_bound.pop_back(); + if (vars.empty()) + return body; + // Create nested single-variable lambdas (curried) to match our curried array encoding. + // ^[X:A, Y:B] : body becomes ^[X:A] : (^[Y:B] : body) with sort Array(A, Array(B, body_sort)) + expr_ref result = body; + for (int i = (int)vars.size() - 1; i >= 0; --i) { + expr_ref abs_body(m); + expr_abstract(m, 0, 1, (expr* const*)&vars[i], result, abs_body); + sort* s = vars[i]->get_sort(); + symbol nm = vars[i]->get_decl()->get_name(); + result = expr_ref(m.mk_lambda(1, &s, &nm, abs_body), m); + } + return result; + } + + // Grammar: ::= | + // ::= + // ::= ~ + // ::= [] : + // ::= + // ::= ! | ? + // Also handles: $ite, $let, lambda (^), parenthesized formulas, and atomic formulas. + expr_ref parse_unary_formula() { + if (accept(token_kind::not_tok)) { + expr_ref e = parse_unary_formula(); + return expr_ref(m.mk_not(ensure_bool(e)), m); + } + + // Modal box operators: [.] or [name] — only when followed by ']' (not a tuple) + if (is(token_kind::lbrack)) { + // Peek: if [.] pattern, parse as modal; if [name] (no comma), parse as modal + // Otherwise fall through to parse_atomic_formula which handles tuples + token saved = m_curr; + next(); // consume '[' + if (accept(token_kind::dot)) { + expect(token_kind::rbrack, "']'"); + expr_ref sub = parse_unary_formula(); + func_decl* f = mk_modal_op("box"); + return expr_ref(m.mk_app(f, sub.get()), m); + } + if ((is(token_kind::id) || is(token_kind::str)) && !is(token_kind::comma)) { + std::string mod_name = "box_" + m_curr.text; + std::string first_name = m_curr.text; + next(); + if (accept(token_kind::rbrack)) { + expr_ref sub = parse_unary_formula(); + func_decl* f = mk_modal_op(mod_name); + return expr_ref(m.mk_app(f, sub.get()), m); + } + // Not a simple [name] modal — it's a tuple starting with this name. + // We've consumed '[' and a name. Parse the name as an expression and + // continue as tuple. + expr_ref first(m); + expr_ref b(m); + if (is_var_name(first_name) && find_bound(first_name, b)) + first = b; + else if (should_create_implicit_var(first_name)) + first = expr_ref(get_or_create_implicit_var(first_name), m); + else { + func_decl* f = mk_decl_or_ho_const(first_name, 0, false); + first = expr_ref(m.mk_const(f), m); + } + while (accept(token_kind::comma)) + parse_formula(); // consume remaining elements + expect(token_kind::rbrack, "']'"); + return first; + } + // Not a modal operator — it's a tuple [expr, expr, ...] + // We already consumed '[', so parse as tuple inline + if (accept(token_kind::rbrack)) + return expr_ref(m.mk_const(symbol("$nil"), m_univ), m); + expr_ref first = parse_formula(); + while (accept(token_kind::comma)) + parse_formula(); // consume remaining elements + expect(token_kind::rbrack, "']'"); + return first; + } + + // Diamond modality: <.>, + if (is(token_kind::lt_tok)) { + next(); + std::string mod_name = "dia"; + if (accept(token_kind::dot)) { + mod_name = "dia"; + } else if (is(token_kind::id) || is(token_kind::str)) { + mod_name = "dia_" + m_curr.text; + next(); + } + expect(token_kind::gt_tok, "'>'"); + expr_ref sub = parse_unary_formula(); + func_decl* f = mk_modal_op(mod_name); + return expr_ref(m.mk_app(f, sub.get()), m); + } + + if (accept(token_kind::lambda_tok)) { + // THF lambda: ^ [X: T, ...] : body + // Approximate as a fresh constant (first-order approximation) + return parse_lambda_expr(); + } + + if (is(token_kind::forall_tok) || is(token_kind::exists_tok)) { + bool is_forall = is(token_kind::forall_tok); + next(); + expect(token_kind::lbrack, "'['"); + + ptr_vector vars; + std::unordered_map scope; + if (!accept(token_kind::rbrack)) { + do { + std::string v = parse_name(); + sort* s = m_univ; + if (accept(token_kind::colon)) { + parsed_type t = parse_type_expr(); + if (!t.domain.empty()) { + // Higher-order variable type — use uninterpreted sort approximation + s = get_ho_sort(t.domain, t.range); + } else { + s = t.range; + } + } + // Monomorphize: $tType-sorted variables become U-sorted + // and register them as sorts for subsequent type references + if (is_ttype(s)) { + s = m_univ; + m_sorts.insert_or_assign(v, m_univ); + } + app* c = m.mk_const(symbol(v), s); + m_pinned_exprs.push_back(c); + vars.push_back(c); + scope.emplace(v, c); + } + while (accept(token_kind::comma)); + expect(token_kind::rbrack, "']'"); + } + expect(token_kind::colon, "':'"); + m_bound.push_back(scope); + expr_ref body = parse_formula(); + m_bound.pop_back(); + return mk_quantifier(is_forall, vars, body); + } + + // Type quantification in formula context: !>[A: $tType, ...] : body + // Erase type variables and parse body as formula + if (is(token_kind::type_forall_tok) || is(token_kind::type_exists_tok)) { + next(); + expect(token_kind::lbrack, "'['"); + if (!accept(token_kind::rbrack)) { + do { + std::string tv = parse_name(); + if (accept(token_kind::colon)) + parse_type_expr(); // consume $tType annotation + m_sorts.insert_or_assign(tv, m_univ); + } while (accept(token_kind::comma)); + expect(token_kind::rbrack, "']'"); + } + expect(token_kind::colon, "':'"); + return parse_formula(); + } + + return parse_atomic_formula(); + } + + // Grammar: ::= | + // ::= + // ::= | + // ::= <=> | => | <= | <~> | ~ | ~& + // ::= + // | + // ::= & + // | & + // Implements a Pratt-style (precedence climbing) parser for binary connectives. + expr_ref parse_expr(unsigned min_prec, bool consume_at = true) { + expr_ref e = parse_unary_formula(); + for (;;) { + // Handle @ (function application) with highest precedence + // But NOT when we're inside a lambda body that's an @ argument + if (consume_at && !m_in_at_arg && is(token_kind::at_tok)) { + next(); + expr_ref arg = parse_at_arg(); + sort* e_sort = e->get_sort(); + if (!m_array.is_array(e_sort)) { + // LHS doesn't have array sort — coerce it to Array(arg_sort, result_sort) + sort* arg_sort = arg->get_sort(); + // If arg is Bool-sorted, result is likely Bool too (modal/connective application) + sort* result_sort = m.is_bool(arg_sort) ? m.mk_bool_sort() : m_univ; + sort* arr_sort = m_array.mk_array_sort(arg_sort, result_sort); + e = coerce_arg(e, arr_sort); + } else { + // Array but domain may not match arg sort — coerce arg + sort* dom = get_array_domain(e_sort, 0); + if (dom != arg->get_sort()) + arg = coerce_arg(arg, dom); + } + e = expr_ref(m_array.mk_select(e, arg), m); + continue; + } + char const* op_name = token_to_op_name(); + if (!op_name) break; + auto it = m_ops.find(op_name); + if (it == m_ops.end() || !it->second.is_infix) break; + if (it->second.precedence < min_prec) break; + next(); // consume the operator token + unsigned next_prec = it->second.right_assoc ? it->second.precedence : it->second.precedence + 1; + expr_ref rhs = parse_expr(next_prec, consume_at); + expr_ref_vector args(m); + args.push_back(e); + args.push_back(rhs); + e = it->second.builder(args); + } + return e; + } + + // Grammar: ::= : + // ::= : + // ::= | + // Declares a new constant or type with the given type signature. + void parse_type_decl_formula() { + unsigned lparen_count = 0; + while (accept(token_kind::lparen)) ++lparen_count; + std::string name = parse_name(); + expect(token_kind::colon, "':'"); + parsed_type t = parse_type_expr(); + while (lparen_count-- > 0) + expect(token_kind::rparen, "')'"); + + if (t.domain.empty() && is_ttype(t.range)) { + // Sort declaration: monomorphize to m_univ + m_sorts.insert_or_assign(name, m_univ); + return; + } + + // Monomorphize: replace $tType in domain/range with m_univ + for (auto& s : t.domain) { + if (is_ttype(s)) s = m_univ; + } + if (t.range && is_ttype(t.range)) t.range = m_univ; + + m_typed_decls.insert_or_assign(mk_typed_key(name, t.domain.size()), std::make_pair(t.domain, t.range)); + } + + static bool file_exists(std::string const& f) { + std::ifstream in(f); + return !in.fail(); + } + + static bool is_absolute_path(std::string const& name) { + return !name.empty() && + (name[0] == '/' || + (name.size() >= 2 && std::isalpha(static_cast(name[0])) && name[1] == ':')); + } + + std::string dirname(std::string const& f) const { + size_t idx = f.find_last_of("/\\"); + return idx == std::string::npos ? "." : f.substr(0, idx); + } + + static std::string normalize_path(std::string path) { +#ifdef _WIN32 + for (auto& c : path) + if (c == '/') c = '\\'; +#endif + return path; + } + + std::string resolve_include(std::string const& curr_file, std::string const& name) const { + if (is_absolute_path(name)) + return normalize_path(name); + // Try relative to current file's directory + std::string local = normalize_path(dirname(curr_file) + "/" + name); + if (file_exists(local)) return local; + // Try TPTP environment variable (standard TPTP convention) + char const* root = std::getenv("TPTP"); + if (root) { + std::string env = normalize_path(std::string(root) + "/" + name); + if (file_exists(env)) return env; + } + // Try relative to current working directory (common when running from TPTP root) + std::string cwd_relative = normalize_path(name); + if (file_exists(cwd_relative)) return cwd_relative; + return local; + } + + // Grammar: ::= include(). + // ::= ,[] | + void parse_include(std::string const& curr_file) { + expect(token_kind::lparen, "'('"); + std::string file = parse_name(); + if (accept(token_kind::comma)) { + if (accept(token_kind::lbrack)) { + skip_balanced(token_kind::lbrack, token_kind::rbrack); + } + else { + skip_annotations_until_rparen(); + } + } + expect(token_kind::rparen, "')'"); + expect(token_kind::dot, "'.'"); + parse_file(resolve_include(curr_file, file)); + } + + // Grammar: ::= | | | + // ::= tff(,,). + // ::= thf(,,). + // ::= axiom | hypothesis | definition | assumption | lemma | + // theorem | corollary | conjecture | negated_conjecture | + // plain | type | ... + // ::= , | + void parse_annotated() { + expect(token_kind::lparen, "'('"); + parse_name(); + expect(token_kind::comma, "','"); + std::string role = to_lower(parse_name()); + expect(token_kind::comma, "','"); + + if (role == "type") { + parse_type_decl_formula(); + } + else if (role == "logic") { + // Modal logic declarations ($modal == [...]) — skip the formula body + skip_annotations_until_rparen(); + } + else { + try { + implicit_var_scope implicit_scope; + scoped_implicit_vars scoped(*this, implicit_scope); + expr_ref f = parse_formula(); + if (!implicit_scope.order.empty()) { + f = mk_quantifier(true, implicit_scope.order, f); + } + // Coerce to Bool if needed (HO encoding may produce U-sorted formulas) + if (!m.is_bool(f)) + f = ensure_bool(f); + if (role == "conjecture") { + m_has_conjecture = true; + f = m.mk_not(f); + } + m_cmd.assert_expr(f); + } catch (z3_exception const& ex) { + // Sort mismatch or other semantic error in this formula — skip it + IF_VERBOSE(2, verbose_stream() << "skipping formula due to: " << ex.what() << "\n"); + // Skip to '.' to resync the parser for the next annotated formula + while (!is(token_kind::eof_tok) && !is(token_kind::dot)) + next(); + if (is(token_kind::dot)) next(); + return; + } + } + + if (accept(token_kind::comma)) { + skip_annotations_until_rparen(); + } + expect(token_kind::rparen, "')'"); + expect(token_kind::dot, "'.'"); + } + + // Grammar: ::= * + // ::= | + // Dispatches to parse_annotated() or parse_include() based on keyword. + void parse_toplevel(std::string const& current_file) { + while (!is(token_kind::eof_tok)) { + std::string kw = to_lower(parse_name()); + if (kw == "include") { + parse_include(current_file); + } + else if (kw == "fof" || kw == "cnf" || kw == "tff" || kw == "thf") { + parse_annotated(); + } + else { + std::ostringstream out; + out << "unsupported TPTP unit '" << kw << "' at " << loc(); + throw parse_error(out.str()); + } + } + } + +public: + tptp_parser(cmd_context& cmd): + m_cmd(cmd), + m(m_cmd.m()), + m_arith(m), + m_array(m), + m_univ(m.mk_uninterpreted_sort(symbol("U"))), + m_pinned_sorts(m), + m_pinned_decls(m), + m_pinned_exprs(m) { + m_pinned_sorts.push_back(m_univ); + sort* tType = m.mk_uninterpreted_sort(symbol("$tType")); + m_pinned_sorts.push_back(tType); + m_sorts.emplace("$tType", tType); + m_sorts.emplace("$i", m_univ); + m_sorts.emplace("$o", m.mk_bool_sort()); + m_sorts.emplace("$int", m_arith.mk_int()); + m_sorts.emplace("$rat", m_arith.mk_real()); + m_sorts.emplace("$real", m_arith.mk_real()); + init_op_table(); + } + + void init_op_table() { + // Prefix arithmetic predicates (is_infix=false, precedence=0) + m_ops["$less"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$less"); + auto [a, b] = coerce_arith2(args); + return expr_ref(m_arith.mk_lt(a, b), m); + }}; + m_ops["$lesseq"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$lesseq"); + auto [a, b] = coerce_arith2(args); + return expr_ref(m_arith.mk_le(a, b), m); + }}; + m_ops["$greater"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$greater"); + auto [a, b] = coerce_arith2(args); + return expr_ref(m_arith.mk_gt(a, b), m); + }}; + m_ops["$greatereq"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$greatereq"); + auto [a, b] = coerce_arith2(args); + return expr_ref(m_arith.mk_ge(a, b), m); + }}; + m_ops["$uminus"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$uminus"); + return expr_ref(m_arith.mk_uminus(args[0]), m); + }}; + m_ops["$sum"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$sum"); + auto [a, b] = coerce_arith2(args); + return expr_ref(m_arith.mk_add(a, b), m); + }}; + m_ops["$plus"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$plus"); + auto [a, b] = coerce_arith2(args); + return expr_ref(m_arith.mk_add(a, b), m); + }}; + m_ops["$difference"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$difference"); + auto [a, b] = coerce_arith2(args); + return expr_ref(m_arith.mk_sub(a, b), m); + }}; + m_ops["$product"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$product"); + auto [a, b] = coerce_arith2(args); + return expr_ref(m_arith.mk_mul(a, b), m); + }}; + m_ops["$quotient"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$quotient"); + return mk_quotient(args); + }}; + m_ops["$quotient_e"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$quotient_e"); + return mk_quotient(args); + }}; + m_ops["$quotient_t"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$quotient_t"); + return mk_quotient(args); + }}; + m_ops["$quotient_f"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$quotient_f"); + return mk_quotient(args); + }}; + m_ops["$remainder_e"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$remainder_e"); + return expr_ref(m_arith.mk_mod(args[0], args[1]), m); + }}; + m_ops["$remainder_t"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$remainder_t"); + return expr_ref(m_arith.mk_mod(args[0], args[1]), m); + }}; + m_ops["$remainder_f"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 2, "$remainder_f"); + return expr_ref(m_arith.mk_mod(args[0], args[1]), m); + }}; + m_ops["$floor"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$floor"); + expr_ref a(args[0], m); + if (m_arith.is_int(a)) return a; + return expr_ref(m_arith.mk_to_int(a), m); + }}; + m_ops["$ceiling"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$ceiling"); + expr_ref a(args[0], m); + if (m_arith.is_int(a)) return a; + // ceiling(x) = -floor(-x) + return expr_ref(m_arith.mk_uminus(m_arith.mk_to_int(m_arith.mk_uminus(a))), m); + }}; + m_ops["$truncate"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$truncate"); + expr_ref a(args[0], m); + if (m_arith.is_int(a)) return a; + // truncate(x) = if x >= 0 then floor(x) else ceiling(x) + expr_ref zero(m_arith.mk_real(0), m); + expr_ref fl(m_arith.mk_to_int(a), m); + expr_ref neg_fl(m_arith.mk_uminus(m_arith.mk_to_int(m_arith.mk_uminus(a))), m); + return expr_ref(m.mk_ite(m_arith.mk_ge(a, zero), fl, neg_fl), m); + }}; + m_ops["$round"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$round"); + expr_ref a(args[0], m); + if (m_arith.is_int(a)) return a; + // round to nearest even + expr_ref i(m_arith.mk_to_int(a), m); + expr_ref half(m_arith.mk_add(m_arith.mk_to_real(i), m_arith.mk_numeral(rational(1, 2), false)), m); + expr_ref i1(m_arith.mk_add(i, m_arith.mk_int(1)), m); + expr_ref is_even(m.mk_eq(m_arith.mk_mod(i, m_arith.mk_int(2)), m_arith.mk_int(0)), m); + return expr_ref(m.mk_ite(m_arith.mk_gt(a, half), i1, + m.mk_ite(m.mk_eq(a, half), m.mk_ite(is_even, i, i1), i)), m); + }}; + m_ops["$to_int"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$to_int"); + expr_ref a(args[0], m); + if (m_arith.is_int(a)) return a; + return expr_ref(m_arith.mk_to_int(a), m); + }}; + m_ops["$to_real"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$to_real"); + expr_ref a(args[0], m); + if (m_arith.is_real(a)) return a; + return expr_ref(m_arith.mk_to_real(a), m); + }}; + m_ops["$to_rat"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$to_rat"); + expr_ref a(args[0], m); + if (m_arith.is_real(a)) return a; + return expr_ref(m_arith.mk_to_real(a), m); + }}; + m_ops["$is_int"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$is_int"); + return expr_ref(m_arith.mk_is_int(args[0]), m); + }}; + m_ops["$is_rat"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$is_rat"); + expr_ref a(args[0], m); + return mk_is_rat(a); + }}; + m_ops["$distinct"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + if (args.size() == 2) return expr_ref(m.mk_not(m.mk_eq(args[0], args[1])), m); + return expr_ref(m.mk_distinct(args.size(), args.data()), m); + }}; + m_ops["$ite"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 3, "$ite"); + expr_ref cond(args[0], m), t(args[1], m), f(args[2], m); + if (!m.is_bool(cond)) + throw parse_error("$ite expects Bool condition as first argument"); + return expr_ref(m.mk_ite(cond, t, f), m); + }}; + m_ops["$abs"] = { false, 0, false, [&](expr_ref_vector const& args) -> expr_ref { + check_arith_arity(args, 1, "$abs"); + expr_ref a(args[0], m); + if (!m_arith.is_int_real(a)) + throw parse_error("$abs expects arithmetic argument"); + expr_ref zero(m_arith.is_int(a) ? m_arith.mk_int(0) : m_arith.mk_numeral(rational(0), false), m); + return expr_ref(m.mk_ite(m_arith.mk_ge(a, zero), a, expr_ref(m_arith.mk_uminus(a), m)), m); + }}; + m_ops["$true"] = { false, 0, false, [&](expr_ref_vector const&) -> expr_ref { + return expr_ref(m.mk_true(), m); + }}; + m_ops["$false"] = { false, 0, false, [&](expr_ref_vector const&) -> expr_ref { + return expr_ref(m.mk_false(), m); + }}; + + // Infix logical operators (token-based, matched by token_to_op_name) + m_ops["<=>"] = { true, PREC_IFF, false, [&](expr_ref_vector const& args) -> expr_ref { + return expr_ref(m.mk_iff(ensure_bool(args[0]), ensure_bool(args[1])), m); + }}; + m_ops["<~>"] = { true, PREC_IFF, false, [&](expr_ref_vector const& args) -> expr_ref { + return expr_ref(m.mk_not(m.mk_iff(ensure_bool(args[0]), ensure_bool(args[1]))), m); + }}; + m_ops["=>"] = { true, PREC_IMPLIES, true, [&](expr_ref_vector const& args) -> expr_ref { + return expr_ref(m.mk_implies(ensure_bool(args[0]), ensure_bool(args[1])), m); + }}; + m_ops["<="] = { true, PREC_IMPLIES, false, [&](expr_ref_vector const& args) -> expr_ref { + return expr_ref(m.mk_implies(ensure_bool(args[1]), ensure_bool(args[0])), m); + }}; + m_ops["|"] = { true, PREC_OR, false, [&](expr_ref_vector const& args) -> expr_ref { + return expr_ref(m.mk_or(ensure_bool(args[0]), ensure_bool(args[1])), m); + }}; + m_ops["~|"] = { true, PREC_OR, false, [&](expr_ref_vector const& args) -> expr_ref { + return expr_ref(m.mk_not(m.mk_or(ensure_bool(args[0]), ensure_bool(args[1]))), m); + }}; + m_ops["&"] = { true, PREC_AND, false, [&](expr_ref_vector const& args) -> expr_ref { + return expr_ref(m.mk_and(ensure_bool(args[0]), ensure_bool(args[1])), m); + }}; + m_ops["~&"] = { true, PREC_AND, false, [&](expr_ref_vector const& args) -> expr_ref { + return expr_ref(m.mk_not(m.mk_and(ensure_bool(args[0]), ensure_bool(args[1]))), m); + }}; + m_ops["="] = { true, PREC_EQ, false, [&](expr_ref_vector const& args) -> expr_ref { + expr_ref lhs(args[0], m); + expr_ref rhs(args[1], m); + lhs = coerce_eq(lhs, rhs); + return expr_ref(m.mk_eq(lhs, rhs), m); + }}; + m_ops["!="] = { true, PREC_EQ, false, [&](expr_ref_vector const& args) -> expr_ref { + expr_ref lhs(args[0], m); + expr_ref rhs(args[1], m); + lhs = coerce_eq(lhs, rhs); + return expr_ref(m.mk_not(m.mk_eq(lhs, rhs)), m); + }}; + } + + void parse_input(std::istream& in, std::string const& current_file) { + // Save parser state so that included files don't clobber the caller's lexer. + std::string saved_input = std::move(m_input); + std::unique_ptr saved_lex = std::move(m_lex); + token saved_curr = m_curr; + + std::ostringstream buf; + buf << in.rdbuf(); + m_input = buf.str(); + m_lex = std::make_unique(m_input); + next(); + parse_toplevel(current_file); + + // Restore caller's parser state. + m_input = std::move(saved_input); + m_lex = std::move(saved_lex); + m_curr = saved_curr; + } + + void parse_file(std::string const& filename) { + if (!m_seen_files.insert(filename).second) return; + std::ifstream in(filename); + if (in.fail()) { + std::ostringstream out; + out << "failed to open file '" << filename << "'"; + throw parse_error(out.str()); + } + parse_input(in, filename); + } + + void parse_stream(std::istream& in) { + parse_input(in, "."); + } + + bool has_conjecture() const { return m_has_conjecture; } +}; + +expr_ref tptp_parser::parse_term() { + expr_ref e = parse_term_primary(); + if (!is(token_kind::at_tok)) return e; + // @ corresponds to array select (function application) + while (accept(token_kind::at_tok)) { + expr_ref arg = parse_at_arg(); + sort* e_sort = e->get_sort(); + if (!m_array.is_array(e_sort)) { + sort* arg_sort = arg->get_sort(); + sort* arr_sort = m_array.mk_array_sort(arg_sort, m_univ); + e = coerce_arg(e, arr_sort); + } else { + sort* dom = get_array_domain(e_sort, 0); + if (dom != arg->get_sort()) + arg = coerce_arg(arg, dom); + } + e = expr_ref(m_array.mk_select(e, arg), m); + } + return e; +} + +expr_ref tptp_parser::parse_formula() { + return parse_expr(PREC_IFF); +} + +} + +static unsigned read_tptp_stream(std::istream& in, char const* current_file) { + register_on_timeout_proc(on_timeout); + try { + cmd_context ctx; + ctx.set_solver_factory(mk_smt_strategic_solver_factory()); + + tptp_parser p(ctx); + p.parse_input(in, current_file ? current_file : "."); + + // Suppress default check-sat output; TPTP frontend reports SZS status explicitly. + std::ostringstream sink; + scoped_regular_stream scoped_stream(ctx, sink); + TRACE(parser, ctx.get_solver()->display(tout)); + ctx.check_sat(0, nullptr); + switch (ctx.cs_state()) { + case cmd_context::css_unsat: + if (p.has_conjecture()) std::cout << "% SZS status Theorem\n"; + else std::cout << "% SZS status Unsatisfiable\n"; + break; + case cmd_context::css_sat: + if (p.has_conjecture()) std::cout << "% SZS status CounterSatisfiable\n"; + else std::cout << "% SZS status Satisfiable\n"; + if (g_display_model) { + model_ref mdl; + if (ctx.is_model_available(mdl)) + ctx.display_model(mdl); + } + break; + case cmd_context::css_unknown: + std::cout << "% SZS status GaveUp\n"; + { + std::string reason = ctx.reason_unknown(); + if (!reason.empty()) std::cout << "% SZS reason " << reason << "\n"; + } + break; + default: + break; + } + + if (g_display_statistics) { + ctx.set_regular_stream("stdout"); + ctx.display_statistics(); + } + return 0; + } + catch (parse_error const& ex) { + std::cerr << "TPTP parse error: " << ex.what() << "\n"; + return ERR_PARSER; + } + catch (z3_error const& ex) { + if (ex.error_code() == ERR_TIMEOUT) { + std::cout << "% SZS status Timeout\n"; + return 0; + } + std::cerr << "TPTP frontend error: " << ex.what() << "\n"; + return ERR_INTERNAL_FATAL; + } + catch (z3_exception const& ex) { + std::cerr << "TPTP frontend error: " << ex.what() << "\n"; + return ERR_INTERNAL_FATAL; + } +} + +unsigned read_tptp(char const* file_name) { + if (!file_name) + return read_tptp_stream(std::cin, "."); + std::ifstream in(file_name); + if (in.fail()) { + std::cerr << "TPTP parse error: failed to open file '" << file_name << "'\n"; + return ERR_PARSER; + } + return read_tptp_stream(in, file_name); +} + +unsigned read_tptp_string(char const* input) { + std::istringstream in(input ? input : ""); + return read_tptp_stream(in, ""); +} diff --git a/src/cmd_context/tptp_frontend.h b/src/cmd_context/tptp_frontend.h new file mode 100644 index 000000000..8e6b2fcb5 --- /dev/null +++ b/src/cmd_context/tptp_frontend.h @@ -0,0 +1,4 @@ +#pragma once + +unsigned read_tptp(char const* file_name); +unsigned read_tptp_string(char const* input); diff --git a/src/math/lp/dioph_eq.cpp b/src/math/lp/dioph_eq.cpp index 714eabc3a..5058bfaf3 100644 --- a/src/math/lp/dioph_eq.cpp +++ b/src/math/lp/dioph_eq.cpp @@ -238,18 +238,13 @@ namespace lp { r.c() -= b.c(); return r; } -#if Z3DEBUG - friend bool operator==(const term_o& a, const term_o& b) { + + friend bool eq(const term_o& a, const term_o& b) { term_o t = a.clone(); t += mpq(-1) * b; return t.c() == mpq(0) && t.size() == 0; } - friend bool operator!=(const term_o& a, const term_o& b) { - return ! (a == b); - } - -#endif term_o& operator+=(const term_o& t) { for (const auto& p : t) { add_monomial(p.coeff(), p.j()); @@ -1541,7 +1536,7 @@ namespace lp { term_o t1 = open_ml(t0); t1.add_monomial(mpq(1), j); term_o rs = fix_vars(t1); - if (ls != rs) { + if (!eq(ls, rs)) { TRACE(dio, tout << "ls:"; print_term_o(ls, tout) << "\n"; tout << "rs:"; print_term_o(rs, tout) << "\n";); return false; @@ -2351,7 +2346,7 @@ namespace lp { return false; } - bool ret = ls == fix_vars(open_ml(m_l_matrix.m_rows[ei])); + bool ret = eq(ls, fix_vars(open_ml(m_l_matrix.m_rows[ei]))); if (!ret) { CTRACE(dio, !ret, { diff --git a/src/math/lp/lar_solver.cpp b/src/math/lp/lar_solver.cpp index 6e689c004..ff5ff973e 100644 --- a/src/math/lp/lar_solver.cpp +++ b/src/math/lp/lar_solver.cpp @@ -36,7 +36,12 @@ namespace lp { struct term_comparer { bool operator()(const lar_term& a, const lar_term& b) const { - return a == b; + if (a.size() != b.size()) return false; + for (const auto& p : a) { + auto const* e = b.coeffs().find_core(p.j()); + if (!e || e->get_data().m_value != p.coeff()) return false; + } + return true; } }; diff --git a/src/math/lp/lar_term.h b/src/math/lp/lar_term.h index cca541801..b2d777493 100644 --- a/src/math/lp/lar_term.h +++ b/src/math/lp/lar_term.h @@ -129,8 +129,8 @@ public: add_monomial(a, v1); add_monomial(b, v2); } - bool operator==(const lar_term & a) const { return false; } // take care not to create identical terms - bool operator!=(const lar_term & a) const { return ! (*this == a);} + bool operator==(const lar_term & a) const = delete; // take care not to create identical terms + bool operator!=(const lar_term & a) const = delete; // some terms get used in add constraint // it is the same as the offset in the m_constraints diff --git a/src/math/lp/monomial_bounds.cpp b/src/math/lp/monomial_bounds.cpp index 28dc9a9e7..ff31f7ef5 100644 --- a/src/math/lp/monomial_bounds.cpp +++ b/src/math/lp/monomial_bounds.cpp @@ -312,6 +312,12 @@ namespace nla { } dep.mul(product, vi, product); } + if (do_propagate_down && c().params().arith_nl_monomial_sandwich() && + propagate_shared_factor(m)) + return true; + if (c().params().arith_nl_monomial_binomial_sign() && + propagate_binomial_sign(m)) + return true; return do_propagate_up && propagate_value(product, m.var()); } @@ -501,11 +507,196 @@ namespace nla { } lpvar monomial_bounds::non_fixed_var(monic const& m) { - for (lpvar v : m) + for (lpvar v : m) if (!c().var_is_fixed(v)) return v; return null_lpvar; } + /** + * Dual-row shared-factor sandwich. For a binary monomial m = u*v, find LP + * term columns whose term has shape a_m * m + a_v * v (exactly two + * variables, both factors of m). The term column's bound is a sound + * interval for (a_m * m + a_v * v). Substituting m = u*v yields + * v * (a_m * u + a_v); dividing by the interval on v (sign-determined) + * gives an interval on (a_m * u + a_v), and an affine shift gives an + * interval on u. The derived interval is fed to the existing + * propagate_value path so the lemma channel and integer rounding are + * shared with the rest of the propagation pipeline. + */ + bool monomial_bounds::propagate_shared_factor(monic const& m) { + if (m.size() != 2) + return false; + lpvar f0 = m.vars()[0], f1 = m.vars()[1]; + if (f0 == f1) + return false; + + unsigned const fanout_limit = c().params().arith_nl_monomial_sandwich_max_fanout(); + + auto try_pair = [&](lpvar u, lpvar v) -> bool { + // Skip if u participates in too many monomials: tightening such a + // factor cascades through ord-binom / monotonicity on every monic + // that contains it. + if (fanout_limit > 0) { + unsigned fanout = 0; + for (auto const& m1 : c().emons().get_use_list(u)) { + (void)m1; + if (++fanout > fanout_limit) + return false; + } + } + scoped_dep_interval vi(dep); + var2interval(v, vi); + if (!dep.separated_from_zero(vi)) + return false; + + auto& lra = c().lra; + unsigned const ROW_CAP = 16; + unsigned scanned = 0; + + for (auto const& cell : lra.A_r().m_columns[m.var()]) { + if (++scanned > ROW_CAP) + break; + unsigned basic = lra.get_base_column_in_row(cell.var()); + if (basic == m.var() || basic == v || basic == u) + continue; + if (!lra.column_has_term(basic)) + continue; + auto const& term = lra.get_term(basic); + if (term.size() != 2 || + !term.contains(m.var()) || !term.contains(v)) + continue; + + rational const& a_m = term.get_coeff(m.var()); + rational const& a_v = term.get_coeff(v); + if (a_m.is_zero()) + continue; + + // Term value = a_m*m + a_v*v; bound on basic bounds the term. + // Substituting m = u*v: term = v * (a_m*u + a_v). + scoped_dep_interval bi(dep); + var2interval(basic, bi); + + scoped_dep_interval inner(dep); + dep.div(bi, vi, inner); + + scoped_dep_interval shift(dep); + dep.set_value(shift, -a_v); + scoped_dep_interval scaled(dep); + dep.add(inner, shift, scaled); + + scoped_dep_interval u_int(dep); + dep.mul(rational::one() / a_m, scaled, u_int); + + TRACE(nla_solver, tout << "sandwich shared-factor basic=" << basic + << " m=" << m.var() << " v=" << v << " u=" << u + << " a_m=" << a_m << " a_v=" << a_v << "\n";); + + if (propagate_value(u_int, u)) + return true; // one lemma per call to keep the channel quiet + } + return false; + }; + + return try_pair(f1, f0) || try_pair(f0, f1); + } + + /** + * Sign-pinned binomial bound. For a binary monomial m = u*v in m_to_refine, + * use the current LP value mv = val(m.var()) as a one-sided anchor on the + * monomial value variable, and derive a deterministic interval for u via + * sign-aware division by v. + * + * Direction is chosen by the disagreement: if val(m.var()) > val(u)*val(v) + * the LP placed the monomial above the factor product, so we condition on + * "m.var() >= mv"; otherwise on "m.var() <= mv". The resulting clause is + * structurally analogous to a propagate_value lemma plus one extra + * snapshot literal on m.var(): under the asserted bounds on v, the clause + * reduces to a 2-disjunct (snapshot literal | factor bound). + * + * Targets the case ord-binom currently handles: factors have determined + * signs, m.var() may have no LP bound at all. The clause is sound modulo + * the monomial definition (the same condition propagate_down, + * propagate_shared_factor and ord-binom rely on). + */ + bool monomial_bounds::propagate_binomial_sign(monic const& m) { + if (m.size() != 2) + return false; + lpvar f0 = m.vars()[0], f1 = m.vars()[1]; + if (f0 == f1) + return false; + + rational const mv = c().val(m.var()); + rational const fp = c().val(f0) * c().val(f1); + if (mv == fp) + return false; + bool const below = mv > fp; // LP placed m.var() too high + llc const anchor_cmp = below ? llc::LT : llc::GT; + + auto try_anchor = [&](lpvar u, lpvar v) -> bool { + // Throttle once per (m.var(), u, v, direction) tuple. Without it + // each new val(m.var()) snapshot would re-emit and the search + // would cascade across model changes the same way ord-binom does. + if (c().throttle().insert_new( + nla_throttle::MONOMIAL_BINOMIAL_SIGN, + m.var(), u, v, below)) + return false; + + scoped_dep_interval vi(dep); + var2interval(v, vi); + if (!dep.separated_from_zero(vi)) + return false; + + // Synthesize a one-sided interval for m.var() at mv. No deps; + // the snapshot literal goes into the lemma body directly. + scoped_dep_interval mi_anchor(dep); + if (below) { + dep.set_lower(mi_anchor, mv); + dep.set_lower_is_inf(mi_anchor, false); + dep.set_lower_is_open(mi_anchor, false); + dep.set_upper_is_inf(mi_anchor, true); + } else { + dep.set_upper(mi_anchor, mv); + dep.set_upper_is_inf(mi_anchor, false); + dep.set_upper_is_open(mi_anchor, false); + dep.set_lower_is_inf(mi_anchor, true); + } + + scoped_dep_interval u_int(dep); + dep.div(mi_anchor, vi, u_int); + + bool emitted = false; + if (should_propagate_lower(u_int, u, 1)) { + auto const& lower = dep.lower(u_int); + if (!is_too_big(lower)) { + auto cmp = dep.lower_is_open(u_int) ? llc::GT : llc::GE; + lp::explanation ex; + dep.get_lower_dep(u_int, ex); + lemma_builder lemma(c(), "binomial sign anchor"); + lemma &= ex; + lemma |= ineq(m.var(), anchor_cmp, mv); + lemma |= ineq(u, cmp, lower); + emitted = true; + } + } + if (should_propagate_upper(u_int, u, 1)) { + auto const& upper = dep.upper(u_int); + if (!is_too_big(upper)) { + auto cmp = dep.upper_is_open(u_int) ? llc::LT : llc::LE; + lp::explanation ex; + dep.get_upper_dep(u_int, ex); + lemma_builder lemma(c(), "binomial sign anchor"); + lemma &= ex; + lemma |= ineq(m.var(), anchor_cmp, mv); + lemma |= ineq(u, cmp, upper); + emitted = true; + } + } + return emitted; + }; + + return try_anchor(f1, f0) || try_anchor(f0, f1); + } + } diff --git a/src/math/lp/monomial_bounds.h b/src/math/lp/monomial_bounds.h index eb536a231..564fda698 100644 --- a/src/math/lp/monomial_bounds.h +++ b/src/math/lp/monomial_bounds.h @@ -33,6 +33,8 @@ namespace nla { u_dependency* explain_fixed(monic const& m, rational const& k); lp::explanation get_explanation(u_dependency* dep); bool propagate_down(monic const& m, dep_interval& mi, lpvar v, unsigned power, dep_interval& product); + bool propagate_shared_factor(monic const& m); + bool propagate_binomial_sign(monic const& m); void analyze_monomial(monic const& m, unsigned& num_free, lpvar& free_v, unsigned& power) const; bool is_free(lpvar v) const; bool is_zero(lpvar v) const; diff --git a/src/math/lp/nla_core.h b/src/math/lp/nla_core.h index 8055c5e93..bf9252450 100644 --- a/src/math/lp/nla_core.h +++ b/src/math/lp/nla_core.h @@ -215,9 +215,9 @@ public: void deregister_monic_from_tables(const monic & m, unsigned i); void add_monic(lpvar v, unsigned sz, lpvar const* vs); - void add_idivision(lpvar q, lpvar x, lpvar y) { m_divisions.add_idivision(q, x, y); } - void add_rdivision(lpvar q, lpvar x, lpvar y) { m_divisions.add_rdivision(q, x, y); } - void add_bounded_division(lpvar q, lpvar x, lpvar y) { m_divisions.add_bounded_division(q, x, y); } + void add_idivision(lpvar q, lpvar x, lpvar y, lpvar r) { m_divisions.add_idivision(q, x, y, r); } + void add_rdivision(lpvar q, lpvar x, lpvar y, lpvar r) { m_divisions.add_rdivision(q, x, y, r); } + void add_bounded_division(lpvar q, lpvar x, lpvar y, lpvar r) { m_divisions.add_bounded_division(q, x, y, r); } void set_add_mul_def_hook(std::function const& f) { m_add_mul_def_hook = f; } lpvar add_mul_def(unsigned sz, lpvar const* vs) { SASSERT(m_add_mul_def_hook); lpvar v = m_add_mul_def_hook(sz, vs); add_monic(v, sz, vs); return v; } diff --git a/src/math/lp/nla_divisions.cpp b/src/math/lp/nla_divisions.cpp index 49b4ee765..5b4501e4e 100644 --- a/src/math/lp/nla_divisions.cpp +++ b/src/math/lp/nla_divisions.cpp @@ -18,26 +18,26 @@ Description: namespace nla { - void divisions::add_idivision(lpvar q, lpvar x, lpvar y) { - if (x == null_lpvar || y == null_lpvar || q == null_lpvar) + void divisions::add_idivision(lpvar q, lpvar x, lpvar y, lpvar r) { + if (x == null_lpvar || y == null_lpvar || q == null_lpvar || r == null_lpvar) return; - m_idivisions.push_back({q, x, y}); + m_idivisions.push_back({q, x, y, r}); m_core.trail().push(push_back_vector(m_idivisions)); } - void divisions::add_rdivision(lpvar q, lpvar x, lpvar y) { - if (x == null_lpvar || y == null_lpvar || q == null_lpvar) + void divisions::add_rdivision(lpvar q, lpvar x, lpvar y, lpvar r) { + if (x == null_lpvar || y == null_lpvar || q == null_lpvar || r == null_lpvar) return; - m_rdivisions.push_back({ q, x, y }); + m_rdivisions.push_back({ q, x, y, r }); m_core.trail().push(push_back_vector(m_rdivisions)); } - void divisions::add_bounded_division(lpvar q, lpvar x, lpvar y) { - if (x == null_lpvar || y == null_lpvar || q == null_lpvar) + void divisions::add_bounded_division(lpvar q, lpvar x, lpvar y, lpvar r) { + if (x == null_lpvar || y == null_lpvar || q == null_lpvar || r == null_lpvar) return; if (m_core.lra.column_has_term(x) || m_core.lra.column_has_term(y) || m_core.lra.column_has_term(q)) return; - m_bounded_divisions.push_back({ q, x, y }); + m_bounded_divisions.push_back({ q, x, y, r }); m_core.trail().push(push_back_vector(m_bounded_divisions)); } @@ -111,7 +111,7 @@ namespace nla { return false; }; - for (auto const & [r, x, y] : m_idivisions) { + for (auto const & [r, x, y, md] : m_idivisions) { if (!c.is_relevant(r)) continue; auto xval = c.val(x); @@ -120,7 +120,7 @@ namespace nla { // idiv semantics if (!xval.is_int() || !yval.is_int() || yval == 0 || rval == div(xval, yval)) continue; - for (auto const& [q2, x2, y2] : m_idivisions) { + for (auto const& [q2, x2, y2, md2] : m_idivisions) { if (q2 == r) continue; if (!c.is_relevant(q2)) @@ -133,7 +133,7 @@ namespace nla { } } - for (auto const& [r, x, y] : m_rdivisions) { + for (auto const& [r, x, y, md] : m_rdivisions) { if (!c.is_relevant(r)) continue; auto xval = c.val(x); @@ -142,7 +142,7 @@ namespace nla { // / semantics if (yval == 0 || rval == xval / yval) continue; - for (auto const& [q2, x2, y2] : m_rdivisions) { + for (auto const& [q2, x2, y2, md2] : m_rdivisions) { if (q2 == r) continue; if (!c.is_relevant(q2)) @@ -154,7 +154,8 @@ namespace nla { return; } } - + + check_mod_mult(); } // if p is bounded, q a value, r = eval(p): @@ -163,11 +164,11 @@ namespace nla { void divisions::check_bounded_divisions() { core& c = m_core; - unsigned offset = c.random(), sz = m_bounded_divisions.size(); + unsigned offset = c.random(), sz = m_bounded_divisions.size(); for (unsigned j = 0; j < sz; ++j) { unsigned i = (offset + j) % sz; - auto [q, x, y] = m_bounded_divisions[i]; + auto [q, x, y, r] = m_bounded_divisions[i]; if (!c.is_relevant(q)) continue; auto xv = c.val(x); @@ -188,9 +189,9 @@ namespace nla { rational lo = yv * div_v; if (xv > hi) { lemma_builder lemma(c, "y = yv & x <= yv * div(xv, yv) + yv - 1 => div(p, y) <= div(xv, yv)"); - lemma |= ineq(y, llc::NE, yv); - lemma |= ineq(x, llc::GT, hi); - lemma |= ineq(q, llc::LE, div_v); + lemma |= ineq(y, llc::NE, yv); + lemma |= ineq(x, llc::GT, hi); + lemma |= ineq(q, llc::LE, div_v); return; } if (xv < lo) { @@ -201,5 +202,45 @@ namespace nla { return; } } - } + } + + // mod(factor, p) = 0 => mod(factor * k, p) = 0 + // For each division (q, x, y, r) where x is a monic m = f1 * f2 * ... * fk, + // if some factor fi has mod(fi, p) = 0 (fixed), then mod(x, p) = 0. + void divisions::check_mod_mult() { + core& c = m_core; + unsigned offset = c.random(), sz = m_bounded_divisions.size(); + + for (unsigned j = 0; j < sz; ++j) { + unsigned i = (offset + j) % sz; + auto [q, x, y, r] = m_bounded_divisions[i]; + if (!c.is_relevant(q)) + continue; + if (c.var_is_fixed_to_zero(r)) + continue; + if (c.val(r).is_zero()) + continue; + if (!c.is_monic_var(x)) + continue; + auto yv = c.val(y); + if (yv <= 0 || !yv.is_int()) + continue; + auto const& m = c.emons()[x]; + for (lpvar f : m.vars()) { + for (auto const& [q2, x2, y2, r2] : m_bounded_divisions) { + if (x2 != f) + continue; + if (c.val(y2) != yv) + continue; + if (!c.var_is_fixed_to_zero(r2)) + continue; + // mod(factor, p) = 0 => mod(product, p) = 0 + lemma_builder lemma(c, "mod(factor, p) = 0 => mod(factor * k, p) = 0"); + lemma |= ineq(r2, llc::NE, 0); + lemma |= ineq(r, llc::EQ, 0); + return; + } + } + } + } } diff --git a/src/math/lp/nla_divisions.h b/src/math/lp/nla_divisions.h index 80bf5be4e..96a50c05a 100644 --- a/src/math/lp/nla_divisions.h +++ b/src/math/lp/nla_divisions.h @@ -22,16 +22,17 @@ namespace nla { class divisions { core& m_core; - vector> m_idivisions; - vector> m_rdivisions; - vector> m_bounded_divisions; - + vector> m_idivisions; + vector> m_rdivisions; + vector> m_bounded_divisions; + public: divisions(core& c):m_core(c) {} - void add_idivision(lpvar q, lpvar x, lpvar y); - void add_rdivision(lpvar q, lpvar x, lpvar y); - void add_bounded_division(lpvar q, lpvar x, lpvar y); + void add_idivision(lpvar q, lpvar x, lpvar y, lpvar r); + void add_rdivision(lpvar q, lpvar x, lpvar y, lpvar r); + void add_bounded_division(lpvar q, lpvar x, lpvar y, lpvar r); void check(); void check_bounded_divisions(); + void check_mod_mult(); }; } diff --git a/src/math/lp/nla_grobner.cpp b/src/math/lp/nla_grobner.cpp index 5df8439a9..67e5a6050 100644 --- a/src/math/lp/nla_grobner.cpp +++ b/src/math/lp/nla_grobner.cpp @@ -10,6 +10,8 @@ Author: Nikolaj Bjorner (nbjorner) --*/ +#include +#include #include "util/uint_set.h" #include "params/smt_params_helper.hpp" #include "math/lp/nla_core.h" @@ -77,56 +79,75 @@ namespace nla { if (!configure()) return; + bool productive = false; + try { if (propagate_gcd_test()) - return; + productive = true; } catch (...) { - + } - m_solver.saturate(); - TRACE(grobner, m_solver.display(tout)); + if (!productive) { + m_solver.saturate(); + TRACE(grobner, m_solver.display(tout)); - if (m_delay_base > 0) - --m_delay_base; - - try { + if (m_delay_base > 0) + --m_delay_base; - if (is_conflicting()) - return; + try { + productive = is_conflicting() + || propagate_quotients() + || propagate_gcd_test() + || propagate_eqs() + || propagate_factorization() + || propagate_linear_equations(); + } + catch (...) { - if (propagate_quotients()) - return; - - if (propagate_gcd_test()) - return; - - if (propagate_eqs()) - return; - - if (propagate_factorization()) - return; - - if (propagate_linear_equations()) - return; - - } - catch (...) { - + } } - // DEBUG_CODE(for (auto e : m_solver.equations()) check_missing_propagation(*e);); + if (c().params().arith_nl_grobner_adaptive()) + update_growth_boost(productive); + + if (productive) + return; - // for (auto e : m_solver.equations()) check_missing_propagation(*e); - ++m_delay_base; if (m_quota > 0) - --m_quota; + --m_quota; IF_VERBOSE(5, verbose_stream() << "grobner miss, quota " << m_quota << "\n"); IF_VERBOSE(5, diagnose_pdd_miss(verbose_stream())); } + void grobner::update_growth_boost(bool productive) { + // Bumping is conservative: requires two consecutive productive runs + // before any boost; misses decay toward unit by 1/4 per call. + unsigned const unit = m_config.m_adaptive_unit; + unsigned const cap = m_config.m_adaptive_max; + if (productive) { + ++m_hit_streak; + if (m_hit_streak >= m_config.m_adaptive_bump_after) { + unsigned next = m_growth_boost + (m_growth_boost >> 1); + m_growth_boost = std::min(next, cap); + m_hit_streak = 0; + } + } + else { + m_hit_streak = 0; + if (m_growth_boost > unit) { + unsigned excess = m_growth_boost - unit; + m_growth_boost -= (excess + 3) / 4; + if (m_growth_boost < unit) + m_growth_boost = unit; + } + } + IF_VERBOSE(5, verbose_stream() << "grobner adaptive boost " << m_growth_boost + << "/" << unit << (productive ? " (hit)" : " (miss)") << "\n"); + } + bool grobner::is_conflicting() { for (auto eq : m_solver.equations()) { if (is_conflicting(*eq)) { @@ -210,8 +231,6 @@ namespace nla { if (vars.empty() || !q.is_linear()) return false; - // IF_VERBOSE(0, verbose_stream() << "factored " << q << " : " << vars << "\n"); - auto [t, offset] = linear_to_term(q); vector ineqs; @@ -226,7 +245,6 @@ namespace nla { add_dependencies(lemma, eq); for (auto const& i : ineqs) lemma |= i; - //lemma.display(verbose_stream()); return true; } @@ -368,6 +386,70 @@ namespace nla { nl_vars.insert(j); } + // mod_residue: derive v's residue mod M from polynomial divisibility. + // + // Common case. Given polynomial + // p = M*v1 + v - M*v2*v3 = 0, + // every monomial except v is M-divisible, so v ≡ 0 (mod M). + // Combined with 0 ≤ v < M, this forces v = 0. + // Emit: dependencies => (v < 0) ∨ (v ≥ M) ∨ (v = 0). + // + // General case. For a linear monomial c_v*v in p with c0 the constant + // term, require c_i/c_v integer for every non-v monomial and c0/c_v + // integer (call it K). Let M = gcd(|c_i/c_v|) over non-v monomials. + // Then p/c_v gives v + M*Q + K = 0 with Q integer, so v ≡ -K (mod M). + // With target = (-K) mod M ∈ [0, M-1], emit + // dependencies => (v < 0) ∨ (v ≥ M) ∨ (v = target). + for (auto const& mv : p) { + if (mv.vars.size() != 1) + continue; + lpvar vv = mv.vars[0]; + if (!c().var_is_int(vv)) + continue; + rational c_v = mv.coeff; + SASSERT(c_v != 0); + rational M(0); // 0 sentinel: "no non-v non-constant monomial seen yet". + rational c0(0); + bool ok = true; + for (auto const& mi : p) { + if (mi.vars.size() == 1 && mi.vars[0] == vv) + continue; // skip the mv monomial itself + if (mi.vars.empty()) { + c0 = mi.coeff; + continue; + } + rational quot = mi.coeff / c_v; + if (!quot.is_int()) { ok = false; break; } + rational a = abs(quot); + SASSERT(a != 0); + M = M == 0 ? a : gcd(M, a); + if (M == 1) { ok = false; break; } // trivial modulus, abort + } + if (!ok || M == 0) + continue; + rational K = c0 / c_v; + if (!K.is_int()) + continue; + rational target = mod(-K, M); // Euclidean: result in [0, M-1]. + SASSERT(target >= 0 && target < M); + // Skip if the lemma is already satisfied by the current model: + // any of (v < 0), (v ≥ M), (v = target) trivially holding means + // emission would be redundant. Without this guard, the lemma + // re-emits every Grobner round on the same polynomial. + rational v_val = c().val(vv); + if (v_val < 0 || v_val >= M || v_val == target) + continue; + lemma_builder lemma(c(), "grobner-mod-residue"); + add_dependencies(lemma, eq); + lemma |= ineq(vv, llc::LT, rational::zero()); + lemma |= ineq(vv, llc::GE, M); + lemma |= ineq(vv, llc::EQ, target); + TRACE(grobner, lemma.display(tout << "mod_residue v=" << vv + << " M=" << M << " c_v=" << c_v << " c0=" << c0 + << " target=" << target << "\n")); + return true; + } + bool found_lemma = false; for (auto v : nl_vars) { auto& m = p.manager(); @@ -559,25 +641,27 @@ namespace nla { } TRACE(grobner, m_solver.display(tout)); -#if 0 - IF_VERBOSE(2, m_pdd_grobner.display(verbose_stream())); - dd::pdd_eval eval(m_pdd_manager); - eval.var2val() = [&](unsigned j){ return val(j); }; - for (auto* e : m_pdd_grobner.equations()) { - dd::pdd p = e->poly(); - rational v = eval(p); - if (p.is_linear() && !eval(p).is_zero()) { - IF_VERBOSE(0, verbose_stream() << "violated linear constraint " << p << "\n"); - } - } -#endif - struct dd::solver::config cfg; cfg.m_max_steps = m_solver.equations().size(); cfg.m_max_simplified = c().params().arith_nl_grobner_max_simplified(); cfg.m_eqs_growth = c().params().arith_nl_grobner_eqs_growth(); cfg.m_expr_size_growth = c().params().arith_nl_grobner_expr_size_growth(); cfg.m_expr_degree_growth = c().params().arith_nl_grobner_expr_degree_growth(); + if (c().params().arith_nl_grobner_adaptive() && m_growth_boost != m_config.m_adaptive_unit) { + // Wider intermediate to prevent overflow when a user param is + // close to UINT_MAX; clamp before assigning back to the unsigned + // config fields. + uint64_t const unit = m_config.m_adaptive_unit; + uint64_t const boost = m_growth_boost; + auto scale = [unit, boost](unsigned x) -> unsigned { + uint64_t y = (static_cast(x) * boost) / unit; + return y > UINT_MAX ? UINT_MAX : static_cast(y); + }; + cfg.m_eqs_growth = scale(cfg.m_eqs_growth); + cfg.m_expr_size_growth = scale(cfg.m_expr_size_growth); + cfg.m_expr_degree_growth = scale(cfg.m_expr_degree_growth); + cfg.m_max_simplified = scale(cfg.m_max_simplified); + } cfg.m_number_of_conflicts_to_report = c().params().arith_nl_grobner_cnfl_to_report(); m_solver.set(cfg); m_solver.adjust_cfg(); @@ -588,14 +672,12 @@ namespace nla { std::ostream& grobner::diagnose_pdd_miss(std::ostream& out) { - // m_pdd_grobner.display(out); - dd::pdd_eval eval; eval.var2val() = [&](unsigned j){ return val(j); }; for (auto* e : m_solver.equations()) { dd::pdd p = e->poly(); rational v = eval(p); - if (!v.is_zero()) { + if (v != 0) { out << p << " := " << v << "\n"; } } @@ -701,7 +783,15 @@ namespace nla { lp::lpvar j = c().lra.add_term(coeffs, UINT_MAX); c().lra.update_column_type_and_bound(j, lp::lconstraint_kind::EQ, offset, e.dep()); - c().m_check_feasible = true; + c().m_check_feasible = true; + TRACE(nla_solver, + // Print the term as installed (post subst_known_terms), not the + // pre-add_term coeffs vector. add_term normalizes/substitutes + // term-column references, so coeffs and the resulting row can + // diverge if any var is itself a term-column. + tout << "grobner-linear-eq: "; + c().lra.print_term(c().lra.get_term(j), tout); + tout << " = " << offset << "\n";); return true; } diff --git a/src/math/lp/nla_grobner.h b/src/math/lp/nla_grobner.h index b9ed043d5..411954343 100644 --- a/src/math/lp/nla_grobner.h +++ b/src/math/lp/nla_grobner.h @@ -24,7 +24,13 @@ namespace nla { bool m_propagate_quotients = false; bool m_gcd_test = false; bool m_expand_terms = false; + // Adaptive growth (gated by arith.nl.grobner_adaptive). m_growth_boost + // is in fixed-point units of 1/m_adaptive_unit (m_adaptive_unit == 1.0x). + unsigned m_adaptive_unit = 16; + unsigned m_adaptive_max = 4 * 16; + unsigned m_adaptive_bump_after = 2; }; + config m_config; dd::pdd_manager m_pdd_manager; dd::solver m_solver; lp::lar_solver& lra; @@ -32,8 +38,9 @@ namespace nla { unsigned m_quota = 0; unsigned m_delay_base = 0; unsigned m_delay = 0; + unsigned m_growth_boost = m_config.m_adaptive_unit; + unsigned m_hit_streak = 0; bool m_add_all_eqs = false; - config m_config; std::unordered_map m_mon2var; lp::lp_settings& lp_settings(); @@ -70,6 +77,9 @@ namespace nla { bool equation_is_true(dd::solver::equation const& eq); + // adaptive growth (gated by arith.nl.grobner_adaptive) + void update_growth_boost(bool productive); + // setup bool configure(); void set_level2var(); diff --git a/src/math/lp/nla_order_lemmas.cpp b/src/math/lp/nla_order_lemmas.cpp index bb413f4c4..e3c8618f7 100644 --- a/src/math/lp/nla_order_lemmas.cpp +++ b/src/math/lp/nla_order_lemmas.cpp @@ -81,9 +81,11 @@ void order::order_lemma_on_binomial(const monic& ac) { */ void order::order_lemma_on_binomial_sign(const monic& xy, lpvar x, lpvar y, int sign) { + if (!c().params().arith_nl_order_binomial_sign()) + return; if (!c().var_is_int(x) && val(x).is_big()) return; - + SASSERT(!_().mon_has_zero(xy.vars())); int sy = rat_sign(val(y)); diff --git a/src/math/lp/nla_solver.cpp b/src/math/lp/nla_solver.cpp index eb669ab4b..562143459 100644 --- a/src/math/lp/nla_solver.cpp +++ b/src/math/lp/nla_solver.cpp @@ -20,16 +20,16 @@ namespace nla { m_core->add_monic(v, sz, vs); } - void solver::add_idivision(lpvar q, lpvar x, lpvar y) { - m_core->add_idivision(q, x, y); + void solver::add_idivision(lpvar q, lpvar x, lpvar y, lpvar r) { + m_core->add_idivision(q, x, y, r); } - void solver::add_rdivision(lpvar q, lpvar x, lpvar y) { - m_core->add_rdivision(q, x, y); + void solver::add_rdivision(lpvar q, lpvar x, lpvar y, lpvar r) { + m_core->add_rdivision(q, x, y, r); } - void solver::add_bounded_division(lpvar q, lpvar x, lpvar y) { - m_core->add_bounded_division(q, x, y); + void solver::add_bounded_division(lpvar q, lpvar x, lpvar y, lpvar r) { + m_core->add_bounded_division(q, x, y, r); } void solver::set_relevant(std::function& is_relevant) { diff --git a/src/math/lp/nla_solver.h b/src/math/lp/nla_solver.h index e6d02e793..36d136d38 100644 --- a/src/math/lp/nla_solver.h +++ b/src/math/lp/nla_solver.h @@ -28,9 +28,9 @@ namespace nla { ~solver(); const auto& monics_with_changed_bounds() const { return m_core->monics_with_changed_bounds(); } void add_monic(lpvar v, unsigned sz, lpvar const* vs); - void add_idivision(lpvar q, lpvar x, lpvar y); - void add_rdivision(lpvar q, lpvar x, lpvar y); - void add_bounded_division(lpvar q, lpvar x, lpvar y); + void add_idivision(lpvar q, lpvar x, lpvar y, lpvar r); + void add_rdivision(lpvar q, lpvar x, lpvar y, lpvar r); + void add_bounded_division(lpvar q, lpvar x, lpvar y, lpvar r); void check_bounded_divisions(); void set_relevant(std::function& is_relevant); void updt_params(params_ref const& p); diff --git a/src/math/lp/nla_tangent_lemmas.cpp b/src/math/lp/nla_tangent_lemmas.cpp index 91676b660..b322a48be 100644 --- a/src/math/lp/nla_tangent_lemmas.cpp +++ b/src/math/lp/nla_tangent_lemmas.cpp @@ -18,6 +18,12 @@ class tangent_imp { rational m_correct_v; // "below" means that the incorrect value is less than the correct one, that is m_v < m_correct_v bool m_below; + // pl is in the strict interior of the bound box (model-driven points + // get_initial_points + push_point); McCormick at the box corners + // requires non-strict inequality because the tangent meets the surface + // along the box's edges (xy = pl.y*x + pl.x*y - pl.x*pl.y at x = pl.x + // or y = pl.y). + bool m_pl_strict_interior = true; rational m_v; // the monomial value lpvar m_j; // the monic variable const monic& m_m; @@ -89,7 +95,10 @@ private: t.add_monomial(- m_y.rat_sign()*pl.x, m_jy); t.add_monomial(- m_x.rat_sign()*pl.y, m_jx); t.add_var(m_j); - lemma |= ineq(t, m_below? llc::GT : llc::LT, - pl.x*pl.y); + llc cmp = m_below + ? (m_pl_strict_interior ? llc::GT : llc::GE) + : (m_pl_strict_interior ? llc::LT : llc::LE); + lemma |= ineq(t, cmp, - pl.x*pl.y); explain(lemma); } @@ -164,14 +173,61 @@ private: return a.x * m_xy.y + a.y * m_xy.x - a.x * a.y; } + // McCormick at box corners: choose m_a, m_b at the corners of + // [x_lo, x_hi] x [y_lo, y_hi] that bound xy from the side dictated by + // m_below. Returns false if either factor has an unbounded side, the + // box is degenerate, or the current LP value of a factor coincides with + // a chosen corner — generate_plane's negate_relation requires + // val(j) != corner_coord (SASSERT in debug; trivially-true literal in + // release). The caller falls back to the model-driven point selection in + // these cases. + bool set_box_corners() { + if (!c().has_lower_bound(m_jx) || !c().has_upper_bound(m_jx)) + return false; + if (!c().has_lower_bound(m_jy) || !c().has_upper_bound(m_jy)) + return false; + rational const& x_lo = c().get_lower_bound(m_jx); + rational const& x_hi = c().get_upper_bound(m_jx); + rational const& y_lo = c().get_lower_bound(m_jy); + rational const& y_hi = c().get_upper_bound(m_jy); + if (x_lo == x_hi || y_lo == y_hi) + return false; + // negate_relation requires the model value to be strictly separated + // from the corner coordinate it's compared to. If LP currently sits + // exactly at a box edge, fall back. + rational const& vx = c().val(m_jx); + rational const& vy = c().val(m_jy); + if (vx == x_lo || vx == x_hi || vy == y_lo || vy == y_hi) + return false; + if (m_below) { + // Under-approximation: tangents at (x_lo, y_lo) and (x_hi, y_hi) + // bound xy from below across the box. + m_a = point(x_lo, y_lo); + m_b = point(x_hi, y_hi); + } else { + // Over-approximation: anti-diagonal corners. + m_a = point(x_lo, y_hi); + m_b = point(x_hi, y_lo); + } + m_pl_strict_interior = false; + return true; + } + void get_points() { + if (c().params().arith_nl_tangents_box_corners() && set_box_corners()) { + // Box corners are extremes; pushing further moves out of the box + // and would invalidate the McCormick property. + TRACE(nla_solver, tout << "xy = " << m_xy << ", box-corner points: "; + print_tangent_domain(tout) << std::endl;); + return; + } get_initial_points(); TRACE(nla_solver, tout << "xy = " << m_xy << ", correct val = " << m_correct_v; print_tangent_domain(tout << "\ntang points:") << std::endl;); - push_point(m_a); + push_point(m_a); push_point(m_b); TRACE(nla_solver, - tout << "pushed a = " << m_a << std::endl + tout << "pushed a = " << m_a << std::endl << "pushed b = " << m_b << std::endl << "tang_plane(a) = " << tang_plane(m_a) << " , val = " << m_a << ", " << "tang_plane(b) = " << tang_plane(m_b) << " , val = " << m_b << std::endl;); diff --git a/src/math/lp/nla_throttle.h b/src/math/lp/nla_throttle.h index f0b84e0c3..6c58918b9 100644 --- a/src/math/lp/nla_throttle.h +++ b/src/math/lp/nla_throttle.h @@ -18,9 +18,10 @@ class nla_throttle { public: enum throttle_kind { ORDER_LEMMA, // order lemma (9 params) - BINOMIAL_SIGN_LEMMA, // binomial sign (6 params) + BINOMIAL_SIGN_LEMMA, // binomial sign (6 params) MONOTONE_LEMMA, // monotonicity (2 params) - TANGENT_LEMMA // tangent lemma (5 params: monic_var, x_var, y_var, below, plane_type) + TANGENT_LEMMA, // tangent lemma (5 params: monic_var, x_var, y_var, below, plane_type) + MONOMIAL_BINOMIAL_SIGN // monomial binomial sign anchor (4 params: monic_var, u, v, below) }; private: diff --git a/src/math/lp/nla_types.h b/src/math/lp/nla_types.h index 401e4eb62..89823e4dc 100644 --- a/src/math/lp/nla_types.h +++ b/src/math/lp/nla_types.h @@ -41,9 +41,8 @@ namespace nla { ineq(const lp::lar_term& term, lp::lconstraint_kind cmp, const rational& rs) : m_cmp(cmp), m_term(term), m_rs(rs) {} ineq(lpvar v, lp::lconstraint_kind cmp, int i): m_cmp(cmp), m_term(v), m_rs(rational(i)) {} ineq(lpvar v, lp::lconstraint_kind cmp, rational const& r): m_cmp(cmp), m_term(v), m_rs(r) {} - bool operator==(const ineq& a) const { - return m_cmp == a.m_cmp && m_term == a.m_term && m_rs == a.m_rs; - } + bool operator==(const ineq& a) const = delete; + bool operator!=(const ineq& a) const = delete; const lp::lar_term& term() const { return m_term; }; lp::lconstraint_kind cmp() const { return m_cmp; }; const rational& rs() const { return m_rs; }; diff --git a/src/math/lp/nra_solver.cpp b/src/math/lp/nra_solver.cpp index 96a1c97a3..f722019e1 100644 --- a/src/math/lp/nra_solver.cpp +++ b/src/math/lp/nra_solver.cpp @@ -64,8 +64,10 @@ struct solver::imp { m_lp2nl.reset(); } - // Create polynomial definition for variable v used in setup_assignment_solver. - // Side-effects: updates m_vars2mon when v is a monic variable. + // Create polynomial definition for variable v used in setup_solver_poly. + // The definition recursively expands monic and term variables into + // polynomials in leaf variables, scaled by an integer denominator + // tracked in `denominators` to keep the coefficients integral. void mk_definition(unsigned v, polynomial_ref_vector &definitions, vector& denominators) { auto &pm = m_nlsat->pm(); polynomial::polynomial_ref p(pm); @@ -100,44 +102,6 @@ struct solver::imp { denominators.push_back(den); } - // Create polynomial definition for variable v used in setup_assignment_solver. - // Side-effects: updates m_vars2mon when v is a monic variable. - void mk_definition_assignment(unsigned v, polynomial_ref_vector &definitions) { - auto &pm = m_nlsat->pm(); - polynomial::polynomial_ref p(pm); - if (m_nla_core.emons().is_monic_var(v)) { - auto const &m = m_nla_core.emons()[v]; - auto vars = m.vars(); - std::sort(vars.begin(), vars.end()); - m_vars2mon.insert(vars, v); - for (auto v2 : vars) { - auto pv = definitions.get(v2); - if (!p) - p = pv; - else - p = pm.mul(p, pv); - } - } - else if (lra.column_has_term(v)) { - rational den(1); - for (auto const& [w, coeff] : lra.get_term(v)) - den = lcm(den, denominator(coeff)); - for (auto const& [w, coeff] : lra.get_term(v)) { - auto pw = definitions.get(w); - polynomial::polynomial_ref term(pm); - term = pm.mul(den * coeff, pw); - if (!p) - p = term; - else - p = pm.add(p, term); - } - } - else { - p = pm.mk_polynomial(lp2nl(v)); - } - definitions.push_back(p); - } - void setup_solver_poly() { m_coi.init(); auto &pm = m_nlsat->pm(); @@ -260,20 +224,9 @@ struct solver::imp { out.close(); } - lbool r = l_undef; statistics& st = m_nla_core.lp_settings().stats().m_st; - try { - r = m_nlsat->check(); - } - catch (z3_exception&) { - if (m_limit.is_canceled()) { - r = l_undef; - } - else { - m_nlsat->collect_statistics(st); - throw; - } - } + lbool r = m_nlsat->check(); + m_nlsat->collect_statistics(st); TRACE(nra, tout << "nra result " << r << "\n"); CTRACE(nra, false, @@ -329,6 +282,24 @@ struct solver::imp { m_coi.init(); auto &pm = m_nlsat->pm(); polynomial_ref_vector definitions(pm); + vector denominators; + + // Create an NLSAT polyvar for each LRA variable (identity mapping), + // seed the assignment from the current LRA model, populate + // m_vars2mon, and build the inlined polynomial definition of v. + // + // The definition expands monic and term variables into polynomials + // over leaf variables. Each definition is scaled by denominators[v] + // so that all coefficients stay integral; the scaling cancels on + // both sides of every constraint we build below (just like in + // setup_solver_poly). + // + // This "de-linearized" representation is what the linear-cell + // construction in NLSAT needs: a cell built around a constraint + // polynomial that mentions several multiplications at once can + // yield a lemma constraining all of them simultaneously, which is + // strictly stronger than the per-multiplication lemmas we would + // get from asserting `v_mon - v1*...*vk = 0` separately. for (unsigned v = 0; v < lra.number_of_vars(); ++v) { auto j = m_nlsat->mk_var(lra.var_is_int(v)); VERIFY(j == v); @@ -336,29 +307,47 @@ struct solver::imp { scoped_anum a(am()); am().set(a, m_nla_core.val(v).to_mpq()); m_values->push_back(a); - mk_definition_assignment(v, definitions); + if (m_nla_core.emons().is_monic_var(v)) { + auto const &m = m_nla_core.emons()[v]; + auto vars = m.vars(); + std::sort(vars.begin(), vars.end()); + m_vars2mon.insert(vars, v); + } + mk_definition(v, definitions, denominators); } + // Substitute each variable in the LRA constraint by its definition + // and rescale to keep integer coefficients. Symbolically: + // + // v == definitions[v] / denominators[v] + // + // sum(coeff_v * v) k rhs + // == sum((coeff_v / denominators[v]) * definitions[v]) k rhs + // + // We pick den := lcm of all denominators(coeff_v / denominators[v]) + // together with denominator(rhs), so that den * coeff_v / denominators[v] + // and den * rhs are all integers. The relation kind k is preserved + // because den > 0. for (auto ci : m_coi.constraints()) { auto &c = lra.constraints()[ci]; - auto &pm = m_nlsat->pm(); auto k = c.kind(); auto rhs = c.rhs(); auto lhs = c.coeffs(); rational den = denominator(rhs); for (auto [coeff, v] : lhs) - den = lcm(den, denominator(coeff)); + den = lcm(den, denominator(coeff / denominators[v])); polynomial::polynomial_ref p(pm); p = pm.mk_const(-den * rhs); - for (auto [coeff, v] : lhs) { polynomial_ref poly(pm); - poly = pm.mul(den * coeff, definitions.get(v)); + poly = definitions.get(v); + poly = poly * constant(den * coeff / denominators[v]); p = p + poly; } auto lit = add_constraint(p, ci, k); m_literal2constraint.setx(lit.index(), ci, lp::null_ci); } + definitions.reset(); } void process_polynomial_check_assignment(polynomial::polynomial const* p, rational& bound, const u_map& nl2lp, lp::lar_term& t) { @@ -406,25 +395,15 @@ struct solver::imp { setup_assignment_solver(); lbool r = l_undef; statistics &st = m_nla_core.lp_settings().stats().m_st; - nlsat::literal_vector clause; - try { - nlsat::assignment rvalues(m_nlsat->am()); - for (auto [j, x] : m_lp2nl) { - scoped_anum a(am()); - am().set(a, m_nla_core.val(j).to_mpq()); - rvalues.set(x, a); - } - r = m_nlsat->check(rvalues, clause); - } - catch (z3_exception &) { - if (m_limit.is_canceled()) { - r = l_undef; - } - else { - m_nlsat->collect_statistics(st); - throw; - } + nlsat::literal_vector clause; + nlsat::assignment rvalues(m_nlsat->am()); + for (auto [j, x] : m_lp2nl) { + scoped_anum a(am()); + am().set(a, m_nla_core.val(j).to_mpq()); + rvalues.set(x, a); } + r = m_nlsat->check(rvalues, clause); + m_nlsat->collect_statistics(st); switch (r) { case l_true: @@ -449,7 +428,6 @@ struct solver::imp { lbool add_lemma(nlsat::literal_vector const &clause) { u_map nl2lp = reverse_lp2nl(); - polynomial::manager &pm = m_nlsat->pm(); lbool result = l_false; { nla::lemma_builder lemma(m_nla_core, __FUNCTION__); @@ -657,20 +635,8 @@ struct solver::imp { add_ub(lra.get_upper_bound(v), w, lra.get_column_upper_bound_witness(v)); } - lbool r = l_undef; - statistics& st = m_nla_core.lp_settings().stats().m_st; - try { - r = m_nlsat->check(); - } - catch (z3_exception&) { - if (m_limit.is_canceled()) { - r = l_undef; - } - else { - m_nlsat->collect_statistics(st); - throw; - } - } + lbool r = m_nlsat->check(); + statistics &st = m_nla_core.lp_settings().stats().m_st; m_nlsat->collect_statistics(st); switch (r) { @@ -719,18 +685,8 @@ struct solver::imp { add_ub(lra.get_upper_bound(v), w); } - lbool r = l_undef; - try { - r = m_nlsat->check(); - } - catch (z3_exception&) { - if (m_limit.is_canceled()) { - r = l_undef; - } - else { - throw; - } - } + + lbool r = m_nlsat->check(); if (r == l_true) return r; @@ -959,19 +915,68 @@ solver::~solver() { lbool solver::check() { - return m_imp->check(); + try { + return m_imp->check(); + } + catch (z3_exception &) { + statistics &st = m_imp->m_nla_core.lp_settings().stats().m_st; + m_imp->m_nlsat->collect_statistics(st); + if (m_imp->m_limit.is_canceled()) { + return l_undef; + } + else { + throw; + } + } } lbool solver::check(vector const& eqs) { - return m_imp->check(eqs); + try { + return m_imp->check(eqs); + } + catch (z3_exception &) { + statistics &st = m_imp->m_nla_core.lp_settings().stats().m_st; + m_imp->m_nlsat->collect_statistics(st); + if (m_imp->m_limit.is_canceled()) { + return l_undef; + } + else { + throw; + } + } } lbool solver::check(dd::solver::equation_vector const& eqs) { - return m_imp->check(eqs); + try { + return m_imp->check(eqs); + } + catch (z3_exception &) { + statistics &st = m_imp->m_nla_core.lp_settings().stats().m_st; + m_imp->m_nlsat->collect_statistics(st); + if (m_imp->m_limit.is_canceled()) { + return l_undef; + } + else { + throw; + } + } } lbool solver::check_assignment() { - return m_imp->check_assignment(); + try { + return m_imp->check_assignment(); + } + catch (z3_exception &) { + statistics &st = m_imp->m_nla_core.lp_settings().stats().m_st; + m_imp->m_nlsat->collect_statistics(st); + IF_VERBOSE(0, verbose_stream() << "check-assignment\n"); + if (m_imp->m_limit.is_canceled()) { + return l_undef; + } + else { + throw; + } + } } bool solver::need_check() { diff --git a/src/math/polynomial/algebraic_numbers.cpp b/src/math/polynomial/algebraic_numbers.cpp index bc654b7c4..07b49e5ce 100644 --- a/src/math/polynomial/algebraic_numbers.cpp +++ b/src/math/polynomial/algebraic_numbers.cpp @@ -2028,6 +2028,20 @@ namespace algebraic_numbers { } IF_VERBOSE(9, verbose_stream() << "sturm 1\n"); + + // Check whether a can be separated from b's interval and vice versa + // this recognizes the case where the intervals overlap, + // but the anums do not lie in the intersection of the intervals. + scoped_mpq l_a(qm()), u_a(qm()), l_b(qm()), u_b(qm()); + to_mpq(qm(), la, l_a); + to_mpq(qm(), ua, u_a); + to_mpq(qm(), lb, l_b); + to_mpq(qm(), ub, u_b); + if (compare(cell_a, l_b) == sign_neg) return sign_neg; + if (compare(cell_a, u_b) == sign_pos) return sign_pos; + if (compare(cell_b, l_a) == sign_neg) return sign_pos; + if (compare(cell_b, u_a) == sign_pos) return sign_neg; + // // EXPENSIVE CASE // Let seq be the Sturm-Tarski sequence for diff --git a/src/math/polynomial/algebraic_numbers.h b/src/math/polynomial/algebraic_numbers.h index 46cb3c6da..e60f8ea1a 100644 --- a/src/math/polynomial/algebraic_numbers.h +++ b/src/math/polynomial/algebraic_numbers.h @@ -381,7 +381,7 @@ namespace algebraic_numbers { - class anum { + class anum { enum anum_kind { BASIC = 0, ROOT }; void* m_cell; public: @@ -389,6 +389,17 @@ namespace algebraic_numbers { anum(basic_cell* cell) :m_cell(TAG(void*, cell, BASIC)) { } anum(algebraic_cell * cell):m_cell(TAG(void*, cell, ROOT)) { } + // Move nulls the source so std::sort's inner shifts stay alias-free + // if the comparator throws between moves (avoids a later double-free). + anum(anum const &) = default; + anum & operator=(anum const &) = default; + anum(anum && other) noexcept : m_cell(other.m_cell) { other.m_cell = nullptr; } + anum & operator=(anum && other) noexcept { + m_cell = other.m_cell; + other.m_cell = nullptr; + return *this; + } + bool is_basic() const { return GET_TAG(m_cell) == BASIC; } basic_cell * to_basic() const { SASSERT(is_basic()); return UNTAG(basic_cell*, m_cell); } algebraic_cell * to_algebraic() const { SASSERT(!is_basic()); return UNTAG(algebraic_cell*, m_cell); } diff --git a/src/model/model_core.cpp b/src/model/model_core.cpp index 887f6c634..65f7d2e50 100644 --- a/src/model/model_core.cpp +++ b/src/model/model_core.cpp @@ -130,19 +130,3 @@ void model_core::unregister_decl(func_decl * d) { } } -void model_core::add_lambda_defs() { - unsigned sz = get_num_decls(); - for (unsigned i = sz; i-- > 0; ) { - func_decl* f = get_decl(i); - quantifier* q = m.is_lambda_def(f); - if (!q) - continue; - if (f->get_arity() > 0) { - func_interp* fi = alloc(func_interp, m, f->get_arity()); - fi->set_else(q); - register_decl(f, fi); - } - else - register_decl(f, q); - } -} diff --git a/src/model/model_core.h b/src/model/model_core.h index 6a52fa10b..6cc52bbcb 100644 --- a/src/model/model_core.h +++ b/src/model/model_core.h @@ -58,6 +58,8 @@ public: return eval(f, r) && m.is_false(r); } + void add_lambda_defs(); + unsigned get_num_constants() const { return m_const_decls.size(); } unsigned get_num_functions() const { return m_func_decls.size(); } func_decl * get_constant(unsigned i) const { return m_const_decls[i]; } @@ -72,8 +74,6 @@ public: void unregister_decl(func_decl * d); func_interp* update_func_interp(func_decl* f, func_interp* fi); - void add_lambda_defs(); - virtual expr * get_some_value(sort * s) = 0; virtual expr * get_fresh_value(sort * s) = 0; virtual bool get_some_values(sort * s, expr_ref & v1, expr_ref & v2) = 0; diff --git a/src/muz/base/dl_context.cpp b/src/muz/base/dl_context.cpp index bf84d7be7..797ce0774 100644 --- a/src/muz/base/dl_context.cpp +++ b/src/muz/base/dl_context.cpp @@ -777,7 +777,7 @@ namespace datalog { datatype_util dt; bv_util bv; array_util ar; - DL_ENGINE m_engine_type; + DL_ENGINE m_engine_type = DATALOG_ENGINE; bool is_large_bv(expr *e) { sort *s = e->get_sort(); @@ -961,7 +961,6 @@ namespace datalog { if (get_engine() == DATALOG_ENGINE) { m_rel = dynamic_cast(m_engine.get()); } - } } diff --git a/src/muz/base/dl_context.h b/src/muz/base/dl_context.h index 394b217df..1181cf82b 100644 --- a/src/muz/base/dl_context.h +++ b/src/muz/base/dl_context.h @@ -192,7 +192,7 @@ namespace datalog { model_converter_ref m_mc; proof_converter_ref m_pc; - rel_context_base* m_rel; + rel_context_base* m_rel = nullptr; scoped_ptr m_engine; bool m_closed; @@ -201,7 +201,7 @@ namespace datalog { execution_result m_last_status; expr_ref m_last_answer; expr_ref m_last_ground_answer; - DL_ENGINE m_engine_type; + DL_ENGINE m_engine_type = LAST_ENGINE; diff --git a/src/muz/base/dl_rule.h b/src/muz/base/dl_rule.h index 0a8fd955c..87c2637a9 100644 --- a/src/muz/base/dl_rule.h +++ b/src/muz/base/dl_rule.h @@ -86,6 +86,7 @@ namespace datalog { case forall_k: m_univ = true; break; case exists_k: m_exist = true; break; case lambda_k: m_lambda = true; break; + case choice_k: break; } } void operator()(app * n) { } diff --git a/src/nlsat/levelwise.cpp b/src/nlsat/levelwise.cpp index 2ff363763..e085d6173 100644 --- a/src/nlsat/levelwise.cpp +++ b/src/nlsat/levelwise.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -85,15 +86,20 @@ namespace nlsat { unsigned ps_idx; // index in m_level_ps root_function(anum_manager& am, poly* p, unsigned idx, anum const& v, unsigned ps_idx) : val(am), ire{ p, idx }, ps_idx(ps_idx) { am.set(val, v); } - root_function(root_function&& other) noexcept : val(other.val.m()), ire(other.ire), ps_idx(other.ps_idx) { val = other.val; } + root_function(root_function&& other) noexcept : val(std::move(other.val)), ire(other.ire), ps_idx(other.ps_idx) { } root_function(root_function const&) = delete; root_function& operator=(root_function const&) = delete; root_function& operator=(root_function&& other) noexcept { - val = other.val; + val.swap(other.val); ire = other.ire; ps_idx = other.ps_idx; return *this; } + friend void swap(root_function& a, root_function& b) noexcept { + a.val.swap(b.val); + std::swap(a.ire, b.ire); + std::swap(a.ps_idx, b.ps_idx); + } }; // Root functions (Theta) and the chosen relation (≼) on a given level. @@ -950,12 +956,48 @@ namespace nlsat { return m_pm.id(a.ire.p) < m_pm.id(b.ire.p); } + // Apply a permutation to a range of root_functions using swap cycles, + // avoiding the bulk anum allocations that std::sort's move operations cause. + void apply_permutation(std_vector& rfs, unsigned offset, std_vector const& perm) { + std_vector done(perm.size(), false); + for (unsigned i = 0; i < perm.size(); ++i) { + if (done[i] || perm[i] == i) + continue; + unsigned j = i; + while (!done[j]) { + done[j] = true; + unsigned k = perm[j]; + if (!done[k]) + swap(rfs[offset + j], rfs[offset + k]); + j = k; + } + } + } + void sort_root_function_partitions(std_vector::iterator mid) { auto& rfs = m_rel.m_rfunc; - std::sort(rfs.begin(), mid, - [&](root_function const& a, root_function const& b) { return root_function_lt(a, b, true); }); - std::sort(mid, rfs.end(), - [&](root_function const& a, root_function const& b) { return root_function_lt(a, b, false); }); + unsigned mid_pos = static_cast(mid - rfs.begin()); + + // Sort lower partition [0, mid_pos) by index permutation + if (mid_pos > 1) { + std_vector perm(mid_pos); + std::iota(perm.begin(), perm.end(), 0u); + std::sort(perm.begin(), perm.end(), [&](unsigned a, unsigned b) { + return root_function_lt(rfs[a], rfs[b], true); + }); + apply_permutation(rfs, 0, perm); + } + + // Sort upper partition [mid_pos, size) by index permutation + unsigned upper_sz = static_cast(rfs.size()) - mid_pos; + if (upper_sz > 1) { + std_vector perm(upper_sz); + std::iota(perm.begin(), perm.end(), 0u); + std::sort(perm.begin(), perm.end(), [&](unsigned a, unsigned b) { + return root_function_lt(rfs[mid_pos + a], rfs[mid_pos + b], false); + }); + apply_permutation(rfs, mid_pos, perm); + } } // Populate Θ (root functions) around the sample, partitioned at `mid`, and sort each partition. @@ -964,6 +1006,9 @@ namespace nlsat { init_poly_has_roots(); std_vector lhalf, uhalf; + // Pre-reserve to reduce reallocation during emplace_back + lhalf.reserve(m_level_ps.size()); + uhalf.reserve(m_level_ps.size()); if (!collect_partitioned_root_functions_around_sample(v, lhalf, uhalf)) return false; @@ -1039,6 +1084,8 @@ namespace nlsat { void add_linear_approximations(anum const& v) { polynomial_ref p_lower(m_pm), p_upper(m_pm); auto& r = m_rel.m_rfunc; + // Reserve space to avoid reallocation during emplace + r.reserve(r.size() + 2); if (m_I[m_level].is_section()) { if (!m_am.is_rational(v)) { NOT_IMPLEMENTED_YET(); diff --git a/src/nlsat/tactic/nlsat_tactic.cpp b/src/nlsat/tactic/nlsat_tactic.cpp index c47a586f8..c12c29570 100644 --- a/src/nlsat/tactic/nlsat_tactic.cpp +++ b/src/nlsat/tactic/nlsat_tactic.cpp @@ -133,8 +133,7 @@ class nlsat_tactic : public tactic { return ok; } - void operator()(goal_ref const & g, - goal_ref_buffer & result) { + void operator()(goal_ref const & g, goal_ref_buffer & result) { tactic_report report("nlsat", *g); if (g->is_decided()) { diff --git a/src/params/smt_parallel_params.pyg b/src/params/smt_parallel_params.pyg index 2dfebd2fc..dde7656ff 100644 --- a/src/params/smt_parallel_params.pyg +++ b/src/params/smt_parallel_params.pyg @@ -4,4 +4,9 @@ def_module_params('smt_parallel', params=( ('inprocessing', BOOL, False, 'integrate in-processing as a heuristic simplification'), ('sls', BOOL, False, 'add sls-tactic as a separate worker thread outside the search tree parallelism'), - )) \ No newline at end of file + ('num_global_bb_fl_threads', UINT, 0, 'run failed-literal backbone worker threads; default is 0 (off), supported values are 1 (negative mode only) or 2 (negative and positive mode)'), + ('num_global_bb_batch_threads', UINT, 0, 'run Janota-style chunking backbone worker threads; default is 0 (off), supported values are 1 (negative mode only) or 2 (negative and positive mode)'), + ('local_backbones', BOOL, False, 'enable local backbones experiment within the search tree parallelism'), + ('core_minimize', BOOL, True, 'minimize unsat cores used for parallel cube backtracking'), + ('ablate_backtracking', BOOL, False, 'ablation: pass entire cube as core instead of unsat core during backtracking'), + )) diff --git a/src/params/smt_params.cpp b/src/params/smt_params.cpp index bc16cb674..2bca286db 100644 --- a/src/params/smt_params.cpp +++ b/src/params/smt_params.cpp @@ -27,6 +27,7 @@ void smt_params::updt_local_params(params_ref const & _p) { m_random_seed = p.random_seed(); m_relevancy_lvl = p.relevancy(); m_ematching = p.ematching(); + m_ho_matching = p.ho_matching(); m_induction = p.induction(); m_clause_proof = p.clause_proof(); m_phase_selection = static_cast(p.phase_selection()); diff --git a/src/params/smt_params.h b/src/params/smt_params.h index bd22cbb09..5fc9744bb 100644 --- a/src/params/smt_params.h +++ b/src/params/smt_params.h @@ -109,6 +109,7 @@ struct smt_params : public preprocessor_params, bool m_display_features = false; bool m_new_core2th_eq = true; bool m_ematching = true; + bool m_ho_matching = false; bool m_induction = false; bool m_clause_proof = false; symbol m_proof_log; diff --git a/src/params/smt_params_helper.pyg b/src/params/smt_params_helper.pyg index dd143974c..eb4c5d4f9 100644 --- a/src/params/smt_params_helper.pyg +++ b/src/params/smt_params_helper.pyg @@ -10,6 +10,7 @@ def_module_params(module_name='smt', ('quasi_macros', BOOL, False, 'try to find universally quantified formulas that are quasi-macros'), ('restricted_quasi_macros', BOOL, False, 'try to find universally quantified formulas that are restricted quasi-macros'), ('ematching', BOOL, True, 'E-Matching based quantifier instantiation'), + ('ho_matching', BOOL, False, 'higher-order matching for quantifier instantiation'), ('phase_selection', UINT, 3, 'phase selection heuristic: 0 - always false, 1 - always true, 2 - phase caching, 3 - phase caching conservative, 4 - phase caching conservative 2, 5 - random, 6 - number of occurrences, 7 - theory'), ('phase_caching_on', UINT, 400, 'number of conflicts while phase caching is on'), ('phase_caching_off', UINT, 100, 'number of conflicts while phase caching is off'), @@ -21,6 +22,7 @@ def_module_params(module_name='smt', ('elim_unconstrained', BOOL, True, 'pre-processing: eliminate unconstrained subterms'), ('solve_eqs', BOOL, True, 'pre-processing: solve equalities'), ('solve_eqs.non_ground', BOOL, True, 'pre-processing: solve equalities. Allow eliminating variables by non-ground solutions which can break behavior for model evaluation.'), + ('solve_eqs.linear', BOOL, False, 'allow only linear substitutions where a variable is replaced by a term having at most one non-constant argument'), ('propagate_values', BOOL, True, 'pre-processing: propagate values'), ('bound_simplifier', BOOL, True, 'apply bounds simplification during pre-processing'), ('pull_nested_quantifiers', BOOL, False, 'pre-processing: pull nested quantifiers'), @@ -66,8 +68,10 @@ def_module_params(module_name='smt', ('arith.nl.expensive_patching', BOOL, False, 'use the expensive of monomials'), ('arith.nl.rounds', UINT, 1024, 'threshold for number of (nested) final checks for non linear arithmetic, relevant only if smt.arith.solver=2'), ('arith.nl.order', BOOL, True, 'run order lemmas'), + ('arith.nl.order.binomial_sign', BOOL, True, 'run order_lemma_on_binomial_sign; disabling it keeps the structural order-lemma splitting'), ('arith.nl.expp', BOOL, False, 'expensive patching'), ('arith.nl.tangents', BOOL, True, 'run tangent lemmas'), + ('arith.nl.tangents.box_corners', BOOL, False, 'choose tangent-plane points at the bound-box corners instead of the model-centered val(x) +/- delta; produces the McCormick under/over envelope and is deterministic and snapshot-independent'), ('arith.nl.horner', BOOL, True, 'run horner\'s heuristic'), ('arith.nl.horner_subs_fixed', UINT, 2, '0 - no subs, 1 - substitute, 2 - substitute fixed zeros only'), ('arith.nl.horner_frequency', UINT, 4, 'horner\'s call frequency'), @@ -83,9 +87,13 @@ def_module_params(module_name='smt', ('arith.nl.grobner_propagate_quotients', BOOL, True, 'detect conflicts x*y + z = 0 where x doesn\'t divide z'), ('arith.nl.grobner_gcd_test', BOOL, True, 'detect gcd conflicts for polynomial powers x^k - y = 0'), ('arith.nl.grobner_exp_delay', BOOL, True, 'use exponential delay between grobner basis attempts'), + ('arith.nl.grobner_adaptive', BOOL, False, 'scale grobner growth knobs (eqs/size/degree/max_simplified) up on productive runs and down on misses'), ('arith.nl.gr_q', UINT, 10, 'grobner\'s quota'), ('arith.nl.grobner_subs_fixed', UINT, 1, '0 - no subs, 1 - substitute, 2 - substitute fixed zeros only'), ('arith.nl.grobner_expand_terms', BOOL, True, 'expand terms before computing grobner basis'), + ('arith.nl.monomial_sandwich', BOOL, False, 'derive bound on a monomial factor by pairing two LP rows that share the other factor'), + ('arith.nl.monomial_sandwich.max_fanout', UINT, 0, 'skip monomial sandwich when the conclusion factor appears in more than this many monomials (0 = no limit)'), + ('arith.nl.monomial_binomial_sign', BOOL, False, 'derive bound on a binomial-monomial factor anchored on the current LP value of the monomial; replaces order_lemma_on_binomial_sign with a deterministic factor bound conditioned on a one-sided snapshot of the monomial value'), ('arith.nl.reduce_pseudo_linear', BOOL, True, 'create incremental linearization axioms for pseudo-linear monomials'), ('arith.nl.delay', UINT, 10, 'number of calls to final check before invoking bounded nlsat check'), ('arith.nl.propagate_linear_monomials', BOOL, True, 'propagate linear monomials'), diff --git a/src/parsers/smt2/smt2parser.cpp b/src/parsers/smt2/smt2parser.cpp index 3601c5a5e..f7bfe6ed5 100644 --- a/src/parsers/smt2/smt2parser.cpp +++ b/src/parsers/smt2/smt2parser.cpp @@ -21,6 +21,7 @@ Revision History: #include "ast/bv_decl_plugin.h" #include "ast/arith_decl_plugin.h" #include "ast/seq_decl_plugin.h" +#include "ast/array_decl_plugin.h" #include "ast/ast_pp.h" #include "ast/well_sorted.h" #include "ast/rewriter/rewriter.h" @@ -79,6 +80,7 @@ namespace smt2 { symbol m_forall; symbol m_exists; symbol m_lambda; + symbol m_choice; symbol m_as; symbol m_not; symbol m_root_obj; @@ -156,12 +158,14 @@ namespace smt2 { unsigned m_expr_spos; unsigned m_param_spos; bool m_as_sort; - app_frame(symbol const & f, unsigned expr_spos, unsigned param_spos, bool as_sort): + bool m_expr_head; + app_frame(symbol const & f, unsigned expr_spos, unsigned param_spos, bool as_sort, bool expr_head = false): expr_frame(EF_APP), m_f(f), m_expr_spos(expr_spos), m_param_spos(param_spos), - m_as_sort(as_sort) {} + m_as_sort(as_sort), + m_expr_head(expr_head) {} }; struct quant_frame : public expr_frame { @@ -420,6 +424,11 @@ namespace smt2 { bool curr_id_is_forall() const { SASSERT(curr_is_identifier()); return curr_id() == m_forall; } bool curr_id_is_exists() const { SASSERT(curr_is_identifier()); return curr_id() == m_exists; } bool curr_id_is_lambda() const { SASSERT(curr_is_identifier()); return curr_id() == m_lambda; } + bool curr_id_is_choice() const { + SASSERT(curr_is_identifier()); + return curr_id() == m_choice; + } + bool curr_id_is_bang() const { SASSERT(curr_is_identifier()); return curr_id() == m_bang; } bool curr_id_is_let() const { SASSERT(curr_is_identifier()); return curr_id() == m_let; } bool curr_id_is_root_obj() const { SASSERT(curr_is_identifier()); return curr_id() == m_root_obj; } @@ -1354,10 +1363,11 @@ namespace smt2 { void push_quant_frame(quantifier_kind k) { SASSERT(curr_is_identifier()); - SASSERT(curr_id_is_forall() || curr_id_is_exists() || curr_id_is_lambda()); + SASSERT(curr_id_is_forall() || curr_id_is_exists() || curr_id_is_lambda() || curr_id_is_choice()); SASSERT((k == forall_k) == curr_id_is_forall()); SASSERT((k == exists_k) == curr_id_is_exists()); SASSERT((k == lambda_k) == curr_id_is_lambda()); + SASSERT((k == choice_k) == curr_id_is_choice()); next(); void * mem = m_stack.allocate(sizeof(quant_frame)); new (mem) quant_frame(k, pattern_stack().size(), nopattern_stack().size(), symbol_stack().size(), @@ -1888,23 +1898,7 @@ namespace smt2 { sexpr_stack().pop_back(); } - void push_app_frame() { - SASSERT(curr_is_lparen() || curr_is_identifier()); - unsigned param_spos = m_param_stack.size(); - unsigned expr_spos = expr_stack().size(); - bool has_as, is_lambda; - auto f = parse_qualified_identifier(has_as, is_lambda); - - void * mem = m_stack.allocate(sizeof(app_frame)); - new (mem) app_frame(f, expr_spos, param_spos, has_as); - m_num_expr_frames++; - if (is_lambda) - push_quant_frame(lambda_k); - } - - void push_expr_frame(expr_frame * curr) { - SASSERT(curr_is_lparen()); - next(); + void push_expr_frame_core(expr_frame * curr) { TRACE(push_expr_frame, tout << "push_expr_frame(), curr(): " << m_curr << "\n";); if (curr_is_identifier()) { TRACE(push_expr_frame, tout << "push_expr_frame(), curr_id(): " << curr_id() << "\n";); @@ -1920,6 +1914,9 @@ namespace smt2 { else if (curr_id_is_lambda()) { push_quant_frame(lambda_k); } + else if (curr_id_is_choice()) { + push_quant_frame(choice_k); + } else if (curr_id_is_bang()) { push_bang_frame(curr); } @@ -1944,6 +1941,49 @@ namespace smt2 { } } + void push_app_frame() { + SASSERT(curr_is_lparen() || curr_is_identifier()); + unsigned param_spos = m_param_stack.size(); + unsigned expr_spos = expr_stack().size(); + bool has_as = false, is_lambda = false; + symbol f = symbol::null; + bool expr_head = false; + + if (curr_is_lparen()) { + next(); + if (curr_is_identifier() && curr_id_is_lambda()) { + is_lambda = true; + f = symbol("select"); + } + else if (curr_is_identifier() && (curr_id_is_underscore() || curr_id_is_as())) { + f = parse_qualified_identifier_core(has_as); + } + else { + expr_head = true; + } + } + else { + f = parse_qualified_identifier(has_as, is_lambda); + } + + void * mem = m_stack.allocate(sizeof(app_frame)); + auto* frame = new (mem) app_frame(f, expr_spos, param_spos, has_as, expr_head); + m_num_expr_frames++; + + if (is_lambda) { + push_quant_frame(lambda_k); + } + else if (expr_head) { + push_expr_frame_core(frame); + } + } + + void push_expr_frame(expr_frame * curr) { + SASSERT(curr_is_lparen()); + next(); + push_expr_frame_core(curr); + } + void pop_app_frame(app_frame * fr) { SASSERT(expr_stack().size() >= fr->m_expr_spos); SASSERT(m_param_stack.size() >= fr->m_param_spos); @@ -1952,15 +1992,15 @@ namespace smt2 { unsigned num_args = expr_stack().size() - fr->m_expr_spos; unsigned num_indices = m_param_stack.size() - fr->m_param_spos; expr_ref t_ref(m()); - local l; - if (m_env.find(fr->m_f, l)) { - push_local(l); - t_ref = expr_stack().back(); - for (unsigned i = 0; i < num_args; ++i) { + if (fr->m_expr_head) { + if (num_args < 2) + throw parser_exception("invalid function application, arguments missing"); + t_ref = expr_stack().get(fr->m_expr_spos); + for (unsigned i = 1; i < num_args; ++i) { expr* arg = expr_stack().get(fr->m_expr_spos + i); expr* args[2] = { t_ref.get(), arg }; - m_ctx.mk_app(symbol("select"), - 2, + m_ctx.mk_app(symbol("select"), + 2, args, 0, nullptr, @@ -1969,13 +2009,31 @@ namespace smt2 { } } else { - m_ctx.mk_app(fr->m_f, - num_args, - expr_stack().data() + fr->m_expr_spos, - num_indices, - m_param_stack.data() + fr->m_param_spos, - fr->m_as_sort ? sort_stack().back() : nullptr, - t_ref); + local l; + if (m_env.find(fr->m_f, l)) { + push_local(l); + t_ref = expr_stack().back(); + for (unsigned i = 0; i < num_args; ++i) { + expr* arg = expr_stack().get(fr->m_expr_spos + i); + expr* args[2] = { t_ref.get(), arg }; + m_ctx.mk_app(symbol("select"), + 2, + args, + 0, + nullptr, + nullptr, + t_ref); + } + } + else { + m_ctx.mk_app(fr->m_f, + num_args, + expr_stack().data() + fr->m_expr_spos, + num_indices, + m_param_stack.data() + fr->m_param_spos, + fr->m_as_sort ? sort_stack().back() : nullptr, + t_ref); + } } expr_stack().shrink(fr->m_expr_spos); m_param_stack.shrink(fr->m_param_spos); @@ -2061,7 +2119,7 @@ namespace smt2 { fr->m_qid = symbol((unsigned)m_scanner.get_line()); if (fr->m_kind != lambda_k && !m().is_bool(expr_stack().back())) throw parser_exception("quantifier body must be a Boolean expression"); - quantifier* new_q = m().mk_quantifier(fr->m_kind, + quantifier* new_q = m().mk_quantifier(fr->m_kind == choice_k ? lambda_k : fr->m_kind, num_decls, sort_stack().data() + fr->m_sort_spos, symbol_stack().data() + fr->m_sym_spos, @@ -2082,8 +2140,11 @@ namespace smt2 { m_env.end_scope(); SASSERT(num_decls <= m_num_bindings); m_num_bindings -= num_decls; - - expr_stack().push_back(new_q); + if (fr->m_kind == choice_k) { + expr_stack().push_back(array_util(m()).mk_choice(new_q)); + } + else + expr_stack().push_back(new_q); m_stack.deallocate(fr); m_num_expr_frames--; } @@ -3088,6 +3149,7 @@ namespace smt2 { m_forall("forall"), m_exists("exists"), m_lambda("lambda"), + m_choice("choice"), m_as("as"), m_not("not"), m_root_obj("root-obj"), @@ -3296,5 +3358,3 @@ sexpr_ref parse_sexpr(cmd_context& ctx, std::istream& is, params_ref const& ps, return p.parse_sexpr_ref(); } - - diff --git a/src/qe/mbp/mbp_dt_tg.cpp b/src/qe/mbp/mbp_dt_tg.cpp index aee54c459..b844843cd 100644 --- a/src/qe/mbp/mbp_dt_tg.cpp +++ b/src/qe/mbp/mbp_dt_tg.cpp @@ -163,6 +163,24 @@ struct mbp_dt_tg::impl { if (is_app(term) && m_dt_util.is_accessor(to_app(term)->get_decl()) && has_var(to_app(term)->get_arg(0))) { + // Only apply rm_accessor if the model confirms the argument + // has the constructor that this accessor belongs to. + // Otherwise we introduce a contradictory is-cons literal. + func_decl *cons = + m_dt_util.get_accessor_constructor(to_app(term)->get_decl()); + func_decl *rec = m_dt_util.get_constructor_recognizer(cons); + expr_ref is_rec(m.mk_app(rec, to_app(term)->get_arg(0)), m); + if (!m_mdl.is_true(is_rec)) { + // Ground the argument so the accessor term becomes + // constructively ground. This preserves any enclosing + // literal (e.g., (not (is-nil (tl nil)))) as a guard in + // the output, preventing an over-approximation. + expr_ref is(m.mk_not(is_rec), m); + m_tg.add_lit(is); + mark_seen(term); + progress = true; + continue; + } mark_seen(term); progress = true; rm_accessor(term); diff --git a/src/sat/sat_aig_finder.cpp b/src/sat/sat_aig_finder.cpp index a1013108f..82ecb4e20 100644 --- a/src/sat/sat_aig_finder.cpp +++ b/src/sat/sat_aig_finder.cpp @@ -192,7 +192,7 @@ namespace sat { return false; } binary b(~y, x, nullptr); - if (!binaries.find(b, b)) { + if (!binaries.find(b, b) || !b.use_list) { return false; } for (auto p : *b.use_list) { diff --git a/src/sat/sat_solver/inc_sat_solver.cpp b/src/sat/sat_solver/inc_sat_solver.cpp index 7e7200832..11b5c771d 100644 --- a/src/sat/sat_solver/inc_sat_solver.cpp +++ b/src/sat/sat_solver/inc_sat_solver.cpp @@ -27,6 +27,7 @@ Notes: #include "solver/tactic2solver.h" #include "solver/parallel_params.hpp" #include "solver/parallel_tactical.h" +#include "solver/parallel_tactical2.h" #include "tactic/tactical.h" #include "tactic/aig/aig_tactic.h" #include "tactic/core/propagate_values_tactic.h" @@ -1183,5 +1184,9 @@ void inc_sat_display(std::ostream& out, solver& _s, unsigned sz, expr*const* sof tactic * mk_psat_tactic(ast_manager& m, params_ref const& p) { parallel_params pp(p); - return pp.enable() ? mk_parallel_tactic(mk_inc_sat_solver(m, p, false), p) : mk_sat_tactic(m); + if (pp.enable()) + return mk_parallel_tactic(mk_inc_sat_solver(m, p, false), p); + if (pp.enable2()) + return mk_parallel_tactic2(mk_inc_sat_solver(m, p, false), p); + return mk_sat_tactic(m); } diff --git a/src/sat/smt/array_axioms.cpp b/src/sat/smt/array_axioms.cpp index cd20e6ae6..e5e98baf2 100644 --- a/src/sat/smt/array_axioms.cpp +++ b/src/sat/smt/array_axioms.cpp @@ -68,6 +68,8 @@ namespace array { return assert_extensionality(r.n->get_expr(), r.select->get_expr()); case axiom_record::kind_t::is_congruence: return assert_congruent_axiom(r.n->get_expr(), r.select->get_expr()); + case axiom_record::kind_t::is_choice: + return assert_choice_axiom(r.n->get_app()); default: UNREACHABLE(); break; @@ -469,6 +471,27 @@ namespace array { return ctx.propagate(e_internalize(alpha), e_internalize(beta), array_axiom()); } + bool solver::assert_choice_axiom(app* choice_term) { + ++m_stats.m_num_choice_axiom; + SASSERT(a.is_choice(choice_term)); + expr* pred = choice_term->get_arg(0); + sort* pred_sort = pred->get_sort(); + SASSERT(a.is_array(pred_sort)); + SASSERT(get_array_arity(pred_sort) == 1); + SASSERT(m.is_bool(get_array_range(pred_sort))); + sort* x_sort = get_array_domain(pred_sort, 0); + expr_ref x(m.mk_var(0, x_sort), m); + expr* args1[2] = { pred, x }; + expr_ref px(a.mk_select(2, args1), m); + expr* args2[2] = { pred, choice_term }; + expr_ref pc(a.mk_select(2, args2), m); + expr_ref body(m.mk_implies(px, pc), m); + symbol x_name("x"); + expr_ref q(m.mk_forall(1, &x_sort, &x_name, body), m); + rewrite(q); + return add_unit(mk_literal(q)); + } + /** \brief assert n1 = n2 => forall vars . (n1 vars) = (n2 vars) */ @@ -691,4 +714,3 @@ namespace array { } } - diff --git a/src/sat/smt/array_diagnostics.cpp b/src/sat/smt/array_diagnostics.cpp index 11ed4384d..0f03abf46 100644 --- a/src/sat/smt/array_diagnostics.cpp +++ b/src/sat/smt/array_diagnostics.cpp @@ -55,6 +55,8 @@ namespace array { return out << "extensionality " << ctx.bpp(r.n) << " " << ctx.bpp(r.select); case axiom_record::kind_t::is_congruence: return out << "congruence " << ctx.bpp(r.n) << " " << ctx.bpp(r.select); + case axiom_record::kind_t::is_choice: + return out << "choice " << ctx.bpp(r.n); default: UNREACHABLE(); } @@ -75,6 +77,7 @@ namespace array { st.update("array def/map", m_stats.m_num_default_map_axiom); st.update("array def/const", m_stats.m_num_default_const_axiom); st.update("array def/store", m_stats.m_num_default_store_axiom); + st.update("array choice ax", m_stats.m_num_choice_axiom); st.update("array ext ax", m_stats.m_num_extensionality_axiom); st.update("array cong ax", m_stats.m_num_congruence_axiom); st.update("array exp ax2", m_stats.m_num_select_store_axiom_delayed); diff --git a/src/sat/smt/array_internalize.cpp b/src/sat/smt/array_internalize.cpp index 7a62286e6..b9018deb5 100644 --- a/src/sat/smt/array_internalize.cpp +++ b/src/sat/smt/array_internalize.cpp @@ -111,6 +111,9 @@ namespace array { case OP_CONST_ARRAY: internalize_lambda_eh(n); break; + case OP_CHOICE: + push_axiom(choice_axiom(n)); + break; case OP_ARRAY_EXT: SASSERT(is_array(n->get_arg(0))); push_axiom(extensionality_axiom(n->get_arg(0), n->get_arg(1))); @@ -169,6 +172,8 @@ namespace array { case OP_ARRAY_DEFAULT: set_prop_upward(find(n->get_arg(0))); break; + case OP_CHOICE: + break; case OP_ARRAY_MAP: case OP_SET_UNION: case OP_SET_INTERSECT: @@ -255,4 +260,3 @@ namespace array { } } - diff --git a/src/sat/smt/array_solver.h b/src/sat/smt/array_solver.h index fce3efaac..41337d726 100644 --- a/src/sat/smt/array_solver.h +++ b/src/sat/smt/array_solver.h @@ -43,7 +43,7 @@ namespace array { unsigned m_num_select_const_axiom, m_num_select_store_axiom_delayed; unsigned m_num_default_store_axiom, m_num_default_map_axiom; unsigned m_num_default_const_axiom, m_num_default_as_array_axiom; - unsigned m_num_select_lambda_axiom; + unsigned m_num_select_lambda_axiom, m_num_choice_axiom; void reset() { memset(this, 0, sizeof(*this)); } stats() { reset(); } }; @@ -86,7 +86,8 @@ namespace array { is_select, is_extensionality, is_default, - is_congruence + is_congruence, + is_choice }; enum class state_t { is_new, @@ -165,6 +166,7 @@ namespace array { axiom_record store_axiom(euf::enode* n) { return axiom_record(axiom_record::kind_t::is_store, n); } axiom_record extensionality_axiom(euf::enode* x, euf::enode* y) { return axiom_record(axiom_record::kind_t::is_extensionality, x, y); } axiom_record congruence_axiom(euf::enode* a, euf::enode* b) { return axiom_record(axiom_record::kind_t::is_congruence, a, b); } + axiom_record choice_axiom(euf::enode* n) { return axiom_record(axiom_record::kind_t::is_choice, n); } scoped_ptr m_constraint; @@ -176,6 +178,7 @@ namespace array { bool assert_select_as_array_axiom(app* select, app* arr); bool assert_select_map_axiom(app* select, app* map); bool assert_select_lambda_axiom(app* select, expr* lambda); + bool assert_choice_axiom(app* choice_term); bool assert_extensionality(expr* e1, expr* e2); bool assert_default_map_axiom(app* map); bool assert_default_const_axiom(app* cnst); diff --git a/src/shell/main.cpp b/src/shell/main.cpp index 19e617e55..0703f731f 100644 --- a/src/shell/main.cpp +++ b/src/shell/main.cpp @@ -30,6 +30,7 @@ Revision History: #include "shell/dimacs_frontend.h" #include "shell/datalog_frontend.h" #include "shell/opt_frontend.h" +#include "cmd_context/tptp_frontend.h" #include "util/timeout.h" #include "util/z3_exception.h" #include "util/error_codes.h" @@ -43,14 +44,14 @@ Revision History: #include #endif -typedef enum { IN_UNSPECIFIED, IN_SMTLIB_2, IN_DATALOG, IN_DIMACS, IN_WCNF, IN_OPB, IN_LP, IN_Z3_LOG, IN_DRAT } input_kind; +typedef enum { IN_UNSPECIFIED, IN_SMTLIB_2, IN_DATALOG, IN_DIMACS, IN_WCNF, IN_OPB, IN_LP, IN_Z3_LOG, IN_DRAT, IN_TPTP } input_kind; static char const * g_input_file = nullptr; static char const * g_drat_input_file = nullptr; static bool g_standard_input = false; static input_kind g_input_kind = IN_UNSPECIFIED; -bool g_display_statistics = false; -bool g_display_model = false; +extern bool g_display_statistics; +extern bool g_display_model; static bool g_display_istatistics = false; static void error(const char * msg) { @@ -84,6 +85,7 @@ void display_usage() { std::cout << " -opb use parser for PB optimization input format.\n"; std::cout << " -lp use parser for a modest subset of CPLEX LP input format.\n"; std::cout << " -log use parser for Z3 log input format.\n"; + std::cout << " -tptp use parser for TPTP input format (fof/cnf/tff/thf fragments).\n"; std::cout << " -in read formula from standard input.\n"; std::cout << " -model display model for satisfiable SMT.\n"; std::cout << "\nMiscellaneous:\n"; @@ -130,6 +132,15 @@ static bool validate_is_ulong(char const* s) { return false; return true; } + +static bool is_tptp_extension(char const* ext) { + static char const* tptp_extensions[] = {"p", "tptp", "fof", "cnf", "tff", "thf"}; + for (char const* known_ext : tptp_extensions) { + if (strcmp(ext, known_ext) == 0) + return true; + } + return false; +} static void parse_cmd_line_args(std::string& input_file, int argc, char ** argv) { long timeout = 0; @@ -214,6 +225,9 @@ static void parse_cmd_line_args(std::string& input_file, int argc, char ** argv) else if (strcmp(opt_name, "log") == 0) { g_input_kind = IN_Z3_LOG; } + else if (strcmp(opt_name, "tptp") == 0) { + g_input_kind = IN_TPTP; + } else if (strcmp(opt_name, "st") == 0) { g_display_statistics = true; gparams::set("stats", "true"); @@ -323,10 +337,35 @@ static void parse_cmd_line_args(std::string& input_file, int argc, char ** argv) } } else if (argv[i][0] != '"' && (eq_pos = strchr(argv[i], '='))) { - char * key = argv[i]; - *eq_pos = 0; - char * value = eq_pos+1; - gparams::set(key, value); + // If the argument looks like a file path (contains path separators + // or has a file extension), treat it as a filename rather than + // a parameter assignment. This handles files with '=' in their names. + bool is_filepath = strchr(argv[i], '/') || strchr(argv[i], '\\'); + if (!is_filepath) { + char const * ext = get_extension(argv[i]); + if (ext && (strcmp(ext, "smt2") == 0 || strcmp(ext, "smt") == 0 || + strcmp(ext, "dimacs") == 0 || strcmp(ext, "cnf") == 0 || + strcmp(ext, "wcnf") == 0 || strcmp(ext, "opb") == 0 || + strcmp(ext, "lp") == 0 || strcmp(ext, "log") == 0 || + strcmp(ext, "drat") == 0 || strcmp(ext, "p") == 0)) + is_filepath = true; + } + if (is_filepath) { + if (get_extension(arg) && strcmp(get_extension(arg), "drat") == 0) { + g_input_kind = IN_DRAT; + g_drat_input_file = arg; + } + else if (g_input_file) + warning_msg("input file was already specified."); + else + g_input_file = arg; + } + else { + char * key = argv[i]; + *eq_pos = 0; + char * value = eq_pos+1; + gparams::set(key, value); + } } else { if (get_extension(arg) && strcmp(get_extension(arg), "drat") == 0) { @@ -387,6 +426,9 @@ int STD_CALL main(int argc, char ** argv) { else if (strcmp(ext, "smt2") == 0) { g_input_kind = IN_SMTLIB_2; } + else if (is_tptp_extension(ext)) { + g_input_kind = IN_TPTP; + } } } switch (g_input_kind) { @@ -415,6 +457,9 @@ int STD_CALL main(int argc, char ** argv) { case IN_DRAT: return_value = read_drat(g_drat_input_file); break; + case IN_TPTP: + return_value = read_tptp(g_input_file); + break; default: UNREACHABLE(); } @@ -434,4 +479,3 @@ int STD_CALL main(int argc, char ** argv) { return ERR_INTERNAL_FATAL; } } - diff --git a/src/smt/arith_eq_adapter.cpp b/src/smt/arith_eq_adapter.cpp index 56729af17..2bbe7a4e1 100644 --- a/src/smt/arith_eq_adapter.cpp +++ b/src/smt/arith_eq_adapter.cpp @@ -87,8 +87,8 @@ namespace smt { tout << mk_ismt2_pp(n1->get_expr(), m) << "\n" << mk_ismt2_pp(n2->get_expr(), m) << "\n";); if (n1->get_owner_id() > n2->get_owner_id()) std::swap(n1, n2); - app * t1 = n1->get_expr(); - app * t2 = n2->get_expr(); + expr * t1 = n1->get_expr(); + expr * t2 = n2->get_expr(); if (m.are_distinct(t1, t2)) { expr_ref eq(m.mk_eq(t1, t2), m); ctx.internalize(eq, true); @@ -233,7 +233,7 @@ namespace smt { void arith_eq_adapter::new_eq_eh(theory_var v1, theory_var v2) { TRACE(arith_eq_adapter, tout << "v" << v1 << " = v" << v2 << " #" << get_enode(v1)->get_owner_id() << " = #" << get_enode(v2)->get_owner_id() << "\n";); - TRACE(arith_eq_adapter_bug, tout << mk_bounded_pp(get_enode(v1)->get_expr(), get_manager()) << "\n" << mk_bounded_pp(get_enode(v2)->get_expr(), get_manager()) << "\n";); + TRACE(arith_eq_adapter_bug, tout << mk_bounded_pp(get_expr(v1), get_manager()) << "\n" << mk_bounded_pp(get_expr(v2), get_manager()) << "\n";); mk_axioms(get_enode(v1), get_enode(v2)); } diff --git a/src/smt/arith_eq_adapter.h b/src/smt/arith_eq_adapter.h index 22dfcacb0..7b1c06876 100644 --- a/src/smt/arith_eq_adapter.h +++ b/src/smt/arith_eq_adapter.h @@ -70,6 +70,7 @@ namespace smt { context & get_context() const { return m_owner.get_context(); } ast_manager & get_manager() const { return m_owner.get_manager(); } enode * get_enode(theory_var v) const { return m_owner.get_enode(v); } + expr * get_expr(theory_var v) const { return m_owner.get_expr(v); } public: arith_eq_adapter(theory & owner, arith_util & u):m_owner(owner), m_util(u) {} diff --git a/src/smt/arith_eq_solver.cpp b/src/smt/arith_eq_solver.cpp index 387d10862..24e0d1e47 100644 --- a/src/smt/arith_eq_solver.cpp +++ b/src/smt/arith_eq_solver.cpp @@ -53,10 +53,7 @@ void arith_eq_solver::prop_mod_const(expr * e, unsigned depth, numeral const& k, numeral n; bool is_int; - if (depth == 0) { - result = e; - } - else if (m_util.is_add(e) || m_util.is_mul(e)) { + if (depth != 0 && (m_util.is_add(e) || m_util.is_mul(e))) { expr_ref_vector args(m); expr_ref tmp(m); app* a = to_app(e); @@ -66,7 +63,7 @@ void arith_eq_solver::prop_mod_const(expr * e, unsigned depth, numeral const& k, } m_arith_rewriter.mk_app(a->get_decl(), args.size(), args.data(), result); } - else if (m_util.is_numeral(e, n, is_int) && is_int) { + else if (depth != 0 && m_util.is_numeral(e, n, is_int) && is_int) { result = m_util.mk_numeral(mod(n, k), true); } else { diff --git a/src/smt/dyn_ack.cpp b/src/smt/dyn_ack.cpp index a2a96e8a4..691bd7fc0 100644 --- a/src/smt/dyn_ack.cpp +++ b/src/smt/dyn_ack.cpp @@ -101,14 +101,14 @@ namespace smt { }; class dyn_ack_eq_justification : public justification { - app * m_app1; - app * m_app2; - app * m_r; + expr * m_app1; + expr * m_app2; + expr * m_r; app * m_eq1; app * m_eq2; app * m_eq3; public: - dyn_ack_eq_justification(app * n1, app * n2, app* r, app* eq1, app* eq2, app* eq3): + dyn_ack_eq_justification(expr * n1, expr * n2, expr* r, app* eq1, app* eq2, app* eq3): justification(false), // dyn_ack_cc_justifications are not stored in regions. m_app1(n1), m_app2(n2), @@ -167,7 +167,7 @@ namespace smt { dyn_ack_manager::~dyn_ack_manager() { reset_app_pairs(); - reset_app_triples(); + reset_expr_triples(); } void dyn_ack_manager::reset_app_pairs() { @@ -189,7 +189,7 @@ namespace smt { m_num_propagations_since_last_gc = 0; m_triple.m_app2num_occs.reset(); - reset_app_triples(); + reset_expr_triples(); m_triple.m_to_instantiate.reset(); m_triple.m_qhead = 0; } @@ -230,7 +230,7 @@ namespace smt { } } - void dyn_ack_manager::eq_eh(app * n1, app * n2, app* r) { + void dyn_ack_manager::eq_eh(expr * n1, expr * n2, expr* r) { if (n1 == n2 || r == n1 || r == n2 || m.is_bool(n1)) { return; } @@ -238,7 +238,7 @@ namespace smt { std::swap(n1,n2); TRACE(dyn_ack, tout << mk_pp(n1, m) << " = " << mk_pp(n2, m) << " = " << mk_pp(r, m) << "\n";); - app_triple tr(n1, n2, r); + expr_triple tr(n1, n2, r); if (m_triple.m_instantiated.contains(tr)) { return; } @@ -361,7 +361,7 @@ namespace smt { SASSERT(!m_app_pair2num_occs.contains(a1, a2)); return; } - app_triple tr(0,0,0); + expr_triple tr(0,0,0); if (m_triple.m_clause2apps.find(cls, tr)) { [[maybe_unused]] auto [a1, a2, a3] = tr; SASSERT(a1 && a2 && a3); @@ -451,9 +451,8 @@ namespace smt { m_triple.m_clause2apps.reset(); } - void dyn_ack_manager::reset_app_triples() { - for (app_triple& p : m_triple.m_apps) { - auto [a1, a2, a3] = p; + void dyn_ack_manager::reset_expr_triples() { + for (auto &[a1,a2,a3] : m_triple.m_apps) { m.dec_ref(a1); m.dec_ref(a2); m.dec_ref(a3); @@ -461,7 +460,7 @@ namespace smt { m_triple.m_apps.reset(); } - void dyn_ack_manager::instantiate(app * n1, app * n2, app* r) { + void dyn_ack_manager::instantiate(expr * n1, expr * n2, expr* r) { context& ctx = m_context; SASSERT(m_params.m_dack != dyn_ack_strategy::DACK_DISABLED); SASSERT(n1 != n2 && n1 != r && n2 != r); @@ -471,7 +470,7 @@ namespace smt { << mk_pp(n2, m) << "\n" << mk_pp(r, m) << "\n"; ); - app_triple tr(n1, n2, r); + expr_triple tr(n1, n2, r); SASSERT(m_triple.m_app2num_occs.contains(n1, n2, r)); m_triple.m_app2num_occs.erase(n1, n2, r); // pair n1,n2 is still in m_triple.m_apps @@ -504,22 +503,22 @@ namespace smt { } - struct app_triple_lt { - typedef triple app_triple; - typedef obj_triple_map app_triple2num_occs; - app_triple2num_occs & m_app_triple2num_occs; + struct expr_triple_lt { + typedef triple expr_triple; + typedef obj_triple_map expr_triple2num_occs; + expr_triple2num_occs & m_expr_triple2num_occs; - app_triple_lt(app_triple2num_occs & m): - m_app_triple2num_occs(m) { + expr_triple_lt(expr_triple2num_occs & m): + m_expr_triple2num_occs(m) { } - bool operator()(app_triple const & p1, app_triple const & p2) const { + bool operator()(expr_triple const & p1, expr_triple const & p2) const { auto [a1_1, a1_2, a1_3] = p1; auto [a2_1, a2_2, a2_3] = p2; unsigned n1 = 0; unsigned n2 = 0; - m_app_triple2num_occs.find(a1_1, a1_2, a1_3, n1); - m_app_triple2num_occs.find(a2_1, a2_2, a2_3, n2); + m_expr_triple2num_occs.find(a1_1, a1_2, a1_3, n1); + m_expr_triple2num_occs.find(a2_1, a2_2, a2_3, n2); SASSERT(n1 > 0); SASSERT(n2 > 0); return n1 > n2; @@ -530,11 +529,11 @@ namespace smt { TRACE(dyn_ack, tout << "dyn_ack GC\n";); m_triple.m_to_instantiate.reset(); m_triple.m_qhead = 0; - svector::iterator it = m_triple.m_apps.begin(); - svector::iterator end = m_triple.m_apps.end(); - svector::iterator it2 = it; + svector::iterator it = m_triple.m_apps.begin(); + svector::iterator end = m_triple.m_apps.end(); + svector::iterator it2 = it; for (; it != end; ++it) { - app_triple & p = *it; + expr_triple & p = *it; auto [a1, a2, a3] = p; if (m_triple.m_instantiated.contains(p)) { TRACE(dyn_ack, tout << "1) erasing:\n" << mk_pp(a1, m) << "\n" << mk_pp(a2, m) << "\n";); @@ -548,7 +547,7 @@ namespace smt { m_triple.m_app2num_occs.find(a1, a2, a3, num_occs); // The following invariant is not true. a1 and // a2 may have been instantiated, and removed from - // m_app_triple2num_occs, but not from m_app_triples. + // m_triple.m_app2num_occs, but not from m_triple.m_apps. // // SASSERT(num_occs > 0); num_occs = static_cast(num_occs * m_params.m_dack_gc_inv_decay); @@ -568,8 +567,8 @@ namespace smt { m_triple.m_to_instantiate.push_back(p); } m_triple.m_apps.set_end(it2); - app_triple_lt f(m_triple.m_app2num_occs); - // app_triple_lt is not a total order + expr_triple_lt f(m_triple.m_app2num_occs); + // expr_triple_lt is not a total order std::stable_sort(m_triple.m_to_instantiate.begin(), m_triple.m_to_instantiate.end(), f); } diff --git a/src/smt/dyn_ack.h b/src/smt/dyn_ack.h index 00c220c43..e04f78fd3 100644 --- a/src/smt/dyn_ack.h +++ b/src/smt/dyn_ack.h @@ -36,11 +36,11 @@ namespace smt { typedef obj_pair_hashtable app_pair_set; typedef obj_map clause2app_pair; - typedef triple app_triple; - typedef obj_triple_map app_triple2num_occs; - typedef svector app_triple_vector; - typedef obj_triple_hashtable app_triple_set; - typedef obj_map clause2app_triple; + typedef triple expr_triple; + typedef obj_triple_map expr_triple2num_occs; + typedef svector expr_triple_vector; + typedef obj_triple_hashtable expr_triple_set; + typedef obj_map clause2expr_triple; context & m_context; ast_manager & m; @@ -55,14 +55,14 @@ namespace smt { clause2app_pair m_clause2app_pair; struct _triple { - app_triple2num_occs m_app2num_occs; - app_triple_vector m_apps; - app_triple_vector m_to_instantiate; + expr_triple2num_occs m_app2num_occs; + expr_triple_vector m_apps; + expr_triple_vector m_to_instantiate; unsigned m_qhead; unsigned m_num_instances; unsigned m_num_propagations_since_last_gc; - app_triple_set m_instantiated; - clause2app_triple m_clause2apps; + expr_triple_set m_instantiated; + clause2expr_triple m_clause2apps; }; _triple m_triple; @@ -76,9 +76,9 @@ namespace smt { literal mk_eq(expr * n1, expr * n2); void cg_eh(app * n1, app * n2); - void eq_eh(app * n1, app * n2, app* r); - void instantiate(app * n1, app * n2, app* r); - void reset_app_triples(); + void eq_eh(expr * n1, expr * n2, expr* r); + void instantiate(expr * n1, expr * n2, expr* r); + void reset_expr_triples(); void gc_triples(); public: @@ -112,7 +112,7 @@ namespace smt { /** \brief This method is invoked when equalities are used during conflict resolution. */ - void used_eq_eh(app * n1, app * n2, app* r) { + void used_eq_eh(expr * n1, expr * n2, expr* r) { if (m_params.m_dack_eq) eq_eh(n1, n2, r); } diff --git a/src/smt/expr_context_simplifier.cpp b/src/smt/expr_context_simplifier.cpp index 852da2f11..0d5bcd605 100644 --- a/src/smt/expr_context_simplifier.cpp +++ b/src/smt/expr_context_simplifier.cpp @@ -401,10 +401,7 @@ void expr_strong_context_simplifier::simplify_basic(expr* fml, expr_ref& result) args.push_back(arg); } } - else if (!m.is_bool(arg)) { - args.push_back(arg); - } - else if (!n2) { + else if (!n2 && m.is_bool(arg)) { n2 = m.mk_app(m_fn, m_arith.mk_numeral(rational(id++), true)); todo.push_back(arg); parent_ids.push_back(self_pos); @@ -677,10 +674,7 @@ void expr_strong_context_simplifier::simplify_model_based(expr* fml, expr_ref& r args.push_back(arg); } } - else if (!m.is_bool(arg)) { - args.push_back(arg); - } - else if (!n2) { + else if (!n2 && m.is_bool(arg)) { n2 = m.mk_app(m_fn, m_arith.mk_numeral(rational(id++), true)); todo.push_back(arg); parent_ids.push_back(self_pos); diff --git a/src/smt/fingerprints.cpp b/src/smt/fingerprints.cpp index f59d1dc3f..4550d22b5 100644 --- a/src/smt/fingerprints.cpp +++ b/src/smt/fingerprints.cpp @@ -20,10 +20,9 @@ Revision History: namespace smt { - fingerprint::fingerprint(region & r, void * d, unsigned d_h, expr* def, unsigned n, enode * const * args): + fingerprint::fingerprint(region & r, void * d, unsigned d_h, unsigned n, enode * const * args): m_data(d), m_data_hash(d_h), - m_def(def), m_num_args(n), m_args(nullptr) { m_args = new (r) enode*[n]; @@ -62,7 +61,7 @@ namespace smt { } - fingerprint * fingerprint_set::insert(void * data, unsigned data_hash, unsigned num_args, enode * const * args, expr* def) { + fingerprint * fingerprint_set::insert(void * data, unsigned data_hash, unsigned num_args, enode * const * args) { struct arg_data { unsigned data_hash; @@ -93,9 +92,8 @@ namespace smt { return nullptr; } TRACE(fingerprint_bug, tout << "inserting @" << m_scopes.size() << " " << *d;); - fingerprint * f = new (m_region) fingerprint(m_region, data, data_hash, def, num_args, d->m_args); + fingerprint * f = new (m_region) fingerprint(m_region, data, data_hash, num_args, d->m_args); m_fingerprints.push_back(f); - m_defs.push_back(def); m_set.insert(f); return f; } @@ -114,7 +112,6 @@ namespace smt { void fingerprint_set::reset() { m_set.reset(); m_fingerprints.reset(); - m_defs.reset(); } void fingerprint_set::push_scope() { @@ -134,7 +131,6 @@ namespace smt { m_set.erase(m_fingerprints[i]); } m_fingerprints.shrink(old_size); - m_defs.shrink(old_size); m_scopes.shrink(new_lvl); TRACE(fingerprint_bug, tout << "pop @" << m_scopes.size() << "\n";); } diff --git a/src/smt/fingerprints.h b/src/smt/fingerprints.h index 6a0bc1ccd..dc1863041 100644 --- a/src/smt/fingerprints.h +++ b/src/smt/fingerprints.h @@ -27,16 +27,14 @@ namespace smt { protected: void* m_data = nullptr; unsigned m_data_hash = 0; - expr* m_def = nullptr; unsigned m_num_args = 0; enode** m_args = nullptr; friend class fingerprint_set; fingerprint() = default; public: - fingerprint(region & r, void * d, unsigned d_hash, expr* def, unsigned n, enode * const * args); + fingerprint(region & r, void * d, unsigned d_hash, unsigned n, enode * const * args); void * get_data() const { return m_data; } - expr * get_def() const { return m_def; } unsigned get_data_hash() const { return m_data_hash; } unsigned get_num_args() const { return m_num_args; } enode * const * get_args() const { return m_args; } @@ -59,7 +57,6 @@ namespace smt { region & m_region; set m_set; ptr_vector m_fingerprints; - expr_ref_vector m_defs; unsigned_vector m_scopes; ptr_vector m_tmp; fingerprint m_dummy; @@ -67,8 +64,8 @@ namespace smt { fingerprint * mk_dummy(void * data, unsigned data_hash, unsigned num_args, enode * const * args); public: - fingerprint_set(ast_manager& m, region & r): m_region(r), m_defs(m) {} - fingerprint * insert(void * data, unsigned data_hash, unsigned num_args, enode * const * args, expr* def); + fingerprint_set(ast_manager& m, region & r): m_region(r) {} + fingerprint * insert(void * data, unsigned data_hash, unsigned num_args, enode * const * args); unsigned size() const { return m_fingerprints.size(); } bool contains(void * data, unsigned data_hash, unsigned num_args, enode * const * args); void reset(); diff --git a/src/smt/mam.cpp b/src/smt/mam.cpp index 3719f30c9..db2ddc4ca 100644 --- a/src/smt/mam.cpp +++ b/src/smt/mam.cpp @@ -215,6 +215,7 @@ namespace { unsigned short m_num_args; unsigned m_ireg; unsigned m_oreg; + unsigned m_curr_max_generation = 0; }; struct get_cgr : public instruction { @@ -1114,8 +1115,9 @@ namespace { best_j = j; } } + if (best == nullptr) + continue; m_mp_already_processed[best_j] = true; - SASSERT(best != 0); app * p = best; func_decl * lbl = p->get_decl(); unsigned short num_args = p->get_num_args(); @@ -1225,7 +1227,11 @@ namespace { SASSERT(head->m_next == 0); - m_seq.push_back(m_ct_manager.mk_yield(m_qa, m_mp, m_qa->get_num_decls(), reinterpret_cast(m_vars.begin()))); + unsigned num_decls = m_qa->get_num_decls(); + unsigned_vector var_regs(num_decls); + for (unsigned i = 0; i < num_decls; ++i) + var_regs[i] = static_cast(m_vars[i]); + m_seq.push_back(m_ct_manager.mk_yield(m_qa, m_mp, num_decls, var_regs.data())); for (instruction* curr : m_seq) { head->m_next = curr; @@ -1882,33 +1888,43 @@ namespace { m_used_enodes.push_back(std::make_tuple(prev, n)); } + void get_f_app(func_decl* lbl, unsigned num_expected_args, enode* curr, enode*& matching_cgr, enode*& min_gen_match) { + if (curr->get_decl() == lbl && curr->get_num_args() == num_expected_args) { + if (curr->is_cgr() && !matching_cgr) + matching_cgr = curr; + + if (!min_gen_match || min_gen_match->get_generation() > curr->get_generation()) { + min_gen_match = curr; + } + } + } + // We have to provide the number of expected arguments because we have flat-assoc applications such as +. // Flat-assoc applications may have arbitrary number of arguments. enode * get_first_f_app(func_decl * lbl, unsigned num_expected_args, enode * curr) { enode * first = curr; + enode *matching_cgr = nullptr, *min_gen_match = nullptr; do { - if (curr->get_decl() == lbl && curr->is_cgr() && curr->get_num_args() == num_expected_args) { - update_max_generation(curr, first); - return curr; - } + get_f_app(lbl, num_expected_args, curr, matching_cgr, min_gen_match); curr = curr->get_next(); } while (curr != first); - return nullptr; + if (matching_cgr) + update_max_generation(min_gen_match, first); + return matching_cgr; } enode * get_next_f_app(func_decl * lbl, unsigned num_expected_args, enode * first, enode * curr) { curr = curr->get_next(); while (curr != first) { - if (curr->get_decl() == lbl && curr->is_cgr() && curr->get_num_args() == num_expected_args) { - update_max_generation(curr, first); + if (curr->get_decl() == lbl && curr->get_num_args() == num_expected_args && curr->is_cgr()) return curr; - } curr = curr->get_next(); } return nullptr; } + /** \brief Execute the is_cgr instruction. Return true if succeeded, and false if backtracking is needed. @@ -2471,6 +2487,7 @@ namespace { m_backtrack_stack[m_top].m_old_max_generation = m_curr_max_generation; \ m_backtrack_stack[m_top].m_old_used_enodes_size = m_curr_used_enodes_size; \ m_backtrack_stack[m_top].m_curr = m_app; \ + const_cast(static_cast(m_pc))->m_curr_max_generation = m_max_generation; \ m_top++; BIND_COMMON(); @@ -2738,7 +2755,8 @@ namespace { #define BBIND_COMMON() m_b = static_cast(bp.m_instr); \ m_n1 = m_registers[m_b->m_ireg]; \ m_app = get_next_f_app(m_b->m_label, m_b->m_num_args, m_n1, bp.m_curr); \ - if (m_app == 0) { \ + m_max_generation = m_b->m_curr_max_generation; \ + if (!m_app) { \ m_top--; \ goto backtrack; \ } \ @@ -2909,6 +2927,8 @@ namespace { SASSERT(m.is_pattern(mp)); SASSERT(first_idx < mp->get_num_args()); app * p = to_app(mp->get_arg(first_idx)); + if (is_ground(p)) + return; func_decl * lbl = p->get_decl(); unsigned lbl_id = lbl->get_small_id(); m_trees.reserve(lbl_id+1, nullptr); @@ -3736,7 +3756,7 @@ namespace { } void match_new_patterns() { - TRACE(mam_new_pat, tout << "matching new patterns:\n";); + TRACE(mam, tout << "matching new patterns:\n";); m_tmp_trees_to_delete.reset(); for (auto const& kv : m_new_patterns) { if (m_context.get_cancel_flag()) { @@ -3782,8 +3802,14 @@ namespace { for (unsigned i = 0; i < num_patterns; ++i) { app * pat = to_app(mp->get_arg(i)); TRACE(mam_pat, tout << mk_ismt2_pp(qa, m) << "\npat:\n" << mk_ismt2_pp(pat, m) << "\n";); - SASSERT(!pat->is_ground()); - todo.push_back(pat); + if (pat->is_ground()) { + enode * e = mk_enode(m_context, qa, pat); + m_context.mark_as_relevant(e); + m_context.push_trail(add_shared_enode_trail(*this, e)); + m_shared_enodes.insert(e); + } + else + todo.push_back(pat); } while (!todo.empty()) { app * n = todo.back(); @@ -3834,10 +3860,10 @@ namespace { // Ground patterns are discarded. // However, the simplifier may turn a non-ground pattern into a ground one. // So, we should check it again here. - unsigned num_patterns = mp->get_num_args(); - for (unsigned i = 0; i < num_patterns; ++i) - if (is_ground(mp->get_arg(i))) - return; // ignore multi-pattern containing ground pattern. + if (all_of(*mp, [](expr *arg) { return is_ground(arg); })) + return; // ignore multi-pattern containing only ground pattern. + if (any_of(*mp, [](expr *arg) { return has_quantifiers(arg); })) + return; // patterns with quantifiers are not handled. update_filters(qa, mp); collect_ground_exprs(qa, mp); m_new_patterns.push_back(qp_pair(qa, mp)); @@ -3845,7 +3871,7 @@ namespace { // e-matching. So, for a multi-pattern [ p_1, ..., p_n ], // we have to make n insertions. In the i-th insertion, // the pattern p_i is assumed to be the first one. - for (unsigned i = 0; i < num_patterns; ++i) + for (unsigned i = 0; i < mp->get_num_args(); ++i) m_trees.add_pattern(qa, mp, i); } @@ -3949,7 +3975,7 @@ namespace { #endif unsigned min_gen = 0, max_gen = 0; m_interpreter.get_min_max_top_generation(min_gen, max_gen); - m_context.add_instance(qa, pat, num_bindings, bindings, nullptr, max_generation, min_gen, max_gen, used_enodes); + m_context.add_instance(qa, pat, num_bindings, bindings, max_generation, min_gen, max_gen, used_enodes); } bool is_shared(enode * n) const override { diff --git a/src/smt/qi_queue.cpp b/src/smt/qi_queue.cpp index d4875d77f..905644354 100644 --- a/src/smt/qi_queue.cpp +++ b/src/smt/qi_queue.cpp @@ -140,7 +140,7 @@ namespace smt { tout << "new instance of " << q->get_qid() << ", weight " << q->get_weight() << ", generation: " << generation << ", scope_level: " << m_context.get_scope_level() << ", cost: " << cost << "\n"; for (unsigned i = 0; i < f->get_num_args(); ++i) { - tout << "#" << f->get_arg(i)->get_expr_id() << " d:" << f->get_arg(i)->get_expr()->get_depth() << " "; + tout << "#" << f->get_arg(i)->get_expr_id() << " d:" << get_depth(f->get_arg(i)->get_expr()) << " "; } tout << "\n";); TRACE(new_entries_bug, tout << "[qi:insert]\n";); @@ -331,9 +331,6 @@ namespace smt { unsigned gen = get_new_gen(q, generation, ent.m_cost); display_instance_profile(f, q, num_bindings, bindings, proof_id, gen); m_context.internalize_instance(lemma, pr1, gen); - if (f->get_def()) { - m_context.internalize(f->get_def(), true); - } TRACE_CODE({ static unsigned num_useless = 0; if (m.is_or(lemma)) { diff --git a/src/smt/smt_almost_cg_table.cpp b/src/smt/smt_almost_cg_table.cpp index dbbd6b888..f50f34dd6 100644 --- a/src/smt/smt_almost_cg_table.cpp +++ b/src/smt/smt_almost_cg_table.cpp @@ -77,7 +77,7 @@ namespace smt { } bool almost_cg_table::cg_eq::operator()(enode * n1, enode * n2) const { - if (n1->get_expr()->get_decl() != n2->get_expr()->get_decl()) + if (n1->get_decl() != n2->get_decl() || !n1->is_app()) return false; unsigned num_args = n1->get_num_args(); if (num_args != n2->get_num_args()) diff --git a/src/smt/smt_case_split_queue.cpp b/src/smt/smt_case_split_queue.cpp index d43dd0fb8..029758b85 100644 --- a/src/smt/smt_case_split_queue.cpp +++ b/src/smt/smt_case_split_queue.cpp @@ -1046,7 +1046,7 @@ namespace { void operator()(expr * e) { if (m_context.e_internalized(e)) { enode * n = m_context.get_enode(e); - n->set_generation(m_context, m_generation); + n->set_generation(&m_context, m_generation); } } }; diff --git a/src/smt/smt_checker.cpp b/src/smt/smt_checker.cpp index 2e405f95a..d2c1758d3 100644 --- a/src/smt/smt_checker.cpp +++ b/src/smt/smt_checker.cpp @@ -22,23 +22,25 @@ Revision History: namespace smt { - bool checker::all_args(app * a, bool is_true) { + bool checker::all_args(app *a, unsigned depth, bool is_true) { for (expr* arg : *a) { - if (!check(arg, is_true)) + if (!check(arg, depth + 1, is_true)) return false; } return true; } - bool checker::any_arg(app * a, bool is_true) { + bool checker::any_arg(app *a, unsigned depth, bool is_true) { for (expr* arg : *a) { - if (check(arg, is_true)) + if (check(arg, depth + 1, is_true)) return true; } return false; } - bool checker::check_core(expr * n, bool is_true) { + bool checker::check_core(expr *n, unsigned depth, bool is_true) { + if (depth > 600) + return false; SASSERT(m_manager.is_bool(n)); if (m_context.b_internalized(n) && m_context.is_relevant(n)) { lbool val = m_context.get_assignment(n); @@ -54,11 +56,11 @@ namespace smt { case OP_FALSE: return !is_true; case OP_NOT: - return check(a->get_arg(0), !is_true); + return check(a->get_arg(0), depth + 1, !is_true); case OP_OR: - return is_true ? any_arg(a, true) : all_args(a, false); + return is_true ? any_arg(a, depth, true) : all_args(a, depth, false); case OP_AND: - return is_true ? all_args(a, true) : any_arg(a, false); + return is_true ? all_args(a, depth, true) : any_arg(a, depth, false); case OP_EQ: if (!m_manager.is_iff(a)) { enode * lhs = get_enode_eq_to(a->get_arg(0)); @@ -74,27 +76,27 @@ namespace smt { } else if (is_true) { return - (check(a->get_arg(0), true) && - check(a->get_arg(1), true)) || - (check(a->get_arg(0), false) && - check(a->get_arg(1), false)); + (check(a->get_arg(0), depth + 1, true) && + check(a->get_arg(1), depth + 1, true)) || + (check(a->get_arg(0), depth + 1, false) && + check(a->get_arg(1), depth + 1, false)); } else { return - (check(a->get_arg(0), true) && - check(a->get_arg(1), false)) || - (check(a->get_arg(0), false) && - check(a->get_arg(1), true)); + (check(a->get_arg(0), depth + 1, true) && + check(a->get_arg(1), depth + 1, false)) || + (check(a->get_arg(0), depth + 1, false) && + check(a->get_arg(1), depth + 1, true)); } case OP_ITE: { if (m_context.lit_internalized(a->get_arg(0)) && m_context.is_relevant(a->get_arg(0))) { switch (m_context.get_assignment(a->get_arg(0))) { - case l_false: return check(a->get_arg(2), is_true); + case l_false: return check(a->get_arg(2), depth + 1, is_true); case l_undef: return false; - case l_true: return check(a->get_arg(1), is_true); + case l_true: return check(a->get_arg(1), depth + 1, is_true); } } - return check(a->get_arg(1), is_true) && check(a->get_arg(2), is_true); + return check(a->get_arg(1), depth + 1, is_true) && check(a->get_arg(2), depth + 1, is_true); } default: break; @@ -108,11 +110,11 @@ namespace smt { return false; } - bool checker::check(expr * n, bool is_true) { + bool checker::check(expr *n, unsigned depth, bool is_true) { bool r; if (n->get_ref_count() > 1 && m_is_true_cache[is_true].find(n, r)) return r; - r = check_core(n, is_true); + r = check_core(n, depth, is_true); if (n->get_ref_count() > 1) m_is_true_cache[is_true].insert(n, r); return r; @@ -156,7 +158,7 @@ namespace smt { bool checker::is_sat(expr * n, unsigned num_bindings, enode * const * bindings) { flet l1(m_num_bindings, num_bindings); flet l2(m_bindings, bindings); - bool r = check(n, true); + bool r = check(n, 0, true); m_is_true_cache[0].reset(); m_is_true_cache[1].reset(); m_to_enode_cache.reset(); @@ -166,7 +168,7 @@ namespace smt { bool checker::is_unsat(expr * n, unsigned num_bindings, enode * const * bindings) { flet l1(m_num_bindings, num_bindings); flet l2(m_bindings, bindings); - bool r = check(n, false); + bool r = check(n, 0,false); m_is_true_cache[0].reset(); m_is_true_cache[1].reset(); m_to_enode_cache.reset(); diff --git a/src/smt/smt_checker.h b/src/smt/smt_checker.h index f2f25e92e..3cd83a106 100644 --- a/src/smt/smt_checker.h +++ b/src/smt/smt_checker.h @@ -37,10 +37,10 @@ namespace smt { unsigned m_num_bindings; enode * const * m_bindings; - bool all_args(app * a, bool is_true); - bool any_arg(app * a, bool is_true); - bool check_core(expr * n, bool is_true); - bool check(expr * n, bool is_true); + bool all_args(app *a, unsigned depth, bool is_true); + bool any_arg(app *a, unsigned depth, bool is_true); + bool check_core(expr * n, unsigned depth, bool is_true); + bool check(expr *n, unsigned depth, bool is_true); enode * get_enode_eq_to_core(app * n); enode * get_enode_eq_to(expr * n); diff --git a/src/smt/smt_conflict_resolution.cpp b/src/smt/smt_conflict_resolution.cpp index c8e378936..6cb22e505 100644 --- a/src/smt/smt_conflict_resolution.cpp +++ b/src/smt/smt_conflict_resolution.cpp @@ -126,7 +126,7 @@ namespace smt { break; case eq_justification::CONGRUENCE: { CTRACE(dyn_ack_target, !lhs->is_eq(), tout << "dyn_ack_target2: " << lhs->get_owner_id() << " " << rhs->get_owner_id() << "\n";); - m_dyn_ack_manager.used_cg_eh(lhs->get_expr(), rhs->get_expr()); + m_dyn_ack_manager.used_cg_eh(lhs->get_app(), rhs->get_app()); unsigned num_args = lhs->get_num_args(); SASSERT(num_args == rhs->get_num_args()); if (js.used_commutativity()) { @@ -787,8 +787,8 @@ namespace smt { SASSERT(m.has_fact(pr)); expr* f1 = nullptr, *f2 = nullptr; app * fact = to_app(m.get_fact(pr)); - app * n1_owner = n1->get_expr(); - app * n2_owner = n2->get_expr(); + expr * n1_owner = n1->get_expr(); + expr * n2_owner = n2->get_expr(); bool is_eq = m.is_eq(fact, f1, f2); if (is_eq && is_quantifier(f1)) { f1 = m_ctx.get_enode(f1)->get_expr(); @@ -855,7 +855,7 @@ namespace smt { case eq_justification::CONGRUENCE: num_args = n1->get_num_args(); SASSERT(num_args == n2->get_num_args()); - SASSERT(n1->get_expr()->get_decl() == n2->get_expr()->get_decl()); + SASSERT(n1->get_decl() == n2->get_decl()); if (js.used_commutativity()) { bool visited = true; SASSERT(num_args == 2); @@ -878,8 +878,8 @@ namespace smt { } if (!visited) return nullptr; - app * e1 = n1->get_expr(); - app * e2 = n2->get_expr(); + app * e1 = n1->get_app(); + app * e2 = n2->get_app(); app * e2_prime = m.mk_app(e2->get_decl(), e2->get_arg(1), e2->get_arg(0)); proof * pr1 = nullptr; if (!prs.empty()) { @@ -910,7 +910,7 @@ namespace smt { } if (!visited) return nullptr; - proof * pr = m.mk_congruence(n1->get_expr(), n2->get_expr(), prs.size(), prs.data()); + proof * pr = m.mk_congruence(n1->get_app(), n2->get_app(), prs.size(), prs.data()); m_new_proofs.push_back(pr); return pr; } diff --git a/src/smt/smt_context.cpp b/src/smt/smt_context.cpp index ecf356cf9..0add0fe6c 100644 --- a/src/smt/smt_context.cpp +++ b/src/smt/smt_context.cpp @@ -70,7 +70,6 @@ namespace smt { m_fingerprints(m, get_region()), m_b_internalized_stack(m), m_e_internalized_stack(m), - m_l_internalized_stack(m), m_final_check_idx(0), m_cg_table(m), m_conflict(null_b_justification), @@ -82,7 +81,6 @@ namespace smt { m_unsat_core(m), m_mk_bool_var_trail(*this), m_mk_enode_trail(*this), - m_mk_lambda_trail(*this), m_lemma_visitor(m) { SASSERT(m_scope_lvl == 0); @@ -217,7 +215,7 @@ namespace smt { } ast_translation tr(src_ctx.m, m, false); for (unsigned i = 0; i < src_ctx.m_user_propagator->get_num_vars(); ++i) { - app* e = src_ctx.m_user_propagator->get_expr(i); + auto e = src_ctx.m_user_propagator->get_expr(i); m_user_propagator->add_expr(tr(e), true); } } @@ -289,8 +287,13 @@ namespace smt { if (!decision && d.m_phase == l.sign()) m_agility += (1.0 - m_fparams.m_agility_factor); } + bool new_phase = !l.sign(); + m_stats.m_num_assignments++; + if (d.m_phase_available && d.m_phase != new_phase) + m_birthdate[l.var()] = m_stats.m_num_assignments; // reset birthdate when phase changes d.m_phase_available = true; - d.m_phase = !l.sign(); + d.m_phase = new_phase; + TRACE(assign_core, tout << (decision?"decision: ":"propagating: ") << l << " "; display_literal_smt2(tout, l) << "\n"; tout << "relevant: " << is_relevant_core(l) << " level: " << m_scope_lvl << " is atom " << d.is_atom() << "\n"; @@ -649,7 +652,7 @@ namespace smt { lbool val = get_assignment(v); if (val != l_true) { if (val == l_false && js.get_kind() == eq_justification::CONGRUENCE) - m_dyn_ack_manager.cg_conflict_eh(n1->get_expr(), n2->get_expr()); + m_dyn_ack_manager.cg_conflict_eh(n1->get_app(), n2->get_app()); assign(literal(v), mk_justification(eq_propagation_justification(lhs, rhs))); } // It is not necessary to reinsert the equality to the congruence table @@ -915,7 +918,7 @@ namespace smt { lbool val2 = get_assignment(v2); if (val2 != val) { if (val2 != l_undef && congruent(source, target) && source->get_num_args() > 0) - m_dyn_ack_manager.cg_conflict_eh(source->get_expr(), target->get_expr()); + m_dyn_ack_manager.cg_conflict_eh(source->get_app(), target->get_app()); assign(literal(v2, sign), mk_justification(mp_iff_justification(source, target))); } target = target->get_next(); @@ -1133,7 +1136,7 @@ namespace smt { m.inc_ref(eq); _this->m_is_diseq_tmp = enode::mk_dummy(m, m_app2enode, eq); } - else if (m_is_diseq_tmp->get_expr()->get_arg(0)->get_sort() != n1->get_sort()) { + else if (m_is_diseq_tmp->get_app()->get_arg(0)->get_sort() != n1->get_sort()) { m.dec_ref(m_is_diseq_tmp->get_expr()); app * eq = m.mk_eq(n1->get_expr(), n2->get_expr()); m.inc_ref(eq); @@ -1280,14 +1283,14 @@ namespace smt { enode * r = m_cg_table.find(tmp); #ifdef Z3DEBUG if (r != nullptr) { - SASSERT(r->get_expr()->get_decl() == f); + SASSERT(r->get_decl() == f); SASSERT(r->get_num_args() == num_args); if (r->is_commutative()) { // TODO } else { for (unsigned i = 0; i < num_args; ++i) { - expr * arg = r->get_expr()->get_arg(i); + expr * arg = r->get_arg(i)->get_expr(); SASSERT(e_internalized(arg)); enode * _arg = get_enode(arg); CTRACE(eq_to_bug, args[i]->get_root() != _arg->get_root(), @@ -1773,9 +1776,11 @@ namespace smt { return m_fingerprints.contains(q, q->get_id(), num_bindings, bindings); } - bool context::add_instance(quantifier * q, app * pat, unsigned num_bindings, enode * const * bindings, expr* def, unsigned max_generation, + bool context::add_instance(quantifier * q, app * pat, unsigned num_bindings, enode * const * bindings, //expr* def, + unsigned max_generation, unsigned min_top_generation, unsigned max_top_generation, vector> & used_enodes) { - return m_qmanager->add_instance(q, pat, num_bindings, bindings, def, max_generation, min_top_generation, max_top_generation, used_enodes); + return m_qmanager->add_instance(q, pat, num_bindings, bindings, + max_generation, min_top_generation, max_top_generation, used_enodes); } void context::rescale_bool_var_activity() { @@ -4188,9 +4193,17 @@ namespace smt { return FC_CONTINUE; } if (m_final_check_idx == old_idx) { - if (level >= max_level || result == FC_DONE || can_propagate()) + if (level >= max_level || result == FC_DONE || result == FC_CONTINUE || can_propagate()) break; ++level; + // Re-evaluate at the higher level: clear the give-up state + // accumulated at lower levels so a level that succeeds is + // not masked by a previous FC_GIVEUP. See e.g. theory_lra + // whose level 2 invokes the full nlsat (m_nra.check) that + // is skipped at level 1. + result = FC_DONE; + f = OK; + m_incomplete_theories.reset(); } } @@ -4670,7 +4683,7 @@ namespace smt { return false; } case 1: { - if (m_qmanager->is_shared(n) && !m.is_lambda_def(n->get_expr()) && !m_lambdas.contains(n)) + if (m_qmanager->is_shared(n) && !m_lambdas.contains(n)) return true; // the variable is shared if the equivalence class of n @@ -4680,8 +4693,8 @@ namespace smt { theory_id th_id = l->get_id(); for (enode * parent : enode::parents(n)) { - app* p = parent->get_expr(); - family_id fid = p->get_family_id(); + auto p = parent->get_expr(); + family_id fid = parent->get_family_id(); if (fid != th_id && fid != m.get_basic_family_id()) { if (is_beta_redex(parent, n)) continue; @@ -4729,7 +4742,7 @@ namespace smt { } bool context::is_beta_redex(enode* p, enode* n) const { - family_id th_id = p->get_expr()->get_family_id(); + family_id th_id = p->get_family_id(); theory * th = get_theory(th_id); return th && th->is_beta_redex(p, n); } diff --git a/src/smt/smt_context.h b/src/smt/smt_context.h index afeabe447..1bd967bdb 100644 --- a/src/smt/smt_context.h +++ b/src/smt/smt_context.h @@ -18,6 +18,7 @@ Revision History: --*/ #pragma once +#include #include "ast/quantifier_stat.h" #include "ast/simplifiers/dependent_expr_state.h" #include "smt/smt_clause.h" @@ -123,7 +124,6 @@ namespace smt { // enodes. Examples: boolean expression nested in an // uninterpreted function. expr_ref_vector m_e_internalized_stack; // stack of the expressions already internalized as enodes. - quantifier_ref_vector m_l_internalized_stack; ptr_vector m_justifications; @@ -139,6 +139,7 @@ namespace smt { scoped_ptr m_fmls; svector m_lit_scores[2]; + svector m_birthdate; // ----------------------------------- @@ -620,8 +621,8 @@ namespace smt { return m_asserted_formulas.has_quantifiers(); } - fingerprint * add_fingerprint(void * data, unsigned data_hash, unsigned num_args, enode * const * args, expr* def = nullptr) { - return m_fingerprints.insert(data, data_hash, num_args, args, def); + fingerprint * add_fingerprint(void * data, unsigned data_hash, unsigned num_args, enode * const * args) { + return m_fingerprints.insert(data, data_hash, num_args, args); } theory_id get_var_theory(bool_var v) const { @@ -785,6 +786,13 @@ namespace smt { return get_bdata(get_bool_var(n)); } + void update_generation(enode * n); + + void update_generation(expr * e) { + if (is_app(e) && e_internalized(e)) + update_generation(get_enode(to_app(e))); + } + typedef std::pair expr_bool_pair; void ts_visit_child(expr * n, bool gate_ctx, svector & todo, bool & visited); @@ -863,16 +871,6 @@ namespace smt { mk_enode_trail m_mk_enode_trail; void undo_mk_enode(); - friend class mk_lambda_trail; - class mk_lambda_trail : public trail { - context& ctx; - public: - mk_lambda_trail(context& ctx) :ctx(ctx) {} - void undo() override { ctx.undo_mk_lambda(); } - }; - mk_lambda_trail m_mk_lambda_trail; - void undo_mk_lambda(); - void apply_sort_cnstr(app * term, enode * e); @@ -1018,7 +1016,7 @@ namespace smt { bool_var mk_bool_var(expr * n); - enode * mk_enode(app * n, bool suppress_args, bool merge_tf, bool cgc_enabled); + enode * mk_enode(expr * n, bool suppress_args, bool merge_tf, bool cgc_enabled); void attach_th_var(enode * n, theory * th, theory_var v); @@ -1106,8 +1104,8 @@ namespace smt { bool contains_instance(quantifier * q, unsigned num_bindings, enode * const * bindings); - bool add_instance(quantifier * q, app * pat, unsigned num_bindings, enode * const * bindings, expr* def, unsigned max_generation, - unsigned min_top_generation, unsigned max_top_generation, vector> & used_enodes /*gives the equalities used for the pattern match, see mam.cpp for more info*/); + bool add_instance(quantifier * q, app * pat, unsigned num_bindings, enode * const * bindings, + unsigned max_generation, unsigned min_top_generation, unsigned max_top_generation, vector> & used_enodes /*gives the equalities used for the pattern match, see mam.cpp for more info*/); void set_global_generation(unsigned generation) { m_generation = generation; } @@ -1147,7 +1145,7 @@ namespace smt { void push_eq(enode * lhs, enode * rhs, eq_justification const & js) { if (lhs->get_root() != rhs->get_root()) { - SASSERT(lhs->get_expr()->get_sort() == rhs->get_expr()->get_sort()); + SASSERT(lhs->get_sort() == rhs->get_sort()); m_eq_propagation_queue.push_back(new_eq(lhs, rhs, js)); } } diff --git a/src/smt/smt_context_pp.cpp b/src/smt/smt_context_pp.cpp index 51e7c7192..8041099e5 100644 --- a/src/smt/smt_context_pp.cpp +++ b/src/smt/smt_context_pp.cpp @@ -423,6 +423,7 @@ namespace smt { st.update("minimized lits", m_stats.m_num_minimized_lits); st.update("num checks", m_stats.m_num_checks); st.update("mk bool var", m_stats.m_num_mk_bool_var ? m_stats.m_num_mk_bool_var - 1 : 0); + st.update("random seed", m_fparams.m_random_seed); m_qmanager->collect_statistics(st); m_asserted_formulas.collect_statistics(st); for (theory* th : m_theory_set) { @@ -544,14 +545,14 @@ namespace smt { out << std::left << n->get_owner_id() << " #"; out.width(5); out << n->get_root()->get_owner_id() << " := " << std::right; - unsigned num = n->get_expr()->get_num_args(); + unsigned num = n->get_num_args(); if (num > 0) out << "("; out << n->get_decl()->get_name(); if (!n->get_decl()->private_parameters()) display_parameters(out, n->get_decl()->get_num_parameters(), n->get_decl()->get_parameters()); for (unsigned i = 0; i < num; ++i) { - expr * arg = n->get_expr()->get_arg(i); + expr * arg = n->get_arg(i)->get_expr(); if (e_internalized(arg)) { enode * n = get_enode(arg)->get_root(); out << " #" << n->get_owner_id(); diff --git a/src/smt/smt_enode.cpp b/src/smt/smt_enode.cpp index 99424c8ed..05b174e5e 100644 --- a/src/smt/smt_enode.cpp +++ b/src/smt/smt_enode.cpp @@ -25,7 +25,7 @@ namespace smt { /** \brief Initialize an enode in the given memory position. */ - enode * enode::init(ast_manager & m, void * mem, app2enode_t const & app2enode, app * owner, + enode * enode::init(ast_manager & m, void * mem, app2enode_t const & app2enode, expr * owner, unsigned generation, bool suppress_args, bool merge_tf, unsigned iscope_lvl, bool cgc_enabled, bool update_children_parent) { SASSERT(m.is_bool(owner) || !merge_tf); @@ -42,7 +42,7 @@ namespace smt { n->m_interpreted = false; n->m_suppress_args = suppress_args; n->m_eq = m.is_eq(owner); - n->m_commutative = n->get_num_args() == 2 && owner->get_decl()->is_commutative(); + n->m_commutative = n->get_num_args() == 2 && n->get_decl()->is_commutative(); n->m_bool = m.is_bool(owner); n->m_merge_tf = merge_tf; n->m_cgc_enabled = cgc_enabled; @@ -52,7 +52,7 @@ namespace smt { n->m_is_shared = 2; unsigned num_args = n->get_num_args(); for (unsigned i = 0; i < num_args; ++i) { - enode * arg = app2enode[owner->get_arg(i)->get_id()]; + enode * arg = app2enode[to_app(owner)->get_arg(i)->get_id()]; n->m_args[i] = arg; arg->get_root()->m_is_shared = 2; SASSERT(n->get_arg(i) == arg); @@ -64,11 +64,11 @@ namespace smt { return n; } - enode * enode::mk(ast_manager & m, region & r, app2enode_t const & app2enode, app * owner, + enode * enode::mk(ast_manager & m, region & r, app2enode_t const & app2enode, expr * owner, unsigned generation, bool suppress_args, bool merge_tf, unsigned iscope_lvl, bool cgc_enabled, bool update_children_parent) { SASSERT(m.is_bool(owner) || !merge_tf); - unsigned sz = get_enode_size(suppress_args ? 0 : owner->get_num_args()); + unsigned sz = get_enode_size(suppress_args || !::is_app(owner) ? 0 : to_app(owner)->get_num_args()); void * mem = r.allocate(sz); return init(m, mem, app2enode, owner, generation, suppress_args, merge_tf, iscope_lvl, cgc_enabled, update_children_parent); } @@ -136,10 +136,11 @@ namespace smt { \brief Push old value of generation on the context trail stack and update the generation. */ - void enode::set_generation(context & ctx, unsigned generation) { + void enode::set_generation(context * ctx, unsigned generation) { if (m_generation == generation) return; - ctx.push_trail(value_trail(m_generation)); + if (ctx) + ctx->push_trail(value_trail(m_generation)); m_generation = generation; } @@ -279,7 +280,7 @@ namespace smt { bool congruent(enode * n1, enode * n2, bool & comm) { comm = false; - if (n1->get_expr()->get_decl() != n2->get_expr()->get_decl()) + if (!n1->is_app() || n1->get_decl() != n2->get_decl()) return false; unsigned num_args = n1->get_num_args(); if (num_args != n2->get_num_args()) diff --git a/src/smt/smt_enode.h b/src/smt/smt_enode.h index 3f488a3b7..e9dc4c4e1 100644 --- a/src/smt/smt_enode.h +++ b/src/smt/smt_enode.h @@ -59,7 +59,7 @@ namespace smt { equality propagation, and the theory central bus of equalities. */ class enode { - app * m_owner; //!< The application that 'owns' this enode. + expr * m_owner; //!< The application that 'owns' this enode. enode * m_root; //!< Representative of the equivalence class enode * m_next; //!< Next element in the equivalence class. enode * m_cg; @@ -132,7 +132,7 @@ namespace smt { friend class tmp_enode; - static enode * init(ast_manager & m, void * mem, app2enode_t const & app2enode, app * owner, + static enode * init(ast_manager & m, void * mem, app2enode_t const & app2enode, expr * owner, unsigned generation, bool suppress_args, bool merge_tf, unsigned iscope_lvl, bool cgc_enabled, bool update_children_parent); public: @@ -141,7 +141,7 @@ namespace smt { return sizeof(enode) + num_args * sizeof(enode*); } - static enode * mk(ast_manager & m, region & r, app2enode_t const & app2enode, app * owner, + static enode * mk(ast_manager & m, region & r, app2enode_t const & app2enode, expr * owner, unsigned generation, bool suppress_args, bool merge_tf, unsigned iscope_lvl, bool cgc_enabled, bool update_children_parent); @@ -166,16 +166,28 @@ namespace smt { void del_eh(ast_manager & m, bool update_children_parent = true); - app * get_expr() const { return m_owner; } + app * get_app() const { SASSERT(is_app()); return to_app(m_owner); } + + expr *get_expr() const { + return m_owner; + } + + bool is_app() const { + return ::is_app(m_owner); + } unsigned get_owner_id() const { return m_owner->get_id(); } unsigned get_expr_id() const { return m_owner->get_id(); } - func_decl * get_decl() const { return m_owner->get_decl(); } - unsigned get_decl_id() const { return m_owner->get_decl()->get_small_id(); } + func_decl * get_decl() const { return is_app() ? to_app(m_owner)->get_decl() : nullptr; } + unsigned get_decl_id() const { return is_app() ? to_app(m_owner)->get_decl()->get_small_id() : 43; } sort* get_sort() const { return m_owner->get_sort(); } + family_id get_family_id() const { + return is_app() ? to_app(m_owner)->get_family_id() : basic_family_id; + } + unsigned hash() const { return m_owner->hash(); } @@ -213,7 +225,7 @@ namespace smt { } unsigned get_num_args() const { - return m_suppress_args ? 0 : m_owner->get_num_args(); + return m_suppress_args || !is_app() ? 0 : to_app(m_owner)->get_num_args(); } enode * get_arg(unsigned idx) const { @@ -386,7 +398,7 @@ namespace smt { return m_generation; } - void set_generation(context & ctx, unsigned generation); + void set_generation(context * ctx, unsigned generation); /** \brief Return the enode n that is in the eqc of *this, and has the minimal generation. diff --git a/src/smt/smt_internalizer.cpp b/src/smt/smt_internalizer.cpp index 081d12ebd..063b7297b 100644 --- a/src/smt/smt_internalizer.cpp +++ b/src/smt/smt_internalizer.cpp @@ -101,6 +101,11 @@ namespace smt { } } + void context::update_generation(enode * e) { + if (0 < m_generation && m_generation < e->get_generation()) + e->set_generation(nullptr, m_generation); + } + void context::ts_visit_child(expr * n, bool gate_ctx, svector & todo, bool & visited) { if (get_color(tcolors, fcolors, n, gate_ctx) == White) { todo.push_back(expr_bool_pair(n, gate_ctx)); @@ -115,12 +120,16 @@ namespace smt { return true; SASSERT(is_app(n)); if (m.is_bool(n)) { - if (b_internalized(n)) + if (b_internalized(n)) { + update_generation(n); return true; + } } else { - if (e_internalized(n)) + update_generation(n); + if (e_internalized(n)) return true; + } bool visited = true; @@ -404,6 +413,8 @@ namespace smt { bool_var v = get_bool_var(n); TRACE(internalize_bug, tout << "#" << n->get_id() << " already has bool_var v" << v << "\n";); + update_generation(n); + // n was already internalized as boolean, but an enode was // not associated with it. So, an enode is necessary, if // n is not in the context of a gate and is an application. @@ -586,31 +597,9 @@ namespace smt { SASSERT(is_lambda(q)); if (e_internalized(q)) return; - app_ref lam_name(m.mk_fresh_const("lambda", q->get_sort()), m); - app_ref eq(m), lam_app(m); - expr_ref_vector vars(m); - vars.push_back(lam_name); - unsigned sz = q->get_num_decls(); - for (unsigned i = 0; i < sz; ++i) - vars.push_back(m.mk_var(sz - i - 1, q->get_decl_sort(i))); - array_util autil(m); - lam_app = autil.mk_select(vars.size(), vars.data()); - eq = m.mk_eq(lam_app, q->get_expr()); - quantifier_ref fa(m); - expr * patterns[1] = { m.mk_pattern(lam_app) }; - fa = m.mk_forall(sz, q->get_decl_sorts(), q->get_decl_names(), eq, 0, m.lambda_def_qid(), symbol::null, 1, patterns); - internalize_quantifier(fa, true); - if (!e_internalized(lam_name)) - internalize_uninterpreted(lam_name); - enode* lam_node = get_enode(lam_name); - push_trail(insert_obj_map(m_lambdas, lam_node)); - m_lambdas.insert(lam_node, q); - m_app2enode.setx(q->get_id(), lam_node, nullptr); - m_l_internalized_stack.push_back(q); - m_trail_stack.push_ptr(&m_mk_lambda_trail); - bool_var bv = get_bool_var(fa); - assign(literal(bv, false), nullptr); - mark_as_relevant(bv); + mk_enode(q, true, /* do suppress args */ + false, /* it is a term, so it should not be merged with true/false */ + true); } bool context::has_lambda() { @@ -810,6 +799,8 @@ namespace smt { */ void context::internalize_term(app * n) { if (e_internalized(n)) { + enode * e = get_enode(n); + update_generation(e); theory * th = m_theories.get_plugin(n->get_family_id()); if (th != nullptr) { // This code is necessary because some theories may decide @@ -822,7 +813,6 @@ namespace smt { // Later, the core tries to internalize (f (* 2 x)). // Now, (* 2 x) is not internal to arithmetic anymore, // and a theory variable must be created for it. - enode * e = get_enode(n); if (!th->is_attached_to_var(e)) th->internalize_term(n); } @@ -935,6 +925,8 @@ namespace smt { m_lit_scores[0].reserve(v + 1); m_lit_scores[1].reserve(v + 1); m_lit_scores[0][v] = m_lit_scores[1][v] = 0.0; + m_birthdate.reserve(v+1); + m_birthdate[v] = 0; literal l(v, false); literal not_l(v, true); @@ -997,7 +989,7 @@ namespace smt { \remark If suppress_args is true, then the enode is viewed as a constant in the egraph. */ - enode * context::mk_enode(app * n, bool suppress_args, bool merge_tf, bool cgc_enabled) { + enode * context::mk_enode(expr * n, bool suppress_args, bool merge_tf, bool cgc_enabled) { TRACE(mk_enode_detail, tout << mk_pp(n, m) << "\nsuppress_args: " << suppress_args << ", merge_tf: " << merge_tf << ", cgc_enabled: " << cgc_enabled << "\n";); SASSERT(!e_internalized(n)); @@ -1043,7 +1035,7 @@ namespace smt { } } if (!e->is_eq()) { - unsigned decl_id = n->get_decl()->get_small_id(); + unsigned decl_id = e->get_decl_id(); if (decl_id >= m_decl2enodes.size()) m_decl2enodes.resize(decl_id+1); m_decl2enodes[decl_id].push_back(e); @@ -1059,19 +1051,11 @@ namespace smt { SCTRACE(causality, m_coming_from_quant, tout << "EN: #" << e->get_owner_id() << "\n";); if (m.has_trace_stream()) - m.trace_stream() << "[attach-enode] #" << n->get_id() << " " << m_generation << "\n"; + m.trace_stream() << "[attach-enode] #" << n->get_id() << " " << generation << "\n"; return e; } - void context::undo_mk_lambda() { - SASSERT(!m_l_internalized_stack.empty()); - m_stats.m_num_del_enode++; - quantifier * n = m_l_internalized_stack.back(); - m_app2enode[n->get_id()] = nullptr; - m_l_internalized_stack.pop_back(); - } - void context::undo_mk_enode() { SASSERT(!m_e_internalized_stack.empty()); m_stats.m_num_del_enode++; @@ -1079,7 +1063,6 @@ namespace smt { TRACE(undo_mk_enode, tout << "undo_enode: #" << n->get_id() << "\n" << mk_pp(n, m) << "\n";); TRACE(mk_var_bug, tout << "undo_mk_enode: " << n->get_id() << "\n";); unsigned n_id = n->get_id(); - SASSERT(is_app(n)); enode * e = m_app2enode[n_id]; m_app2enode[n_id] = nullptr; if (e->is_cgr() && !e->is_true_eq() && e->is_cgc_enabled()) { @@ -1087,7 +1070,7 @@ namespace smt { m_cg_table.erase(e); } if (e->get_num_args() > 0 && !e->is_eq()) { - unsigned decl_id = to_app(n)->get_decl()->get_small_id(); + unsigned decl_id = e->get_decl_id(); SASSERT(decl_id < m_decl2enodes.size()); SASSERT(m_decl2enodes[decl_id].back() == e); m_decl2enodes[decl_id].pop_back(); @@ -1884,4 +1867,3 @@ namespace smt { SASSERT(th->is_attached_to_var(n)); } }; - diff --git a/src/smt/smt_model_checker.cpp b/src/smt/smt_model_checker.cpp index 93988497d..0037bb3da 100644 --- a/src/smt/smt_model_checker.cpp +++ b/src/smt/smt_model_checker.cpp @@ -203,7 +203,7 @@ namespace smt { unsigned num_decls = q->get_num_decls(); // Remark: sks were created for the flat version of q. SASSERT(sks.size() >= num_decls); - expr_ref_vector bindings(m), defs(m); + expr_ref_vector bindings(m); expr_ref def(m); bindings.resize(num_decls); unsigned max_generation = 0; @@ -249,6 +249,7 @@ namespace smt { sk_value = get_type_compatible_term(sk_value); } func_decl * f = nullptr; + expr_ref sk_term(sk_value, m); if (autil.is_as_array(sk_value, f) && cex->get_func_interp(f) && cex->get_func_interp(f)->get_interp()) { expr_ref body(cex->get_func_interp(f)->get_interp(), m); if (contains_model_value(body)) @@ -260,27 +261,23 @@ namespace smt { defined_names dn(m); body = replace_value_from_ctx(body); body = m.mk_lambda(sorts.size(), sorts.data(), names.data(), body); - // sk_value = m.mk_fresh_const(0, m.get_sort(sk_value)); // get rid of as-array - body = dn.mk_definition(body, to_app(sk_value)); - defs.push_back(body); + sk_term = body; } - bindings.set(num_decls - i - 1, sk_value); + bindings.set(num_decls - i - 1, sk_term); } - TRACE(model_checker, tout << q->get_qid() << " found (use_inv: " << use_inv << ") new instance: " << bindings << "\ndefs:\n" << defs << "\n";); - if (!defs.empty()) def = mk_and(defs); + TRACE(model_checker, tout << q->get_qid() << " found (use_inv: " << use_inv << ") new instance: " << bindings << "\n"); max_generation = std::max(m_qm->get_generation(q), max_generation); - add_instance(q, bindings, max_generation, def.get()); + add_instance(q, bindings, max_generation); return true; } - void model_checker::add_instance(quantifier* q, expr_ref_vector const& bindings, unsigned max_generation, expr* def) { + void model_checker::add_instance(quantifier* q, expr_ref_vector const& bindings, unsigned max_generation) { SASSERT(q->get_num_decls() == bindings.size()); unsigned offset = m_pinned_exprs.size(); m_pinned_exprs.append(bindings); m_pinned_exprs.push_back(q); - m_pinned_exprs.push_back(def); - m_new_instances.push_back(instance(q, offset, def, max_generation)); + m_new_instances.push_back(instance(q, offset, max_generation)); } void model_checker::operator()(expr *n) { @@ -457,12 +454,6 @@ namespace smt { TRACE(model_checker, tout << "MODEL_CHECKER INVOKED\n"; tout << "model:\n"; model_pp(tout, *m_curr_model);); - - for (quantifier* q : *m_qm) - if (m.is_lambda_def(q)) { - md->add_lambda_defs(); - break; - } md->compress(); @@ -518,8 +509,7 @@ namespace smt { for (quantifier * q : *m_qm) { if (!(m_qm->mbqi_enabled(q) && m_context->is_relevant(q) && - m_context->get_assignment(q) == l_true && - (!m_context->get_fparams().m_ematching || !m.is_lambda_def(q)))) { + m_context->get_assignment(q) == l_true)) { if (!m_qm->mbqi_enabled(q)) ++num_failures; continue; @@ -588,27 +578,11 @@ namespace smt { bindings.push_back(m_context->get_enode(b)); } - if (inst.m_def) { - unsigned n = 1; - expr* const* args = &inst.m_def; - if (m.is_and(inst.m_def)) { - n = to_app(inst.m_def)->get_num_args(); - args = to_app(inst.m_def)->get_args(); - } - for (unsigned i = 0; i < n; ++i) { - proof* pr = nullptr; - expr* arg = args[i]; - if (m.proofs_enabled()) - pr = m.mk_def_intro(arg); - m_context->internalize_assertion(arg, pr, gen); - } - } - TRACE(model_checker_bug_detail, tout << "instantiating... q:\n" << mk_pp(q, m) << "\n"; tout << "inconsistent: " << m_context->inconsistent() << "\n"; tout << "bindings:\n" << expr_ref_vector(m, num_decls, m_pinned_exprs.data() + offset) << "\n"; - tout << "def " << mk_pp(inst.m_def, m) << "\n";); - m_context->add_instance(q, nullptr, num_decls, bindings.data(), inst.m_def, gen, gen, gen, dummy); + ); + m_context->add_instance(q, nullptr, num_decls, bindings.data(), gen, gen, gen, dummy); TRACE(model_checker_bug_detail, tout << "after instantiating, inconsistent: " << m_context->inconsistent() << "\n";); } } diff --git a/src/smt/smt_model_checker.h b/src/smt/smt_model_checker.h index fec9e2df5..c816b3cc0 100644 --- a/src/smt/smt_model_checker.h +++ b/src/smt/smt_model_checker.h @@ -70,9 +70,8 @@ namespace smt { struct instance { quantifier * m_q; unsigned m_generation; - expr * m_def; unsigned m_bindings_offset; - instance(quantifier * q, unsigned offset, expr* def, unsigned gen):m_q(q), m_generation(gen), m_def(def), m_bindings_offset(offset) {} + instance(quantifier * q, unsigned offset, unsigned gen):m_q(q), m_generation(gen), m_bindings_offset(offset) {} }; svector m_new_instances; @@ -86,7 +85,7 @@ namespace smt { struct is_model_value {}; expr_mark m_visited; bool contains_model_value(expr * e); - void add_instance(quantifier * q, expr_ref_vector const & bindings, unsigned max_generation, expr * def); + void add_instance(quantifier * q, expr_ref_vector const & bindings, unsigned max_generation); bool is_safe_for_mbqi(quantifier * q) const; public: diff --git a/src/smt/smt_model_finder.cpp b/src/smt/smt_model_finder.cpp index b10178f20..27516b3dc 100644 --- a/src/smt/smt_model_finder.cpp +++ b/src/smt/smt_model_finder.cpp @@ -291,8 +291,8 @@ namespace smt { } void insert(expr* n, unsigned generation) { - SASSERT(is_ground(n)); - get_root()->m_set->insert(n, generation); + if (is_ground(n)) + get_root()->m_set->insert(n, generation); } void display(std::ostream& out, ast_manager& m) const { @@ -1378,7 +1378,7 @@ namespace smt { Store in arrays, all enodes that match the pattern */ - void get_auf_arrays(app* auf_arr, context* ctx, ptr_buffer& arrays) { + void get_auf_arrays(expr* auf_arr, context* ctx, ptr_buffer& arrays) { if (is_ground(auf_arr)) { if (ctx->e_internalized(auf_arr)) { enode* e = ctx->get_enode(auf_arr); @@ -1387,8 +1387,8 @@ namespace smt { } } } - else { - app* nested_array = to_app(auf_arr->get_arg(0)); + else if (is_app(auf_arr)) { + app* nested_array = to_app(to_app(auf_arr)->get_arg(0)); ptr_buffer nested_arrays; get_auf_arrays(nested_array, ctx, nested_arrays); for (enode* curr : nested_arrays) { @@ -1396,7 +1396,7 @@ namespace smt { enode_vector::iterator end2 = curr->end_parents(); for (; it2 != end2; ++it2) { enode* p = *it2; - if (ctx->is_relevant(p) && p->get_expr()->get_decl() == auf_arr->get_decl()) { + if (ctx->is_relevant(p) && p->get_decl() == to_app(auf_arr)->get_decl()) { arrays.push_back(p); } } @@ -1411,9 +1411,9 @@ namespace smt { unsigned m_arg_i; unsigned m_var_j; - app* get_array() const { return to_app(m_select->get_arg(0)); } + expr* get_array() const { return m_select->get_arg(0); } - func_decl* get_array_func_decl(app* ground_array, auf_solver& s) { + func_decl* get_array_func_decl(expr* ground_array, auf_solver& s) { TRACE(model_evaluator, tout << expr_ref(ground_array, m) << "\n";); expr* ground_array_interp = s.eval(ground_array, false); if (ground_array_interp && m_array.is_as_array(ground_array_interp)) @@ -1449,7 +1449,7 @@ namespace smt { }); node* n1 = s.get_uvar(q, m_var_j); for (enode* n : arrays) { - app* ground_array = n->get_expr(); + auto ground_array = n->get_expr(); func_decl* f = get_array_func_decl(ground_array, s); if (f) { SASSERT(m_arg_i >= 1); @@ -1463,7 +1463,7 @@ namespace smt { ptr_buffer arrays; get_auf_arrays(get_array(), ctx, arrays); for (enode* curr : arrays) { - app* ground_array = curr->get_expr(); + auto ground_array = curr->get_expr(); func_decl* f = get_array_func_decl(ground_array, s); if (f) { node* A_f_i = s.get_A_f_i(f, m_arg_i - 1); @@ -1471,8 +1471,8 @@ namespace smt { enode_vector::iterator end2 = curr->end_parents(); for (; it2 != end2; ++it2) { enode* p = *it2; - if (ctx->is_relevant(p) && p->get_expr()->get_decl() == m_select->get_decl()) { - SASSERT(m_arg_i < p->get_expr()->get_num_args()); + if (ctx->is_relevant(p) && p->get_decl() == m_select->get_decl()) { + SASSERT(m_arg_i < p->get_num_args()); enode* e_arg = p->get_arg(m_arg_i); A_f_i->insert(e_arg->get_expr(), e_arg->get_generation()); } @@ -1690,7 +1690,7 @@ namespace smt { typedef ptr_vector::const_iterator macro_iterator; static quantifier_ref mk_flat(ast_manager& m, quantifier* q) { - if (has_quantifiers(q->get_expr()) && !m.is_lambda_def(q)) { + if (has_quantifiers(q->get_expr())) { proof_ref pr(m); expr_ref new_q(m); pull_quant pull(m); @@ -2279,7 +2279,6 @@ namespace smt { void operator()(quantifier_info* d) { m_info = d; quantifier* q = d->get_flat_q(); - if (m.is_lambda_def(q)) return; expr* e = q->get_expr(); reset_cache(); if (!m.inc()) return; diff --git a/src/smt/smt_model_generator.cpp b/src/smt/smt_model_generator.cpp index e4a3538d5..a558a9611 100644 --- a/src/smt/smt_model_generator.cpp +++ b/src/smt/smt_model_generator.cpp @@ -105,7 +105,7 @@ namespace smt { proc = alloc(expr_wrapper_proc, m.mk_false()); } else if (m.is_model_value(r->get_expr())) - proc = alloc(expr_wrapper_proc, r->get_expr()); + proc = alloc(expr_wrapper_proc, r->get_app()); else { family_id fid = s->get_family_id(); theory * th = m_context->get_theory(fid); @@ -386,7 +386,7 @@ namespace smt { // send model for (enode * n : m_context->enodes()) { if (is_uninterp_const(n->get_expr()) && m_context->is_relevant(n)) { - func_decl * d = n->get_expr()->get_decl(); + func_decl * d = n->get_decl(); TRACE(mg_top_sort, tout << d->get_name() << " " << (m_hidden_ufs.contains(d)?"hidden":"visible") << "\n";); if (m_hidden_ufs.contains(d)) continue; expr * val = get_value(n); @@ -430,6 +430,8 @@ namespace smt { if (!m_context->is_relevant(t)) continue; enode * n = m_context->get_enode(t); + if (!n->is_app()) + continue; unsigned num_args = n->get_num_args(); func_decl * f = n->get_decl(); if (num_args == 0 && include_func_interp(f)) { diff --git a/src/smt/smt_parallel.cpp b/src/smt/smt_parallel.cpp index 952372120..64848d890 100644 --- a/src/smt/smt_parallel.cpp +++ b/src/smt/smt_parallel.cpp @@ -29,6 +29,7 @@ Author: #include #include +#include class bounded_pp_exprs { expr_ref_vector const &es; @@ -61,13 +62,18 @@ namespace smt { #include #define LOG_WORKER(lvl, s) IF_VERBOSE(lvl, verbose_stream() << "Worker " << id << s) +#define LOG_BB_WORKER(lvl, s) IF_VERBOSE(lvl, verbose_stream() << "Backbones Worker " << id << s) namespace smt { + static bool is_cancellation_exception(char const *msg) { + return msg && (strstr(msg, "canceled") != nullptr || strstr(msg, "cancelled") != nullptr); + } + void parallel::sls_worker::run() { ptr_vector assertions; p.ctx.get_assertions(assertions); - for (expr* e : assertions) + for (expr *e : assertions) m_sls->assert_expr(m_g2l(e)); lbool res = l_undef; @@ -75,8 +81,7 @@ namespace smt { if (!m.inc()) return; res = m_sls->check(); - } - catch (z3_exception& ex) { + } catch (z3_exception &ex) { // Cancellation is normal in portfolio mode if (m.limit().is_canceled()) { IF_VERBOSE(1, verbose_stream() << "SLS worker canceled\n"); @@ -94,13 +99,515 @@ namespace smt { return; } - if (res == l_true) { + if (res == l_true) { IF_VERBOSE(2, verbose_stream() << "SLS worker found SAT\n"); model_ref mdl = m_sls->get_model(); b.set_sat(m_l2g, *mdl); } } + void parallel::backbones_worker::run() { + if (m_use_failed_literal_test) + run_failed_literal_mode(); + else + run_batch_mode(); + } + + void parallel::backbones_worker::run_failed_literal_mode() { + ctx->get_fparams().m_max_conflicts = 10; + + auto is_unit = [&](unsigned v) { + return ctx->get_assignment(v) != l_undef && ctx->get_assign_level(v) == ctx->m_base_lvl; + }; + + auto probe_var = [&](unsigned v, expr* preferred, bool is_retry) -> lbool { + expr_ref e(ctx->bool_var2expr(v), m); + if (!e) + return l_undef; + if (m.is_or(e) || m.is_ite(e) || m.is_and(e) || m.is_iff(e)) + return l_undef; + + if (is_unit(v)) { + bool is_true = ctx->get_assignment(v) == l_true; + IF_VERBOSE(2, verbose_stream() << "backbone on trail " << mk_bounded_pp(e.get(), m) << "\n"); + if (!is_true) + e = m.mk_not(e); + if (b.collect_global_backbone(m_l2g, e)) { + m_stats.m_internal_backbones_found++; + if (is_retry) + m_stats.m_retry_backbones_found++; + } + return l_undef; + } + + expr_ref first(e, m), second(mk_not(e), m); + if (preferred) { + expr* atom = preferred; + bool is_negated = m.is_not(preferred, atom); + first = is_negated ? mk_not(e) : e; + second = is_negated ? e : mk_not(e); + } + + lbool r = probe_literal(v, first.get(), is_retry); + if (r != l_undef || is_unit(v)) + return r; + + return probe_literal(v, second.get(), is_retry); + }; + + bb_candidates bb_candidates; + while (m.inc()) { + if (!b.wait_for_backbone_job(id, m_g2l, bb_candidates, m.limit())) + return; + + if (bb_candidates.empty()) + continue; + + collect_shared_clauses(); + + unsigned local_cancel_epoch = b.get_cancel_epoch(); + auto canceled = [&] { return local_cancel_epoch != b.get_cancel_epoch(); }; + bool is_retry = false; + unsigned bb_candidate_epoch = b.get_bb_candidate_epoch(); + + expr_ref_vector bb_candidate_lits(m); + for (auto const& c : bb_candidates) + bb_candidate_lits.push_back(c.lit); + + while (m.inc() && !canceled()) { + lbool terminal_result = l_undef; + uint_set seen_vars; // polarity dedup (since the same variable can appear in both polarities in the candidate list) + for (expr* lit : bb_candidate_lits) { + if (is_retry && b.has_new_backbone_candidates(bb_candidate_epoch)) + break; + if (!m.inc() || canceled()) + break; + + expr* atom = lit; + m.is_not(lit, atom); + if (!ctx->b_internalized(atom)) + continue; + sat::bool_var v = ctx->get_bool_var(atom); + if (v == sat::null_bool_var || seen_vars.contains(v)) + continue; + seen_vars.insert(v); + + terminal_result = probe_var(v, lit, is_retry); + if (terminal_result != l_undef) + break; + } + + if (terminal_result != l_undef) + break; + + if (b.has_new_backbone_candidates(bb_candidate_epoch) || canceled() || !m.inc()) + break; + + is_retry = true; + + expr_ref_vector bb_snapshot = b.get_global_backbones_snapshot(m_g2l); + expr_mark bb_mark; + for (expr* e : bb_snapshot) { + bb_mark.mark(e); + bb_mark.mark(mk_not(m, e)); + } + bb_candidate_lits.reset(); + for (auto const& c : bb_candidates) + if (!bb_mark.is_marked(c.lit.get())) + bb_candidate_lits.push_back(c.lit); + } + + if (!m.inc()) + return; + if (!canceled()) + b.cancel_current_backbone_batch(); + bb_candidates.reset(); + } + } + + lbool parallel::backbones_worker::probe_literal(bool_var v, expr *e, bool is_retry) { + asms.push_back(e); + auto terminal_result = b.check(asms, *ctx); + asms.pop_back(); + if (terminal_result == l_false) { + // If the tested literal is not part of the unsat core, then the + // formula is UNSAT independently of this failed-literal probe. + if (!ctx->unsat_core().contains(e)) { + b.set_unsat(m_l2g, ctx->unsat_core()); + return l_false; + } + // Ordinary failed-literal backbone discovery is non-terminal: + // share/assert the backbone, then continue probing. + IF_VERBOSE(2, verbose_stream() << "failed literal " << mk_bounded_pp(e, m) << "\n"); + expr_ref not_e(mk_not(m, e), m); + + m_stats.m_backbones_detected++; + if (b.collect_global_backbone(m_l2g, not_e)) { + m_stats.m_internal_backbones_found++; + if (is_retry) + m_stats.m_retry_backbones_found++; + } + ctx->assert_expr(not_e); + terminal_result = l_undef; + } + if (terminal_result == l_true) { + model_ref mdl; + ctx->get_model(mdl); + b.set_sat(m_l2g, *mdl); + } + return terminal_result; + } + + void parallel::backbones_worker::run_batch_mode() { + bb_candidates bb_curr_batch_candidates; + + while (m.inc()) { + if (!b.wait_for_backbone_job(id, m_g2l, bb_curr_batch_candidates, m.limit())) + return; + + if (bb_curr_batch_candidates.empty()) + continue; + + LOG_BB_WORKER(1, " received batch of " << bb_curr_batch_candidates.size() << " candidates\n"); + collect_shared_clauses(); + + unsigned local_cancel_epoch = b.get_cancel_epoch(); + auto canceled = [&] { return local_cancel_epoch != b.get_cancel_epoch(); }; + unsigned bb_candidate_epoch = b.get_bb_candidate_epoch(); + + auto fallback_failed_literal_probe = [&](expr_ref_vector const& chunk_lits, expr_ref_vector& bb_candidate_lits, bool is_retry = false) { + unsigned old_max_conflicts = ctx->get_fparams().m_max_conflicts; + ctx->get_fparams().m_max_conflicts = 10; + if (is_retry) + ++m_stats.m_bb_retries; + else + ++m_stats.m_fallback_singleton_checks; + + for (expr* lit : chunk_lits) { + if (is_retry && b.has_new_backbone_candidates(bb_candidate_epoch)) { + ctx->get_fparams().m_max_conflicts = old_max_conflicts; + return; + } + if (!m.inc() || canceled()) { + ctx->get_fparams().m_max_conflicts = old_max_conflicts; + return; + } + if (!bb_candidate_lits.contains(lit)) // already handled in singleton core → backbone case below + continue; + + expr_ref bb_ref(lit, m); + if (m_mode == bb_mode::bb_positive) + bb_ref = mk_not(m, bb_ref); // Normalize to the backbone literal for this mode; probe_literal tests its negation + + if (!b.is_global_backbone_or_negation(m_l2g, bb_ref)) { + expr_ref backbone(m); + if (try_get_unit_backbone(bb_ref.get(), backbone)) { + m_stats.m_backbones_detected++; + LOG_BB_WORKER(1, " fallback found unit backbone: " << mk_bounded_pp(backbone.get(), m, 3) << "\n"); + if (b.collect_global_backbone(m_l2g, backbone)) + m_stats.m_internal_backbones_found++; + } else { + expr* atom = bb_ref.get(); + m.is_not(bb_ref.get(), atom); + if (ctx->b_internalized(atom)) { + sat::bool_var v = ctx->get_bool_var(atom); + + if (v != sat::null_bool_var) { + lbool terminal_result = probe_literal(v, mk_not(m, bb_ref), is_retry); // failed literal probing (i.e. probe the negation of the bb candidate) + LOG_BB_WORKER(1, " RESULT: " << terminal_result << " FOR CANDIDATE: " << mk_bounded_pp(bb_ref.get(), m, 3) << "\n"); + } + } + } + } + bb_candidate_lits.erase(lit); + } + ctx->get_fparams().m_max_conflicts = old_max_conflicts; + }; + + m_stats.m_batches_total++; + m_stats.m_candidates_total += bb_curr_batch_candidates.size(); + + expr_ref_vector bb_candidate_lits(m); + for (auto const& c : bb_curr_batch_candidates) + bb_candidate_lits.push_back(c.lit); + + unsigned chunk_delta = 1; + + // in mode bb_neg this is Algorithm 7 from https://sat.inesc-id.pt/~mikolas/bb-aicom-preprint.pdf + while (!bb_candidate_lits.empty() && !canceled() && m.inc()) { + // remove candidates that the other threads found to be backbones + { + unsigned j = 0; + for (auto lit : bb_candidate_lits) { + if (!b.is_global_backbone_or_negation(m_l2g, lit)) + bb_candidate_lits[j++] = lit; + } + bb_candidate_lits.shrink(j); + } + + // remove candidates that are units and assert them as backbones + { + unsigned j = 0; + for (expr* lit : bb_candidate_lits) { + expr_ref backbone(m); + if (try_get_unit_backbone(lit, backbone)) { + IF_VERBOSE(2, verbose_stream() << "backbone on trail " << mk_bounded_pp(backbone.get(), m) << "\n"); + if (b.collect_global_backbone(m_l2g, backbone)) + m_stats.m_internal_backbones_found++; + m_stats.m_backbones_detected++; + continue; + } + bb_candidate_lits[j++] = lit; + } + bb_candidate_lits.shrink(j); + } + + unsigned chunk_size = std::min(m_bb_chunk_size * chunk_delta, bb_candidate_lits.size()); + expr_ref_vector chunk_lits(m); + expr_ref_vector negated_chunk_lits(m); + expr_mark chunk_atoms; + + // Keep at most one polarity per atom in a chunk since this otherwise this leads to + // immediate contradictions and thus no progress on finding backbones in the batch + for (unsigned i = 0; i < bb_candidate_lits.size() && chunk_lits.size() < chunk_size; ++i) { + expr* lit = bb_candidate_lits.get(i); + expr* atom = lit; + m.is_not(lit, atom); + if (chunk_atoms.is_marked(atom)) + continue; + chunk_atoms.mark(atom); + chunk_lits.push_back(lit); + negated_chunk_lits.push_back(mk_not(m, lit)); + } + + expr_ref_vector bb_asms(m); + if (m_mode == bb_mode::bb_negated) + bb_asms.append(negated_chunk_lits); // F ∧ ¬U + else + bb_asms.append(chunk_lits); // F ∧ U + + collect_shared_clauses(); + + while (true) { + + if (!m.inc()) + return; + if (canceled()) + break; + + m_stats.m_core_refinement_rounds++; + unsigned base_asms_sz = asms.size(); + for (expr* a : bb_asms) + asms.push_back(a); + lbool r = b.check(asms, *ctx); + asms.shrink(base_asms_sz); + + if (!m.inc() || canceled()) + break; + + if (r == l_undef) { + LOG_BB_WORKER(1, " UNDEF at chunk_size=" << chunk_size << "\n"); + + if (chunk_size < bb_candidate_lits.size()) { + chunk_delta++; // try again with a bigger chunk + m_stats.m_num_chunk_increases++; + break; + } + + LOG_BB_WORKER(1, " UNDEF and max chunk → fallback\n"); + + fallback_failed_literal_probe(chunk_lits, bb_candidate_lits); + m_stats.m_fallback_reason_undef++; + chunk_delta = 1; + break; + } + + if (r == l_true) { + LOG_BB_WORKER(1, " batch check returned SAT, thus entire formula is SAT\n"); + model_ref mdl; + ctx->get_model(mdl); + b.set_sat(m_l2g, *mdl); + bb_curr_batch_candidates.reset(); + return; + } + + // ----- UNSAT: inspect core ----- + expr_ref_vector bb_asms_in_core(m); + auto const& unsat_core = ctx->unsat_core(); + + for (expr* a : unsat_core) + if (bb_asms.contains(a)) + bb_asms_in_core.push_back(a); + + // ---- empty core intersection → formula is UNSAT independent of backbone assumptions ---- + if (bb_asms_in_core.empty()) { + b.set_unsat(m_l2g, unsat_core); + return; + } + + // ---- singleton core → backbone ---- + if (bb_asms_in_core.size() == 1) { + expr* a = bb_asms_in_core.back(); + expr_ref backbone_lit(mk_not(m, a), m); + + m_stats.m_singleton_backbones++; + m_stats.m_backbones_detected++; + + if (b.collect_global_backbone(m_l2g, backbone_lit)) { + m_stats.m_internal_backbones_found++; + ctx->assert_expr(backbone_lit.get()); // since bb workers don't collect clauses they themselves shared + } + + expr* candidate_to_remove = + (m_mode == bb_mode::bb_negated) + ? backbone_lit.get() // since core contains ¬candidates in negated mode + : a; // since core contains candidates in positive mode + + bb_candidate_lits.erase(candidate_to_remove); + } + + unsigned sz_before = bb_asms.size(); + for (expr* a : bb_asms_in_core) + bb_asms.erase(a); + m_stats.m_lits_removed_by_core += sz_before - bb_asms.size(); + chunk_delta = 1; + + if (bb_asms.empty()) { + LOG_BB_WORKER(1, " no more negated chunk literals, fallback to individual checks\n"); + fallback_failed_literal_probe(chunk_lits, bb_candidate_lits); + m_stats.m_fallback_reason_chunk_exhausted++; + break; + } + } + } + + // Retry loop: keeps the thread active while waiting for new backbone candidates. + // Only retries if at least one new backbone was found in the previous round, to avoid + // spinning indefinitely when progress has stalled. + while (!b.has_new_backbone_candidates(bb_candidate_epoch) && !canceled() && m.inc()) { + collect_shared_clauses(); + unsigned found_before = m_stats.m_internal_backbones_found; + + // filter candidates for retry + expr_ref_vector bb_snapshot = b.get_global_backbones_snapshot(m_g2l); + expr_mark bb_mark; + for (expr* e : bb_snapshot) { + bb_mark.mark(e); + bb_mark.mark(mk_not(m, e)); + } + bb_candidate_lits.reset(); + for (auto const& c : bb_curr_batch_candidates) + if (!bb_mark.is_marked(c.lit.get())) + bb_candidate_lits.push_back(c.lit); + + if (bb_candidate_lits.empty()) + break; + + fallback_failed_literal_probe(bb_candidate_lits, bb_candidate_lits, true); + + // Break if no progress was made; further retries on this batch are unlikely to succeed. + if (m_stats.m_internal_backbones_found == found_before) + break; + } + + if (!canceled()) + b.cancel_current_backbone_batch(); + + bb_curr_batch_candidates.reset(); + } + } + + void parallel::backbones_worker::cancel() { + LOG_BB_WORKER(1, " BACKBONES WORKER cancelling\n"); + m.limit().cancel(); + } + + // returns true if the global bb is new, false if it was already known + bool parallel::batch_manager::collect_global_backbone(ast_translation &l2g, expr_ref const &backbone, unsigned source_worker_id) { + IF_VERBOSE(1, verbose_stream() << "collect-global-backbone\n"); + std::scoped_lock lock(mux); + SASSERT(&m == &l2g.to()); + + if (is_global_backbone_unlocked(l2g, backbone)) + return false; + + expr_ref g_bb_ref(l2g(backbone.get()), m); + m_global_backbones.insert(g_bb_ref.get()); + ++m_stats.m_backbones_found; + + IF_VERBOSE(1, verbose_stream() << " Found and sharing new global backbone: " << mk_bounded_pp(g_bb_ref, m, 3) << "\n"); + collect_clause_unlocked(l2g, source_worker_id, backbone.get()); + + expr_ref neg_g_bb_ref(mk_not(g_bb_ref), m); + vector g_core; + g_core.push_back(neg_g_bb_ref); + vector targets; + collect_matching_targets_unlocked(nullptr, neg_g_bb_ref, g_core, targets); + + if (!targets.empty()) { + IF_VERBOSE(1, verbose_stream() << " Closing negation of the new global backbone: " << mk_bounded_pp(g_bb_ref, m, 3) << "\n"); + + if (m_ablate_backtracking) { + // Ablation: for each target, pass the entire path from root to that node + for (auto const& target : targets) { + if (m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch)) + continue; + + // Reconstruct the full path from root to this target node + expr_ref_vector full_cube(l2g.from()); + node* n = target.leased_node; + while (n) { + if (!cube_config::literal_is_null(n->get_literal())) { + expr* lit = n->get_literal().get(); + full_cube.push_back(expr_ref(lit, l2g.from())); + } + n = n->parent(); + } + + // Backtrack this one target with its full path + vector single_target = { target }; + backtrack_unlocked(l2g, UINT_MAX, full_cube, nullptr, &single_target); + } + } else { + // Normal: just use the negated backbone + expr_ref_vector l_core(l2g.from()); + l_core.push_back(mk_not(backbone)); + backtrack_unlocked(l2g, UINT_MAX, l_core, nullptr, &targets); + } + } + + return true; + } + + void parallel::backbones_worker::collect_statistics(::statistics& st) const { + st.update("bb-batches-total", m_stats.m_batches_total); + st.update("bb-candidates-total", m_stats.m_candidates_total); + st.update("bb-backbones-detected", m_stats.m_backbones_detected); + st.update("bb-internal-backbones-found", m_stats.m_internal_backbones_found); + st.update("bb-retry-backbones-found", m_stats.m_retry_backbones_found); + st.update("bb-retries", m_stats.m_bb_retries); + st.update("bb-core-refinement-rounds", m_stats.m_core_refinement_rounds); + st.update("bb-singleton-backbones", m_stats.m_singleton_backbones); + st.update("bb-fallback-singleton-checks", m_stats.m_fallback_singleton_checks); + st.update("bb-fallback-chunk-exhausted", m_stats.m_fallback_reason_chunk_exhausted); + st.update("bb-fallback-undef", m_stats.m_fallback_reason_undef); + st.update("bb-literals-removed-by-core", m_stats.m_lits_removed_by_core); + st.update("bb-num-chunk-increases", m_stats.m_num_chunk_increases); + + auto safe_ratio = [](double num, double den) -> double { + return den > 0 ? num / den : 0.0; + }; + + st.update("bb-backbone-yield-pct", + 100.0 * safe_ratio(m_stats.m_internal_backbones_found, m_stats.m_candidates_total)); + st.update("bb-avg-backbones-per-batch", + safe_ratio(m_stats.m_internal_backbones_found, m_stats.m_batches_total)); + st.update("bb-core-refinement-rounds-per-batch", + safe_ratio(m_stats.m_core_refinement_rounds, m_stats.m_batches_total)); + st.update("bb-core-effectiveness-lit-removed-per-round", + safe_ratio(m_stats.m_lits_removed_by_core, m_stats.m_core_refinement_rounds)); + } + void parallel::sls_worker::cancel() { IF_VERBOSE(1, verbose_stream() << " SLS WORKER cancelling\n"); m.limit().cancel(); @@ -110,26 +617,168 @@ namespace smt { m_sls->collect_statistics(st); } + parallel::core_minimizer_worker::core_minimizer_worker(parallel& p, expr_ref_vector const& _asms) + : b(p.m_batch_manager), asms(m), m_smt_params(p.ctx.get_fparams()), m_g2l(p.ctx.m, m), m_l2g(m, p.ctx.m) { + for (expr* e : _asms) + asms.push_back(m_g2l(e)); + IF_VERBOSE(1, verbose_stream() << "Initialized core minimizer thread\n"); + ctx = alloc(context, m, m_smt_params, p.ctx.get_params()); + ctx->set_logic(p.ctx.m_setup.get_logic()); + context::copy(p.ctx, *ctx, true); + ctx->pop_to_base_lvl(); + ctx->get_fparams().m_preprocess = false; + } + + void parallel::core_minimizer_worker::cancel() { + IF_VERBOSE(1, verbose_stream() << "Core minimizer cancelling\n"); + m.limit().cancel(); + } + + void parallel::core_minimizer_worker::collect_statistics(::statistics& st) const { + ctx->collect_statistics(st); + st.update("parallel-core-minimize-calls", m_num_core_minimize_calls); + st.update("parallel-core-minimize-undef", m_num_core_minimize_undef); + st.update("parallel-core-minimize-refined", m_num_core_minimize_refined); + st.update("parallel-core-minimize-lits-removed", m_num_core_minimize_lits_removed); + st.update("parallel-core-minimize-found-sat", m_num_core_minimize_found_sat); + } + + void parallel::core_minimizer_worker::minimize_unsat_core(expr_ref_vector& core) { + expr_ref_vector unknown(core), mus(m), trial(m); // mus = literals we have NOT managed to eliminate + + unsigned original_size = core.size(); + ++m_num_core_minimize_calls; + + // Invariant: F and mus and unknown is UNSAT. + while (!unknown.empty()) { + if (!m.inc()) { + core.reset(); + core.append(mus); + core.append(unknown); + return; + } + + expr* lit = unknown.back(); + unknown.pop_back(); + expr_ref not_lit(mk_not(m, lit), m); + + trial.reset(); + trial.append(mus); + trial.append(unknown); + trial.push_back(not_lit); + + lbool r = l_undef; + try { + flet _max_conflicts(ctx->get_fparams().m_max_conflicts, m_core_minimize_conflict_budget); + r = ctx->check(trial.size(), trial.data()); + } + catch (...) { + r = l_undef; + } + + switch (r) { + case l_undef: // the solver failed to show that lit is removable, so we must keep it to be safe + ++m_num_core_minimize_undef; + mus.push_back(lit); + break; + case l_true: { // If all asms are true (or as an approximation, if asms is empty), it found a model. It can report sat and exit the minimization worker thread. + if (!asms.empty()) { + mus.push_back(lit); + break; + } + ++m_num_core_minimize_found_sat; + model_ref mdl; + ctx->get_model(mdl); + b.set_sat(m_l2g, *mdl); + return; + } + case l_false: { + auto const& unsat_core = ctx->unsat_core(); + if (!unsat_core.contains(not_lit)) { + ++m_num_core_minimize_refined; + unknown.reset(); + expr_ref_vector new_mus(m); + for (expr* c : unsat_core) { + if (mus.contains(c)) + new_mus.push_back(c); + else + unknown.push_back(c); + } + mus.reset(); + mus.append(new_mus); + } + break; + } + default: + UNREACHABLE(); + } + } + + core.reset(); + core.append(mus); + core.append(unknown); // to reflect loop invariant, and in case we add an early exit + if (core.size() < original_size) + m_num_core_minimize_lits_removed += original_size - core.size(); + return; + } + + void parallel::core_minimizer_worker::run() { + while (m.inc()) { + node* source = nullptr; + expr_ref_vector core(m); + if (!b.wait_for_core_min_job(m_g2l, source, core, m.limit())) + return; + + unsigned original_size = core.size(); + if (original_size <= 1) + continue; + + collect_shared_clauses(); + + expr_ref_vector minimized(m); + minimized.append(core); + minimize_unsat_core(minimized); + + if (minimized.size() < original_size) + b.publish_minimized_core(m_l2g, asms, source, original_size, minimized); + } + } + void parallel::worker::run() { - search_tree::node *node = nullptr; + bool is_first_run = true; + node_lease lease; expr_ref_vector cube(m); while (true) { - if (!b.get_cube(m_g2l, id, cube, node)) { + if (!b.get_cube(m_g2l, id, cube, is_first_run, lease)) { LOG_WORKER(1, " no more cubes\n"); return; } + is_first_run = false; collect_shared_clauses(); check_cube_start: LOG_WORKER(1, " CUBE SIZE IN MAIN LOOP: " << cube.size() << "\n"); + + if (m_config.m_global_backbones) { + bb_candidates local_candidates = find_backbone_candidates(); + b.collect_backbone_candidates(m_l2g, local_candidates); + if (!m.inc()) + return; + } + lbool r = check_cube(cube); - if (!m.inc()) { - b.set_exception("context cancelled"); - return; + if (b.lease_canceled(lease)) { + LOG_WORKER(1, " abandoning canceled lease\n"); + lease = {}; + m.limit().dec_cancel(); + continue; } + if (!m.inc()) + return; + switch (r) { case l_undef: { update_max_thread_conflicts(); @@ -140,7 +789,8 @@ namespace smt { auto atom = get_split_atom(); if (!atom) goto check_cube_start; - b.split(m_l2g, id, node, atom); + b.try_split(m_l2g, id, lease, atom, m_config.m_threads_max_conflicts); + lease = {}; simplify(); break; } @@ -164,7 +814,18 @@ namespace smt { } LOG_WORKER(1, " found unsat cube\n"); - b.backtrack(m_l2g, unsat_core, node); + node* source = lease.leased_node; + + // When ablating backtracking, use the entire cube path instead of the unsat core + expr_ref_vector const& core_to_use = m_config.m_ablate_backtracking ? cube : unsat_core; + if (m_config.m_ablate_backtracking) { + LOG_WORKER(1, " ablating backtracking: using full cube path of size " << core_to_use.size() << "\n"); + } + + b.backtrack(m_l2g, id, core_to_use, lease); + if (m_config.m_core_minimize) + b.enqueue_core_minimization(m_l2g, source, unsat_core); + lease = {}; if (m_config.m_share_conflicts) b.collect_clause(m_l2g, id, mk_not(mk_and(unsat_core))); @@ -189,12 +850,21 @@ namespace smt { context::copy(p.ctx, *ctx, true); // don't share initial units ctx->pop_to_base_lvl(); - m_num_shared_units = ctx->assigned_literals().size(); + m_shared_units_prefix = ctx->assigned_literals().size(); m_num_initial_atoms = ctx->get_num_bool_vars(); ctx->get_fparams().m_preprocess = false; // avoid preprocessing lemmas that are exchanged smt_parallel_params pp(p.ctx.m_params); m_config.m_inprocessing = pp.inprocessing(); + m_config.m_global_backbones = pp.num_global_bb_batch_threads() > 0 || pp.num_global_bb_fl_threads() > 0; + m_config.m_local_backbones = pp.local_backbones(); + m_config.m_core_minimize = pp.core_minimize(); + m_config.m_ablate_backtracking = pp.ablate_backtracking(); + + // When ablating backtracking, disable core minimization since we're using the full cube path + if (m_config.m_ablate_backtracking) { + m_config.m_core_minimize = false; + } } parallel::sls_worker::sls_worker(parallel& p) @@ -204,16 +874,113 @@ namespace smt { m_sls = alloc(sls::smt_solver, m, m_params); } + parallel::backbones_worker::backbones_worker(unsigned id, parallel &p, expr_ref_vector const &_asms) + : id(id), b(p.m_batch_manager), m(), asms(m), m_smt_params(p.ctx.get_fparams()), m_g2l(p.ctx.m, m), m_l2g(m, p.ctx.m) { + for (auto e : _asms) + asms.push_back(m_g2l(e)); + IF_VERBOSE(1, verbose_stream() << "Initialized backbones thread " << id << "\n"); + m_mode = id == 0 ? bb_mode::bb_negated : bb_mode::bb_positive; + ctx = alloc(context, m, m_smt_params, p.ctx.get_params()); + ctx->set_logic(p.ctx.m_setup.get_logic()); + ctx->get_fparams().m_max_conflicts = m_bb_conflicts_per_chunk; + context::copy(p.ctx, *ctx, true); + ctx->pop_to_base_lvl(); + m_shared_units_prefix = ctx->assigned_literals().size(); + m_num_initial_atoms = ctx->get_num_bool_vars(); + + smt_parallel_params pp(p.ctx.m_params); + m_use_failed_literal_test = pp.num_global_bb_fl_threads() > 0; + } + + parallel::bb_candidates parallel::worker::find_backbone_candidates(unsigned k) { + bb_candidates backbone_candidates; + expr_ref candidate(m); + unsigned curr_time = ctx->m_stats.m_num_assignments; + + for (bool_var v = 0; v < ctx->get_num_bool_vars(); ++v) { + if (ctx->get_assignment(v) != l_undef && ctx->get_assign_level(v) == ctx->m_base_lvl) + continue; + + candidate = ctx->bool_var2expr(v); + if (!candidate) + continue; + + auto birth = ctx->m_birthdate[v]; + auto age = curr_time - birth; + + auto const& d = ctx->get_bdata(v); + if (d.m_phase_available && !d.m_phase) + candidate = m.mk_not(candidate); + + if (b.is_global_backbone_or_negation(m_l2g, candidate)) + continue; + + bb_candidate bb_cand(m, candidate, age, 1); + backbone_candidates.push_back(bb_cand); + } + + // sort from oldest to youngest + std::stable_sort( + backbone_candidates.begin(), + backbone_candidates.end(), + [](bb_candidate const& a, bb_candidate const& b) { + return a.age > b.age; + } + ); + + // take top-k oldest + if (backbone_candidates.size() > k) + backbone_candidates.shrink(k); + + return backbone_candidates; + } + + // checks if candidate or its negation is a unit backbone on the trail and returns the backbone if so + bool parallel::backbones_worker::try_get_unit_backbone(expr* candidate, expr_ref& backbone) { + expr* atom = candidate; + m.is_not(candidate, atom); + if (!ctx->b_internalized(atom)) + return false; + sat::bool_var v = ctx->get_bool_var(atom); + if (v == sat::null_bool_var || ctx->get_assignment(v) == l_undef || ctx->get_assign_level(v) != ctx->m_base_lvl) + return false; + bool is_true = ctx->get_assignment(v) == l_true; + backbone = expr_ref(atom, m); + if (!is_true) + backbone = mk_not(backbone); + return true; + } + + // NSB review: the code appares to use the assumption that we are not at base level + // there can be literals above base level (see "filter by assign level" test). + // when existing the loop we update m_shared_units_prefix even if the assigned-literals can go beyond base level + // we could be missing units. + // fixes; we could maintain a set uint_set seen_units to avoid resharing the same units + // we could only update m_shared_units_prefix until the size of the base level prefix. + // so we would re-examine literals that are not necessarily on base level in later calls. + // void parallel::worker::share_units() { - // Collect new units learned locally by this worker and send to batch manager + // Collect new base-level units learned locally by this worker. + // Such units are globally valid and are thus part of the backbone unsigned sz = ctx->assigned_literals().size(); - for (unsigned j = m_num_shared_units; j < sz; ++j) { // iterate only over new literals since last sync + unsigned prefix_sz = m_shared_units_prefix; + bool at_prefix = true; + for (unsigned j = m_shared_units_prefix; j < sz; ++j) { // iterate only over new literals since last sync literal lit = ctx->assigned_literals()[j]; // filter by assign level: do not pop to base level as this destroys the current search state - if (ctx->get_assign_level(lit) > ctx->m_base_lvl) + if (ctx->get_assign_level(lit) > ctx->m_base_lvl) { + at_prefix = false; continue; + } + + if (at_prefix) + ++prefix_sz; + + if (m_known_units.contains(lit.var())) + continue; + m_known_units.insert(lit.var()); if (!ctx->is_relevant(lit.var()) && m_config.m_share_units_relevant_only) continue; @@ -223,15 +990,16 @@ namespace smt { continue; // skip non-initial atoms if configured to do so } - expr_ref e(ctx->bool_var2expr(lit.var()), ctx->m); // turn literal into a Boolean expression - if (m.is_and(e) || m.is_or(e)) + expr_ref e(ctx->bool_var2expr(lit.var()), ctx->m); // turn literal into a Boolean expression + if (m.is_and(e) || m.is_or(e) || m.is_ite(e) || m.is_iff(e)) continue; if (lit.sign()) - e = m.mk_not(e); // negate if literal is negative - b.collect_clause(m_l2g, id, e); + e = mk_not(e); // negate if literal is negative + + b.collect_global_backbone(m_l2g, e, id); } - m_num_shared_units = sz; + m_shared_units_prefix = prefix_sz; } void parallel::worker::simplify() { @@ -300,7 +1068,7 @@ namespace smt { ctx->setup_context(true); ctx->internalize_assertions(); auto old_atoms = m_num_initial_atoms; - m_num_shared_units = ctx->assigned_literals().size(); + m_shared_units_prefix = ctx->assigned_literals().size(); m_num_initial_atoms = ctx->get_num_bool_vars(); LOG_WORKER(1, " inprocess " << old_atoms << " -> " << m_num_initial_atoms << "\n"); } @@ -314,20 +1082,323 @@ namespace smt { m.limit().cancel(); } - void parallel::batch_manager::backtrack(ast_translation &l2g, expr_ref_vector const &core, - search_tree::node *node) { + void parallel::worker::cancel_lease() { + LOG_WORKER(1, " canceling lease\n"); + m.limit().inc_cancel(); + } + + lbool parallel::batch_manager::check(expr_ref_vector const &asms, context &ctx) { + lbool r = l_undef; + auto &m = asms.m(); + try { + r = ctx.check(asms.size(), asms.data()); + } catch (z3_error &err) { + if (!m.limit().is_canceled()) + set_exception(err.error_code()); + } catch (z3_exception &ex) { + if (!m.limit().is_canceled() && !is_cancellation_exception(ex.what())) + set_exception(ex.what()); + } catch (...) { + if (!m.limit().is_canceled()) + set_exception("unknown exception"); + } + return r; + } + + void parallel::batch_manager::release_lease_unlocked(unsigned worker_id, node* n) { + if (worker_id >= m_worker_leases.size()) + return; + auto &lease = m_worker_leases[worker_id]; + if (!lease.leased_node || lease.leased_node != n) + return; + m_search_tree.dec_active_workers(lease.leased_node); + lease = {}; + } + + void parallel::batch_manager::cancel_closed_leases_unlocked(unsigned source_worker_id) { + unsigned n = std::min(m_worker_leases.size(), p.m_workers.size()); + for (unsigned worker_id = 0; worker_id < n; ++worker_id) { + if (worker_id == source_worker_id) + continue; + auto const& lease = m_worker_leases[worker_id]; + + // only cancel workers that currently hold a lease, whose lease is canceled, + // and haven't already been signaled (prevents multiple inc_cancel() for same lease) + if (lease.leased_node && !lease.cancel_signaled && m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch)) { + p.m_workers[worker_id]->cancel_lease(); + m_worker_leases[worker_id].cancel_signaled = true; + } + } + } + + void parallel::batch_manager::backtrack(ast_translation &l2g, unsigned worker_id, expr_ref_vector const &core, + node_lease const &lease) { std::scoped_lock lock(mux); - IF_VERBOSE(1, verbose_stream() << "Batch manager backtracking.\n"); + vector g_core; + for (auto c : core) + g_core.push_back(expr_ref(l2g(c), m)); + + vector targets; + collect_matching_targets_unlocked(lease.leased_node, lease.leased_node->get_literal().get(), g_core, targets); + backtrack_unlocked(l2g, worker_id, core, &lease, targets.empty() ? nullptr : &targets); + } + + void parallel::batch_manager::enqueue_core_minimization(ast_translation& l2g, node* source, + expr_ref_vector const& core) { + std::scoped_lock lock(mux); + if (m_state != state::is_running || !p.m_core_minimizer_worker || !source || core.empty()) + return; + if (core.size() <= 1) { + ++m_stats.m_core_min_jobs_skipped; + return; + } + + source = find_core_source_unlocked(l2g, source, core); + if (!source) { + ++m_stats.m_core_min_jobs_skipped; + return; + } + + scoped_ptr job = alloc(core_min_job, m, source); + for (expr* c : core) + job->core.push_back(l2g(c)); + m_core_min_jobs.push_back(job.detach()); + ++m_stats.m_core_min_jobs_enqueued; + m_core_min_cv.notify_one(); + } + + bool parallel::batch_manager::wait_for_core_min_job(ast_translation& g2l, node*& source, + expr_ref_vector& core, reslimit& lim) { + std::unique_lock lock(mux); + m_core_min_cv.wait(lock, [&]() { + return lim.is_canceled() || m_state != state::is_running || !m_core_min_jobs.empty(); + }); + + if (lim.is_canceled() || m_state != state::is_running) + return false; + + unsigned best_idx = select_best_core_min_job_unlocked(); + m_core_min_jobs.swap(best_idx, m_core_min_jobs.size() - 1); + core_min_job* job = m_core_min_jobs.detach_back(); + m_core_min_jobs.pop_back(); + SASSERT(job); + source = job->source; + core.reset(); + for (expr* c : job->core) + core.push_back(g2l(c)); + dealloc(job); + return source != nullptr; + } + + // Given a newly closed node, source, and its core, find the lowest ancestor of source that + // contains a core literal, and return it as the source for the core minimization job + parallel::node* parallel::batch_manager::find_core_source_unlocked( + ast_translation& l2g, node* source, expr_ref_vector const& core) { + if (!source) + return nullptr; + + vector g_core; + for (expr* c : core) + g_core.push_back(expr_ref(l2g(c), m)); + + for (node* cur = source; cur; cur = cur->parent()) { + if (cube_config::literal_is_null(cur->get_literal())) + continue; + if (any_of(g_core, [&](cube_config::literal const& lit) { return lit == cur->get_literal(); })) + return cur; + } + return nullptr; + } + + unsigned parallel::batch_manager::select_best_core_min_job_unlocked() const { + SASSERT(!m_core_min_jobs.empty()); + + unsigned best_idx = 0; + node* best_source = m_core_min_jobs[0]->source; + unsigned best_depth = best_source ? best_source->depth() : 0; + unsigned best_core_size = m_core_min_jobs[0]->core.size(); + + for (unsigned i = 1; i < m_core_min_jobs.size(); ++i) { + core_min_job* job = m_core_min_jobs[i]; + node* job_source = job->source; + unsigned job_depth = job_source ? job_source->depth() : 0; + unsigned job_core_size = job->core.size(); + + // rank first by core source node depth (deepest -> shallowest), then by core size (largest -> smallest) + if (job_depth > best_depth || (job_depth == best_depth && job_core_size > best_core_size)) { + best_idx = i; + best_depth = job_depth; + best_core_size = job_core_size; + } + } + return best_idx; + } + + void parallel::batch_manager::publish_minimized_core(ast_translation& l2g, expr_ref_vector const& asms, node* source, + unsigned original_core_size, expr_ref_vector const& minimized_core) { + std::scoped_lock lock(mux); + if (m_state != state::is_running || !source || minimized_core.size() >= original_core_size) { + ++m_stats.m_core_min_jobs_skipped; + return; + } + + vector g_core; + for (expr* c : minimized_core) + g_core.push_back(expr_ref(l2g(c), m)); + + // don't publish a minimized core if the node already has an equal-or-smaller core by the time the minimizer thread finishes + // (e.g. from another thread or from backtracking resulotion propagation) + if (source->get_core().size() <= g_core.size()) { + ++m_stats.m_core_min_jobs_skipped; + return; + } + + IF_VERBOSE(1, verbose_stream() << "Batch manager publishing minimized core " + << original_core_size << " -> " << g_core.size() << "\n"); + + if (all_of(g_core, [&](cube_config::literal const& lit) { return asms.contains(lit.get()); })) { + IF_VERBOSE(1, verbose_stream() << "Minimized core removed all path literals, setting UNSAT\n"); + m_state = state::is_unsat; + SASSERT(p.ctx.m_unsat_core.empty()); + for (expr* e : minimized_core) + p.ctx.m_unsat_core.push_back(l2g(e)); + ++m_stats.m_core_min_jobs_published; + ++m_stats.m_core_min_global_unsat; + cancel_background_threads(); + return; + } + + // do not backtrack through the batch manager since this only handles non-closed leases + // and the batch manager also tries to search for external matching targets in the tree + // which is a problem since we must backtrack only on the source node or the core is invalid + m_search_tree.backtrack(source, g_core); + + vector targets; + if (!g_core.empty()) { + collect_matching_targets_unlocked(source, g_core[0].get(), g_core, targets); + for (auto const& target : targets) { + if (!m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch)) + m_search_tree.backtrack(target.leased_node, g_core); + } + } + + ++m_stats.m_core_min_jobs_published; + cancel_closed_leases_unlocked(UINT_MAX); + + IF_VERBOSE(2, m_search_tree.display(verbose_stream() << bounded_pp_exprs(minimized_core) << "\n");); + if (m_search_tree.is_closed()) { + IF_VERBOSE(1, verbose_stream() << "Search tree closed by minimized core, setting UNSAT\n"); + m_state = state::is_unsat; + SASSERT(p.ctx.m_unsat_core.empty()); + for (auto e : m_search_tree.get_core_from_root()) + p.ctx.m_unsat_core.push_back(e); + cancel_background_threads(); + } + } + + void parallel::batch_manager::collect_matching_targets_unlocked(node* source, expr* lit, vector const& core, + vector& targets) { + targets.reset(); + if (!lit) + return; + + auto is_ancestor_of = [&](node* ancestor, node* cur) { + if (!ancestor) + return false; + for (node* p = cur; p; p = p->parent()) { + if (p == ancestor) + return true; + } + return false; + }; + + auto path_contains = [&](node* cur, cube_config::literal const& lit) { + for (node* p = cur; p; p = p->parent()) { + if (p->get_literal() == lit) + return true; + } + return false; + }; + + auto path_contains_core = [&](node* cur) { + return all_of(core, [&](cube_config::literal const& c) { + return path_contains(cur, c); + }); + }; + + ptr_vector matches; + m_search_tree.find_nonclosed_nodes_with_literal(expr_ref(lit, m), matches); + for (node* t : matches) { + if (!t || t == source) + continue; + if (m_search_tree.is_lease_canceled(t, t->get_cancel_epoch())) + continue; + + // When source is provided, keep only external matches. Nodes in the + // same branch are already closed by backtracking on the source node. + if (source && (is_ancestor_of(source, t) || is_ancestor_of(t, source))) + continue; + + // Reusing a conflict on another branch is sound only if that + // the path from that node->root contains every literal in the core. + // Matching on the closing literal alone is insufficient: F & a & l + // may be UNSAT while F & c & l is SAT. + if (!path_contains_core(t)) + continue; + + // Keep only highest matching nodes: closing an ancestor also closes + // all of its matching descendants. + bool is_highest_ancestor = true; + for (node* p = t->parent(); p; p = p->parent()) { + if (any_of(targets, [&](node_lease const& target) { return target.leased_node == p; })) { + is_highest_ancestor = false; + break; + } + } + if (!is_highest_ancestor) + continue; + + targets.push_back({ t, t->get_cancel_epoch() }); + } + } + + void parallel::batch_manager::backtrack_unlocked(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, + node_lease const* lease, vector const* targets) { if (m_state != state::is_running) return; - vector g_core; - for (auto c : core) { - expr_ref g_c(l2g(c), m); - g_core.push_back(expr_ref(l2g(c), m)); - } - m_search_tree.backtrack(node, g_core); - IF_VERBOSE(1, m_search_tree.display(verbose_stream() << bounded_pp_exprs(core) << "\n");); + vector g_core; + for (auto c : core) + g_core.push_back(expr_ref(l2g(c), m)); + + SASSERT(lease != nullptr || targets != nullptr); + bool did_backtrack = false; + + if (lease && !m_search_tree.is_lease_canceled(lease->leased_node, lease->cancel_epoch)) { + // we close/backtrack regardless of whether this lease is stale or not, as long as the lease isn't canceled + // i.e. worker 1 splits this node, but then worker 2 determines UNSAT --> worker 2 is stale but we still close this node and backtrack + did_backtrack = true; + IF_VERBOSE(1, verbose_stream() << "Batch manager backtracking.\n"); + release_lease_unlocked(worker_id, lease->leased_node); + m_search_tree.backtrack(lease->leased_node, g_core); + } + if (targets) { + for (auto const& target : *targets) { + if (m_search_tree.is_lease_canceled(target.leased_node, target.cancel_epoch)) + continue; + + did_backtrack = true; + IF_VERBOSE(1, verbose_stream() << "Batch manager backtracking external targets.\n"); + m_search_tree.backtrack(target.leased_node, g_core); + } + } + if (!did_backtrack) + return; + + // terminate on-demand the workers that are currently exploring the now-closed nodes + cancel_closed_leases_unlocked(worker_id); + + IF_VERBOSE(2, m_search_tree.display(verbose_stream() << bounded_pp_exprs(core) << "\n");); if (m_search_tree.is_closed()) { IF_VERBOSE(1, verbose_stream() << "Search tree closed, setting UNSAT\n"); m_state = state::is_unsat; @@ -338,26 +1409,46 @@ namespace smt { } } - void parallel::batch_manager::split(ast_translation &l2g, unsigned source_worker_id, - search_tree::node *node, expr *atom) { + void parallel::batch_manager::try_split(ast_translation &l2g, unsigned worker_id, + node_lease const &lease, expr *atom, unsigned effort) { std::scoped_lock lock(mux); + + if (m_state != state::is_running) + return; + + if (m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch)) + return; + expr_ref lit(m), nlit(m); lit = l2g(atom); nlit = mk_not(m, lit); - IF_VERBOSE(1, verbose_stream() << "Batch manager splitting on literal: " << mk_bounded_pp(lit, m, 3) << "\n"); - if (m_state != state::is_running) - return; - // optional heuristic: - // node->get_status() == status::active - // and depth is 'high' enough - // then ignore split, and instead set the status of node to open. - ++m_stats.m_num_cubes; - m_stats.m_max_cube_depth = std::max(m_stats.m_max_cube_depth, node->depth() + 1); - m_search_tree.split(node, lit, nlit); + bool did_split = m_search_tree.try_split(lease.leased_node, lease.cancel_epoch, lit, nlit, effort); + + release_lease_unlocked(worker_id, lease.leased_node); + + if (did_split) { + ++m_stats.m_num_cubes; + m_stats.m_max_cube_depth = std::max(m_stats.m_max_cube_depth, lease.leased_node->depth() + 1); + IF_VERBOSE(1, verbose_stream() << "Batch manager splitting on literal: " << mk_bounded_pp(lit, m, 3) << "\n"); + } + } + + void parallel::batch_manager::release_lease(unsigned worker_id, node_lease const &lease) { + std::scoped_lock lock(mux); + release_lease_unlocked(worker_id, lease.leased_node); + } + + bool parallel::batch_manager::lease_canceled(node_lease const &lease) { + std::scoped_lock lock(mux); + return m_state == state::is_running && m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch); } void parallel::batch_manager::collect_clause(ast_translation &l2g, unsigned source_worker_id, expr *clause) { std::scoped_lock lock(mux); + collect_clause_unlocked(l2g, source_worker_id, clause); + } + + void parallel::batch_manager::collect_clause_unlocked(ast_translation &l2g, unsigned source_worker_id, expr *clause) { expr *g_clause = l2g(clause); if (!shared_clause_set.contains(g_clause)) { shared_clause_set.insert(g_clause); @@ -375,6 +1466,137 @@ namespace smt { } } + void parallel::backbones_worker::collect_shared_clauses() { + expr_ref_vector new_clauses = b.return_shared_clauses(m_g2l, m_shared_clause_limit, UINT_MAX); + // iterate over new clauses and assert them in the local context + for (expr *e : new_clauses) { + ctx->assert_expr(e); + LOG_BB_WORKER(4, " asserting shared clause: " << mk_bounded_pp(e, m, 3) << "\n"); + } + } + + void parallel::core_minimizer_worker::collect_shared_clauses() { + expr_ref_vector new_clauses = b.return_shared_clauses(m_g2l, m_shared_clause_limit, UINT_MAX); + // iterate over new clauses and assert them in the local context + for (expr *e : new_clauses) { + ctx->assert_expr(e); + IF_VERBOSE(4, verbose_stream() << "Core minimizer asserting shared clause: " + << mk_bounded_pp(e, m, 3) << "\n";); + } + } + + void parallel::batch_manager::collect_backbone_candidates(ast_translation& l2g, bb_candidates& bb_candidates) { + std::scoped_lock lock(mux); + bool changed = false; + + auto find_existing_candidate_idx = [&](expr* e) -> int { + for (unsigned i = 0; i < m_bb_candidates.size(); ++i) { + if (m_bb_candidates[i].lit.get() == e) + return i; + } + return -1; + }; + + auto rank_of = [&](bb_candidate const& c) { + return c.age * std::log2(2.0 + c.hits); + }; + + for (auto const& c : bb_candidates) { + expr_ref g_lit(l2g(c.lit.get()), m); + if (is_global_backbone_or_negation_unlocked(l2g, c.lit)) + continue; + + double age = c.age; + int idx = find_existing_candidate_idx(g_lit.get()); + + if (idx >= 0) { + auto& existing = m_bb_candidates[idx]; + existing.age = (existing.age * existing.hits + age) / (existing.hits + 1); + existing.hits++; + continue; + } + + if (m_bb_candidates.size() < m_max_global_bb_candidates) { + m_bb_candidates.push_back(bb_candidate(m, g_lit.get(), age, 1)); + changed = true; + continue; + } + + bb_candidate new_bb_candidate = bb_candidate(m, g_lit.get(), age, 1); + auto worst_it = std::min_element( + m_bb_candidates.begin(), + m_bb_candidates.end(), + [&](bb_candidate const& a, bb_candidate const& b) { + return rank_of(a) < rank_of(b); + } + ); + if (worst_it != m_bb_candidates.end() && rank_of(new_bb_candidate) > rank_of(*worst_it)) { + *worst_it = new_bb_candidate; // replace worst candidate with new candidate + changed = true; + } + } + + if (changed && !m_bb_candidates.empty()) { + m_bb_candidate_epoch.fetch_add(1, std::memory_order_release); + std::sort( + m_bb_candidates.begin(), + m_bb_candidates.end(), + [&](bb_candidate const& a, bb_candidate const& b) { + return rank_of(a) < rank_of(b); // sort ascending so we can pop off the best candidates from the end in O(1) in the bb threads + } + ); + m_bb_cv.notify_all(); + } + } + + bool parallel::batch_manager::wait_for_backbone_job(unsigned bb_thread_id, ast_translation& g2l, bb_candidates& out, reslimit& lim) { + out.reset(); + std::unique_lock lock(mux); + + // ---- WAIT UNTIL: + // (a) a new batch is ready that this thread hasn't seen yet, OR + // (b) candidates are available AND the previous batch is finished (not in progress) + m_bb_cv.wait(lock, [&]() { + return lim.is_canceled() || + m_state != state::is_running || + m_bb_last_batch_processed[bb_thread_id] < m_bb_batch_id || + !m_bb_candidates.empty(); + }); + + if (lim.is_canceled()) + return false; + + if (m_state != state::is_running) + return false; + + // ---- NEED NEW BATCH? ---- + // Only create a new batch if this thread has already seen the current batch. + if (m_bb_last_batch_processed[bb_thread_id] == m_bb_batch_id) { + + // pop new batch once + unsigned n = std::min(m_bb_batch_size, m_bb_candidates.size()); + + m_bb_current_batch.reset(); + for (unsigned i = 0; i < n; ++i) { + m_bb_current_batch.push_back(m_bb_candidates.back()); + m_bb_candidates.pop_back(); + } + + m_bb_batch_id++; + + // wake all threads to see new batch + m_bb_cv.notify_all(); + } + + for (auto const& gc : m_bb_current_batch) { + expr_ref l_lit(g2l(gc.lit.get()), g2l.to()); + out.push_back(bb_candidate(g2l.to(), l_lit, gc.age, gc.hits)); + } + + m_bb_last_batch_processed[bb_thread_id] = m_bb_batch_id; + return true; + } + expr_ref_vector parallel::batch_manager::return_shared_clauses(ast_translation &g2l, unsigned &worker_limit, unsigned worker_id) { std::scoped_lock lock(mux); @@ -390,21 +1612,13 @@ namespace smt { lbool parallel::worker::check_cube(expr_ref_vector const &cube) { for (auto &atom : cube) asms.push_back(atom); - lbool r = l_undef; ctx->get_fparams().m_max_conflicts = std::min(m_config.m_threads_max_conflicts, m_config.m_max_conflicts); IF_VERBOSE(1, verbose_stream() << " Checking cube\n" << bounded_pp_exprs(cube) << "with max_conflicts: " << ctx->get_fparams().m_max_conflicts << "\n";); - try { - r = ctx->check(asms.size(), asms.data()); - } catch (z3_error &err) { - b.set_exception(err.error_code()); - } catch (z3_exception &ex) { - b.set_exception(ex.what()); - } catch (...) { - b.set_exception("unknown exception"); - } + lbool r = b.check(asms, *ctx); + asms.shrink(asms.size() - cube.size()); LOG_WORKER(1, " DONE checking cube " << r << "\n";); return r; @@ -422,10 +1636,20 @@ namespace smt { if (!e) continue; - double new_score = ctx->m_lit_scores[0][v] * ctx->m_lit_scores[1][v]; + // don't split on a backbone or its negation + if (m_config.m_global_backbones) { + if (b.is_global_backbone_or_negation(m_l2g, e)) + continue; + } - ctx->m_lit_scores[0][v] /= 2; - ctx->m_lit_scores[1][v] /= 2; + // Lightweight Proof Skeleton Approach + // double new_score = ctx->m_lit_scores[0][v] * ctx->m_lit_scores[1][v]; + + // ctx->m_lit_scores[0][v] /= 2; + // ctx->m_lit_scores[1][v] /= 2; + + // VSIDS Approach + double new_score = ctx->get_activity(v); if (new_score > score || !result || (new_score == score && m_rand(++n) == 0)) { score = new_score; @@ -500,9 +1724,10 @@ namespace smt { } } - bool parallel::batch_manager::get_cube(ast_translation &g2l, unsigned id, expr_ref_vector &cube, node *&n) { + bool parallel::batch_manager::get_cube(ast_translation &g2l, unsigned id, expr_ref_vector &cube, bool is_first_run, node_lease &lease) { + std::scoped_lock lock(mux); cube.reset(); - std::unique_lock lock(mux); + if (m_search_tree.is_closed()) { IF_VERBOSE(1, verbose_stream() << "all done\n";); return false; @@ -511,13 +1736,20 @@ namespace smt { IF_VERBOSE(1, verbose_stream() << "aborting get_cube\n";); return false; } - node *t = m_search_tree.activate_node(n); - if (!t) - t = m_search_tree.find_active_node(); + + node *t = is_first_run ? m_search_tree.activate_root() : m_search_tree.activate_best_node(); + if (!t) return false; - IF_VERBOSE(1, m_search_tree.display(verbose_stream()); verbose_stream() << "\n";); - n = t; + + IF_VERBOSE(2, m_search_tree.display(verbose_stream()); verbose_stream() << "\n";); + + lease.leased_node = t; + lease.cancel_epoch = t->get_cancel_epoch(); + if (id >= m_worker_leases.size()) + m_worker_leases.resize(id + 1); + m_worker_leases[id] = lease; + while (t) { if (cube_config::literal_is_null(t->get_literal())) break; @@ -526,21 +1758,58 @@ namespace smt { cube.push_back(std::move(lit)); t = t->parent(); } + return true; } - void parallel::batch_manager::initialize() { + void parallel::batch_manager::initialize(unsigned num_global_bb_threads, unsigned initial_max_thread_conflicts) { m_state = state::is_running; + + m_num_global_bb_threads = num_global_bb_threads; + m_bb_last_batch_processed.reset(); + m_bb_last_batch_processed.resize(m_num_global_bb_threads); + m_bb_candidates.reset(); + m_global_backbones.reset(); + m_bb_candidate_epoch.store(0, std::memory_order_release); + m_core_min_jobs.reset(); + m_search_tree.reset(); + m_search_tree.set_effort_unit(initial_max_thread_conflicts); + + m_worker_leases.reset(); + m_worker_leases.resize(p.m_workers.size()); + + smt_parallel_params pp(p.ctx.m_params); + m_ablate_backtracking = pp.ablate_backtracking(); } void parallel::batch_manager::collect_statistics(::statistics &st) const { st.update("parallel-num_cubes", m_stats.m_num_cubes); st.update("parallel-max-cube-size", m_stats.m_max_cube_depth); + st.update("bb-backbones-found", m_stats.m_backbones_found); + st.update("parallel-core-min-jobs-enqueued", m_stats.m_core_min_jobs_enqueued); + st.update("parallel-core-min-jobs-published", m_stats.m_core_min_jobs_published); + st.update("parallel-core-min-jobs-skipped", m_stats.m_core_min_jobs_skipped); + st.update("parallel-core-min-global-unsat", m_stats.m_core_min_global_unsat); } lbool parallel::operator()(expr_ref_vector const &asms) { - IF_VERBOSE(1, verbose_stream() << "Parallel SMT with " << num_threads << " threads\n";); + smt_parallel_params pp(ctx.m_params); + unsigned num_global_bb_batch_threads = pp.num_global_bb_batch_threads(); + if (num_global_bb_batch_threads > 2) + throw default_exception("smt_parallel.num_global_bb_batch_threads must be 0, 1, or 2"); + unsigned num_workers = std::min((unsigned)std::thread::hardware_concurrency(), ctx.get_fparams().m_threads); + unsigned num_sls_threads = (pp.sls() ? 1 : 0); + unsigned num_core_min_threads = (pp.core_minimize() ? 1 : 0); + unsigned num_global_bb_fl_threads = pp.num_global_bb_fl_threads(); + if (num_global_bb_fl_threads > 2) + throw default_exception("smt_parallel.num_global_bb_fl_threads must be 0, 1, or 2"); + if (num_global_bb_fl_threads > 0 && num_global_bb_batch_threads > 0) + throw default_exception("smt_parallel.num_global_bb_fl_threads and smt_parallel.num_global_bb_batch_threads cannot both be enabled"); + unsigned num_global_bb_threads = num_global_bb_fl_threads > 0 ? num_global_bb_fl_threads : num_global_bb_batch_threads; + unsigned total_threads = num_workers + num_sls_threads + num_core_min_threads + num_global_bb_threads; + + IF_VERBOSE(1, verbose_stream() << "Parallel SMT with " << total_threads << " threads\n";); ast_manager &m = ctx.m; if (m.has_trace_stream()) @@ -552,37 +1821,54 @@ namespace smt { ~scoped_clear() { p.m_workers.reset(); p.m_sls_worker = nullptr; + p.m_core_minimizer_worker = nullptr; + p.m_global_backbones_workers.reset(); } }; scoped_clear clear(*this); - m_batch_manager.initialize(); m_workers.reset(); - - smt_parallel_params pp(ctx.m_params); - m_should_run_sls = pp.sls(); - + m_core_minimizer_worker = nullptr; scoped_limits sl(m.limit()); flet _nt(ctx.m_fparams.m_threads, 1); - SASSERT(num_threads > 1); - for (unsigned i = 0; i < num_threads; ++i) + SASSERT(num_workers > 1); + for (unsigned i = 0; i < num_workers; ++i) m_workers.push_back(alloc(worker, i, *this, asms)); for (auto w : m_workers) sl.push_child(&(w->limit())); - if (m_should_run_sls) { + + if (num_sls_threads == 1) { m_sls_worker = alloc(sls_worker, *this); sl.push_child(&(m_sls_worker->limit())); } + if (pp.core_minimize()) { + m_core_minimizer_worker = alloc(core_minimizer_worker, *this, asms); + sl.push_child(&(m_core_minimizer_worker->limit())); + } + for (unsigned i = 0; i < num_global_bb_threads; ++i) { + auto *w = alloc(backbones_worker, i, *this, asms); + m_global_backbones_workers.push_back(w); + sl.push_child(&(w->limit())); + } + IF_VERBOSE(1, verbose_stream() << "Launched " << m_workers.size() << " CDCL threads, " + << (m_sls_worker ? 1 : 0) << " SLS threads, " + << (m_core_minimizer_worker ? 1 : 0) << " core minimizer threads, " + << m_global_backbones_workers.size() << " global backbone threads.\n";); - // Launch threads - vector threads; - threads.resize(m_should_run_sls ? num_threads + 1 : num_threads); // +1 for sls worker - for (unsigned i = 0; i < num_threads; ++i) - threads[i] = std::thread([&, i]() { m_workers[i]->run(); }); + m_batch_manager.initialize(num_global_bb_threads); - // the final thread runs the sls worker - if (m_should_run_sls) - threads[num_threads] = std::thread([&]() { m_sls_worker->run(); }); + // Launch threads + vector threads(total_threads); + unsigned thread_idx = 0; + for (auto* w : m_workers) + threads[thread_idx++] = std::thread([&, w]() { w->run(); }); + if (m_sls_worker) + threads[thread_idx++] = std::thread([&]() { m_sls_worker->run(); }); + if (m_core_minimizer_worker) + threads[thread_idx++] = std::thread([&]() { m_core_minimizer_worker->run(); }); + for (auto* w : m_global_backbones_workers) + threads[thread_idx++] = std::thread([&, w]() { w->run(); }); + // Wait for all threads to finish for (auto &th : threads) @@ -591,8 +1877,12 @@ namespace smt { for (auto w : m_workers) w->collect_statistics(ctx.m_aux_stats); m_batch_manager.collect_statistics(ctx.m_aux_stats); - if (m_should_run_sls) + if (m_sls_worker) m_sls_worker->collect_statistics(ctx.m_aux_stats); + if (m_core_minimizer_worker) + m_core_minimizer_worker->collect_statistics(ctx.m_aux_stats); + for (auto* bb_w : m_global_backbones_workers) + bb_w->collect_statistics(ctx.m_aux_stats); return m_batch_manager.get_result(); } diff --git a/src/smt/smt_parallel.h b/src/smt/smt_parallel.h index a9c751aa0..64fcc1186 100644 --- a/src/smt/smt_parallel.h +++ b/src/smt/smt_parallel.h @@ -21,8 +21,10 @@ Revision History: #include "smt/smt_context.h" #include "util/search_tree.h" #include "ast/sls/sls_smt_solver.h" +#include #include #include +#include namespace smt { @@ -35,14 +37,37 @@ namespace smt { class parallel { context& ctx; - unsigned num_threads; - bool m_should_run_sls = false; + class core_minimizer_worker; + using node = search_tree::node; struct shared_clause { unsigned source_worker_id; expr_ref clause; }; + struct bb_candidate { + expr_ref lit; + double age; + unsigned hits; // how many cubes reported it + bb_candidate(ast_manager& m, expr* e, double s, unsigned h) : lit(e, m), age(s), hits(h) {} + }; + + using bb_candidates = vector; + + struct node_lease { + node* leased_node = nullptr; + + // Cancellation generation counter for this node/subtree. + // Incremented when the node is closed; used to signal that all + // workers holding leases on this node (or its descendants) + // must abandon work immediately. + unsigned cancel_epoch = 0; + + // Guards against multiple inc_cancel() calls for the same lease. + // Set when cancel_lease() is signaled; cleared when a new lease is assigned. + bool cancel_signaled = false; + }; + class batch_manager { enum state { @@ -56,22 +81,50 @@ namespace smt { struct stats { unsigned m_max_cube_depth = 0; unsigned m_num_cubes = 0; + unsigned m_backbones_found = 0; + unsigned m_core_min_jobs_enqueued = 0; + unsigned m_core_min_jobs_published = 0; + unsigned m_core_min_jobs_skipped = 0; + unsigned m_core_min_global_unsat = 0; + }; + struct core_min_job { + node* source = nullptr; + expr_ref_vector core; + core_min_job(ast_manager& m, node* source) : source(source), core(m) {} }; - - ast_manager& m; parallel& p; std::mutex mux; state m_state = state::is_running; stats m_stats; - using node = search_tree::node; search_tree::tree m_search_tree; + vector m_worker_leases; unsigned m_exception_code = 0; std::string m_exception_msg; vector shared_clause_trail; // store all shared clauses with worker IDs obj_hashtable shared_clause_set; // for duplicate filtering on per-thread clause expressions + bb_candidates m_bb_candidates; + unsigned m_max_global_bb_candidates = 100; + unsigned m_bb_batch_size = 150; + obj_hashtable m_global_backbones; + std::atomic m_bb_candidate_epoch = 0; + + // Backbone job queue + std::condition_variable m_bb_cv; + bb_candidates m_bb_current_batch; + unsigned m_bb_batch_id = 0; + unsigned m_num_global_bb_threads = 0; + unsigned_vector m_bb_last_batch_processed; + unsigned m_bb_cancel_epoch = 0; // When a backbone worker finishes early, it increments m_bb_cancel_epoch and notifies all + + // Core minimization job queue + std::condition_variable m_core_min_cv; + scoped_ptr_vector m_core_min_jobs; + + bool m_ablate_backtracking = false; + // called from batch manager to cancel other workers if we've reached a verdict void cancel_workers() { IF_VERBOSE(1, verbose_stream() << "Canceling workers\n"); @@ -86,17 +139,51 @@ namespace smt { p.m_sls_worker->cancel(); } - void cancel_background_threads() { - cancel_workers(); - cancel_sls_worker(); + void cancel_backbones_worker() { + IF_VERBOSE(1, verbose_stream() << "Canceling backbones workers\n"); + for (auto* w : p.m_global_backbones_workers) + w->cancel(); } - void init_parameters_state(); + void cancel_background_threads() { + cancel_workers(); + cancel_sls_worker(); + if (!p.m_global_backbones_workers.empty()) { + cancel_backbones_worker(); + m_bb_cv.notify_all(); + } + if (p.m_core_minimizer_worker) { + p.m_core_minimizer_worker->cancel(); + m_core_min_cv.notify_all(); + } + } + + // to avoid deadlock + bool is_global_backbone_unlocked(ast_translation& l2g, expr* bb_cand) { + expr_ref cand(l2g(bb_cand), l2g.to()); + return m_global_backbones.contains(cand.get()); + } + + bool is_global_backbone_or_negation_unlocked(ast_translation& l2g, expr* bb_cand) { + expr_ref cand(l2g(bb_cand), l2g.to()); + expr_ref neg_cand(mk_not(l2g.to(), cand), l2g.to()); + return m_global_backbones.contains(cand.get()) || m_global_backbones.contains(neg_cand.get()); + } + + void backtrack_unlocked(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, + node_lease const* lease = nullptr, vector const* targets = nullptr); + void collect_clause_unlocked(ast_translation &l2g, unsigned source_worker_id, expr *clause); + void release_lease_unlocked(unsigned worker_id, node* n); + void cancel_closed_leases_unlocked(unsigned source_worker_id); + void collect_matching_targets_unlocked(node* source, expr* lit, vector const& core, + vector& targets); + node* find_core_source_unlocked(ast_translation& l2g, node* source, expr_ref_vector const& core); + unsigned select_best_core_min_job_unlocked() const; public: batch_manager(ast_manager& m, parallel& p) : m(m), p(p), m_search_tree(expr_ref(m)) { } - void initialize(); + void initialize(unsigned num_global_bb_threads, unsigned initial_max_thread_conflicts = 1000); // TODO: pass in from worker config void set_unsat(ast_translation& l2g, expr_ref_vector const& unsat_core); void set_sat(ast_translation& l2g, model& m); @@ -104,14 +191,57 @@ namespace smt { void set_exception(unsigned error_code); void collect_statistics(::statistics& st) const; - bool get_cube(ast_translation& g2l, unsigned id, expr_ref_vector& cube, node*& n); - void backtrack(ast_translation& l2g, expr_ref_vector const& core, node* n); - void split(ast_translation& l2g, unsigned id, node* n, expr* atom); + void collect_backbone_candidates(ast_translation& l2g, bb_candidates& bb_candidates); + void collect_backbone_evidence(ast_translation& l2g, expr* lit, double delta); + bool collect_global_backbone(ast_translation& l2g, expr_ref const& backbone, unsigned source_worker_id = UINT_MAX); + bool wait_for_backbone_job(unsigned bb_thread_id, ast_translation& g2l, vector& out, reslimit& lim); + bool has_new_backbone_candidates(unsigned epoch) { + return m_bb_candidate_epoch.load(std::memory_order_acquire) != epoch; + } + unsigned get_bb_candidate_epoch() const { + return m_bb_candidate_epoch.load(std::memory_order_acquire); + } + expr_ref_vector get_global_backbones_snapshot(ast_translation& g2l) { + std::scoped_lock lock(mux); + expr_ref_vector snapshot(g2l.to()); + for (expr* gb : m_global_backbones) + snapshot.push_back(g2l(gb)); + return snapshot; + } + + bool get_cube(ast_translation& g2l, unsigned id, expr_ref_vector& cube, bool is_first_run, node_lease& lease); + void backtrack(ast_translation& l2g, unsigned worker_id, expr_ref_vector const& core, node_lease const& lease); + void enqueue_core_minimization(ast_translation& l2g, node* source, expr_ref_vector const& core); + bool wait_for_core_min_job(ast_translation& g2l, node*& source, + expr_ref_vector& core, reslimit& lim); + void publish_minimized_core(ast_translation& l2g, expr_ref_vector const& asms, node* source, + unsigned original_core_size, expr_ref_vector const& minimized_core); + void try_split(ast_translation& l2g, unsigned worker_id, node_lease const& lease, expr* atom, unsigned effort); + void release_lease(unsigned worker_id, node_lease const& lease); + bool lease_canceled(node_lease const& lease); void collect_clause(ast_translation& l2g, unsigned source_worker_id, expr* clause); expr_ref_vector return_shared_clauses(ast_translation& g2l, unsigned& worker_limit, unsigned worker_id); lbool get_result() const; + + bool is_global_backbone_or_negation(ast_translation& l2g, expr* bb_cand) { + std::scoped_lock lock(mux); + return is_global_backbone_or_negation_unlocked(l2g, bb_cand); + } + + void cancel_current_backbone_batch() { + std::scoped_lock lock(mux); + m_bb_cancel_epoch++; + m_bb_cv.notify_all(); + } + + unsigned get_cancel_epoch() { + std::scoped_lock lock(mux); + return m_bb_cancel_epoch; + } + + lbool check(expr_ref_vector const &asms, context &ctx); }; class worker { @@ -123,14 +253,16 @@ namespace smt { bool m_share_units_initial_only = true; double m_max_conflict_mul = 1.5; bool m_inprocessing = false; + bool m_global_backbones = false; + bool m_local_backbones = false; bool m_sls = false; unsigned m_inprocessing_delay = 1; unsigned m_max_cube_depth = 20; unsigned m_max_conflicts = UINT_MAX; + bool m_core_minimize = false; + bool m_ablate_backtracking = false; }; - using node = search_tree::node; - unsigned id; // unique identifier for the worker parallel& p; batch_manager& b; @@ -141,8 +273,9 @@ namespace smt { random_gen m_rand; scoped_ptr ctx; ast_translation m_g2l, m_l2g; + uint_set m_known_units; - unsigned m_num_shared_units = 0; + unsigned m_shared_units_prefix = 0; unsigned m_num_initial_atoms = 0; unsigned m_shared_clause_limit = 0; // remembers the index into shared_clause_trail marking the boundary between "old" and "new" clauses to share @@ -152,10 +285,12 @@ namespace smt { void share_units(); void update_max_thread_conflicts() { + // allow for backoff scheme of conflicts within the thread for cube timeouts. m_config.m_threads_max_conflicts = (unsigned)(m_config.m_max_conflict_mul * m_config.m_threads_max_conflicts); - } // allow for backoff scheme of conflicts within the thread for cube timeouts. + } void simplify(); + bb_candidates find_backbone_candidates(unsigned k = 10); public: worker(unsigned id, parallel& p, expr_ref_vector const& _asms); @@ -164,6 +299,7 @@ namespace smt { void collect_shared_clauses(); void cancel(); + void cancel_lease(); void collect_statistics(::statistics& st) const; reslimit& limit() { @@ -191,16 +327,93 @@ namespace smt { } }; + class core_minimizer_worker { + batch_manager &b; + ast_manager m; + expr_ref_vector asms; + smt_params m_smt_params; + scoped_ptr ctx; + ast_translation m_g2l, m_l2g; + + unsigned m_num_core_minimize_calls = 0; + unsigned m_num_core_minimize_undef = 0; + unsigned m_num_core_minimize_refined = 0; + unsigned m_num_core_minimize_lits_removed = 0; + unsigned m_num_core_minimize_found_sat = 0; + unsigned m_core_minimize_conflict_budget = 5000; + unsigned m_shared_clause_limit = 0; + + void minimize_unsat_core(expr_ref_vector& core); + void collect_shared_clauses(); + + public: + core_minimizer_worker(parallel& p, expr_ref_vector const& _asms); + void run(); + void cancel(); + void collect_statistics(::statistics& st) const; + reslimit& limit() { return m.limit(); } + }; + + class backbones_worker { + struct stats { + unsigned m_batches_total = 0; + unsigned m_candidates_total = 0; + unsigned m_singleton_backbones = 0; + unsigned m_backbones_detected = 0; + unsigned m_internal_backbones_found = 0; + unsigned m_retry_backbones_found = 0; + unsigned m_bb_retries = 0; + unsigned m_fallback_singleton_checks = 0; + unsigned m_fallback_reason_chunk_exhausted = 0; + unsigned m_fallback_reason_undef = 0; + unsigned m_core_refinement_rounds = 0; + unsigned m_lits_removed_by_core = 0; + unsigned m_num_chunk_increases = 0; + }; + + enum bb_mode { + bb_negated, + bb_positive + }; + + unsigned id; // unique identifier for the worker + batch_manager& b; + ast_manager m; + expr_ref_vector asms; + smt_params m_smt_params; + scoped_ptr ctx; + ast_translation m_g2l, m_l2g; + unsigned m_bb_chunk_size = 20; + unsigned m_bb_conflicts_per_chunk = 1000; + uint_set m_known_units; + bool m_use_failed_literal_test; + stats m_stats; + bb_mode m_mode; + unsigned m_shared_clause_limit = 0; // remembers the index into shared_clause_trail marking the boundary between "old" and "new" clauses to share + unsigned m_shared_units_prefix = 0; + unsigned m_num_initial_atoms = 0; + bool try_get_unit_backbone(expr* candidate, expr_ref& backbone); + void run_batch_mode(); + void run_failed_literal_mode(); + lbool probe_literal(bool_var v, expr *e, bool is_retry); + public: + backbones_worker(unsigned id, parallel &p, expr_ref_vector const &_asms); + void cancel(); + void collect_statistics(::statistics& st) const; + void run(); + void collect_shared_clauses(); + reslimit &limit() { return m.limit(); } + }; + batch_manager m_batch_manager; scoped_ptr_vector m_workers; scoped_ptr m_sls_worker; + scoped_ptr m_core_minimizer_worker; + scoped_ptr_vector m_global_backbones_workers; public: parallel(context& ctx) : ctx(ctx), - num_threads(std::min( - (unsigned)std::thread::hardware_concurrency(), - ctx.get_fparams().m_threads)), m_batch_manager(ctx.m, *this) {} lbool operator()(expr_ref_vector const& asms); diff --git a/src/smt/smt_quantifier.cpp b/src/smt/smt_quantifier.cpp index 9cd270f1d..19953be75 100644 --- a/src/smt/smt_quantifier.cpp +++ b/src/smt/smt_quantifier.cpp @@ -19,6 +19,8 @@ Revision History: #include "ast/ast_pp.h" #include "ast/ast_ll_pp.h" #include "ast/quantifier_stat.h" +#include "ast/euf/ho_matcher.h" +#include "ast/rewriter/var_subst.h" #include "smt/smt_quantifier.h" #include "smt/smt_context.h" #include "smt/smt_model_finder.h" @@ -154,7 +156,8 @@ namespace smt { } unsigned get_generation(quantifier * q) const { - return get_stat(q)->get_generation(); + auto* s = m_quantifier_stat.find_core(q); + return s ? s->get_data().get_value()->get_generation() : 0; } void add(quantifier * q, unsigned generation) { @@ -289,16 +292,24 @@ namespace smt { bool add_instance(quantifier * q, app * pat, unsigned num_bindings, enode * const * bindings, - expr* def, unsigned max_generation, unsigned min_top_generation, unsigned max_top_generation, vector> & used_enodes) { + // Try higher-order refinement first + if (pat && m_plugin->refine_instance(q, pat, num_bindings, bindings, max_generation, min_top_generation, max_top_generation, used_enodes)) + return true; + + if (!m_quantifier_stat.contains(q)) { + IF_VERBOSE(2, verbose_stream() << "add_instance: quantifier not in stat map: " << mk_pp(q, m()) << "\n"); + return false; + } + max_generation = std::max(max_generation, get_generation(q)); get_stat(q)->update_max_generation(max_generation); - fingerprint * f = m_context.add_fingerprint(q, q->get_id(), num_bindings, bindings, def); + fingerprint * f = m_context.add_fingerprint(q, q->get_id(), num_bindings, bindings); if (f) { if (is_trace_enabled(TraceTag::causality)) { log_causality(f,pat,used_enodes); @@ -472,17 +483,17 @@ namespace smt { bool quantifier_manager::add_instance(quantifier * q, app * pat, unsigned num_bindings, enode * const * bindings, - expr* def, unsigned max_generation, unsigned min_top_generation, unsigned max_top_generation, vector> & used_enodes) { - return m_imp->add_instance(q, pat, num_bindings, bindings, def, max_generation, min_top_generation, max_generation, used_enodes); + return m_imp->add_instance(q, pat, num_bindings, bindings, max_generation, min_top_generation, max_top_generation, used_enodes); } - bool quantifier_manager::add_instance(quantifier * q, unsigned num_bindings, enode * const * bindings, expr* def, unsigned generation) { + bool quantifier_manager::add_instance(quantifier * q, unsigned num_bindings, enode * const * bindings, unsigned generation) { vector> tmp; - return add_instance(q, nullptr, num_bindings, bindings, def, generation, generation, generation, tmp); + return add_instance(q, nullptr, num_bindings, bindings, + generation, generation, generation, tmp); } void quantifier_manager::init_search_eh() { @@ -599,9 +610,23 @@ namespace smt { scoped_ptr m_lazy_mam; scoped_ptr m_model_finder; scoped_ptr m_model_checker; + scoped_ptr m_ho_matcher; unsigned m_new_enode_qhead; unsigned m_lazy_matching_idx; bool m_active; + + // State for higher-order match refinement callback + struct ho_match_state { + quantifier* m_q = nullptr; + app* m_pat = nullptr; + unsigned m_num_bindings = 0; + enode* const* m_bindings = nullptr; + unsigned m_max_generation = 0; + unsigned m_min_top_generation = 0; + unsigned m_max_top_generation = 0; + vector>* m_used_enodes = nullptr; + }; + ho_match_state m_ho_state; public: default_qm_plugin(): m_qm(nullptr), @@ -625,10 +650,110 @@ namespace smt { m_model_finder->set_context(m_context); m_model_checker->set_qm(qm); + + if (m_fparams->m_ho_matching) { + m_ho_matcher = alloc(euf::ho_matcher, m, m_context->get_trail_stack()); + std::function on_match = [&](euf::ho_subst& s) { + on_ho_match(s); + }; + m_ho_matcher->set_on_match(on_match); + } } quantifier_manager_plugin * mk_fresh() override { return alloc(default_qm_plugin); } + void on_ho_match(euf::ho_subst& s) { + ast_manager& m = m_context->get_manager(); + auto& st = m_ho_state; + auto* hoq = st.m_q; + auto* q = m_ho_matcher->hoq2q(hoq); + + expr_ref_vector binding(m); + for (unsigned i = 0; i < s.size(); ++i) + binding.push_back(s.get(i)); + + // Shrink binding to original quantifier's num_decls + // The HO quantifier has extra vars at higher indices; drop them. + // Binding is indexed by var index: binding[i] = value for var i. + // First substitute any remaining vars, then keep only original vars. + TRACE(ho_matching, tout << "num bound variables " << q->get_num_decls() << " for " << mk_bounded_pp(q, m) + << "\n" + << binding << "\n";); + if (binding.size() > q->get_num_decls()) { + var_subst sub(m); + bool change = true; + while (change) { + change = false; + for (unsigned i = 1; i < binding.size(); ++i) { + if (!binding.get(i)) continue; + auto r = sub(binding.get(i), binding); + change |= r != binding.get(i); + binding[i] = r; + } + } + binding.shrink(q->get_num_decls()); + } + if (binding.size() < q->get_num_decls()) + return; + + binding.reverse(); + + // Create enodes for the refined bindings and add instance + ptr_buffer new_bindings; + unsigned max_gen = st.m_max_generation; + for (expr* e : binding) { + if (!e) + return; // incomplete binding + if (!m_context->e_internalized(e)) { + m_context->internalize(e, false); + } + enode* n = m_context->get_enode(e); + new_bindings.push_back(n); + if (n->get_generation() > max_gen) + max_gen = n->get_generation(); + } + + TRACE(ho_matching, + tout << "ho_match refined for " << mk_pp(q, m) << "\n"; + for (unsigned i = 0; i < new_bindings.size(); ++i) + tout << " binding[" << i << "] = " << mk_bounded_pp(new_bindings[i]->get_expr(), m) << "\n";); + + vector> used_enodes; + m_context->add_instance(q, nullptr, new_bindings.size(), new_bindings.data(), + max_gen, st.m_min_top_generation, st.m_max_top_generation, used_enodes); + } + + bool try_ho_refine(quantifier* qa, app* pat, unsigned num_bindings, enode* const* bindings, + unsigned max_generation, unsigned min_top_gen, unsigned max_top_gen, + vector>& used_enodes) { + if (!m_ho_matcher || !m_ho_matcher->is_ho_pattern(pat)) + return false; + + ast_manager& m = m_context->get_manager(); + expr_ref_vector s(m); + // With var_subst(std_order=true): var idx maps to s[s.size()-idx-1] + // SMT MAM bindings: bindings[i] = var at index (num_bindings-1-i) + // So bindings[i] corresponds to s[i] with std_order + for (unsigned i = 0; i < num_bindings; ++i) + s.push_back(bindings[i]->get_expr()); + + m_ho_state.m_q = qa; + m_ho_state.m_pat = pat; + m_ho_state.m_num_bindings = num_bindings; + m_ho_state.m_bindings = bindings; + m_ho_state.m_max_generation = max_generation; + m_ho_state.m_min_top_generation = min_top_gen; + m_ho_state.m_max_top_generation = max_top_gen; + m_ho_state.m_used_enodes = &used_enodes; + + IF_VERBOSE(10, verbose_stream() << "try_ho_refine: q=" << mk_pp(qa, m) << "\n pat=" << mk_pp(pat, m) << "\n"; + for (unsigned i = 0; i < num_bindings; ++i) + verbose_stream() << " s[" << i << "] = " << mk_pp(s.get(i), m) << " sort=" << mk_pp(s.get(i)->get_sort(), m) << "\n";); + + m_ho_matcher->refine_ho_match(pat, s); + return true; + } + bool model_based() const override { return m_fparams->m_mbqi; } bool mbqi_enabled(quantifier *q) const override { @@ -656,13 +781,13 @@ namespace smt { void push() override { m_mam->push_scope(); m_lazy_mam->push_scope(); - m_model_finder->push_scope(); + m_model_finder->push_scope(); } void pop(unsigned num_scopes) override { m_mam->pop_scope(num_scopes); m_lazy_mam->pop_scope(num_scopes); - m_model_finder->pop_scope(num_scopes); + m_model_finder->pop_scope(num_scopes); } void init_search_eh() override { @@ -704,6 +829,19 @@ namespace smt { TRACE(quantifier, tout << "adding:\n" << expr_ref(mp, m) << "\n";); m_mam->add_pattern(q, mp); } + // Compile HO pattern and also register the compiled version with MAM + if (m_ho_matcher) { + auto [q1, p1] = m_ho_matcher->compile_ho_pattern(q, mp); + IF_VERBOSE(10, verbose_stream() << "ho_matching: q=" << q->get_qid() + << " compiled=" << (p1 != mp) + << " p1=" << mk_pp(p1, m) << "\n"); + if (p1 != mp) { + if (!unary && j >= num_eager_multi_patterns) + m_lazy_mam->add_pattern(q1, p1); + else + m_mam->add_pattern(q1, p1); + } + } if (!unary) j++; } @@ -713,6 +851,13 @@ namespace smt { return m_fparams->m_ematching && !m_qm->empty(); } + + bool refine_instance(quantifier* q, app* pat, unsigned num_bindings, enode* const* bindings, + unsigned max_generation, unsigned min_top_generation, unsigned max_top_generation, + vector>& used_enodes) override { + return try_ho_refine(q, pat, num_bindings, bindings, max_generation, min_top_generation, max_top_generation, used_enodes); + } + void add_eq_eh(enode * e1, enode * e2) override { if (use_ematching()) m_mam->add_eq_eh(e1, e2); @@ -726,7 +871,9 @@ namespace smt { } bool can_propagate() const override { - return m_active && m_mam->has_work(); + bool r = m_active && m_mam->has_work(); + IF_VERBOSE(11, if (r) verbose_stream() << "ho_matching: can_propagate=true\n"); + return r; } void restart_eh() override { @@ -750,6 +897,7 @@ namespace smt { void propagate() override { if (!m_active) return; + IF_VERBOSE(10, verbose_stream() << "ho_matching: propagate(), mam.has_work=" << m_mam->has_work() << "\n"); m_mam->match(); if (!m_context->relevancy() && use_ematching()) { ptr_vector::const_iterator it = m_context->begin_enodes(); diff --git a/src/smt/smt_quantifier.h b/src/smt/smt_quantifier.h index 981647606..6d9a44822 100644 --- a/src/smt/smt_quantifier.h +++ b/src/smt/smt_quantifier.h @@ -60,12 +60,11 @@ namespace smt { bool add_instance(quantifier * q, app * pat, unsigned num_bindings, enode * const * bindings, - expr* def, unsigned max_generation, unsigned min_top_generation, unsigned max_top_generation, vector> & used_enodes /*gives the equalities used for the pattern match, see mam.cpp for more info*/); - bool add_instance(quantifier * q, unsigned num_bindings, enode * const * bindings, expr* def, unsigned generation = 0); + bool add_instance(quantifier * q, unsigned num_bindings, enode * const * bindings, unsigned generation = 0); void init_search_eh(); void assign_eh(quantifier * q); @@ -178,8 +177,14 @@ namespace smt { virtual void push() = 0; virtual void pop(unsigned num_scopes) = 0; - + /** + \brief Try to refine a match using higher-order matching. + Returns true if the pattern was an HO pattern and refinement was attempted. + In that case, the plugin handles adding instances via the refined bindings. + */ + virtual bool refine_instance(quantifier* q, app* pat, unsigned num_bindings, enode* const* bindings, + unsigned max_generation, unsigned min_top_generation, unsigned max_top_generation, + vector>& used_enodes) { return false; } }; }; - diff --git a/src/smt/smt_quick_checker.cpp b/src/smt/smt_quick_checker.cpp index c1b3a7a37..f267cb481 100644 --- a/src/smt/smt_quick_checker.cpp +++ b/src/smt/smt_quick_checker.cpp @@ -236,7 +236,7 @@ namespace smt { TRACE(quick_checker_sizes, tout << "found new candidate\n"; for (unsigned i = 0; i < m_num_bindings; ++i) tout << "#" << m_bindings[i]->get_owner_id() << " "; tout << "\n";); unsigned max_generation = get_max_generation(m_num_bindings, m_bindings.data()); - if (m_context.add_instance(q, nullptr /* no pattern was used */, m_num_bindings, m_bindings.data(), nullptr, + if (m_context.add_instance(q, nullptr /* no pattern was used */, m_num_bindings, m_bindings.data(), max_generation, 0, // min_top_generation is only available for instances created by the MAM 0, // max_top_generation is only available for instances created by the MAM diff --git a/src/smt/smt_statistics.h b/src/smt/smt_statistics.h index ce773864a..11f7612e6 100644 --- a/src/smt/smt_statistics.h +++ b/src/smt/smt_statistics.h @@ -45,6 +45,7 @@ namespace smt { unsigned m_num_checks; unsigned m_num_simplifications; unsigned m_num_del_clauses; + unsigned m_num_assignments; statistics() { reset(); } diff --git a/src/smt/smt_theory.cpp b/src/smt/smt_theory.cpp index 4b3a53baf..be32edacb 100644 --- a/src/smt/smt_theory.cpp +++ b/src/smt/smt_theory.cpp @@ -204,7 +204,7 @@ namespace smt { log_axiom_instantiation(mk_or(fmls)); } - void theory::log_axiom_instantiation(app * r, unsigned axiom_id, unsigned num_bindings, app * const * bindings, unsigned pattern_id, const vector> & used_enodes) { + void theory::log_axiom_instantiation(app * r, unsigned axiom_id, unsigned num_bindings, expr * const * bindings, unsigned pattern_id, const vector> & used_enodes) { ast_manager & m = get_manager(); SASSERT(r->get_ref_count() > 0); std::ostream& out = m.trace_stream(); diff --git a/src/smt/smt_theory.h b/src/smt/smt_theory.h index abd8ae798..70b5556d6 100644 --- a/src/smt/smt_theory.h +++ b/src/smt/smt_theory.h @@ -259,7 +259,7 @@ namespace smt { \brief This method is invoked when the theory application n is marked as relevant. */ - virtual void relevant_eh(app * n) { + virtual void relevant_eh(expr * n) { } /** @@ -435,7 +435,7 @@ namespace smt { return m_var2enode[v]; } - app * get_expr(theory_var v) const { + expr * get_expr(theory_var v) const { return get_enode(v)->get_expr(); } @@ -482,11 +482,11 @@ namespace smt { protected: void log_axiom_instantiation(app * r, unsigned axiom_id = UINT_MAX, unsigned num_bindings = 0, - app * const * bindings = nullptr, unsigned pattern_id = UINT_MAX, + expr * const * bindings = nullptr, unsigned pattern_id = UINT_MAX, const vector> & used_enodes = vector>()); void log_axiom_instantiation(expr * r, unsigned axiom_id = UINT_MAX, unsigned num_bindings = 0, - app * const * bindings = nullptr, unsigned pattern_id = UINT_MAX, + expr * const * bindings = nullptr, unsigned pattern_id = UINT_MAX, const vector> & used_enodes = vector>()) { log_axiom_instantiation(to_app(r), axiom_id, num_bindings, bindings, pattern_id, used_enodes); } diff --git a/src/smt/tactic/smt_tactic_core.cpp b/src/smt/tactic/smt_tactic_core.cpp index 69d38a35b..6a624b59a 100644 --- a/src/smt/tactic/smt_tactic_core.cpp +++ b/src/smt/tactic/smt_tactic_core.cpp @@ -31,6 +31,7 @@ Notes: #include "solver/solver.h" #include "solver/mus.h" #include "solver/parallel_tactical.h" +#include "solver/parallel_tactical2.h" #include "solver/parallel_params.hpp" #include @@ -429,18 +430,30 @@ static tactic * mk_seq_smt_tactic(ast_manager& m, params_ref const & p) { tactic * mk_parallel_smt_tactic(ast_manager& m, params_ref const& p) { + parallel_params pp(p); + if (pp.enable2()) + return mk_parallel_tactic2(mk_smt_solver(m, p, symbol::null), p); return mk_parallel_tactic(mk_smt_solver(m, p, symbol::null), p); } tactic * mk_smt_tactic_core(ast_manager& m, params_ref const& p, symbol const& logic) { parallel_params pp(p); - return pp.enable() ? mk_parallel_tactic(mk_smt_solver(m, p, logic), p) : mk_seq_smt_tactic(m, p); + if (pp.enable()) + return mk_parallel_tactic(mk_smt_solver(m, p, logic), p); + if (pp.enable2()) + return mk_parallel_tactic2(mk_smt_solver(m, p, logic), p); + return mk_seq_smt_tactic(m, p); } tactic * mk_smt_tactic_core_using(ast_manager& m, bool auto_config, params_ref const& _p) { parallel_params pp(_p); params_ref p = _p; p.set_bool("auto_config", auto_config); - return using_params(pp.enable() ? mk_parallel_smt_tactic(m, p) : mk_seq_smt_tactic(m, p), p); + tactic *t = nullptr; + if (pp.enable() || pp.enable2()) + t = mk_parallel_smt_tactic(m, p); + else + t = mk_seq_smt_tactic(m, p); + return using_params(t, p); } diff --git a/src/smt/theory_arith.h b/src/smt/theory_arith.h index 13e7a0986..c5dc9df66 100644 --- a/src/smt/theory_arith.h +++ b/src/smt/theory_arith.h @@ -524,7 +524,7 @@ namespace smt { bool has_var(expr * v) const { return get_context().e_internalized(v) && get_context().get_enode(v)->get_th_var(get_id()) != null_theory_var; } theory_var expr2var(expr * v) const { SASSERT(get_context().e_internalized(v)); return get_context().get_enode(v)->get_th_var(get_id()); } - expr * var2expr(theory_var v) const { return get_enode(v)->get_expr(); } + expr * var2expr(theory_var v) const { return get_expr(v); } bool reflection_enabled() const; bool reflect(app * n) const; unsigned lazy_pivoting_lvl() const { return m_params.m_arith_lazy_pivoting_lvl; } @@ -656,7 +656,7 @@ namespace smt { void push_scope_eh() override; void pop_scope_eh(unsigned num_scopes) override; - void relevant_eh(app * n) override; + void relevant_eh(expr * n) override; void restart_eh() override; void init_search_eh() override; @@ -966,7 +966,7 @@ namespace smt { \brief A monomial is 'pure' if does not have a numeric coefficient. */ bool is_pure_monomial(expr * m) const; - bool is_pure_monomial(theory_var v) const { return is_pure_monomial(get_enode(v)->get_expr()); } + bool is_pure_monomial(theory_var v) const { return is_pure_monomial(get_expr(v)); } void mark_var(theory_var v, svector & vars, var_set & already_found); void mark_dependents(theory_var v, svector & vars, var_set & already_found, row_set & already_visited_rows); void get_non_linear_cluster(svector & vars); diff --git a/src/smt/theory_arith_aux.h b/src/smt/theory_arith_aux.h index d4818cec8..c8fd25dba 100644 --- a/src/smt/theory_arith_aux.h +++ b/src/smt/theory_arith_aux.h @@ -1086,7 +1086,7 @@ namespace smt { expr_ref theory_arith::mk_gt(theory_var v) { ast_manager& m = get_manager(); inf_numeral const& val = get_value(v); - expr* obj = get_enode(v)->get_expr(); + expr* obj = get_expr(v); expr_ref e(m); rational r = val.get_rational(); if (m_util.is_int(obj->get_sort())) { @@ -1124,7 +1124,7 @@ namespace smt { expr_ref theory_arith::mk_ge(generic_model_converter& fm, theory_var v, inf_numeral const& val) { ast_manager& m = get_manager(); std::ostringstream strm; - strm << val << " <= " << mk_pp(get_enode(v)->get_expr(), get_manager()); + strm << val << " <= " << mk_pp(get_expr(v), get_manager()); app* b = m.mk_const(symbol(strm.str()), m.mk_bool_sort()); expr_ref result(b, m); TRACE(opt, tout << result << "\n";); @@ -1799,7 +1799,7 @@ namespace smt { */ template typename theory_arith::max_min_t theory_arith::max_min(theory_var v, bool max, bool maintain_integrality, bool& has_shared) { - expr* e = get_enode(v)->get_expr(); + expr* e = get_expr(v); (void)e; SASSERT(!maintain_integrality || valid_assignment()); SASSERT(satisfy_bounds()); @@ -2179,8 +2179,8 @@ namespace smt { TRACE(shared, tout << ctx.get_scope_level() << " " << v << " " << r->get_num_parents() << "\n";); for (; it != end; ++it) { enode * parent = *it; - app * o = parent->get_expr(); - if (o->get_family_id() == get_id()) { + app* o = parent->get_app(); + if (parent->get_family_id() == get_id()) { switch (o->get_decl_kind()) { case OP_DIV: case OP_IDIV: diff --git a/src/smt/theory_arith_core.h b/src/smt/theory_arith_core.h index 498fa03f4..6ba5eef74 100644 --- a/src/smt/theory_arith_core.h +++ b/src/smt/theory_arith_core.h @@ -1381,18 +1381,19 @@ namespace smt { } template - void theory_arith::relevant_eh(app * n) { + void theory_arith::relevant_eh(expr * n) { TRACE(arith_relevant_eh, tout << "relevant_eh: " << mk_pp(n, m) << "\n";); - if (m_util.is_mod(n)) - mk_idiv_mod_axioms(n->get_arg(0), n->get_arg(1)); - else if (m_util.is_rem(n)) - mk_rem_axiom(n->get_arg(0), n->get_arg(1)); - else if (m_util.is_div(n)) - mk_div_axiom(n->get_arg(0), n->get_arg(1)); + expr* x = nullptr, *y = nullptr; + if (m_util.is_mod(n, x, y)) + mk_idiv_mod_axioms(x, y); + else if (m_util.is_rem(n, x, y)) + mk_rem_axiom(x, y); + else if (m_util.is_div(n, x, y)) + mk_div_axiom(x, y); else if (m_util.is_to_int(n)) - mk_to_int_axiom(n); + mk_to_int_axiom(to_app(n)); else if (m_util.is_is_int(n)) - mk_is_int_axiom(n); + mk_is_int_axiom(to_app(n)); } template @@ -1451,8 +1452,8 @@ namespace smt { template void theory_arith::new_diseq_eh(theory_var v1, theory_var v2) { - TRACE(arith_new_diseq_eh, tout << mk_bounded_pp(get_enode(v1)->get_expr(), m) << "\n" << - mk_bounded_pp(get_enode(v2)->get_expr(), m) << "\n";); + TRACE(arith_new_diseq_eh, tout << mk_bounded_pp(get_expr(v1), m) << "\n" << + mk_bounded_pp(get_expr(v2), m) << "\n";); m_stats.m_assert_diseq++; m_arith_eq_adapter.new_diseq_eh(v1, v2); } diff --git a/src/smt/theory_arith_int.h b/src/smt/theory_arith_int.h index 8c70ac66f..fe02fe884 100644 --- a/src/smt/theory_arith_int.h +++ b/src/smt/theory_arith_int.h @@ -215,7 +215,7 @@ namespace smt { tout << "k = " << k << ", _k = "<< _k << std::endl; ); expr_ref bound(m); - expr* e = get_enode(v)->get_expr(); + expr* e = get_expr(v); bound = m_util.mk_ge(e, m_util.mk_numeral(_k, m_util.is_int(e))); context & ctx = get_context(); { @@ -413,7 +413,7 @@ namespace smt { for (; it != end; ++it) { if (!it->is_dead() && it->m_var != b && is_free(it->m_var)) { theory_var v = it->m_var; - expr* e = get_enode(v)->get_expr(); + expr* e = get_expr(v); bool _is_int = m_util.is_int(e); expr_ref bound(m_util.mk_ge(e, m_util.mk_numeral(rational::zero(), _is_int)), get_manager()); context & ctx = get_context(); @@ -629,9 +629,9 @@ namespace smt { } rational _k = k.to_rational(); if (is_lower) - bound = m_util.mk_ge(get_enode(v)->get_expr(), m_util.mk_numeral(_k, is_int(v))); + bound = m_util.mk_ge(get_expr(v), m_util.mk_numeral(_k, is_int(v))); else - bound = m_util.mk_le(get_enode(v)->get_expr(), m_util.mk_numeral(_k, is_int(v))); + bound = m_util.mk_le(get_expr(v), m_util.mk_numeral(_k, is_int(v))); } else { if (num_ints > 0) { diff --git a/src/smt/theory_arith_nl.h b/src/smt/theory_arith_nl.h index 1fd80c365..749ac8251 100644 --- a/src/smt/theory_arith_nl.h +++ b/src/smt/theory_arith_nl.h @@ -653,7 +653,7 @@ theory_var theory_arith::find_nl_var_for_branching() { bool computed_epsilon = false; bool r = check_monomial_assignment(v, computed_epsilon); if (!r) { - expr * m = get_enode(v)->get_expr(); + expr * m = get_expr(v); SASSERT(is_pure_monomial(m)); for (expr * arg : *to_app(m)) { theory_var curr = ctx.get_enode(arg)->get_th_var(get_id()); diff --git a/src/smt/theory_arith_pp.h b/src/smt/theory_arith_pp.h index 7448f6db8..81ed03762 100644 --- a/src/smt/theory_arith_pp.h +++ b/src/smt/theory_arith_pp.h @@ -484,7 +484,7 @@ namespace smt { pp.set_benchmark_name("lemma"); int n = get_num_vars(); for (theory_var v = 0; v < n; ++v) { - expr * n = get_enode(v)->get_expr(); + expr * n = get_expr(v); if (is_fixed(v)) { inf_numeral k_inf = lower_bound(v); rational k = k_inf.get_rational().to_rational(); diff --git a/src/smt/theory_array.cpp b/src/smt/theory_array.cpp index 5c9ac3a80..2b9f5ba51 100644 --- a/src/smt/theory_array.cpp +++ b/src/smt/theory_array.cpp @@ -42,7 +42,7 @@ namespace smt { // v1 is the new root TRACE(array, tout << "merging v" << v1 << " v" << v2 << "\n"; display_var(tout, v1); - tout << mk_pp(get_enode(v1)->get_expr(), m) << " <- " << mk_pp(get_enode(v2)->get_expr(), m) << "\n";); + tout << mk_pp(get_expr(v1), m) << " <- " << mk_pp(get_expr(v2), m) << "\n";); SASSERT(v1 == find(v1)); var_data * d1 = m_var_data[v1]; var_data * d2 = m_var_data[v2]; @@ -68,12 +68,12 @@ namespace smt { m_var_data.push_back(alloc(var_data)); var_data * d = m_var_data[r]; TRACE(array, tout << mk_bounded_pp(n->get_expr(), m) << "\nis_array: " << is_array_sort(n) << ", is_select: " << is_select(n) << - ", is_store: " << is_store(n) << "\n";); + ", is_store: " << is_store(n) << ", is_lambda: " << is_lambda(n->get_expr()) << "\n";); d->m_is_array = is_array_sort(n); if (d->m_is_array) register_sort(n->get_expr()->get_sort()); d->m_is_select = is_select(n); - if (is_store(n)) + if (is_store(n) || is_lambda(n->get_expr())) d->m_stores.push_back(n); ctx.attach_th_var(n, this, r); if (laziness() <= 1 && is_store(n)) @@ -88,14 +88,14 @@ namespace smt { v = find(v); var_data * d = m_var_data[v]; d->m_parent_selects.push_back(s); - TRACE(array, tout << v << " " << mk_pp(s->get_expr(), m) << " " << mk_pp(get_enode(v)->get_expr(), m) << "\n";); + TRACE(array, tout << v << " " << mk_pp(s->get_expr(), m) << " " << mk_pp(get_expr(v), m) << "\n";); m_trail_stack.push(push_back_trail(d->m_parent_selects)); for (enode* n : d->m_stores) instantiate_axiom2a(s, n); if (!m_params.m_array_delay_exp_axiom && d->m_prop_upward) { for (enode* store : d->m_parent_stores) { - SASSERT(is_store(store)); + SASSERT(is_store(store) || is_lambda(store->get_expr())); if (!m_params.m_array_cg || store->is_cgr()) { instantiate_axiom2b(s, store); } @@ -106,7 +106,7 @@ namespace smt { void theory_array::add_parent_store(theory_var v, enode * s) { if (m_params.m_array_cg && !s->is_cgr()) return; - SASSERT(is_store(s)); + SASSERT(is_store(s) || is_lambda(s->get_expr())); v = find(v); var_data * d = m_var_data[v]; d->m_parent_stores.push_back(s); @@ -177,7 +177,7 @@ namespace smt { void theory_array::add_store(theory_var v, enode * s) { if (m_params.m_array_cg && !s->is_cgr()) return; - SASSERT(is_store(s)); + SASSERT(is_store(s) || is_lambda(s->get_expr())); v = find(v); var_data * d = m_var_data[v]; unsigned lambda_equiv_class_size = get_lambda_equiv_size(v, d); @@ -204,7 +204,7 @@ namespace smt { void theory_array::instantiate_axiom2a(enode * select, enode * store) { TRACE(array, tout << "axiom 2a: #" << select->get_owner_id() << " #" << store->get_owner_id() << "\n";); SASSERT(is_select(select)); - SASSERT(is_store(store)); + SASSERT(is_store(store) || is_lambda(store->get_expr())); if (assert_store_axiom2(store, select)) m_stats.m_num_axiom2a++; } @@ -212,7 +212,7 @@ namespace smt { bool theory_array::instantiate_axiom2b(enode * select, enode * store) { TRACE(array_axiom2b, tout << "axiom 2b: #" << select->get_owner_id() << " #" << store->get_owner_id() << "\n";); SASSERT(is_select(select)); - SASSERT(is_store(store)); + SASSERT(is_store(store) || is_lambda(store->get_expr())); if (assert_store_axiom2(store, select)) { m_stats.m_num_axiom2b++; return true; @@ -261,7 +261,7 @@ namespace smt { } bool theory_array::internalize_term(app * n) { - if (!is_store(n) && !is_select(n)) { + if (!is_store(n) && !is_select(n) && !is_lambda(n)) { if (!is_array_ext(n)) found_unsupported_op(n); return false; @@ -282,7 +282,7 @@ namespace smt { if (is_select(n)) { add_parent_select(v_arg, ctx.get_enode(n)); } - else if (is_store(n)) { + else if (is_store(n) || is_lambda(n)) { add_parent_store(v_arg, ctx.get_enode(n)); } } @@ -298,11 +298,6 @@ namespace smt { void theory_array::new_eq_eh(theory_var v1, theory_var v2) { m_find.merge(v1, v2); - enode* n1 = get_enode(v1), *n2 = get_enode(v2); - if (n1->get_expr()->get_decl()->is_lambda() || - n2->get_expr()->get_decl()->is_lambda()) { - assert_congruent(n1, n2); - } } void theory_array::new_diseq_eh(theory_var v1, theory_var v2) { @@ -310,8 +305,8 @@ namespace smt { v2 = find(v2); var_data * d1 = m_var_data[v1]; TRACE(ext, tout << "extensionality: " << d1->m_is_array << "\n" - << mk_bounded_pp(get_enode(v1)->get_expr(), m, 5) << "\n" - << mk_bounded_pp(get_enode(v2)->get_expr(), m, 5) << "\n";); + << mk_bounded_pp(get_expr(v1), m, 5) << "\n" + << mk_bounded_pp(get_expr(v2), m, 5) << "\n";); if (d1->m_is_array) { SASSERT(m_var_data[v2]->m_is_array); @@ -319,16 +314,18 @@ namespace smt { } } - void theory_array::relevant_eh(app * n) { + void theory_array::relevant_eh(expr * n) { if (laziness() == 0) return; if (m.is_ite(n)) { TRACE(array, tout << "relevant ite " << mk_pp(n, m) << "\n";); } - if (!is_store(n) && !is_select(n)) + if (!is_store(n) && !is_select(n) && !is_lambda(n)) return; if (!ctx.e_internalized(n)) ctx.internalize(n, false); - enode * arg = ctx.get_enode(n->get_arg(0)); + if (is_lambda(n)) + return; + enode * arg = ctx.get_enode(to_app(n)->get_arg(0)); theory_var v_arg = arg->get_th_var(get_id()); SASSERT(v_arg != null_theory_var); diff --git a/src/smt/theory_array.h b/src/smt/theory_array.h index 6e840e342..d6ce0f4b9 100644 --- a/src/smt/theory_array.h +++ b/src/smt/theory_array.h @@ -28,7 +28,7 @@ namespace smt { unsigned m_num_axiom1, m_num_axiom2a, m_num_axiom2b, m_num_extensionality, m_num_eq_splits; unsigned m_num_map_axiom, m_num_default_map_axiom; unsigned m_num_select_const_axiom, m_num_default_store_axiom, m_num_default_const_axiom, m_num_default_as_array_axiom; - unsigned m_num_select_as_array_axiom, m_num_default_lambda_axiom; + unsigned m_num_select_as_array_axiom, m_num_default_lambda_axiom, m_num_choice_axiom; void reset() { memset(this, 0, sizeof(theory_array_stats)); } theory_array_stats() { reset(); } }; @@ -59,7 +59,7 @@ namespace smt { void apply_sort_cnstr(enode * n, sort * s) override; void new_eq_eh(theory_var v1, theory_var v2) override; void new_diseq_eh(theory_var v1, theory_var v2) override; - void relevant_eh(app * n) override; + void relevant_eh(expr * n) override; void push_scope_eh() override; void pop_scope_eh(unsigned num_scopes) override; final_check_status final_check_eh(unsigned) override; @@ -115,4 +115,3 @@ namespace smt { }; - diff --git a/src/smt/theory_array_base.cpp b/src/smt/theory_array_base.cpp index 979c55de6..1bfa05584 100644 --- a/src/smt/theory_array_base.cpp +++ b/src/smt/theory_array_base.cpp @@ -108,7 +108,7 @@ namespace smt { } void theory_array_base::assert_store_axiom1_core(enode * e) { - app * n = e->get_expr(); + app * n = e->get_app(); SASSERT(is_store(n)); ptr_buffer sel_args; unsigned num_args = n->get_num_args(); @@ -217,28 +217,48 @@ namespace smt { if (m.has_trace_stream()) m.trace_stream() << "[end-of-instance]\n"; } } + + void theory_array_base::assert_lambda_axiom_core(enode* n, enode* select) { + SASSERT(is_lambda(n->get_expr())); + SASSERT(is_select(select)); + expr *e = n->get_expr(); + SASSERT(is_lambda(e)); + app *s = select->get_app(); + auto q = to_quantifier(e); + SASSERT(q); + + SASSERT(q->get_num_decls() == s->get_num_args() - 1); + // do the same thing as in sat/smt/array_axioms: + ptr_vector args(s->get_num_args(), s->get_args()); + args[0] = q; + array_util a(m); + expr_ref alpha(a.mk_select(args), m); + expr_ref beta(alpha); + ctx.get_rewriter()(beta); + TRACE(array, tout << alpha << " == " << beta << "\n";); + auto alpha_n = ensure_enode(alpha); + auto beta_n = ensure_enode(beta); + ctx.assign_eq(alpha_n, beta_n, eq_justification::mk_axiom()); + } bool theory_array_base::assert_store_axiom2(enode * store, enode * select) { + SASSERT(is_store(store) || is_lambda(store->get_expr())); unsigned num_args = select->get_num_args(); unsigned i = 1; for (; i < num_args; ++i) - if (store->get_arg(i)->get_root() != select->get_arg(i)->get_root()) + if (is_store(store) && store->get_arg(i)->get_root() != select->get_arg(i)->get_root()) break; if (i == num_args) return false; if (ctx.add_fingerprint(store, store->get_owner_id(), select->get_num_args() - 1, select->get_args() + 1)) { TRACE(array, tout << "adding axiom2 to todo queue\n";); - m_axiom2_todo.push_back(std::make_pair(store, select)); + m_axiom2_todo.push_back({store, select}); return true; } TRACE(array, tout << "axiom already instantiated: #" << store->get_owner_id() << " #" << select->get_owner_id() << "\n";); return false; } - - - - func_decl_ref_vector * theory_array_base::register_sort(sort * s_array) { unsigned dimension = get_dimension(s_array); func_decl_ref_vector * ext_skolems = nullptr; @@ -333,8 +353,8 @@ namespace smt { void theory_array_base::assert_extensionality_core(enode * n1, enode * n2) { - app * e1 = n1->get_expr(); - app * e2 = n2->get_expr(); + expr * e1 = n1->get_expr(); + expr * e2 = n2->get_expr(); func_decl_ref_vector * funcs = nullptr; sort * s = e1->get_sort(); @@ -371,15 +391,15 @@ namespace smt { \brief assert n1 = n2 => forall vars . (n1 vars) = (n2 vars) */ void theory_array_base::assert_congruent_core(enode * n1, enode * n2) { - app * e1 = n1->get_expr(); - app * e2 = n2->get_expr(); + expr * e1 = n1->get_expr(); + expr * e2 = n2->get_expr(); sort* s = e1->get_sort(); unsigned dimension = get_array_arity(s); literal n1_eq_n2 = mk_eq(e1, e2, true); ctx.mark_as_relevant(n1_eq_n2); expr_ref_vector args1(m), args2(m); - args1.push_back(instantiate_lambda(e1)); - args2.push_back(instantiate_lambda(e2)); + args1.push_back(e1); + args2.push_back(e2); svector names; sort_ref_vector sorts(m); for (unsigned i = 0; i < dimension; ++i) { @@ -403,17 +423,6 @@ namespace smt { assert_axiom(~n1_eq_n2, fa_eq); } - expr_ref theory_array_base::instantiate_lambda(app* e) { - quantifier * q = m.is_lambda_def(e->get_decl()); - expr_ref f(e, m); - if (q) { - // the variables in q are maybe not consecutive. - var_subst sub(m, false); - f = sub(q, e->get_num_args(), e->get_args()); - } - return f; - } - bool theory_array_base::can_propagate() { return !m_axiom1_todo.empty() || @@ -424,13 +433,16 @@ namespace smt { } void theory_array_base::propagate() { - while (can_propagate()) { + while (theory_array_base::can_propagate()) { for (unsigned i = 0; i < m_axiom1_todo.size(); ++i) assert_store_axiom1_core(m_axiom1_todo[i]); m_axiom1_todo.reset(); for (unsigned i = 0; i < m_axiom2_todo.size(); ++i) { auto [store, select] = m_axiom2_todo[i]; - assert_store_axiom2_core(store, select); + if (is_store(store)) + assert_store_axiom2_core(store, select); + else + assert_lambda_axiom_core(store, select); } m_axiom2_todo.reset(); for (unsigned i = 0; i < m_extensionality_todo.size(); ++i) { @@ -561,13 +573,13 @@ namespace smt { TRACE(array_bug, tout << "mk_interface_eqs: processing: v" << *it1 << "\n";); theory_var v1 = *it1; enode * n1 = get_enode(v1); - sort * s1 = n1->get_expr()->get_sort(); + sort * s1 = n1->get_sort(); sbuffer::iterator it2 = it1; ++it2; for (; it2 != end1; ++it2) { theory_var v2 = *it2; enode * n2 = get_enode(v2); - sort * s2 = n2->get_expr()->get_sort(); + sort * s2 = n2->get_sort(); if (s1 == s2 && !ctx.is_diseq(n1, n2)) { app * eq = mk_eq_atom(n1->get_expr(), n2->get_expr()); if (!ctx.b_internalized(eq) || !ctx.is_relevant(eq)) { @@ -974,7 +986,7 @@ namespace smt { model_value_proc * theory_array_base::mk_value(enode * n, model_generator & mg) { theory_var v = n->get_th_var(get_id()); SASSERT(v != null_theory_var); - sort * s = n->get_expr()->get_sort(); + sort * s = n->get_sort(); enode * else_val_n = get_default(v); array_value_proc * result = nullptr; diff --git a/src/smt/theory_array_base.h b/src/smt/theory_array_base.h index 9a6a6a173..629faec98 100644 --- a/src/smt/theory_array_base.h +++ b/src/smt/theory_array_base.h @@ -34,25 +34,31 @@ namespace smt { virtual void set_prop_upward(theory_var v) {} void found_unsupported_op(expr * n); void found_unsupported_op(enode* n) { found_unsupported_op(n->get_expr()); } - void found_unsupported_op(theory_var v) { found_unsupported_op(get_enode(v)->get_expr()); } + void found_unsupported_op(theory_var v) { found_unsupported_op(get_expr(v)); } - bool is_store(app const* n) const { return n->is_app_of(get_id(), OP_STORE); } - bool is_map(app const* n) const { return n->is_app_of(get_id(), OP_ARRAY_MAP); } - bool is_select(app const* n) const { return n->is_app_of(get_id(), OP_SELECT); } - bool is_default(app const* n) const { return n->is_app_of(get_id(), OP_ARRAY_DEFAULT); } - bool is_const(app const* n) const { return n->is_app_of(get_id(), OP_CONST_ARRAY); } - bool is_array_ext(app const * n) const { return n->is_app_of(get_id(), OP_ARRAY_EXT); } - bool is_as_array(app const * n) const { return n->is_app_of(get_id(), OP_AS_ARRAY); } + bool is_store(expr const* n) const { return is_app(n) && to_app(n)->is_app_of(get_id(), OP_STORE); } + bool is_map(expr const* n) const { return is_app(n) && to_app(n)->is_app_of(get_id(), OP_ARRAY_MAP); } + bool is_select(expr const* n) const { return is_app(n) && to_app(n)->is_app_of(get_id(), OP_SELECT); } + bool is_default(expr const* n) const { return is_app(n) && to_app(n)->is_app_of(get_id(), OP_ARRAY_DEFAULT); } + bool is_const(expr const* n) const { return is_app(n) && to_app(n)->is_app_of(get_id(), OP_CONST_ARRAY); } + bool is_array_ext(expr const * n) const { return is_app(n) && to_app(n)->is_app_of(get_id(), OP_ARRAY_EXT); } + bool is_as_array(expr const * n) const { return is_app(n) && to_app(n)->is_app_of(get_id(), OP_AS_ARRAY); } + bool is_choice(expr const* n) const { return is_app(n) && to_app(n)->is_app_of(get_id(), OP_CHOICE); } bool is_array_sort(sort const* s) const { return s->is_sort_of(get_id(), ARRAY_SORT); } - bool is_array_sort(app const* n) const { return is_array_sort(n->get_sort()); } + bool is_array_sort(expr const* n) const { return is_array_sort(n->get_sort()); } + bool is_store(enode const * n) const { return is_store(n->get_expr()); } bool is_map(enode const* n) const { return is_map(n->get_expr()); } bool is_select(enode const* n) const { return is_select(n->get_expr()); } bool is_const(enode const* n) const { return is_const(n->get_expr()); } bool is_as_array(enode const * n) const { return is_as_array(n->get_expr()); } + bool is_choice(enode const* n) const { return is_choice(n->get_expr()); } bool is_default(enode const* n) const { return is_default(n->get_expr()); } - bool is_array_sort(enode const* n) const { return is_array_sort(n->get_expr()); } + bool is_array_sort(enode const* n) const { return is_array_sort(n->get_sort()); } + + + bool is_select_arg(enode* r); app * mk_select(unsigned num_args, expr * const * args); @@ -74,13 +80,14 @@ namespace smt { void assert_axiom(literal l); void assert_store_axiom1_core(enode * n); void assert_store_axiom2_core(enode * store, enode * select); + void assert_lambda_axiom_core(enode *lambda, enode *select); void assert_store_axiom1(enode * n) { m_axiom1_todo.push_back(n); } bool assert_store_axiom2(enode * store, enode * select); void assert_extensionality_core(enode * a1, enode * a2); bool assert_extensionality(enode * a1, enode * a2); - expr_ref instantiate_lambda(app* e); + expr_ref instantiate_lambda(expr* e); void assert_congruent_core(enode * a1, enode * a2); void assert_congruent(enode * a1, enode * a2); @@ -208,4 +215,3 @@ namespace smt { }; - diff --git a/src/smt/theory_array_full.cpp b/src/smt/theory_array_full.cpp index 5b316249e..137359841 100644 --- a/src/smt/theory_array_full.cpp +++ b/src/smt/theory_array_full.cpp @@ -248,7 +248,7 @@ namespace smt { instantiate_default_as_array_axiom(n); d->m_as_arrays.push_back(n); } - else if (m.is_lambda_def(n->get_decl())) { + else if (is_lambda(n->get_expr())) { instantiate_default_lambda_def_axiom(n); d->m_lambdas.push_back(n); m_lambdas.push_back(n); @@ -271,7 +271,7 @@ namespace smt { return theory_array::internalize_term(n); } - if (!is_const(n) && !is_default(n) && !is_map(n) && !is_as_array(n)) { + if (!is_const(n) && !is_default(n) && !is_map(n) && !is_as_array(n) && !is_choice(n)) { if (!is_array_ext(n)) found_unsupported_op(n); return false; @@ -368,8 +368,8 @@ namespace smt { TRACE(array, tout << "v" << v << " " << pp(get_enode(v), m) << " " << d->m_prop_upward << " " << m_params.m_array_delay_exp_axiom << "\n";); for (enode * store : d->m_stores) { - SASSERT(is_store(store)); - instantiate_default_store_axiom(store); + if (is_store(store)) + instantiate_default_store_axiom(store); } if (!m_params.m_array_delay_exp_axiom && d->m_prop_upward) { @@ -403,22 +403,23 @@ namespace smt { } } - void theory_array_full::relevant_eh(app* n) { + void theory_array_full::relevant_eh(expr* n) { TRACE(array, tout << mk_pp(n, m) << "\n";); theory_array::relevant_eh(n); - if (!is_default(n) && !is_select(n) && !is_map(n) && !is_const(n) && !is_as_array(n)){ + if (!is_default(n) && !is_select(n) && !is_map(n) && + !is_const(n) && !is_as_array(n) && !is_choice(n)) { return; } ctx.ensure_internalized(n); enode* node = ctx.get_enode(n); if (is_select(n)) { - enode * arg = ctx.get_enode(n->get_arg(0)); + enode * arg = ctx.get_enode(to_app(n)->get_arg(0)); theory_var v = arg->get_th_var(get_id()); SASSERT(v != null_theory_var); add_parent_select(find(v), node); } else if (is_default(n)) { - enode * arg = ctx.get_enode(n->get_arg(0)); + enode * arg = ctx.get_enode(to_app(n)->get_arg(0)); theory_var v = arg->get_th_var(get_id()); SASSERT(v != null_theory_var); set_prop_upward(v); @@ -431,7 +432,7 @@ namespace smt { add_parent_default(find(v)); } else if (is_map(n)) { - for (expr * e : *n) { + for (expr * e : *to_app(n)) { enode* arg = ctx.get_enode(e); theory_var v_arg = find(arg->get_th_var(get_id())); add_parent_map(v_arg, node); @@ -442,6 +443,10 @@ namespace smt { else if (is_as_array(n)) { instantiate_default_as_array_axiom(node); } + else if (is_choice(n)) { + m_choice_terms.push_back(node); + ctx.push_trail(push_back_vector(m_choice_terms)); + } } bool theory_array_full::should_research(expr_ref_vector & unsat_core) { @@ -456,8 +461,8 @@ namespace smt { // select(map[f](a, ... d), i) = f(select(a,i),...,select(d,i)) // bool theory_array_full::instantiate_select_map_axiom(enode* sl, enode* mp) { - app* map = mp->get_expr(); - app* select = sl->get_expr(); + app* map = mp->get_app(); + app* select = sl->get_app(); SASSERT(is_map(map)); SASSERT(is_select(select)); SASSERT(map->get_num_args() > 0); @@ -523,7 +528,7 @@ namespace smt { bool theory_array_full::instantiate_default_map_axiom(enode* mp) { SASSERT(is_map(mp)); - app* map = mp->get_expr(); + app* map = mp->get_app(); if (!ctx.add_fingerprint(this, m_default_map_fingerprint, 1, &mp)) { return false; } @@ -573,13 +578,12 @@ namespace smt { if (!ctx.add_fingerprint(this, m_default_lambda_fingerprint, 1, &arr)) return false; m_stats.m_num_default_lambda_axiom++; - expr* e = arr->get_expr(); - expr_ref def(mk_default(e), m); - quantifier* lam = m.is_lambda_def(arr->get_decl()); - TRACE(array, tout << mk_pp(lam, m) << "\n" << mk_pp(e, m) << "\n"); + quantifier *lam = to_quantifier(arr->get_expr()); + expr_ref def(mk_default(arr->get_expr()), m); + TRACE(array, tout << mk_pp(lam, m) << "\n"); expr_ref_vector args(m); var_subst subst(m, false); - args.push_back(subst(lam, to_app(e)->get_num_args(), to_app(e)->get_args())); + args.push_back(lam); for (unsigned i = 0; i < lam->get_num_decls(); ++i) args.push_back(mk_epsilon(lam->get_decl_sort(i)).first); expr_ref val(mk_select(args), m); @@ -596,6 +600,33 @@ namespace smt { return try_assign_eq(val.get(), def); } + bool theory_array_full::instantiate_choice_axiom(enode* ch) { + if (!ctx.add_fingerprint(this, m_choice_fingerprint, 1, &ch)) + return false; + ++m_stats.m_num_choice_axiom; + SASSERT(is_choice(ch)); + app* choice_term = ch->get_app(); + expr* pred = choice_term->get_arg(0); + sort* pred_sort = pred->get_sort(); + SASSERT(is_array_sort(pred_sort)); + SASSERT(get_array_arity(pred_sort) == 1); + SASSERT(m.is_bool(get_array_range(pred_sort))); + sort* x_sort = get_array_domain(pred_sort, 0); + expr_ref x(m.mk_var(0, x_sort), m); + expr* args1[2] = { pred, x }; + expr_ref px(mk_select(2, args1), m); + expr* args2[2] = { pred, choice_term }; + expr_ref pc(mk_select(2, args2), m); + expr_ref body(m.mk_implies(px, pc), m); + symbol x_name("x"); + expr_ref q(m.mk_forall(1, &x_sort, &x_name, body), m); + ctx.get_rewriter()(q); + TRACE(array, tout << "choice " << q << "\n"); + ctx.assert_expr(q); + ctx.internalize_assertions(); + return true; + } + // // Assert axiom: // select(const v, i_1, ..., i_n) = v @@ -613,10 +644,10 @@ namespace smt { ptr_buffer sel_args; sel_args.push_back(cnst->get_expr()); for (unsigned short i = 1; i < num_args; ++i) { - sel_args.push_back(select->get_expr()->get_arg(i)); + sel_args.push_back(select->get_app()->get_arg(i)); } expr * sel = mk_select(sel_args.size(), sel_args.data()); - expr * val = cnst->get_expr()->get_arg(0); + expr * val = cnst->get_app()->get_arg(0); TRACE(array, tout << "new select-const axiom...\n"; tout << "const: " << mk_bounded_pp(cnst->get_expr(), m) << "\n"; tout << "select: " << mk_bounded_pp(select->get_expr(), m) << "\n"; @@ -647,7 +678,7 @@ namespace smt { ptr_buffer sel_args; sel_args.push_back(arr->get_expr()); for (unsigned short i = 1; i < num_args; ++i) { - sel_args.push_back(select->get_expr()->get_arg(i)); + sel_args.push_back(select->get_app()->get_arg(i)); } expr * sel = mk_select(sel_args.size(), sel_args.data()); func_decl * f = array_util(m).get_as_array_func_decl(arr->get_expr()); @@ -669,7 +700,7 @@ namespace smt { bool theory_array_full::instantiate_default_store_axiom(enode* store) { SASSERT(is_store(store)); SASSERT(store->get_num_args() >= 3); - app* store_app = store->get_expr(); + app* store_app = store->get_app(); if (!ctx.add_fingerprint(this, m_default_store_fingerprint, store->get_num_args(), store->get_args())) { return false; } @@ -747,6 +778,17 @@ namespace smt { return {eps, diag}; } + void theory_array_full::propagate() { + theory_array::propagate(); + if (m_choice_qhead == m_choice_terms.size()) + return; + ctx.push_trail(value_trail(m_choice_qhead)); + for (; m_choice_qhead < m_choice_terms.size(); ++m_choice_qhead) { + enode *choice = m_choice_terms[m_choice_qhead]; + instantiate_choice_axiom(choice); + } + } + final_check_status theory_array_full::assert_delayed_axioms() { final_check_status r = FC_DONE; if (!m_params.m_array_delay_exp_axiom) { @@ -839,5 +881,6 @@ namespace smt { st.update("array def as-array", m_stats.m_num_default_as_array_axiom); st.update("array sel as-array", m_stats.m_num_select_as_array_axiom); st.update("array def lambda", m_stats.m_num_default_lambda_axiom); + st.update("array choice ax", m_stats.m_num_choice_axiom); } } diff --git a/src/smt/theory_array_full.h b/src/smt/theory_array_full.h index 1a5b72814..f66762e6b 100644 --- a/src/smt/theory_array_full.h +++ b/src/smt/theory_array_full.h @@ -37,12 +37,15 @@ namespace smt { ast2ast_trailmap m_sort2epsilon; ast2ast_trailmap m_sort2diag; obj_pair_map m_eqs; + enode_vector m_choice_terms; + unsigned m_choice_qhead = 0; static unsigned const m_default_map_fingerprint = UINT_MAX - 112; static unsigned const m_default_store_fingerprint = UINT_MAX - 113; static unsigned const m_default_const_fingerprint = UINT_MAX - 115; static unsigned const m_default_as_array_fingerprint = UINT_MAX - 116; static unsigned const m_default_lambda_fingerprint = UINT_MAX - 117; + static unsigned const m_choice_fingerprint = UINT_MAX - 118; protected: @@ -59,7 +62,7 @@ namespace smt { bool internalize_atom(app * atom, bool gate_ctx) override; void pop_scope_eh(unsigned num_scopes) override; theory_var mk_var(enode * n) override; - void relevant_eh(app * n) override; + void relevant_eh(expr * n) override; bool should_research(expr_ref_vector & unsat_core) override; void add_theory_assumptions(expr_ref_vector & assumptions) override; @@ -80,6 +83,8 @@ namespace smt { bool instantiate_default_map_axiom(enode* map); bool instantiate_default_as_array_axiom(enode* arr); bool instantiate_default_lambda_def_axiom(enode* arr); + bool instantiate_select_lambda_axiom(enode *lambda); + bool instantiate_choice_axiom(enode* ch); bool instantiate_parent_stores_default(theory_var v); @@ -108,8 +113,9 @@ namespace smt { void merge_eh(theory_var v1, theory_var v2, theory_var, theory_var) override; void display_var(std::ostream & out, theory_var v) const override; void collect_statistics(::statistics & st) const override; + bool can_propagate() override { return theory_array::can_propagate() || m_choice_qhead < m_choice_terms.size(); } + void propagate() override; }; }; - diff --git a/src/smt/theory_bv.cpp b/src/smt/theory_bv.cpp index 14cfc943f..b53583843 100644 --- a/src/smt/theory_bv.cpp +++ b/src/smt/theory_bv.cpp @@ -38,7 +38,7 @@ namespace smt { return r; } - app * theory_bv::mk_bit2bool(app * bv, unsigned idx) { + app * theory_bv::mk_bit2bool(expr * bv, unsigned idx) { parameter p(idx); expr * args[1] = {bv}; return get_manager().mk_app(get_id(), OP_BIT2BOOL, 1, &p, 1, args); @@ -46,7 +46,7 @@ namespace smt { void theory_bv::mk_bits(theory_var v) { enode * n = get_enode(v); - app * owner = n->get_expr(); + expr * owner = n->get_expr(); unsigned bv_size = get_bv_size(n); bool is_relevant = ctx.is_relevant(n); literal_vector & bits = m_bits[v]; @@ -179,11 +179,15 @@ namespace smt { if (params().m_bv_reflect) { return n->get_arg(idx); } - else { - app * arg = to_app(n->get_expr()->get_arg(idx)); + else if (n->is_app()) { + app * arg = to_app(n->get_app()->get_arg(idx)); SASSERT(ctx.e_internalized(arg)); return ctx.get_enode(arg); } + else { + UNREACHABLE(); + return nullptr; + } } inline theory_var theory_bv::get_arg_var(enode * n, unsigned idx) { @@ -236,8 +240,8 @@ namespace smt { TRACE(bv_diseq_axiom, tout << "found new diseq axiom\n"; display_var(tout, v1); display_var(tout, v2);); // found new disequality m_stats.m_num_diseq_static++; - app * e1 = get_expr(v1); - app * e2 = get_expr(v2); + expr * e1 = get_expr(v1); + expr * e2 = get_expr(v2); expr_ref eq(m.mk_eq(e1, e2), m); literal l = ~mk_literal(eq); std::function logfn = [&]() { @@ -438,8 +442,8 @@ namespace smt { return; } ++m_stats.m_num_eq_dynamic; - app* o1 = get_enode(v1)->get_expr(); - app* o2 = get_enode(v2)->get_expr(); + expr* o1 = get_expr(v1); + expr* o2 = get_expr(v2); literal oeq = mk_eq(o1, o2, true); ctx.mark_as_relevant(oeq); @@ -475,7 +479,7 @@ namespace smt { VERIFY(get_fixed_value(v, val)); enode* n = get_enode(v); if (ctx.watches_fixed(n)) { - expr_ref num(m_util.mk_numeral(val, n->get_expr()->get_sort()), m); + expr_ref num(m_util.mk_numeral(val, n->get_sort()), m); literal_vector& lits = m_tmp_literals; lits.reset(); for (literal b : m_bits[v]) { @@ -1124,15 +1128,18 @@ namespace smt { // Determine whether bit-vector expression should be approximated // based on the number of bits used by the arguments. // - bool theory_bv::approximate_term(app* n) { + bool theory_bv::approximate_term(expr *e) { if (params().m_bv_blast_max_size == INT_MAX) { return false; } + if (!is_app(e)) + return false; + app *n = to_app(e); unsigned num_args = n->get_num_args(); for (unsigned i = 0; i <= num_args; ++i) { - expr* arg = (i == num_args)?n:n->get_arg(i); - sort* s = arg->get_sort(); - if (m_util.is_bv_sort(s) && m_util.get_bv_size(arg) > params().m_bv_blast_max_size) { + expr *arg = (i == num_args) ? n : n->get_arg(i); + sort *s = arg->get_sort(); + if (m_util.is_bv_sort(s) && m_util.get_bv_size(arg) > params().m_bv_blast_max_size) { if (!m_approximates_large_bvs) { TRACE(bv, tout << "found large size bit-vector:\n" << mk_pp(n, m) << "\n";); ctx.push_trail(value_trail(m_approximates_large_bvs)); @@ -1154,7 +1161,7 @@ namespace smt { } void theory_bv::new_eq_eh(theory_var v1, theory_var v2) { - TRACE(bv_eq, tout << "new_eq: " << mk_pp(get_enode(v1)->get_expr(), m) << " = " << mk_pp(get_enode(v2)->get_expr(), m) << "\n";); + TRACE(bv_eq, tout << "new_eq: " << mk_pp(get_expr(v1), m) << " = " << mk_pp(get_expr(v2), m) << "\n";); TRACE(bv, tout << "new_eq_eh v" << v1 << " = v" << v2 << " @ " << ctx.get_scope_level() << " relevant1: " << ctx.is_relevant(get_enode(v1)) << " relevant2: " << ctx.is_relevant(get_enode(v2)) << "\n";); @@ -1218,7 +1225,7 @@ namespace smt { literal_vector & lits = m_tmp_literals; lits.reset(); - literal eq = mk_eq(get_enode(v1)->get_expr(), get_enode(v2)->get_expr(), true); + literal eq = mk_eq(get_expr(v1), get_expr(v2), true); lits.push_back(eq); it1 = bits1.begin(); it2 = bits2.begin(); @@ -1232,7 +1239,7 @@ namespace smt { lits.push_back(arg); } TRACE(bv, - tout << mk_pp(get_enode(v1)->get_expr(), m) << " = " << mk_pp(get_enode(v2)->get_expr(), m) << " " + tout << mk_pp(get_expr(v1), m) << " = " << mk_pp(get_expr(v2), m) << " " << ctx.get_scope_level() << "\n"; ctx.display_literals_smt2(tout, lits);); @@ -1385,10 +1392,12 @@ namespace smt { } } - void theory_bv::relevant_eh(app * n) { + void theory_bv::relevant_eh(expr * n) { TRACE(arith, tout << "relevant: #" << n->get_id() << " " << ctx.e_internalized(n) << ": " << mk_bounded_pp(n, m) << "\n";); TRACE(bv, tout << "relevant: #" << n->get_id() << " " << ctx.e_internalized(n) << ": " << mk_pp(n, m) << "\n";); if (m.is_bool(n)) { + if (!ctx.b_internalized(n)) + return; bool_var v = ctx.get_bool_var(n); atom * a = get_bv2a(v); if (a && !a->is_bit()) { @@ -1401,18 +1410,18 @@ namespace smt { } } else if (params().m_bv_enable_int2bv2int && m_util.is_ubv2int(n)) { - ctx.mark_as_relevant(n->get_arg(0)); - assert_bv2int_axiom(n); + ctx.mark_as_relevant(to_app(n)->get_arg(0)); + assert_bv2int_axiom(to_app(n)); } else if (params().m_bv_enable_int2bv2int && m_util.is_int2bv(n)) { - ctx.mark_as_relevant(n->get_arg(0)); - assert_int2bv_axiom(n); + ctx.mark_as_relevant(to_app(n)->get_arg(0)); + assert_int2bv_axiom(to_app(n)); } #if ENABLE_QUOT_REM_ENCODING else if (m_util.is_bv_udivi(n)) { - ctx.mark_as_relevant(n->get_arg(0)); - ctx.mark_as_relevant(n->get_arg(1)); - assert_udiv_quot_rem_axiom(n); + ctx.mark_as_relevant(to_app(n)->get_arg(0)); + ctx.mark_as_relevant(to_app(n)->get_arg(1)); + assert_udiv_quot_rem_axiom(to_app(n)); } #endif else if (ctx.e_internalized(n)) { diff --git a/src/smt/theory_bv.h b/src/smt/theory_bv.h index 476912117..247424e18 100644 --- a/src/smt/theory_bv.h +++ b/src/smt/theory_bv.h @@ -144,13 +144,13 @@ namespace smt { unsigned get_bv_size(app const * n) const { return m_util.get_bv_size(n); } unsigned get_bv_size(enode const * n) const { return m_util.get_bv_size(n->get_expr()); } unsigned get_bv_size(theory_var v) const { return get_bv_size(get_enode(v)); } - bool is_bv(app const* n) const { return m_util.is_bv_sort(n->get_sort()); } + bool is_bv(expr const* n) const { return m_util.is_bv_sort(n->get_sort()); } bool is_bv(enode const* n) const { return is_bv(n->get_expr()); } bool is_bv(theory_var v) const { return is_bv(get_enode(v)); } region & get_region() { return m_trail_stack.get_region(); } - bool is_numeral(theory_var v) const { return m_util.is_numeral(get_enode(v)->get_expr()); } - app * mk_bit2bool(app * bv, unsigned idx); + bool is_numeral(theory_var v) const { return m_util.is_numeral(get_expr(v)); } + app * mk_bit2bool(expr * bv, unsigned idx); void mk_bits(theory_var v); friend class mk_atom_trail; void mk_bit2bool(app * n); @@ -217,7 +217,7 @@ namespace smt { void internalize_smul_no_overflow(app *n); void internalize_smul_no_underflow(app *n); - bool approximate_term(app* n); + bool approximate_term(expr* e); template void internalize_le(app * atom); @@ -240,7 +240,7 @@ namespace smt { void new_diseq_eh(theory_var v1, theory_var v2) override; virtual void expand_diseq(theory_var v1, theory_var v2); void assign_eh(bool_var v, bool is_true) override; - void relevant_eh(app * n) override; + void relevant_eh(expr * n) override; void push_scope_eh() override; void pop_scope_eh(unsigned num_scopes) override; final_check_status final_check_eh(unsigned) override; diff --git a/src/smt/theory_datatype.cpp b/src/smt/theory_datatype.cpp index e59ba7f10..3782f54d1 100644 --- a/src/smt/theory_datatype.cpp +++ b/src/smt/theory_datatype.cpp @@ -138,7 +138,7 @@ namespace smt { where acc_i are the accessors of constructor c. */ void theory_datatype::assert_is_constructor_axiom(enode * n, func_decl * c, literal antecedent) { - app* e = n->get_expr(); + app* e = n->get_app(); TRACE(datatype_bug, tout << "creating axiom (= n (c (acc_1 n) ... (acc_m n))) for\n" << mk_pp(c, m) << " " << mk_pp(e, m) << "\n";); m_stats.m_assert_cnstr++; @@ -171,7 +171,7 @@ namespace smt { func_decl * d = n->get_decl(); ptr_vector const & accessors = *m_util.get_constructor_accessors(d); SASSERT(n->get_num_args() == accessors.size()); - app_ref_vector bindings(m); + expr_ref_vector bindings(m); vector> used_enodes; used_enodes.push_back(std::make_tuple(nullptr, n)); for (unsigned i = 0; i < n->get_num_args(); ++i) { @@ -223,7 +223,7 @@ namespace smt { void theory_datatype::assert_update_field_axioms(enode * n) { m_stats.m_assert_update_field++; SASSERT(is_update_field(n)); - app* own = n->get_expr(); + app* own = n->get_app(); expr* arg1 = own->get_arg(0); func_decl * upd = n->get_decl(); func_decl * acc = to_func_decl(upd->get_parameter(0).get_ast()); @@ -706,7 +706,7 @@ namespace smt { return result; } - void theory_datatype::relevant_eh(app * n) { + void theory_datatype::relevant_eh(expr * n) { force_push(); TRACE(datatype, tout << "relevant_eh: " << mk_pp(n, m) << "\n";); SASSERT(ctx.relevancy()); @@ -1137,11 +1137,23 @@ namespace smt { }; model_value_proc * theory_datatype::mk_value(enode * n, model_generator & mg) { + auto mk_fallback = [&]() -> model_value_proc * { + app* val = to_app(m_factory->get_some_value(n->get_sort())); + TRACE(datatype, + tout << "fallback datatype value for " << pp(n, m) + << " = " << mk_pp(val, m) << "\n";); + return alloc(expr_wrapper_proc, val); + }; theory_var v = n->get_th_var(get_id()); + // Guard before using union-find: null_theory_var is not a valid index for m_find. + if (v == null_theory_var) + return mk_fallback(); v = m_find.find(v); - SASSERT(v != null_theory_var); + if (v == null_theory_var || static_cast(v) >= m_var_data.size() || m_var_data[v] == nullptr) + return mk_fallback(); var_data * d = m_var_data[v]; - SASSERT(d->m_constructor); + if (d->m_constructor == nullptr) + return mk_fallback(); func_decl * c_decl = d->m_constructor->get_decl(); datatype_value_proc * result = alloc(datatype_value_proc, c_decl); for (enode* arg : enode::args(d->m_constructor)) diff --git a/src/smt/theory_datatype.h b/src/smt/theory_datatype.h index 88c3be3a0..be6b3d61a 100644 --- a/src/smt/theory_datatype.h +++ b/src/smt/theory_datatype.h @@ -68,17 +68,17 @@ namespace smt { datatype_factory * m_factory; stats m_stats; - bool is_constructor(app * f) const { return m_util.is_constructor(f); } - bool is_recognizer(app * f) const { return m_util.is_recognizer(f); } - bool is_subterm_predicate(app * f) const { return m_util.is_subterm_predicate(f); } - bool is_accessor(app * f) const { return m_util.is_accessor(f); } - bool is_update_field(app * f) const { return m_util.is_update_field(f); } + bool is_constructor(expr * f) const { return m_util.is_constructor(f); } + bool is_recognizer(expr * f) const { return m_util.is_recognizer(f); } + bool is_subterm_predicate(expr * f) const { return m_util.is_subterm_predicate(f); } + bool is_accessor(expr * f) const { return m_util.is_accessor(f); } + bool is_update_field(expr * f) const { return m_util.is_update_field(f); } bool is_constructor(enode * n) const { return is_constructor(n->get_expr()); } bool is_recognizer(enode * n) const { return is_recognizer(n->get_expr()); } bool is_subterm_predicate(enode * n) const { return is_subterm_predicate(n->get_expr()); } bool is_accessor(enode * n) const { return is_accessor(n->get_expr()); } - bool is_update_field(enode * n) const { return m_util.is_update_field(n->get_expr()); } + bool is_update_field(enode * n) const { return is_update_field(n->get_expr()); } void assert_eq_axiom(enode * lhs, expr * rhs, literal antecedent); void assert_is_constructor_axiom(enode * n, func_decl * c, literal antecedent); @@ -148,7 +148,7 @@ namespace smt { bool use_diseqs() const override; void new_diseq_eh(theory_var v1, theory_var v2) override; void assign_eh(bool_var v, bool is_true) override; - void relevant_eh(app * n) override; + void relevant_eh(expr * n) override; void push_scope_eh() override; void pop_scope_eh(unsigned num_scopes) override; final_check_status final_check_eh(unsigned) override; diff --git a/src/smt/theory_dense_diff_logic_def.h b/src/smt/theory_dense_diff_logic_def.h index 83c65d810..243629db7 100644 --- a/src/smt/theory_dense_diff_logic_def.h +++ b/src/smt/theory_dense_diff_logic_def.h @@ -721,7 +721,7 @@ namespace smt { TRACE(ddl_model, tout << "ddl model\n"; for (theory_var v = 0; v < num_vars; ++v) { - tout << "#" << mk_pp(get_enode(v)->get_expr(), m) << " = " << m_assignment[v] << "\n"; + tout << "#" << mk_pp(get_expr(v), m) << " = " << m_assignment[v] << "\n"; }); } @@ -799,11 +799,11 @@ namespace smt { enode * n = get_enode(v); if (m_autil.is_zero(n->get_expr()) && !m_assignment[v].is_zero()) { numeral val = m_assignment[v]; - sort * s = n->get_expr()->get_sort(); + sort * s = n->get_sort(); // adjust the value of all variables that have the same sort. for (int v2 = 0; v2 < num_vars; ++v2) { enode * n2 = get_enode(v2); - if (n2->get_expr()->get_sort() == s) { + if (n2->get_sort() == s) { m_assignment[v2] -= val; } } @@ -813,7 +813,7 @@ namespace smt { TRACE(ddl_model, tout << "ddl model\n"; for (theory_var v = 0; v < num_vars; ++v) { - tout << "#" << mk_pp(get_enode(v)->get_expr(), m) << " = " << m_assignment[v] << "\n"; + tout << "#" << mk_pp(get_expr(v), m) << " = " << m_assignment[v] << "\n"; }); } diff --git a/src/smt/theory_diff_logic.h b/src/smt/theory_diff_logic.h index 720cdb9bb..ac73ca820 100644 --- a/src/smt/theory_diff_logic.h +++ b/src/smt/theory_diff_logic.h @@ -263,7 +263,7 @@ namespace smt { m_arith_eq_adapter.restart_eh(); } - void relevant_eh(app* e) override {} + void relevant_eh(expr* e) override {} void init_search_eh() override { m_arith_eq_adapter.init_search_eh(); diff --git a/src/smt/theory_diff_logic_def.h b/src/smt/theory_diff_logic_def.h index 7b7519d55..2d1909ba7 100644 --- a/src/smt/theory_diff_logic_def.h +++ b/src/smt/theory_diff_logic_def.h @@ -384,7 +384,7 @@ final_check_status theory_diff_logic::final_check_eh(unsigned level) { } for (enode* n : ctx.enodes()) { - family_id fid = n->get_expr()->get_family_id(); + family_id fid = n->get_family_id(); if (fid != get_family_id() && fid != m.get_basic_family_id() && !is_uninterp_const(n->get_expr())) { @@ -974,10 +974,9 @@ theory_var theory_diff_logic::expand(bool pos, theory_var v, rational & k) enode* e = get_enode(v); rational r; for (;;) { - app* n = e->get_expr(); - if (m_util.is_add(n) && n->get_num_args() == 2) { - app* x = to_app(n->get_arg(0)); - app* y = to_app(n->get_arg(1)); + expr *x = nullptr, *y = nullptr; + expr* n = e->get_expr(); + if (m_util.is_add(n, x, y)) { if (m_util.is_numeral(x, r)) { e = ctx.get_enode(y); } @@ -1024,8 +1023,8 @@ void theory_diff_logic::new_eq_or_diseq(bool is_eq, theory_var v1, theory_v app_ref eq(m), s2(m), t2(m); - app* s1 = get_enode(s)->get_expr(); - app* t1 = get_enode(t)->get_expr(); + expr* s1 = get_expr(s); + expr* t1 = get_expr(t); s2 = m_util.mk_sub(t1, s1); t2 = m_util.mk_numeral(k, s2->get_sort()); // t1 - s1 = k diff --git a/src/smt/theory_dl.cpp b/src/smt/theory_dl.cpp index ee8c94d9a..a3cd84853 100644 --- a/src/smt/theory_dl.cpp +++ b/src/smt/theory_dl.cpp @@ -166,20 +166,20 @@ namespace smt { } void apply_sort_cnstr(enode * n, sort * s) override { - app* term = n->get_expr(); + auto term = n->get_expr(); if (u().is_finite_sort(term)) { mk_rep(term); } } - void relevant_eh(app * n) override { + void relevant_eh(expr * n) override { if (u().is_finite_sort(n)) { sort* s = n->get_sort(); func_decl* r, *v; get_rep(s, r, v); - if (n->get_decl() != v) { + if (is_app(n) && to_app(n)->get_decl() != v) { expr* rep = m().mk_app(r, n); uint64_t vl; if (u().is_numeral_ext(n, vl)) { @@ -214,11 +214,12 @@ namespace smt { } } - bool mk_rep(app* n) { - unsigned num_args = n->get_num_args(); + bool mk_rep(expr* n) { + enode * e = nullptr; - for (unsigned i = 0; i < num_args; ++i) { - ctx.internalize(n->get_arg(i), false); + if (is_app(n)) { + for (auto arg : *to_app(n)) + ctx.internalize(arg, false); } if (ctx.e_internalized(n)) { e = ctx.get_enode(n); diff --git a/src/smt/theory_finite_set.cpp b/src/smt/theory_finite_set.cpp index 845914427..899b4d1de 100644 --- a/src/smt/theory_finite_set.cpp +++ b/src/smt/theory_finite_set.cpp @@ -239,8 +239,9 @@ namespace smt { return true; } - void theory_finite_set::relevant_eh(app* t) { - add_immediate_axioms(t); + void theory_finite_set::relevant_eh(expr* t) { + if (is_app(t)) + add_immediate_axioms(to_app(t)); } void theory_finite_set::apply_sort_cnstr(enode* n, sort* s) { diff --git a/src/smt/theory_finite_set.h b/src/smt/theory_finite_set.h index 472249960..eb7e08fec 100644 --- a/src/smt/theory_finite_set.h +++ b/src/smt/theory_finite_set.h @@ -155,7 +155,7 @@ namespace smt { bool can_propagate() override; void propagate() override; void assign_eh(bool_var v, bool is_true) override; - void relevant_eh(app *n) override; + void relevant_eh(expr *n) override; theory * mk_fresh(context * new_ctx) override; char const * get_name() const override { return "finite_set"; } diff --git a/src/smt/theory_fpa.cpp b/src/smt/theory_fpa.cpp index ec64bfd61..107456455 100644 --- a/src/smt/theory_fpa.cpp +++ b/src/smt/theory_fpa.cpp @@ -298,9 +298,9 @@ namespace smt { SASSERT(s->get_family_id() == get_family_id()); SASSERT(m_fpa_util.is_float(s) || m_fpa_util.is_rm(s)); SASSERT(m_fpa_util.is_float(n->get_expr()) || m_fpa_util.is_rm(n->get_expr())); - SASSERT(n->get_expr()->get_decl()->get_range() == s); + SASSERT(n->get_decl()->get_range() == s); - app * owner = n->get_expr(); + expr * owner = n->get_expr(); if (!is_attached_to_var(n)) { attach_new_th_var(n); @@ -437,7 +437,7 @@ namespace smt { assert_cnstr(cnstr); } - void theory_fpa::relevant_eh(app * n) { + void theory_fpa::relevant_eh(expr * n) { TRACE(t_fpa, tout << "relevant_eh for: " << mk_ismt2_pp(n, m) << "\n";); mpf_manager & mpfm = m_fpa_util.fm(); @@ -472,10 +472,26 @@ namespace smt { wu = m.mk_eq(m_converter.unwrap(wrapped, n->get_sort()), n); TRACE(t_fpa, tout << "w/u eq: " << std::endl << mk_ismt2_pp(wu, m) << std::endl;); assert_cnstr(wu); + + // For non-FPA-family terms (e.g. datatype accessors like + // get-fp), mk_uf creates a separate BV UF that is not + // linked to bvwrap. Assert wrap(n) == concat(conv_components) + // to close the constraint gap (same pattern as numerals above). + if (!is_app(n) || to_app(n)->get_family_id() != get_family_id()) { + expr_ref conv_e = convert(n); + if (m_fpa_util.is_fp(conv_e) && to_app(conv_e)->get_num_args() == 3) { + app_ref conv_a(m); + conv_a = to_app(conv_e.get()); + expr_ref cc(m); + cc = m_bv_util.mk_concat({conv_a->get_arg(0), conv_a->get_arg(1), conv_a->get_arg(2)}); + assert_cnstr(m.mk_eq(wrapped, cc)); + assert_cnstr(mk_side_conditions()); + } + } } } } - else if (n->get_family_id() == get_family_id()) { + else if (is_app(n) && to_app(n)->get_family_id() == get_family_id()) { // These are the conversion functions fp.to_* */ SASSERT(!m_fpa_util.is_float(n) && !m_fpa_util.is_rm(n)); } diff --git a/src/smt/theory_fpa.h b/src/smt/theory_fpa.h index badce4e2a..14797f62a 100644 --- a/src/smt/theory_fpa.h +++ b/src/smt/theory_fpa.h @@ -104,7 +104,7 @@ namespace smt { model_value_proc * mk_value(enode * n, model_generator & mg) override; void assign_eh(bool_var v, bool is_true) override; - void relevant_eh(app * n) override; + void relevant_eh(expr * n) override; void init_model(model_generator & m) override; void finalize_model(model_generator & mg) override; diff --git a/src/smt/theory_intblast.cpp b/src/smt/theory_intblast.cpp index 1a17784b9..2d2c69951 100644 --- a/src/smt/theory_intblast.cpp +++ b/src/smt/theory_intblast.cpp @@ -153,7 +153,7 @@ namespace smt { void theory_intblast::apply_sort_cnstr(enode* n, sort* s) { SASSERT(bv.is_bv_sort(s)); if (!is_attached_to_var(n)) { - m_translator.internalize_bv(n->get_expr()); + m_translator.internalize_bv(n->get_app()); auto v = mk_var(n); ctx.attach_th_var(n, this, v); } diff --git a/src/smt/theory_lra.cpp b/src/smt/theory_lra.cpp index fa35c2372..b6ef6340a 100644 --- a/src/smt/theory_lra.cpp +++ b/src/smt/theory_lra.cpp @@ -228,7 +228,7 @@ class theory_lra::imp { bool is_real(enode* n) const { return a.is_real(n->get_expr()); } enode* get_enode(theory_var v) const { return th.get_enode(v); } enode* get_enode(expr* e) const { return ctx().get_enode(e); } - expr* get_owner(theory_var v) const { return get_enode(v)->get_expr(); } + expr* get_expr(theory_var v) const { return get_enode(v)->get_expr(); } enode_pp pp(enode* n) const { return enode_pp(n, ctx()); } enode_pp pp(theory_var v) const { return pp(get_enode(v)); } mk_bounded_pp bpp(expr* e) { return mk_bounded_pp(e, m); } @@ -449,25 +449,42 @@ class theory_lra::imp { internalize_term(to_app(n)); internalize_term(to_app(n1)); internalize_term(to_app(n2)); + internalize_term(to_app(mod)); theory_var q = mk_var(n); theory_var x = mk_var(n1); theory_var y = mk_var(n2); - m_nla->add_idivision(register_theory_var_in_lar_solver(q), register_theory_var_in_lar_solver(x), register_theory_var_in_lar_solver(y)); + theory_var rv = mk_var(mod); + m_nla->add_idivision(register_theory_var_in_lar_solver(q), register_theory_var_in_lar_solver(x), register_theory_var_in_lar_solver(y), register_theory_var_in_lar_solver(rv)); } if (a.is_numeral(n2) && a.is_bounded(n1)) { ensure_nla(); internalize_term(to_app(n)); internalize_term(to_app(n1)); internalize_term(to_app(n2)); + internalize_term(to_app(mod)); theory_var q = mk_var(n); theory_var x = mk_var(n1); theory_var y = mk_var(n2); - m_nla->add_bounded_division(register_theory_var_in_lar_solver(q), register_theory_var_in_lar_solver(x), register_theory_var_in_lar_solver(y)); + theory_var rv = mk_var(mod); + m_nla->add_bounded_division(register_theory_var_in_lar_solver(q), register_theory_var_in_lar_solver(x), register_theory_var_in_lar_solver(y), register_theory_var_in_lar_solver(rv)); } } else if (a.is_mod(n, n1, n2)) { if (!a.is_numeral(n2, r) || r.is_zero()) found_underspecified(n); - if (!ctx().relevancy()) mk_idiv_mod_axioms(n1, n2); + if (!ctx().relevancy()) mk_idiv_mod_axioms(n1, n2); + if (m_nla && a.is_numeral(n2) && !r.is_zero()) { + app_ref div(a.mk_idiv(n1, n2), m); + ctx().internalize(div, false); + internalize_term(to_app(div)); + internalize_term(to_app(n1)); + internalize_term(to_app(n2)); + internalize_term(t); + theory_var q = mk_var(div); + theory_var x = mk_var(n1); + theory_var y = mk_var(n2); + theory_var rv = mk_var(n); + m_nla->add_bounded_division(register_theory_var_in_lar_solver(q), register_theory_var_in_lar_solver(x), register_theory_var_in_lar_solver(y), register_theory_var_in_lar_solver(rv)); + } } else if (a.is_rem(n, n1, n2)) { if (!a.is_numeral(n2, r) || r.is_zero()) found_underspecified(n); @@ -1101,7 +1118,7 @@ public: m_nla->simplify(); } - void relevant_eh(app* n) { + void relevant_eh(expr* n) { expr* n1, *n2; if (a.is_mod(n, n1, n2)) mk_idiv_mod_axioms(n1, n2); @@ -1110,11 +1127,11 @@ public: else if (a.is_div(n, n1, n2)) mk_div_axiom(n1, n2); else if (a.is_to_int(n)) - mk_to_int_axiom(n); + mk_to_int_axiom(to_app(n)); else if (a.is_is_int(n)) - mk_is_int_axiom(n); + mk_is_int_axiom(to_app(n)); else if (m.is_ite(n)) - mk_ite_axiom(n); + mk_ite_axiom(to_app(n)); else if (a.is_power(n, n1, n2)) mk_power_axiom(n, n1, n2); } @@ -1235,9 +1252,9 @@ public: /// abs(r) > r >= 0 void assert_idiv_mod_axioms(theory_var u, theory_var v, theory_var w, rational const& r) { app_ref term(m); - term = a.mk_mul(a.mk_numeral(r, true), get_enode(w)->get_expr()); - term = a.mk_add(get_enode(v)->get_expr(), term); - term = a.mk_sub(get_enode(u)->get_expr(), term); + term = a.mk_mul(a.mk_numeral(r, true), get_expr(w)); + term = a.mk_add(get_expr(v), term); + term = a.mk_sub(get_expr(u), term); theory_var z = internalize_def(term); lpvar zi = register_theory_var_in_lar_solver(z); lpvar vi = register_theory_var_in_lar_solver(v); @@ -1820,7 +1837,7 @@ public: rational lc = denominator(k); for (auto const& kv : coeffs) { theory_var w = kv.m_key; - expr* o = get_enode(w)->get_expr(); + expr* o = get_expr(w); is_int = a.is_int(o); if (!is_int) break; lc = lcm(lc, denominator(kv.m_value)); @@ -2109,6 +2126,14 @@ public: m_explanation = l.expl(); literal_vector core; SASSERT(!m_lemma.is_empty()); + TRACE(nla_solver, + tout << "varmap:"; + for (lpvar j : m_nla->get_core().collect_vars(l)) { + auto ext = lp().local_to_external(j); + if (ext != lp::null_lpvar && static_cast(ext) < th.get_num_vars()) + tout << " " << lp().get_variable_name(j) << "=" << pp(ext); + } + tout << "\n";); for (auto const& ineq : m_lemma.ineqs()) { auto lit = mk_literal(ineq); core.push_back(~lit); @@ -2487,7 +2512,7 @@ public: lpvar vi = be.m_j; if (lp().column_has_term(vi)) return; - expr_ref w(get_enode(v)->get_expr(), m); + expr_ref w(get_expr(v), m); if (a.is_add(w) || a.is_numeral(w) || m.is_ite(w)) return; literal bound = null_literal; @@ -3392,7 +3417,7 @@ public: theory_var v = lp().local_to_external(vi); rational val; TRACE(arith, tout << lp().get_variable_name(vi) << " " << v << "\n";); - if (v != null_theory_var && a.is_numeral(get_owner(v), val) && bound == val) { + if (v != null_theory_var && a.is_numeral(get_expr(v), val) && bound == val) { dep = nullptr; return bound == val; } @@ -4121,7 +4146,7 @@ public: // Overload: create blocker from a saved impq value (used when x has been restored) expr_ref mk_gt(theory_var v, lp::impq const& val) { - expr* obj = get_enode(v)->get_expr(); + expr* obj = get_expr(v); rational r = val.x; expr_ref e(m); if (a.is_int(obj->get_sort())) { @@ -4179,7 +4204,7 @@ public: app_ref coeffs2app(u_map const& coeffs, rational const& offset, bool is_int) { expr_ref_vector args(m); for (auto const& [w, coeff] : coeffs) { - expr* o = get_enode(w)->get_expr(); + expr* o = get_expr(w); if (coeff.is_zero()) { // continue } @@ -4226,13 +4251,14 @@ public: app_ref mk_obj(theory_var v) { auto t = get_lpvar(v); - bool is_int = a.is_int(get_enode(v)->get_expr()); + auto e = th.get_expr(v); + bool is_int = a.is_int(e); if (lp().column_has_term(t)) { return mk_term(lp().get_term(t), is_int); } else { // theory_var w = lp().external_to_local(vi); - return app_ref(get_enode(v)->get_expr(), m); + return app_ref(to_app(e), m); } } @@ -4240,7 +4266,7 @@ public: rational r = val.get_rational(); bool is_strict = val.get_infinitesimal().is_pos(); app_ref b(m); - bool is_int = a.is_int(get_enode(v)->get_expr()); + bool is_int = a.is_int(get_expr(v)); TRACE(arith, display(tout << "v" << v << "\n");); if (is_strict) { b = a.mk_le(mk_obj(v), a.mk_numeral(r, is_int)); @@ -4446,7 +4472,7 @@ void theory_lra::pop_scope_eh(unsigned num_scopes) { void theory_lra::restart_eh() { m_imp->restart_eh(); } -void theory_lra::relevant_eh(app* e) { +void theory_lra::relevant_eh(expr* e) { m_imp->relevant_eh(e); } void theory_lra::init_search_eh() { diff --git a/src/smt/theory_lra.h b/src/smt/theory_lra.h index a6bfcb570..05c46cf3f 100644 --- a/src/smt/theory_lra.h +++ b/src/smt/theory_lra.h @@ -59,7 +59,7 @@ namespace smt { void restart_eh() override; - void relevant_eh(app* e) override; + void relevant_eh(expr* e) override; void init_search_eh() override; diff --git a/src/smt/theory_pb.cpp b/src/smt/theory_pb.cpp index 862440e43..40c2cec2a 100644 --- a/src/smt/theory_pb.cpp +++ b/src/smt/theory_pb.cpp @@ -2367,11 +2367,10 @@ namespace smt { } model_value_proc * theory_pb::mk_value(enode * n, model_generator & mg) { - app* a = n->get_expr(); + auto a = n->get_app(); pb_model_value_proc* p = alloc(pb_model_value_proc, a); - for (unsigned i = 0; i < a->get_num_args(); ++i) { - p->add(ctx.get_enode(a->get_arg(i))); - } + for (auto arg : *a) + p->add(ctx.get_enode(arg)); return p; } diff --git a/src/smt/theory_recfun.cpp b/src/smt/theory_recfun.cpp index d2265532a..2875e7460 100644 --- a/src/smt/theory_recfun.cpp +++ b/src/smt/theory_recfun.cpp @@ -99,7 +99,7 @@ namespace smt { * then case-expand `n`. If it's a macro we can also immediately * body-expand it. */ - void theory_recfun::relevant_eh(app * n) { + void theory_recfun::relevant_eh(expr * n) { SASSERT(ctx.relevancy()); // TRACEFN("relevant_eh: (defined) " << u().is_defined(n) << " " << mk_pp(n, m)); if (u().is_defined(n) && u().has_defs()) diff --git a/src/smt/theory_recfun.h b/src/smt/theory_recfun.h index 25e77a469..16746f27e 100644 --- a/src/smt/theory_recfun.h +++ b/src/smt/theory_recfun.h @@ -61,8 +61,8 @@ namespace smt { bool is_disabled_guard(expr* guard) { return m_disabled_guards.contains(guard); } recfun::util & u() const { return m_util; } - bool is_defined(app * f) const { return u().is_defined(f); } - bool is_case_pred(app * f) const { return u().is_case_pred(f); } + bool is_defined(expr * f) const { return u().is_defined(f); } + bool is_case_pred(expr * f) const { return u().is_case_pred(f); } bool is_defined(enode * e) const { return is_defined(e->get_expr()); } bool is_case_pred(enode * e) const { return is_case_pred(e->get_expr()); } @@ -90,7 +90,7 @@ namespace smt { bool internalize_atom(app * atom, bool gate_ctx) override; bool internalize_term(app * term) override; void reset_eh() override; - void relevant_eh(app * n) override; + void relevant_eh(expr * n) override; char const * get_name() const override; final_check_status final_check_eh(unsigned) override; void assign_eh(bool_var v, bool is_true) override; diff --git a/src/smt/theory_seq.cpp b/src/smt/theory_seq.cpp index ec05fdb16..8175e5192 100644 --- a/src/smt/theory_seq.cpp +++ b/src/smt/theory_seq.cpp @@ -1485,8 +1485,7 @@ bool theory_seq::internalize_term(app* term) { return true; } - if (m.is_bool(term) && - (m_util.str.is_in_re(term) || m_sk.is_skolem(term))) { + if (m.is_bool(term) && (m_util.str.is_in_re(term) || m_sk.is_skolem(term))) { bool_var bv = ctx.mk_bool_var(term); ctx.set_var_theory(bv, get_id()); ctx.mark_as_relevant(bv); @@ -2104,7 +2103,7 @@ app* theory_seq::get_ite_value(expr* e) { } model_value_proc * theory_seq::mk_value(enode * n, model_generator & mg) { - app* e = n->get_expr(); + expr* e = n->get_expr(); TRACE(seq, tout << mk_pp(e, m) << "\n";); // Shortcut for well-founded values to avoid some quadratic overhead @@ -2164,7 +2163,7 @@ model_value_proc * theory_seq::mk_value(enode * n, model_generator & mg) { } -app* theory_seq::mk_value(app* e) { +app* theory_seq::mk_value(expr* e) { expr_ref result(m); e = get_ite_value(e); result = m_rep.find(e); @@ -3287,7 +3286,10 @@ void theory_seq::pop_scope_eh(unsigned num_scopes) { void theory_seq::restart_eh() { } -void theory_seq::relevant_eh(app* n) { +void theory_seq::relevant_eh(expr* _n) { + if (!is_app(_n)) + return; + app *n = to_app(_n); if (m_util.str.is_index(n) || m_util.str.is_replace(n) || m_util.str.is_extract(n) || diff --git a/src/smt/theory_seq.h b/src/smt/theory_seq.h index 800fe5600..ee2ae002a 100644 --- a/src/smt/theory_seq.h +++ b/src/smt/theory_seq.h @@ -392,7 +392,7 @@ namespace smt { void push_scope_eh() override; void pop_scope_eh(unsigned num_scopes) override; void restart_eh() override; - void relevant_eh(app* n) override; + void relevant_eh(expr* n) override; bool should_research(expr_ref_vector &) override; void add_theory_assumptions(expr_ref_vector & assumptions) override; theory* mk_fresh(context* new_ctx) override { return alloc(theory_seq, *new_ctx); } @@ -629,7 +629,7 @@ namespace smt { void init() override; // model building - app* mk_value(app* a); + app* mk_value(expr* a); trail_stack& get_trail_stack() { return m_trail_stack; } void merge_eh(theory_var, theory_var, theory_var v1, theory_var v2) {} diff --git a/src/smt/theory_special_relations.cpp b/src/smt/theory_special_relations.cpp index aec069a02..4547264fb 100644 --- a/src/smt/theory_special_relations.cpp +++ b/src/smt/theory_special_relations.cpp @@ -174,8 +174,8 @@ namespace smt { } void theory_special_relations::new_eq_eh(theory_var v1, theory_var v2) { - app* t1 = get_expr(v1); - app* t2 = get_expr(v2); + expr* t1 = get_expr(v1); + expr* t2 = get_expr(v2); literal eq = mk_eq(t1, t2, false); for (auto const& kv : m_relations) { relation& r = *kv.m_value; diff --git a/src/smt/theory_utvpi.h b/src/smt/theory_utvpi.h index a917910e9..e94df49eb 100644 --- a/src/smt/theory_utvpi.h +++ b/src/smt/theory_utvpi.h @@ -239,7 +239,7 @@ namespace smt { m_arith_eq_adapter.restart_eh(); } - void relevant_eh(app* e) override {} + void relevant_eh(expr* e) override {} void init_search_eh() override { m_arith_eq_adapter.init_search_eh(); @@ -323,7 +323,7 @@ namespace smt { void new_eq_or_diseq(bool is_eq, th_var v1, th_var v2, justification& eq_just); - bool is_int(theory_var v) const { return a.is_int(get_enode(v)->get_expr()); } + bool is_int(theory_var v) const { return a.is_int(get_expr(v)); } th_var get_zero(sort* s) { return a.is_int(s) ? m_izero : m_rzero; } diff --git a/src/smt/theory_utvpi_def.h b/src/smt/theory_utvpi_def.h index 9086f13aa..9d011299d 100644 --- a/src/smt/theory_utvpi_def.h +++ b/src/smt/theory_utvpi_def.h @@ -170,8 +170,8 @@ namespace smt { // app_ref eq(m), s2(m), t2(m); - app* s1 = get_enode(s)->get_expr(); - app* t1 = get_enode(t)->get_expr(); + expr* s1 = get_expr(s); + expr* t1 = get_expr(t); s2 = a.mk_sub(t1, s1); t2 = a.mk_numeral(k, s2->get_sort()); eq = m.mk_eq(s2.get(), t2.get()); @@ -588,7 +588,7 @@ namespace smt { expr* x, *y; rational r; for (;;) { - app* n = e->get_expr(); + auto n = e->get_expr(); if (a.is_add(n, x, y)) { if (a.is_numeral(x, r)) { e = ctx.get_enode(y); @@ -906,7 +906,7 @@ namespace smt { num = num/rational(2); SASSERT(!is_int || num.is_int()); TRACE(utvpi, - expr* n = get_enode(v)->get_expr(); + expr* n = get_expr(v); tout << mk_pp(n, m) << " |-> (" << val1 << " - " << val2 << ")/2 = " << num << "\n";); return num; diff --git a/src/solver/CMakeLists.txt b/src/solver/CMakeLists.txt index 86316c86e..9c6ba9d6e 100644 --- a/src/solver/CMakeLists.txt +++ b/src/solver/CMakeLists.txt @@ -5,6 +5,7 @@ z3_add_component(solver combined_solver.cpp mus.cpp parallel_tactical.cpp + parallel_tactical2.cpp simplifier_solver.cpp slice_solver.cpp smt_logics.cpp diff --git a/src/solver/parallel_params.pyg b/src/solver/parallel_params.pyg index 60a77d49a..2aa2acf77 100644 --- a/src/solver/parallel_params.pyg +++ b/src/solver/parallel_params.pyg @@ -4,6 +4,7 @@ def_module_params('parallel', export=True, params=( ('enable', BOOL, False, 'enable parallel solver by default on selected tactics (for QF_BV)'), + ('enable2', BOOL, False, 'enable (experimental) parallel solver by default on selected tactics (for QF_BV)'), ('threads.max', UINT, 10000, 'caps maximal number of threads below the number of processors'), ('conquer.batch_size', UINT, 100, 'number of cubes to batch together for fast conquer'), ('conquer.restart.max', UINT, 5, 'maximal number of restarts during conquer phase'), diff --git a/src/solver/parallel_tactical2.cpp b/src/solver/parallel_tactical2.cpp new file mode 100644 index 000000000..ebae4cb57 --- /dev/null +++ b/src/solver/parallel_tactical2.cpp @@ -0,0 +1,904 @@ +/*++ +Copyright (c) 2024 Microsoft Corporation + +Module Name: + + parallel_tactical2.cpp + +Abstract: + + Parallel portfolio solver using the solver API. + + Models the internals after smt/smt_parallel.cpp but operates on generic + solver objects (smt_solver, inc_sat_solver, etc.) via the solver interface + instead of accessing smt::context internals directly. + + Key features compared to parallel_tactical.cpp: + - Search tree for coordinated non-chronological backtracking (from smt_parallel). + - Shared clause pool: learned conflict clauses are broadcast to all workers. + - Shared backbone/unit pool: base-level units propagated by one worker are + asserted as facts on every other worker's solver. + - Workers reuse their solver state across multiple cube checks, accumulating + learned clauses (same pattern as smt_parallel workers). + + Key differences from smt_parallel: + - Uses the solver API throughout (translate, check_sat, get_trail, cube, + get_model, get_unsat_core, assert_expr, push, pop, updt_params, …) + rather than accessing smt::context members directly. + - Works with any conforming solver implementation. + + Cube path management follows the assumption-based pattern from smt_parallel: + - The worker's solver base assertion set is fixed at construction (the full + problem is translated into the worker's own ast_manager once). + - Shared clauses discovered by other workers are appended to the base set via + assert_expr at any time. + - The current cube path is passed as extra assumptions on every check_sat call, + so the solver can reuse learned clauses across different cube checks. + + Split atom selection is performed by temporarily pushing the cube path onto + the solver, calling solver::cube(), retrieving the first proposed literal, and + then popping, so that the base state is preserved. + +Author: + + (based on smt_parallel.cpp by nbjorner / Ilana Shapiro, and + parallel_tactical.cpp by nbjorner / Miguel Neves) + +--*/ + +#include "util/scoped_ptr_vector.h" +#include "util/uint_set.h" +#include "ast/ast_pp.h" +#include "ast/ast_ll_pp.h" +#include "ast/ast_util.h" +#include "ast/ast_translation.h" +#include "solver/solver.h" +#include "solver/parallel_tactical2.h" +#include "solver/parallel_params.hpp" +#include "solver/solver_preprocess.h" +#include "util/search_tree.h" +#include "tactic/tactic.h" +#include "tactic/tactical.h" +#include "solver/solver2tactic.h" + +#include +#include + +/* ------------------------------------------------------------------ */ +/* Single-threaded stub */ +/* ------------------------------------------------------------------ */ + +class non_parallel_tactic2 : public tactic { +public: + non_parallel_tactic2(solver*, params_ref const&) {} + char const* name() const override { return "parallel_tactic2"; } + void operator()(const goal_ref&, goal_ref_buffer&) override { + throw default_exception("parallel_tactic2 is disabled in single-threaded mode"); + } + tactic* translate(ast_manager&) override { return nullptr; } + void cleanup() override {} +}; + +#ifdef SINGLE_THREAD + +tactic* mk_parallel_tactic2(solver* s, params_ref const& p) { + return alloc(non_parallel_tactic2, s, p); +} + +#else + +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* Search-tree literal configuration */ +/* ------------------------------------------------------------------ */ + +struct solver_cube_config { + using literal = expr_ref; + static bool literal_is_null(expr_ref const& l) { return l == nullptr; } + static std::ostream& display_literal(std::ostream& out, expr_ref const& l) { + if (l) return out << mk_bounded_pp(l, l.get_manager()); + return out << "(null)"; + } +}; + +/* ------------------------------------------------------------------ */ +/* parallel_solver – the core portfolio engine */ +/* ------------------------------------------------------------------ */ + +class parallel_solver { + + /* ---- forward declarations ---- */ + class worker; + + /* ---- node lease (mirrors smt_parallel) ---- */ + struct node_lease { + search_tree::node* leased_node = nullptr; + unsigned cancel_epoch = 0; + bool cancel_signaled = false; + }; + + /* ---- shared clause entry ---- */ + struct shared_clause { + unsigned source_worker_id; + expr_ref clause; + }; + + /* ================================================================ + * batch_manager + * Coordinates workers: distributes cubes, collects clauses/units, + * stores the final result (sat model / unsat core / exception). + * ================================================================ */ + class batch_manager { + + enum state { + is_running, + is_sat, + is_unsat, + is_exception_msg, + is_exception_code + }; + + struct stats { + unsigned m_num_cubes = 0; + unsigned m_max_cube_depth = 0; + unsigned m_backbones_found = 0; + }; + + ast_manager& m; + parallel_solver& p; + std::mutex mux; + state m_state = state::is_running; + stats m_stats; + + search_tree::tree m_search_tree; + vector m_worker_leases; + + /* shared clause pool (guarded by mux) */ + vector m_shared_clause_trail; + obj_hashtable m_shared_clause_set; + + /* shared backbone / unit pool (guarded by mux) */ + obj_hashtable m_global_backbones; + + /* result storage (guarded by mux) */ + unsigned m_exception_code = 0; + std::string m_exception_msg; + model_ref m_model; /* sat model translated to m */ + expr_ref_vector m_unsat_core; /* unsat core translated to m */ + + /* ---- cancellation helpers (called under mux) ---- */ + void cancel_workers_unlocked() { + IF_VERBOSE(1, verbose_stream() << "par2: canceling workers\n"); + for (auto* w : p.m_workers) + w->cancel(); + } + + void release_lease_unlocked(unsigned worker_id, + search_tree::node* n) { + if (worker_id >= m_worker_leases.size()) return; + auto& lease = m_worker_leases[worker_id]; + if (!lease.leased_node || lease.leased_node != n) return; + m_search_tree.dec_active_workers(lease.leased_node); + lease = {}; + } + + void cancel_closed_leases_unlocked(unsigned source_worker_id) { + unsigned n = std::min(m_worker_leases.size(), p.m_workers.size()); + for (unsigned id = 0; id < n; ++id) { + if (id == source_worker_id) continue; + auto const& lease = m_worker_leases[id]; + if (lease.leased_node && !lease.cancel_signaled && + m_search_tree.is_lease_canceled(lease.leased_node, lease.cancel_epoch)) { + p.m_workers[id]->cancel_lease(); + m_worker_leases[id].cancel_signaled = true; + } + } + } + + void collect_clause_unlocked(ast_translation& l2g, + unsigned source_worker_id, + expr* clause) { + expr* g_clause = l2g(clause); + if (!m_shared_clause_set.contains(g_clause)) { + m_shared_clause_set.insert(g_clause); + shared_clause sc{source_worker_id, expr_ref(g_clause, m)}; + m_shared_clause_trail.push_back(std::move(sc)); + } + } + + bool is_global_backbone_unlocked(ast_translation& l2g, + expr* bb_cand) { + expr_ref cand(l2g(bb_cand), m); + return m_global_backbones.contains(cand.get()); + } + + public: + + batch_manager(ast_manager& m, parallel_solver& p) + : m(m), p(p), + m_search_tree(expr_ref(m)), + m_unsat_core(m) {} + + /* ---- initialisation ---- */ + void initialize(unsigned num_workers, + unsigned initial_max_thread_conflicts = 1000) { + m_state = state::is_running; + m_search_tree.reset(); + m_search_tree.set_effort_unit(initial_max_thread_conflicts); + m_worker_leases.reset(); + m_worker_leases.resize(num_workers); + m_shared_clause_trail.reset(); + m_shared_clause_set.reset(); + m_global_backbones.reset(); + m_model = nullptr; + m_unsat_core.reset(); + } + + /* ---- result setters (called by workers, guarded by mux) ---- */ + void set_sat(ast_translation& l2g, model& mdl) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "par2: batch_manager SAT\n"); + if (m_state != state::is_running) return; + m_state = state::is_sat; + m_model = mdl.translate(l2g); + cancel_workers_unlocked(); + } + + void set_unsat(ast_translation& l2g, + expr_ref_vector const& core) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "par2: batch_manager UNSAT\n"); + if (m_state != state::is_running) return; + m_state = state::is_unsat; + SASSERT(m_unsat_core.empty()); + for (expr* c : core) + m_unsat_core.push_back(l2g(c)); + cancel_workers_unlocked(); + } + + void set_exception(std::string const& msg) { + std::scoped_lock lock(mux); + IF_VERBOSE(1, verbose_stream() << "par2: batch_manager exception: " << msg << "\n"); + if (m_state != state::is_running) return; + m_state = state::is_exception_msg; + m_exception_msg = msg; + cancel_workers_unlocked(); + } + + void set_exception(unsigned error_code) { + std::scoped_lock lock(mux); + if (m_state != state::is_running) return; + m_state = state::is_exception_code; + m_exception_code = error_code; + cancel_workers_unlocked(); + } + + /* ---- cube distribution (called by workers) ---- */ + bool get_cube(ast_translation& g2l, unsigned id, + expr_ref_vector& cube, bool is_first_run, + node_lease& lease) { + std::scoped_lock lock(mux); + cube.reset(); + if (m_search_tree.is_closed()) return false; + if (m_state != state::is_running) return false; + + auto* t = is_first_run + ? m_search_tree.activate_root() + : m_search_tree.activate_best_node(); + if (!t) return false; + + lease.leased_node = t; + lease.cancel_epoch = t->get_cancel_epoch(); + if (id >= m_worker_leases.size()) + m_worker_leases.resize(id + 1); + m_worker_leases[id] = lease; + + /* build cube from path root → t */ + for (auto* cur = t; cur; cur = cur->parent()) { + if (solver_cube_config::literal_is_null(cur->get_literal())) + break; + cube.push_back(expr_ref(g2l(cur->get_literal().get()), g2l.to())); + } + return true; + } + + /* ---- backtrack on conflict (called by workers) ---- */ + void backtrack(ast_translation& l2g, unsigned worker_id, + expr_ref_vector const& core, + node_lease const& lease) { + std::scoped_lock lock(mux); + if (m_state != state::is_running) return; + + vector g_core; + for (auto c : core) + g_core.push_back(expr_ref(l2g(c), m)); + + if (!m_search_tree.is_lease_canceled( + lease.leased_node, lease.cancel_epoch)) { + release_lease_unlocked(worker_id, lease.leased_node); + m_search_tree.backtrack(lease.leased_node, g_core); + } + + cancel_closed_leases_unlocked(worker_id); + + IF_VERBOSE(2, m_search_tree.display(verbose_stream() << "\n");); + + if (m_search_tree.is_closed()) { + IF_VERBOSE(1, verbose_stream() << "par2: search tree closed → UNSAT\n"); + m_state = state::is_unsat; + for (auto& e : m_search_tree.get_core_from_root()) + m_unsat_core.push_back(e.get()); + cancel_workers_unlocked(); + } + } + + /* ---- try to split (called on undef) ---- */ + void try_split(ast_translation& l2g, unsigned worker_id, + node_lease const& lease, + expr* atom, unsigned effort) { + std::scoped_lock lock(mux); + if (m_state != state::is_running) return; + if (m_search_tree.is_lease_canceled( + lease.leased_node, lease.cancel_epoch)) return; + + expr_ref lit(m), nlit(m); + lit = l2g(atom); + nlit = mk_not(m, lit); + + bool did_split = m_search_tree.try_split( + lease.leased_node, lease.cancel_epoch, + lit, nlit, effort); + + release_lease_unlocked(worker_id, lease.leased_node); + + if (did_split) { + ++m_stats.m_num_cubes; + m_stats.m_max_cube_depth = std::max( + m_stats.m_max_cube_depth, + lease.leased_node->depth() + 1); + IF_VERBOSE(1, verbose_stream() << "par2: split on " + << mk_bounded_pp(lit, m, 3) << "\n"); + } + } + + void release_lease(unsigned worker_id, node_lease const& lease) { + std::scoped_lock lock(mux); + release_lease_unlocked(worker_id, lease.leased_node); + } + + bool lease_canceled(node_lease const& lease) { + std::scoped_lock lock(mux); + return m_state == state::is_running && + m_search_tree.is_lease_canceled( + lease.leased_node, lease.cancel_epoch); + } + + /* ---- clause sharing ---- */ + void collect_clause(ast_translation& l2g, + unsigned source_worker_id, + expr* clause) { + std::scoped_lock lock(mux); + collect_clause_unlocked(l2g, source_worker_id, clause); + } + + expr_ref_vector return_shared_clauses(ast_translation& g2l, + unsigned& worker_limit, + unsigned worker_id) { + std::scoped_lock lock(mux); + expr_ref_vector result(g2l.to()); + for (unsigned i = worker_limit; i < m_shared_clause_trail.size(); ++i) { + if (m_shared_clause_trail[i].source_worker_id != worker_id) + result.push_back(g2l(m_shared_clause_trail[i].clause.get())); + } + worker_limit = m_shared_clause_trail.size(); + return result; + } + + /* ---- backbone / unit sharing ---- */ + bool collect_global_backbone(ast_translation& l2g, + expr_ref const& backbone, + unsigned source_worker_id = UINT_MAX) { + std::scoped_lock lock(mux); + if (is_global_backbone_unlocked(l2g, backbone.get())) + return false; + expr_ref g_bb(l2g(backbone.get()), m); + m_global_backbones.insert(g_bb.get()); + ++m_stats.m_backbones_found; + IF_VERBOSE(2, verbose_stream() << "par2: new backbone " + << mk_bounded_pp(g_bb, m, 3) << "\n"); + /* share it as a unit clause so other workers pick it up */ + collect_clause_unlocked(l2g, source_worker_id, backbone.get()); + return true; + } + + /* ---- result accessors ---- */ + lbool get_result() const { + if (m.limit().is_canceled()) return l_undef; + switch (m_state) { + case state::is_running: + throw default_exception("par2: inconsistent end state"); + case state::is_sat: return l_true; + case state::is_unsat: return l_false; + case state::is_exception_msg: + throw default_exception(m_exception_msg.c_str()); + case state::is_exception_code: + throw z3_error(m_exception_code); + default: + UNREACHABLE(); + return l_undef; + } + } + + model_ref& get_model() { return m_model; } + + expr_ref_vector const& get_unsat_core() const { return m_unsat_core; } + + void collect_statistics(statistics& st) const { + st.update("par2-cubes", m_stats.m_num_cubes); + st.update("par2-cube-depth", m_stats.m_max_cube_depth); + st.update("par2-backbones", m_stats.m_backbones_found); + } + }; // class batch_manager + + /* ================================================================ + * worker + * Each worker owns a translated copy of the original solver plus + * its own ast_manager. Workers communicate only through the + * batch_manager (mutex-protected). + * ================================================================ */ + class worker { + struct config { + unsigned m_threads_max_conflicts = 1000; + double m_max_conflict_mul = 1.5; + unsigned m_max_conflicts = UINT_MAX; + bool m_share_units = true; + bool m_share_conflicts = true; + unsigned m_max_cube_depth = 20; + }; + + unsigned id; + parallel_solver& p; + batch_manager& b; + ast_manager m; /* worker-local manager */ + ref s; /* translated solver copy */ + expr_ref_vector asms; /* translated assumptions */ + ast_translation m_g2l, m_l2g; /* global↔local translations */ + config m_config; + expr_mark m_known_units; /* units already shared by this worker */ + unsigned m_shared_clause_limit = 0; + + void update_max_conflicts() { + m_config.m_threads_max_conflicts = static_cast( + m_config.m_max_conflict_mul * m_config.m_threads_max_conflicts); + /* cap at the configured global maximum to prevent runaway cube checks */ + if (m_config.m_threads_max_conflicts > m_config.m_max_conflicts) + m_config.m_threads_max_conflicts = m_config.m_max_conflicts; + } + + /* Check the current cube (passed as additional assumptions). + * The solver's conflict budget is set via updt_params before + * each call so that long-running cubes are interrupted. */ + lbool check_cube(expr_ref_vector const& cube) { + params_ref p; + p.set_uint("max_conflicts", + std::min(m_config.m_threads_max_conflicts, + m_config.m_max_conflicts)); + s->updt_params(p); + + expr_ref_vector combined(m); + combined.append(asms); + combined.append(cube); + + IF_VERBOSE(2, verbose_stream() << "par2 worker " << id + << ": checking cube of size " << cube.size() << "\n"); + lbool r = l_undef; + try { + r = s->check_sat(combined); + } + catch (z3_error& err) { + if (!m.limit().is_canceled()) + b.set_exception(err.error_code()); + } + catch (z3_exception& ex) { + if (!m.limit().is_canceled()) + b.set_exception(ex.what()); + } + IF_VERBOSE(2, verbose_stream() << "par2 worker " << id + << ": cube result " << r << "\n"); + return r; + } + + /* Assert shared clauses discovered by other workers into the + * base assertion set of this worker's solver. The solver + * automatically re-uses them on the next check_sat call. */ + void collect_shared_clauses() { + expr_ref_vector nc = b.return_shared_clauses( + m_g2l, m_shared_clause_limit, id); + for (expr* e : nc) { + IF_VERBOSE(4, verbose_stream() << "par2 worker " << id + << ": asserting shared clause " + << mk_bounded_pp(e, m, 3) << "\n"); + s->assert_expr(e); + } + } + + /* Propagate any new base-level units (backbone literals) this + * worker has learned to the shared backbone pool. + * + * Uses solver::get_trail(0) which returns all literals + * propagated at decision level 0. */ + void share_units() { + if (!m_config.m_share_units) return; + expr_ref_vector trail = s->get_trail(0); + for (expr* e : trail) { + /* get_trail may include ground terms; skip complex ones */ + expr* atom = e; + m.is_not(e, atom); + if (!is_uninterp_const(atom)) continue; + if (m_known_units.is_marked(e)) continue; + m_known_units.mark(e); + expr_ref lit(e, m); + b.collect_global_backbone(m_l2g, lit, id); + } + } + + /* Select a split atom using solver::cube() on a temporary + * solver state that includes the current cube path. + * + * We push the cube literals, call cube(), take the first + * literal, then pop to restore the base state. */ + expr_ref get_split_atom(expr_ref_vector const& cube) { + if (cube.size() >= m_config.m_max_cube_depth) + return expr_ref(nullptr, m); + + s->push(); + for (expr* c : cube) + s->assert_expr(c); + + expr_ref_vector vars(m); + expr_ref_vector c = s->cube(vars, UINT_MAX); + + s->pop(1); + + /* solver::cube() convention: an empty result means done; a result + * whose last element is true means the problem is trivially sat; + * a result whose last element is false means unsat was detected. + * In all other cases every element (including index 0) is a + * valid literal that can serve as a split atom. */ + if (c.empty() || m.is_true(c.back()) || m.is_false(c.back())) + return expr_ref(nullptr, m); + + return expr_ref(c.get(0), m); + } + + public: + + worker(unsigned id, parallel_solver& p, + solver& src, params_ref const& params, + expr_ref_vector const& src_asms) + : id(id), p(p), b(p.m_batch_manager), + asms(m), m_g2l(src.get_manager(), m), m_l2g(m, src.get_manager()) + { + /* create translated solver copy */ + s = src.translate(m, params); + + /* translate assumptions */ + for (expr* a : src_asms) + asms.push_back(m_g2l(a)); + + IF_VERBOSE(1, verbose_stream() << "par2: worker " << id + << " created (" << asms.size() << " assumptions)\n"); + } + + void run() { + bool is_first_run = true; + node_lease lease; + expr_ref_vector cube(m); + + while (true) { + if (!b.get_cube(m_g2l, id, cube, is_first_run, lease)) { + IF_VERBOSE(1, verbose_stream() << "par2 worker " << id + << ": no more cubes\n"); + return; + } + is_first_run = false; + + collect_shared_clauses(); + + lbool r = check_cube(cube); + + if (b.lease_canceled(lease)) { + IF_VERBOSE(1, verbose_stream() << "par2 worker " << id + << ": lease canceled\n"); + lease = {}; + m.limit().dec_cancel(); + continue; + } + + if (!m.inc()) return; + + switch (r) { + + case l_undef: { + update_max_conflicts(); + IF_VERBOSE(1, verbose_stream() << "par2 worker " << id + << ": undef – attempting split\n"); + expr_ref atom = get_split_atom(cube); + if (atom) { + b.try_split(m_l2g, id, lease, atom.get(), + m_config.m_threads_max_conflicts); + } + else { + b.release_lease(id, lease); + } + if (m_config.m_share_units) share_units(); + break; + } + + case l_true: { + IF_VERBOSE(1, verbose_stream() << "par2 worker " << id + << ": SAT\n"); + model_ref mdl; + s->get_model(mdl); + if (mdl) + b.set_sat(m_l2g, *mdl); + return; + } + + case l_false: { + IF_VERBOSE(1, verbose_stream() << "par2 worker " << id + << ": UNSAT cube\n"); + expr_ref_vector core(m); + s->get_unsat_core(core); + + /* Filter to only cube literals (exclude base assumptions). */ + expr_ref_vector cube_core(m); + for (expr* c : core) { + if (cube.contains(c)) + cube_core.push_back(c); + } + + /* If core contains none of the cube lits, the whole + * problem is UNSAT independent of the cube path. */ + if (cube_core.empty()) { + b.set_unsat(m_l2g, core); + return; + } + + b.backtrack(m_l2g, id, cube_core, lease); + + if (m_config.m_share_conflicts) { + /* Share the negation of the cube-core conjunction + * as a learned clause: ¬(c₁ ∧ … ∧ cₙ) ≡ ¬c₁ ∨ … ∨ ¬cₙ */ + expr_ref_vector neg_lits(m); + for (expr* c : cube_core) + neg_lits.push_back(mk_not(expr_ref(c, m))); + expr_ref clause(mk_or(neg_lits), m); + b.collect_clause(m_l2g, id, clause.get()); + } + if (m_config.m_share_units) share_units(); + break; + } + + } // switch + } // while + } // run() + + void cancel() { + m.limit().cancel(); + } + + void cancel_lease() { + m.limit().inc_cancel(); + } + + void collect_statistics(statistics& st) const { + s->collect_statistics(st); + } + + reslimit& limit() { return m.limit(); } + }; // class worker + + /* ---- members ---- */ + ref m_solver; + ast_manager& m_manager; + params_ref m_params; + scoped_ptr_vector m_workers; + batch_manager m_batch_manager; + statistics m_stats; + +public: + + parallel_solver(solver* s, params_ref const& p) + : m_solver(s), + m_manager(s->get_manager()), + m_params(p), + m_batch_manager(s->get_manager(), *this) {} + + /* Run the portfolio. Returns sat/unsat/undef. + * + * On sat: *mdl is populated (translated into m_manager). + * On unsat: *core is populated (translated into m_manager). + * asms: original external assumptions (in m_manager). */ + lbool solve(expr_ref_vector const& asms, + model_ref& mdl, + expr_ref_vector& core) { + + parallel_params pp(m_params); + unsigned num_threads = std::min( + static_cast(std::thread::hardware_concurrency()), + pp.threads_max()); + if (num_threads < 2) num_threads = 2; + + IF_VERBOSE(1, verbose_stream() << "par2: launching " << num_threads + << " threads\n"); + + if (m_manager.has_trace_stream()) + throw default_exception( + "parallel_tactic2 does not work with trace streams"); + + /* Build workers – each gets a translated solver copy. */ + m_workers.reset(); + scoped_limits sl(m_manager.limit()); + params_ref worker_params(m_params); + worker_params.set_bool("override_incremental", true); + + for (unsigned i = 0; i < num_threads; ++i) { + auto* w = alloc(worker, i, *this, *m_solver, worker_params, asms); + m_workers.push_back(w); + sl.push_child(&(w->limit())); + } + + m_batch_manager.initialize(num_threads); + + /* Launch threads. */ + vector threads; + for (auto* w : m_workers) + threads.push_back(std::thread([w]() { w->run(); })); + + for (auto& t : threads) + t.join(); + + /* Collect per-worker statistics. */ + for (auto* w : m_workers) + w->collect_statistics(m_stats); + m_batch_manager.collect_statistics(m_stats); + + m_manager.limit().reset_cancel(); + + lbool result = m_batch_manager.get_result(); + + if (result == l_true) + mdl = m_batch_manager.get_model(); + + if (result == l_false) { + for (expr* c : m_batch_manager.get_unsat_core()) + core.push_back(c); + } + + m_workers.reset(); + return result; + } + + void collect_statistics(statistics& st) const { + st.copy(m_stats); + } + + void reset_statistics() { + m_stats.reset(); + } +}; // class parallel_solver + +/* ------------------------------------------------------------------ */ +/* parallel_tactic2 – wraps parallel_solver as a tactic */ +/* ------------------------------------------------------------------ */ + +class parallel_tactic2 : public tactic { + + solver_ref m_solver; + ast_manager& m_manager; + params_ref m_params; + statistics m_stats; + +public: + + parallel_tactic2(solver* s, params_ref const& p) + : m_solver(s), m_manager(s->get_manager()), m_params(p) {} + + char const* name() const override { return "parallel_tactic2"; } + + void operator()(const goal_ref& g, goal_ref_buffer& result) override { + fail_if_proof_generation("parallel_tactic2", g); + ast_manager& m = g->m(); + + if (m.has_trace_stream()) + throw default_exception( + "parallel_tactic2 does not work with trace streams"); + + /* Translate goal into a set of clauses + assumptions. */ + solver* s = m_solver->translate(m, m_params); + expr_ref_vector clauses(m); + ptr_vector assumptions_raw; + obj_map bool2dep; + ref fmc; + extract_clauses_and_dependencies(g, clauses, assumptions_raw, + bool2dep, fmc); + for (expr* cl : clauses) + s->assert_expr(cl); + + expr_ref_vector asms(m); + asms.append(assumptions_raw.size(), assumptions_raw.data()); + + parallel_solver ps(s, m_params); + + model_ref mdl; + expr_ref_vector core(m); + lbool is_sat = ps.solve(asms, mdl, core); + + ps.collect_statistics(m_stats); + + switch (is_sat) { + case l_true: + g->reset(); + if (g->models_enabled() && mdl) { + if (fmc) + g->add(concat(fmc.get(), model2model_converter(mdl.get()))); + else + g->add(model2model_converter(mdl.get())); + } + break; + + case l_false: { + SASSERT(!g->proofs_enabled()); + expr_dependency* lcore = nullptr; + proof* pr = nullptr; + if (!core.empty()) { + for (expr* c : core) { + expr* dep = nullptr; + if (bool2dep.find(c, dep)) + lcore = m.mk_join(lcore, m.mk_leaf(dep)); + } + } + g->assert_expr(m.mk_false(), pr, lcore); + break; + } + + case l_undef: + if (!m.inc()) + throw tactic_exception(Z3_CANCELED_MSG); + break; + } + + result.push_back(g.get()); + } + + void cleanup() override { + m_stats.reset(); + } + + tactic* translate(ast_manager& m) override { + solver* s = m_solver->translate(m, m_params); + return alloc(parallel_tactic2, s, m_params); + } + + void updt_params(params_ref const& p) override { + m_params.copy(p); + } + + void collect_statistics(statistics& st) const override { + st.copy(m_stats); + } + + void reset_statistics() override { + m_stats.reset(); + } +}; // class parallel_tactic2 + +tactic* mk_parallel_tactic2(solver* s, params_ref const& p) { + return alloc(parallel_tactic2, s, p); +} + +#endif /* !SINGLE_THREAD */ diff --git a/src/solver/parallel_tactical2.h b/src/solver/parallel_tactical2.h new file mode 100644 index 000000000..b27a4c740 --- /dev/null +++ b/src/solver/parallel_tactical2.h @@ -0,0 +1,25 @@ +/*++ +Copyright (c) 2024 Microsoft Corporation + +Module Name: + + parallel_tactical2.h + +Abstract: + + Parallel portfolio solver using the solver API. + Models the internals after smt/smt_parallel.cpp but operates + on generic solver objects instead of smt::context. + +Author: + + (based on smt_parallel.cpp and parallel_tactical.cpp) + +--*/ +#pragma once + +class tactic; +class solver; +class params_ref; + +tactic * mk_parallel_tactic2(solver* s, params_ref const& p); diff --git a/src/solver/smt_logics.cpp b/src/solver/smt_logics.cpp index a02b90880..b47669c6e 100644 --- a/src/solver/smt_logics.cpp +++ b/src/solver/smt_logics.cpp @@ -50,10 +50,7 @@ bool smt_logics::logic_has_arith(symbol const & s) { str.find("IDL") != std::string::npos || str.find("RDL") != std::string::npos || str == "QF_BVRE" || - str == "QF_FP" || - str == "FP" || - str == "QF_FPBV" || - str == "QF_BVFP" || + logic_has_fpa(s) || str == "QF_S" || logic_is_all(s) || str == "QF_FD" || @@ -102,11 +99,7 @@ bool smt_logics::logic_has_str(symbol const & s) { bool smt_logics::logic_has_fpa(symbol const & s) { auto str = s.str(); - return str == "FP" || - str == "QF_FP" || - str == "QF_FPBV" || - str == "QF_BVFP" || - str == "QF_FPLRA" || + return str.find("FP") != std::string::npos || logic_is_all(s); } diff --git a/src/solver/smt_logics.h b/src/solver/smt_logics.h index 9a32e5708..f33ad7f17 100644 --- a/src/solver/smt_logics.h +++ b/src/solver/smt_logics.h @@ -22,7 +22,7 @@ class smt_logics { public: static bool supported_logic(symbol const & s); static bool logic_has_reals_only(symbol const& l); - static bool logic_is_all(symbol const& s) { return s == "ALL"; } + static bool logic_is_all(symbol const& s) { return s == "ALL" || s == "HO_ALL"; } static bool logic_has_uf(symbol const& s); static bool logic_has_arith(symbol const & s); static bool logic_has_bv(symbol const & s); diff --git a/src/tactic/core/CMakeLists.txt b/src/tactic/core/CMakeLists.txt index 8ab41f155..02e62aec0 100644 --- a/src/tactic/core/CMakeLists.txt +++ b/src/tactic/core/CMakeLists.txt @@ -34,6 +34,7 @@ z3_add_component(core_tactics elim_uncnstr_tactic.h elim_uncnstr2_tactic.h eliminate_predicates_tactic.h + fold_unfold_tactic.h injectivity_tactic.h nnf_tactic.h occf_tactic.h diff --git a/src/tactic/core/collect_statistics_tactic.cpp b/src/tactic/core/collect_statistics_tactic.cpp index 5e8af0b43..b689d8058 100644 --- a/src/tactic/core/collect_statistics_tactic.cpp +++ b/src/tactic/core/collect_statistics_tactic.cpp @@ -119,6 +119,9 @@ protected: case lambda_k: m_stats["lambda-variables"] += q->get_num_decls(); break; + case choice_k: + m_stats["choice-variables"] += q->get_num_decls(); + break; } m_stats["patterns"] += q->get_num_patterns(); m_stats["no-patterns"] += q->get_num_no_patterns(); diff --git a/src/tactic/core/elim_uncnstr_tactic.cpp b/src/tactic/core/elim_uncnstr_tactic.cpp index 432f0e9e1..9caf76c4a 100644 --- a/src/tactic/core/elim_uncnstr_tactic.cpp +++ b/src/tactic/core/elim_uncnstr_tactic.cpp @@ -25,6 +25,7 @@ Notes: #include "ast/array_decl_plugin.h" #include "ast/datatype_decl_plugin.h" #include "ast/seq_decl_plugin.h" +#include "ast/for_each_expr.h" #include "tactic/core/collect_occs.h" #include "ast/ast_smt2_pp.h" #include "ast/ast_ll_pp.h" @@ -39,7 +40,8 @@ class elim_uncnstr_tactic : public tactic { struct rw_cfg : public default_rewriter_cfg { bool m_produce_proofs; obj_hashtable & m_vars; - obj_hashtable& m_nonvars; + obj_hashtable& m_nonvars; + expr_mark & m_disabled; ref m_mc; arith_util m_a_util; bv_util m_bv_util; @@ -52,11 +54,14 @@ class elim_uncnstr_tactic : public tactic { unsigned long long m_max_memory; unsigned m_max_steps; - rw_cfg(ast_manager & m, bool produce_proofs, obj_hashtable & vars, obj_hashtable & nonvars, mc * _m, + rw_cfg(ast_manager & m, bool produce_proofs, obj_hashtable & vars, + obj_hashtable & nonvars, expr_mark& disabled, + mc * _m, unsigned long long max_memory, unsigned max_steps): m_produce_proofs(produce_proofs), m_vars(vars), - m_nonvars(nonvars), + m_nonvars(nonvars), + m_disabled(disabled), m_mc(_m), m_a_util(m), m_bv_util(m), @@ -78,7 +83,7 @@ class elim_uncnstr_tactic : public tactic { } bool uncnstr(expr * arg) const { - return m_vars.contains(arg) && !m_nonvars.contains(arg); + return m_vars.contains(arg) && !m_nonvars.contains(arg) && !m_disabled.is_marked(arg); } bool uncnstr(unsigned num, expr * const * args) const { @@ -878,10 +883,11 @@ class elim_uncnstr_tactic : public tactic { class rw : public rewriter_tpl { rw_cfg m_cfg; public: - rw(ast_manager & m, bool produce_proofs, obj_hashtable & vars, obj_hashtable& nonvars, mc * _m, + rw(ast_manager & m, bool produce_proofs, obj_hashtable & vars, obj_hashtable& nonvars, + expr_mark& disabled, mc * _m, unsigned long long max_memory, unsigned max_steps): rewriter_tpl(m, produce_proofs, m_cfg), - m_cfg(m, produce_proofs, vars, nonvars, _m, max_memory, max_steps) { + m_cfg(m, produce_proofs, vars, nonvars, disabled, _m, max_memory, max_steps) { } }; @@ -889,6 +895,8 @@ class elim_uncnstr_tactic : public tactic { ref m_mc; obj_hashtable m_vars; obj_hashtable m_nonvars; + expr_mark m_disabled; + expr_ref_vector m_pinned; scoped_ptr m_rw; unsigned m_num_elim_apps = 0; unsigned long long m_max_memory; @@ -903,7 +911,7 @@ class elim_uncnstr_tactic : public tactic { } void init_rw(bool produce_proofs) { - m_rw = alloc(rw, m(), produce_proofs, m_vars, m_nonvars, m_mc.get(), m_max_memory, m_max_steps); + m_rw = alloc(rw, m(), produce_proofs, m_vars, m_nonvars, m_disabled, m_mc.get(), m_max_memory, m_max_steps); } void run(goal_ref const & g, goal_ref_buffer & result) { @@ -914,6 +922,7 @@ class elim_uncnstr_tactic : public tactic { m_vars.reset(); collect_occs p; p(*g, m_vars); + disable_quantified(g); if (m_vars.empty() || recfun::util(m()).has_rec_defs()) { result.push_back(g.get()); // did not increase depth since it didn't do anything. @@ -931,6 +940,7 @@ class elim_uncnstr_tactic : public tactic { unsigned round = 0; unsigned size = g->size(); unsigned idx = 0; + while (true) { for (; idx < size; ++idx) { expr * f = g->form(idx); @@ -964,6 +974,7 @@ class elim_uncnstr_tactic : public tactic { size = g->size(); m_rw->reset(); // reset cache m_vars.reset(); + disable_quantified(g); { collect_occs p; p(*g, m_vars); @@ -974,11 +985,42 @@ class elim_uncnstr_tactic : public tactic { idx = 0; } } + + void disable(expr* e) { + if (m_disabled.is_marked(e)) + return; + m_pinned.push_back(e); + + ptr_buffer todo; + todo.push_back(e); + while (!todo.empty()) { + e = todo.back(); + todo.pop_back(); + if (m_disabled.is_marked(e)) + continue; + m_disabled.mark(e); + if (is_app(e)) + for (auto arg : *to_app(e)) + todo.push_back(arg); + } + } + + void disable_quantified(goal_ref const &g) { + m_disabled.reset(); + m_pinned.reset(); + + for (unsigned idx = 0; idx < g->size(); ++idx) { + expr *f = g->form(idx); + for (expr *e : subterms::all(expr_ref(f, m()))) + if (is_quantifier(e)) + disable(to_quantifier(e)->get_expr()); + } + } params_ref m_params; public: elim_uncnstr_tactic(ast_manager & m, params_ref const & p): - m_manager(m), m_params(p) { + m_manager(m), m_pinned(m), m_params(p) { updt_params(p); } diff --git a/src/tactic/core/fold_unfold_tactic.h b/src/tactic/core/fold_unfold_tactic.h new file mode 100644 index 000000000..6fa3e5505 --- /dev/null +++ b/src/tactic/core/fold_unfold_tactic.h @@ -0,0 +1,47 @@ + +/*++ +Copyright (c) 2022 Microsoft Corporation + +Module Name: + + fold_unfold_tactic.h + +Abstract: + + Tactic for solving variables + +Author: + + Nikolaj Bjorner (nbjorner) 2026-4-30 + +Tactic Documentation: + +## Tactic fold-unfold + +### Short Description + +Solve for variables using fold-unfold transformations. + +### Notes + +* supports unsat cores +* does not support fine-grained proofs +* alternative to solve-eqs + +--*/ + +#pragma once +#include "util/params.h" +#include "tactic/tactic.h" +#include "tactic/dependent_expr_state_tactic.h" +#include "ast/simplifiers/fold_unfold.h" + +inline tactic *mk_fold_unfold_tactic(ast_manager &m, params_ref const &p = params_ref()) { + return alloc(dependent_expr_state_tactic, m, p, + [](auto &m, auto &p, auto &s) -> dependent_expr_simplifier * { return alloc(euf::fold_unfold, m, s); }); +} + +/* + ADD_TACTIC("fold-unfold", "solve for variables.", "mk_fold_unfold_tactic(m, p)") + ADD_SIMPLIFIER("fold-unfold", "solve for variables.", "alloc(euf::fold_unfold, m, s)") +*/ diff --git a/src/tactic/smtlogics/qflia_tactic.cpp b/src/tactic/smtlogics/qflia_tactic.cpp index e728a33ac..294df6f58 100644 --- a/src/tactic/smtlogics/qflia_tactic.cpp +++ b/src/tactic/smtlogics/qflia_tactic.cpp @@ -204,7 +204,7 @@ tactic * mk_preamble_tactic(ast_manager& m) { using_params(mk_ctx_simplify_tactic(m), ctx_simp_p), using_params(mk_simplify_tactic(m), pull_ite_p), mk_solve_eqs_tactic(m), - mk_lia2card_tactic(m, lia2card_p), + using_params(mk_lia2card_tactic(m), lia2card_p), mk_elim_uncnstr_tactic(m)); } diff --git a/src/tactic/smtlogics/qfnia_tactic.cpp b/src/tactic/smtlogics/qfnia_tactic.cpp index 25dc34d9c..d17167f75 100644 --- a/src/tactic/smtlogics/qfnia_tactic.cpp +++ b/src/tactic/smtlogics/qfnia_tactic.cpp @@ -71,14 +71,22 @@ static tactic * mk_qfnia_preamble(ast_manager & m, params_ref const & p_ref) { params_ref elim_p = p_ref; elim_p.set_uint("max_memory",20); - + + // Match the throttle applied in mk_preamble_tactic (qflia_tactic.cpp): + // lia2card is by default harmful (see commit 99cbfa715). Limit it to + // 0-1 integer variables. + params_ref lia2card_p = p_ref; + lia2card_p.set_uint("lia2card.max_range", 1); + lia2card_p.set_uint("lia2card.max_ite_nesting", 1); + return - and_then(mk_simplify_tactic(m), - mk_propagate_values_tactic(m), + and_then(mk_simplify_tactic(m), + mk_propagate_values_tactic(m), + mk_solve_eqs_tactic(m), using_params(mk_ctx_simplify_tactic(m), ctx_simp_p), using_params(mk_simplify_tactic(m), pull_ite_p), mk_elim_uncnstr_tactic(m), - mk_lia2card_tactic(m), + using_params(mk_lia2card_tactic(m, lia2card_p), lia2card_p), mk_card2bv_tactic(m, p_ref), skip_if_failed(using_params(mk_cofactor_term_ite_tactic(m), elim_p))); } @@ -89,7 +97,8 @@ static tactic * mk_qfnia_sat_solver(ast_manager & m, params_ref const & p) { params_ref simp_p = p; simp_p.set_bool("hoist_mul", true); // hoist multipliers to create smaller circuits. - return and_then(using_params(mk_simplify_tactic(m), simp_p), + return and_then(mk_report_verbose_tactic("(qfnia-sat)", 2), + using_params(mk_simplify_tactic(m), simp_p), mk_nla2bv_tactic(m, nia2sat_p), skip_if_failed(mk_qfnia_bv_solver(m, p)), mk_fail_if_undecided_tactic()); @@ -100,7 +109,8 @@ static tactic * mk_qfnia_nlsat_solver(ast_manager & m, params_ref const & p) { simp_p.set_bool("som", true); // expand into sums of monomials simp_p.set_bool("factor", false); - return and_then(using_params(mk_simplify_tactic(m), simp_p), + return and_then(mk_report_verbose_tactic("(qfnia-nlsat)", 2), + using_params(mk_simplify_tactic(m), simp_p), try_for(mk_qfnra_nlsat_tactic(m, simp_p), 3000), mk_fail_if_undecided_tactic()); } @@ -108,14 +118,14 @@ static tactic * mk_qfnia_nlsat_solver(ast_manager & m, params_ref const & p) { static tactic * mk_qfnia_smt_solver(ast_manager& m, params_ref const& p) { params_ref simp_p = p; simp_p.set_bool("som", true); // expand into sums of monomials - return and_then( + return and_then(mk_report_verbose_tactic("(qfnia-smt)", 2), using_params(mk_simplify_tactic(m), simp_p), mk_smt_tactic(m)); } tactic * mk_qfnia_tactic(ast_manager & m, params_ref const & p) { return and_then( - mk_report_verbose_tactic("(qfnia-tactic)", 10), + mk_report_verbose_tactic("(qfnia-tactic)", 2), mk_qfnia_preamble(m, p), or_else(mk_qfnia_sat_solver(m, p), try_for(mk_qfnia_smt_solver(m, p), 2000), diff --git a/src/tactic/tactical.cpp b/src/tactic/tactical.cpp index 148873c03..22e0f498b 100644 --- a/src/tactic/tactical.cpp +++ b/src/tactic/tactical.cpp @@ -1056,7 +1056,16 @@ public: cancel_eh eh(in->m().limit()); { scoped_timer timer(m_timeout, &eh); - m_t->operator()(in, result); + try { + m_t->operator()(in, result); + } catch (z3_error &ex) { + throw ex; + } catch (tactic_exception &) { + throw; + } catch (z3_exception &ex) { + // convert all Z3 exceptions into tactic exceptions. + throw tactic_exception(ex.what()); + } } } diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index aca048132..70b42dd8e 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -89,6 +89,7 @@ add_executable(test-z3 memory.cpp model2expr.cpp model_based_opt.cpp + mod_factor.cpp model_evaluator.cpp model_retrieval.cpp monomial_bounds.cpp @@ -116,6 +117,7 @@ add_executable(test-z3 prime_generator.cpp proof_checker.cpp qe_arith.cpp + mbp_qel.cpp quant_elim.cpp quant_solve.cpp random.cpp @@ -151,6 +153,7 @@ add_executable(test-z3 theory_dl.cpp theory_pb.cpp timeout.cpp + tptp.cpp total_order.cpp totalizer.cpp trigo.cpp @@ -169,12 +172,11 @@ add_executable(test-z3 z3_add_install_tactic_rule(${z3_test_deps}) z3_add_memory_initializer_rule(${z3_test_deps}) z3_add_gparams_register_modules_rule(${z3_test_deps}) -target_compile_definitions(test-z3 PRIVATE ${Z3_COMPONENT_CXX_DEFINES}) +target_compile_definitions(test-z3 PRIVATE + ${Z3_COMPONENT_CXX_DEFINES} +) target_compile_options(test-z3 PRIVATE ${Z3_COMPONENT_CXX_FLAGS}) target_link_libraries(test-z3 PRIVATE ${Z3_DEPENDENT_LIBS}) target_include_directories(test-z3 PRIVATE ${Z3_COMPONENT_EXTRA_INCLUDE_DIRS}) z3_append_linker_flag_list_to_target(test-z3 ${Z3_DEPENDENT_EXTRA_CXX_LINK_FLAGS}) z3_add_component_dependencies_to_target(test-z3 ${z3_test_expanded_deps}) - - - diff --git a/src/test/algebraic_numbers.cpp b/src/test/algebraic_numbers.cpp index aed544714..9e4662359 100644 --- a/src/test/algebraic_numbers.cpp +++ b/src/test/algebraic_numbers.cpp @@ -104,6 +104,48 @@ void test_algebraic_comparison() { VERIFY(!am.eq(a, b)); // 2 != 3 } +void test_algebraic_comparison_edge_case() { + std::cout << "test_algebraic_comparison edge case\n"; + + // Let p1 = 1073741837 x^2 - 576460758745874510 x - 16106127555 + // Let p2 = p1 * (1073741837 x^2 - 576460759819616347 x -16106127555) + // = 1152921532524134569 x^4 - 1237940069261339757601884309 x^3 + // + 332307006992839334837849081482577900 x^2 + 18569101038920096364028264635 x + // + 259407344817930278025 + // Compare a = root(p1, 1) in (-8, 0) and b = root(p2, 2) in (-15/2^29, -7/2^28) + // The two numbers are different (a < b), but very close, and both are roots of p2 + + reslimit rl; + unsynch_mpq_manager qm; + anum_manager am(rl, qm); + manager m(rl, qm); + polynomial_ref x(m); + x = m.mk_polynomial(m.mk_var()); + + rational a0, a1, a2; + a0 = 161061; + a0 = (a0 * 100000) + 27555; + a1 = 576460758; + a1 = (a1 * 1000000000) + 745874510; + a2 = 10737; + a2 = (a2 * 100000) + 41837; + + rational b1; + b1 = 576460759; + b1 = (b1 * 1000000000) + 819616347; + + polynomial_ref p1(m); + polynomial_ref p2(m); + p1 = ((a2*x*x) - (a1*x)) - a0; + p2 = p1 * (((a2*x*x) - (b1*x)) - a0); + + scoped_anum a(am), b(am); + am.mk_root(p1, 1, a); + am.mk_root(p2, 2, b); + + VERIFY(!am.eq(a, b)); +} + void test_algebraic_degree() { std::cout << "test_algebraic_degree\n"; @@ -158,6 +200,7 @@ void test_algebraic_numbers() { test_algebraic_basic_operations(); test_algebraic_arithmetic(); test_algebraic_comparison(); + test_algebraic_comparison_edge_case(); test_algebraic_degree(); test_algebraic_signs(); } diff --git a/src/test/api.cpp b/src/test/api.cpp index a4b404f41..a7e7f329b 100644 --- a/src/test/api.cpp +++ b/src/test/api.cpp @@ -248,6 +248,7 @@ void test_max_reg() { Z3_optimize_dec_ref(ctx, opt); } + #if 0 // Approach 3: Weighted sum method (Python loop over weights) int weights[][2] = {{1, 4}, {2, 3}, {1, 1}, {3, 2}, {4, 1}}; for (auto& w : weights) { @@ -271,9 +272,10 @@ void test_max_reg() { } Z3_optimize_dec_ref(ctx, opt); } + #endif - std::cout << "BNH: " << num_sat << "/7 optimizations returned sat" << std::endl; - ENSURE(num_sat == 7); + std::cout << "BNH: " << num_sat << "/2 optimizations returned sat" << std::endl; + ENSURE(num_sat == 2); Z3_del_context(ctx); std::cout << "BNH optimization test done" << std::endl; } diff --git a/src/test/api_datalog.cpp b/src/test/api_datalog.cpp index 45422d515..6e88a8ade 100644 --- a/src/test/api_datalog.cpp +++ b/src/test/api_datalog.cpp @@ -64,5 +64,31 @@ void tst_api_datalog() { Z3_fixedpoint_dec_ref(ctx, fp); } + // Regression test for Spacer model construction on ADT CHCs + { + char const* chc = + "(set-logic HORN)\n" + "(set-option :fp.engine spacer)\n" + "(set-option :fp.spacer.random_seed 51)\n" + "(set-option :timeout 2000)\n" + "(declare-datatypes ((L 0)) (((cons (hd Int) (tl L)) (nil))))\n" + "(declare-fun reva (L L L) Bool)\n" + "(assert (forall ((a L)) (reva nil a a)))\n" + "(assert (forall ((x L) (acc L) (r L) (h Int))\n" + " (=> (reva x (cons h acc) r)\n" + " (reva (cons h x) acc r))))\n" + "(assert (forall ((B L) (C L) (D L) (E L) (F L))\n" + " (=> (and (reva B C D)\n" + " (reva D nil E)\n" + " (reva C B F)\n" + " (not (= E F)))\n" + " false)))\n" + "(check-sat)\n"; + + Z3_string response = Z3_eval_smtlib2_string(ctx, chc); + ENSURE(response != nullptr); + ENSURE(Z3_get_error_code(ctx) == Z3_OK); + } + Z3_del_context(ctx); -} \ No newline at end of file +} diff --git a/src/test/dl_table.cpp b/src/test/dl_table.cpp index 0f429d223..e435e70e6 100644 --- a/src/test/dl_table.cpp +++ b/src/test/dl_table.cpp @@ -23,10 +23,13 @@ static void test_table(mk_table_fn mk_table) { sig.push_back(8); sig.push_back(4); smt_params params; + params_ref fp_params; + gparams::set("fp.engine", "datalog"); + // fp_params.set_sym("fp.engine", symbol("datalog")); ast_manager ast_m; reg_decl_plugins(ast_m); datalog::register_engine re; - datalog::context ctx(ast_m, re, params); + datalog::context ctx(ast_m, re, params, fp_params); datalog::relation_manager & m = ctx.get_rel_context()->get_rmanager(); m.register_plugin(alloc(datalog::bitvector_table_plugin, m)); @@ -48,13 +51,10 @@ static void test_table(mk_table_fn mk_table) { table.add_fact(row2); table.display(std::cout); - datalog::table_base::iterator it = table.begin(); - datalog::table_base::iterator end = table.end(); - for (; it != end; ++it) { - it->get_fact(row); - for (unsigned j = 0; j < row.size(); ++j) { - std::cout << row[j] << " "; - } + for (auto &r : table) { + r.get_fact(row); + for (auto v : row) + std::cout << v << " "; std::cout << "\n"; } diff --git a/src/test/dl_util.cpp b/src/test/dl_util.cpp index 8c9af0790..497298094 100644 --- a/src/test/dl_util.cpp +++ b/src/test/dl_util.cpp @@ -4,6 +4,7 @@ Copyright (c) 2015 Microsoft Corporation --*/ +#include "util/gparams.h" #include "muz/base/dl_util.h" using namespace datalog; @@ -49,6 +50,7 @@ void dl_util_cycle_from_permutation() { } void tst_dl_util() { + gparams::set("fp.engine", "datalog"); dl_util_two_array_sort(); dl_util_cycle_from_permutation(); } diff --git a/src/test/doc.cpp b/src/test/doc.cpp index d4909a35d..9ea741152 100644 --- a/src/test/doc.cpp +++ b/src/test/doc.cpp @@ -436,7 +436,7 @@ public: //sub:{xxx \ {1x0, 0x1}} //result:{100} - for (unsigned i = 0; i < 1000; ++i) { + for (unsigned i = 0; i < 100; ++i) { udoc d1, d2; mk_rand_udoc(3, 3, d1); mk_rand_udoc(3, 3, d2); @@ -453,7 +453,7 @@ public: void test_intersect() { expr_ref fml1(m), fml2(m), fml3(m); - for (unsigned i = 0; i < 10000; ++i) { + for (unsigned i = 0; i < 100; ++i) { udoc d1, d2; mk_rand_udoc(3, 3, d1); mk_rand_udoc(3, 3, d2); diff --git a/src/test/fpa.cpp b/src/test/fpa.cpp index 12b423066..632865cee 100644 --- a/src/test/fpa.cpp +++ b/src/test/fpa.cpp @@ -60,6 +60,43 @@ static void test_fp_to_real_denormal() { true); } +// Regression test for soundness bug in to_fp (from real) with symbolic real interval. +// When the rounding mode is RTZ and the real variable is constrained to an interval +// that includes the exact rational value of a float, Z3 should return SAT. +// This was broken because mk_to_real computed 2^(1/|exp|) instead of 1/(2^|exp|) +// for floats with negative exponents, causing a conflict in the NRA solver. +static void test_to_fp_from_real_interval() { + // The interval (-4127125/16777216, -16508499/67108864] contains -16508499/67108864 + // which is the exact rational value of fp #b1 #b01111100 #b11110111110011001010011. + // to_fp(RTZ, r) for r in this closed interval must equal that float. + run_fp_test( + "(set-logic QF_FPLRA)\n" + "(declare-const x Float32)\n" + "(assert (= x (fp #b1 #b01111100 #b11110111110011001010011)))\n" + "(declare-const r Real)\n" + "(assert (and (> r (- (/ 4127125.0 16777216.0))) (<= r (- (/ 16508499.0 67108864.0)))))\n" + "(declare-const w Float32)\n" + "(assert (= w ((_ to_fp 8 24) RTZ r)))\n" + "(assert (= x w))\n" + "(check-sat)\n", + true); +} + +static void test_recfun_defined_function_soundness() { + run_fp_test( + "(set-option :model_validate true)\n" + "(declare-fun fixedAdd () Int)\n" + "(declare-fun variableAdd () Int)\n" + "(define-fun-rec $$add$$ ((a Int) (b Int)) Int\n" + " (ite (= 0 b) 2 (- a (+ 0 (- fixedAdd b)))))\n" + "(assert (= fixedAdd (* 9 fixedAdd)))\n" + "(assert (= 1 ($$add$$ 1 3)))\n" + "(check-sat)\n", + false); +} + void tst_fpa() { test_fp_to_real_denormal(); + test_to_fp_from_real_interval(); + test_recfun_defined_function_soundness(); } diff --git a/src/test/lp/lp.cpp b/src/test/lp/lp.cpp index ee1989c20..7a2b27155 100644 --- a/src/test/lp/lp.cpp +++ b/src/test/lp/lp.cpp @@ -1971,28 +1971,28 @@ void test_lp_local(int argn, char **argv) { if (args_parser.option_is_used("-nla_blfmz_mf")) { #ifdef Z3DEBUG - nla::test_basic_lemma_for_mon_zero_from_monomial_to_factors(); + // nla::test_basic_lemma_for_mon_zero_from_monomial_to_factors(); #endif return finalize(0); } if (args_parser.option_is_used("-nla_blfmz_fm")) { #ifdef Z3DEBUG - nla::test_basic_lemma_for_mon_zero_from_factors_to_monomial(); + //nla::test_basic_lemma_for_mon_zero_from_factors_to_monomial(); #endif return finalize(0); } if (args_parser.option_is_used("-nla_blnt_mf")) { #ifdef Z3DEBUG - nla::test_basic_lemma_for_mon_neutral_from_monomial_to_factors(); + // nla::test_basic_lemma_for_mon_neutral_from_monomial_to_factors(); #endif return finalize(0); } if (args_parser.option_is_used("-nla_blnt_fm")) { #ifdef Z3DEBUG - nla::test_basic_lemma_for_mon_neutral_from_factors_to_monomial(); + // nla::test_basic_lemma_for_mon_neutral_from_factors_to_monomial(); #endif return finalize(0); } diff --git a/src/test/lp/nla_solver_test.cpp b/src/test/lp/nla_solver_test.cpp index 1ec8fe8fa..d2e2e2f19 100644 --- a/src/test/lp/nla_solver_test.cpp +++ b/src/test/lp/nla_solver_test.cpp @@ -150,7 +150,7 @@ void create_abcde(solver & nla, nla.add_monic(lp_be, vec.size(), vec.begin()); } - +#if 0 void test_basic_lemma_for_mon_neutral_from_factors_to_monomial_0() { std::cout << "test_basic_lemma_for_mon_neutral_from_factors_to_monomial_0\n"; enable_trace("nla_solver"); @@ -222,6 +222,7 @@ void test_basic_lemma_for_mon_neutral_from_factors_to_monomial_0() { } +#endif void s_set_column_value_test(lp::lar_solver&s, lpvar j, const rational & v) { s.set_column_value_test(j, lp::impq(v)); @@ -231,6 +232,7 @@ void s_set_column_value_test(lp::lar_solver&s, lpvar j, const lp::impq & v) { s.set_column_value_test(j, v); } +#if 0 void test_basic_lemma_for_mon_neutral_from_factors_to_monomial_1() { std::cout << "test_basic_lemma_for_mon_neutral_from_factors_to_monomial_1\n"; TRACE(nla_solver,); @@ -367,6 +369,7 @@ void test_basic_lemma_for_mon_zero_from_factors_to_monomial() { VERIFY(found0 && found1); } + void test_basic_lemma_for_mon_zero_from_monomial_to_factors() { std::cout << "test_basic_lemma_for_mon_zero_from_monomial_to_factors\n"; enable_trace("nla_solver"); @@ -420,6 +423,7 @@ void test_basic_lemma_for_mon_zero_from_monomial_to_factors() { } + void test_basic_lemma_for_mon_neutral_from_monomial_to_factors() { std::cout << "test_basic_lemma_for_mon_neutral_from_monomial_to_factors\n"; enable_trace("nla_solver"); @@ -489,6 +493,7 @@ void test_basic_lemma_for_mon_neutral_from_monomial_to_factors() { VERIFY(found0 && found1); } +#endif void test_horner() { enable_trace("nla_solver"); diff --git a/src/test/main.cpp b/src/test/main.cpp index 7dbcf78b9..fd2c1d812 100644 --- a/src/test/main.cpp +++ b/src/test/main.cpp @@ -154,6 +154,7 @@ X(hilbert_basis) \ X(heap_trie) \ X(karr) \ + X(mod_factor) \ X(no_overflow) \ X(datalog_parser) \ X_ARGV(datalog_parser_file) \ @@ -162,6 +163,7 @@ X(rcf) \ X(polynorm) \ X(qe_arith) \ + X(mbp_qel) \ X(expr_substitution) \ X(sorting_network) \ X(theory_pb) \ @@ -539,7 +541,7 @@ int main(int argc, char ** argv) { } #ifndef __EMSCRIPTEN__ - if (num_jobs > 0) + if (num_jobs > 0 && (test_all || requested_tests.size() > 1)) return run_parallel(argv[0], test_all, num_jobs, extra_args, requested_tests); #endif diff --git a/src/test/mbp_qel.cpp b/src/test/mbp_qel.cpp new file mode 100644 index 000000000..1ff262785 --- /dev/null +++ b/src/test/mbp_qel.cpp @@ -0,0 +1,250 @@ + +/*++ +Copyright (c) 2025 Microsoft Corporation + +Module Name: + + mbp_qel.cpp + +Abstract: + + Unit tests for model-based projection with QEL (term-graph based) + +Author: + + Hari Govind V K (hgvk94) 2025-05-25 + +--*/ + +#include "qe/qe_mbp.h" +#include "ast/reg_decl_plugins.h" +#include "ast/datatype_decl_plugin.h" +#include "ast/arith_decl_plugin.h" +#include "ast/ast_pp.h" +#include "smt/smt_context.h" +#include "params/smt_params.h" +#include + +// Test that MBP with QEL does not return false for a satisfiable formula +// involving datatype accessors applied past the end of a list. +// +// Formula: (and ((_ is cons) x) ((_ is nil) (tl x)) (= nil (tl (tl x))) (< 8 n)) +// Project: x +// Expected: result should imply n >= 9 (and model should satisfy it) +// Bug: QEL was returning false because rm_accessor unconditionally +// assumed (tl x) has constructor cons when eliminating (tl (tl x)), +// contradicting the ((_ is nil) (tl x)) literal. +static void test_dt_accessor_past_end() { + std::cout << "test_dt_accessor_past_end\n"; + ast_manager m; + reg_decl_plugins(m); + datatype_util dt(m); + arith_util a(m); + + // Create list datatype: (declare-datatypes ((L 0)) (((cons (hd Int) (tl L)) (nil)))) + sort_ref int_sort(a.mk_int(), m); + func_decl_ref cons(m), is_cons(m), head(m), tail(m), nil(m), is_nil(m); + sort_ref L = dt.mk_list_datatype(int_sort, symbol("L"), + cons, is_cons, head, tail, nil, is_nil); + + // Declare variables + app_ref x(m.mk_const("x", L), m); + app_ref n(m.mk_const("n", int_sort), m); + + // Build formula: (and ((_ is cons) x) ((_ is nil) (tl x)) (= nil (tl (tl x))) (< 8 n)) + expr_ref tl_x(m.mk_app(tail, x.get()), m); + expr_ref tl_tl_x(m.mk_app(tail, tl_x.get()), m); + expr_ref nil_val(m.mk_const(nil), m); + + expr_ref is_cons_x(m.mk_app(is_cons, x.get()), m); + expr_ref is_nil_tl_x(m.mk_app(is_nil, tl_x.get()), m); + expr_ref eq_nil_tl_tl_x(m.mk_eq(nil_val, tl_tl_x), m); + expr_ref lt_8_n(a.mk_lt(a.mk_int(8), n), m); + + expr_ref_vector conjs(m); + conjs.push_back(is_cons_x).push_back(is_nil_tl_x).push_back(eq_nil_tl_tl_x).push_back(lt_8_n); + expr_ref fml(m.mk_and(conjs), m); + + std::cout << " formula:\n " << mk_pp(fml, m, 5) << "\n"; + + // Get a model + smt_params params; + params.m_model = true; + model_ref mdl; + { + smt::context ctx(m, params); + ctx.assert_expr(fml); + lbool result = ctx.check(); + VERIFY(result == l_true); + ctx.get_model(mdl); + } + + std::cout << " model: x = " << mk_pp((*mdl)(x), m) + << ", n = " << mk_pp((*mdl)(n), m) << "\n"; + + // Call MBP with QEL enabled + app_ref_vector vars(m); + vars.push_back(x); + + params_ref p; + p.set_bool("qsat_use_qel", true); + qe::mbproj mbp(m, p); + expr_ref projected(fml, m); + mbp.spacer(vars, *mdl.get(), projected); + + std::cout << " projected (qel=true):\n " << mk_pp(projected, m, 5) << "\n"; + + // The result must not be false + VERIFY(!m.is_false(projected)); + + // The model should satisfy the projected formula + VERIFY(mdl->is_true(projected)); + + // x should have been eliminated + VERIFY(vars.empty()); + + std::cout << " PASS\n\n"; +} + +// Same test but with a deeper list structure: +// x is a 2-element list with a past-end accessor constraint +// Formula: (and ((_ is cons) x) ((_ is cons) (tl x)) ((_ is nil) (tl (tl x))) +// (= nil (tl (tl (tl x)))) (< 8 n)) +static void test_dt_accessor_past_end_depth2() { + std::cout << "test_dt_accessor_past_end_depth2\n"; + ast_manager m; + reg_decl_plugins(m); + datatype_util dt(m); + arith_util a(m); + + sort_ref int_sort(a.mk_int(), m); + func_decl_ref cons(m), is_cons(m), head(m), tail(m), nil(m), is_nil(m); + sort_ref L = dt.mk_list_datatype(int_sort, symbol("L"), + cons, is_cons, head, tail, nil, is_nil); + + app_ref x(m.mk_const("x", L), m); + app_ref n(m.mk_const("n", int_sort), m); + + // Build: (and (is-cons x) (is-cons (tl x)) (is-nil (tl (tl x))) + // (= nil (tl (tl (tl x)))) (< 8 n)) + expr_ref tl_x(m.mk_app(tail, x.get()), m); + expr_ref tl_tl_x(m.mk_app(tail, tl_x.get()), m); + expr_ref tl_tl_tl_x(m.mk_app(tail, tl_tl_x.get()), m); + expr_ref nil_val(m.mk_const(nil), m); + + expr_ref is_cons_x(m.mk_app(is_cons, x.get()), m); + expr_ref is_cons_tl_x(m.mk_app(is_cons, tl_x.get()), m); + expr_ref is_nil_tl_tl_x(m.mk_app(is_nil, tl_tl_x.get()), m); + expr_ref eq_nil_tl3(m.mk_eq(nil_val, tl_tl_tl_x), m); + expr_ref lt_8_n(a.mk_lt(a.mk_int(8), n), m); + + expr_ref_vector conjs(m); + conjs.push_back(is_cons_x).push_back(is_cons_tl_x).push_back(is_nil_tl_tl_x).push_back(eq_nil_tl3).push_back(lt_8_n); + expr_ref fml(m.mk_and(conjs), m); + + std::cout << " formula:\n " << mk_pp(fml, m, 5) << "\n"; + + smt_params sparams; + sparams.m_model = true; + model_ref mdl; + { + smt::context ctx(m, sparams); + ctx.assert_expr(fml); + lbool result = ctx.check(); + VERIFY(result == l_true); + ctx.get_model(mdl); + } + + std::cout << " model: x = " << mk_pp((*mdl)(x), m) + << ", n = " << mk_pp((*mdl)(n), m) << "\n"; + + app_ref_vector vars(m); + vars.push_back(x); + + params_ref p; + p.set_bool("qsat_use_qel", true); + qe::mbproj mbp(m, p); + expr_ref projected(fml, m); + mbp.spacer(vars, *mdl.get(), projected); + + std::cout << " projected (qel=true):\n " << mk_pp(projected, m, 5) << "\n"; + + VERIFY(!m.is_false(projected)); + VERIFY(mdl->is_true(projected)); + VERIFY(vars.empty()); + + std::cout << " PASS\n\n"; +} + +// Test with multiple DT variables projected simultaneously +// Formula: (and (= nil (tl (tl (tl x)))) ((_ is nil) (tl (tl x))) +// ((_ is cons) y) ((_ is nil) (tl y)) (< 8 n)) +// Project: x, y +static void test_dt_multiple_vars() { + std::cout << "test_dt_multiple_vars\n"; + ast_manager m; + reg_decl_plugins(m); + datatype_util dt(m); + arith_util a(m); + + sort_ref int_sort(a.mk_int(), m); + func_decl_ref cons(m), is_cons(m), head(m), tail(m), nil(m), is_nil(m); + sort_ref L = dt.mk_list_datatype(int_sort, symbol("L"), + cons, is_cons, head, tail, nil, is_nil); + + app_ref x(m.mk_const("x", L), m); + app_ref y(m.mk_const("y", L), m); + app_ref n(m.mk_const("n", int_sort), m); + + expr_ref tl_x(m.mk_app(tail, x.get()), m); + expr_ref tl_tl_x(m.mk_app(tail, tl_x.get()), m); + expr_ref tl_tl_tl_x(m.mk_app(tail, tl_tl_x.get()), m); + expr_ref tl_y(m.mk_app(tail, y.get()), m); + expr_ref nil_val(m.mk_const(nil), m); + + expr_ref eq_nil_tl3x(m.mk_eq(nil_val, tl_tl_tl_x), m); + expr_ref is_nil_tl2x(m.mk_app(is_nil, tl_tl_x.get()), m); + expr_ref is_cons_y(m.mk_app(is_cons, y.get()), m); + expr_ref is_nil_tl_y(m.mk_app(is_nil, tl_y.get()), m); + expr_ref lt_8_n(a.mk_lt(a.mk_int(8), n), m); + + expr_ref_vector conjs(m); + conjs.push_back(eq_nil_tl3x).push_back(is_nil_tl2x).push_back(is_cons_y).push_back(is_nil_tl_y).push_back(lt_8_n); + expr_ref fml(m.mk_and(conjs), m); + + std::cout << " formula:\n " << mk_pp(fml, m, 5) << "\n"; + + smt_params sparams; + sparams.m_model = true; + model_ref mdl; + { + smt::context ctx(m, sparams); + ctx.assert_expr(fml); + lbool result = ctx.check(); + VERIFY(result == l_true); + ctx.get_model(mdl); + } + + app_ref_vector vars(m); + vars.push_back(x); + vars.push_back(y); + + params_ref p; + p.set_bool("qsat_use_qel", true); + qe::mbproj mbp(m, p); + expr_ref projected(fml, m); + mbp.spacer(vars, *mdl.get(), projected); + + std::cout << " projected (qel=true):\n " << mk_pp(projected, m, 5) << "\n"; + + VERIFY(!m.is_false(projected)); + VERIFY(mdl->is_true(projected)); + + std::cout << " PASS\n\n"; +} + +void tst_mbp_qel() { + test_dt_accessor_past_end(); + test_dt_accessor_past_end_depth2(); + test_dt_multiple_vars(); +} diff --git a/src/test/mod_factor.cpp b/src/test/mod_factor.cpp new file mode 100644 index 000000000..314537ca8 --- /dev/null +++ b/src/test/mod_factor.cpp @@ -0,0 +1,91 @@ +/*++ +Copyright (c) 2025 Microsoft Corporation +--*/ + +#include "api/z3.h" +#include "util/util.h" +#include + +// x mod 7 = 0 & (x*y) mod 7 != 0 should be unsat +// Exercises: mod internalization path (is_mod with numeric divisor) +static void test_mod_factor_mod_path() { + Z3_config cfg = Z3_mk_config(); + Z3_context ctx = Z3_mk_context(cfg); + Z3_solver s = Z3_mk_solver_for_logic(ctx, Z3_mk_string_symbol(ctx, "QF_NIA")); + Z3_solver_inc_ref(ctx, s); + Z3_sort int_sort = Z3_mk_int_sort(ctx); + Z3_ast x = Z3_mk_const(ctx, Z3_mk_string_symbol(ctx, "x"), int_sort); + Z3_ast y = Z3_mk_const(ctx, Z3_mk_string_symbol(ctx, "y"), int_sort); + Z3_ast seven = Z3_mk_int(ctx, 7, int_sort); + Z3_ast zero = Z3_mk_int(ctx, 0, int_sort); + Z3_ast xy_args[] = {x, y}; + Z3_ast xy = Z3_mk_mul(ctx, 2, xy_args); + // assert mul term first so ensure_nla() fires before mod internalization + Z3_solver_assert(ctx, s, Z3_mk_not(ctx, Z3_mk_eq(ctx, Z3_mk_mod(ctx, xy, seven), zero))); + Z3_solver_assert(ctx, s, Z3_mk_eq(ctx, Z3_mk_mod(ctx, x, seven), zero)); + ENSURE(Z3_solver_check(ctx, s) == Z3_L_FALSE); + Z3_solver_dec_ref(ctx, s); + Z3_del_config(cfg); + Z3_del_context(ctx); +} + +// (x mod 100) mod 7 = 0 => ((x mod 100) * y) mod 7 = 0 +// Exercises: idiv internalization path (is_idiv + numeric divisor + bounded dividend) +// because (x mod 100) is recognized as bounded by is_bounded() +static void test_mod_factor_idiv_path() { + Z3_config cfg = Z3_mk_config(); + Z3_context ctx = Z3_mk_context(cfg); + Z3_solver s = Z3_mk_solver_for_logic(ctx, Z3_mk_string_symbol(ctx, "QF_NIA")); + Z3_solver_inc_ref(ctx, s); + Z3_sort int_sort = Z3_mk_int_sort(ctx); + Z3_ast x = Z3_mk_const(ctx, Z3_mk_string_symbol(ctx, "x"), int_sort); + Z3_ast y = Z3_mk_const(ctx, Z3_mk_string_symbol(ctx, "y"), int_sort); + Z3_ast seven = Z3_mk_int(ctx, 7, int_sort); + Z3_ast zero = Z3_mk_int(ctx, 0, int_sort); + Z3_ast hundred = Z3_mk_int(ctx, 100, int_sort); + // xm = x mod 100 (bounded by is_bounded) + Z3_ast xm = Z3_mk_mod(ctx, x, hundred); + // (xm * y) — assert mul term first so ensure_nla() fires before mod internalization + Z3_ast xm_y_args[] = {xm, y}; + Z3_ast xm_y = Z3_mk_mul(ctx, 2, xm_y_args); + Z3_ast xm_y_div = Z3_mk_div(ctx, xm_y, seven); + // assert (xm * y) mod 7 != 0 + Z3_solver_assert(ctx, s, Z3_mk_not(ctx, Z3_mk_eq(ctx, Z3_mk_mod(ctx, xm_y, seven), zero))); + // use div to keep it alive + Z3_solver_assert(ctx, s, Z3_mk_ge(ctx, xm_y_div, zero)); + // xm mod 7 = 0 + Z3_solver_assert(ctx, s, Z3_mk_eq(ctx, Z3_mk_mod(ctx, xm, seven), zero)); + ENSURE(Z3_solver_check(ctx, s) == Z3_L_FALSE); + Z3_solver_dec_ref(ctx, s); + Z3_del_config(cfg); + Z3_del_context(ctx); +} + +static void test_const_array_store_chain_unsat() { + Z3_config cfg = Z3_mk_config(); + Z3_context ctx = Z3_mk_context(cfg); + const char* script = R"( +(set-logic QF_ABV) +(declare-const x (_ BitVec 8)) +(declare-const y (_ BitVec 8)) +(define-fun A0 () (Array (_ BitVec 2) (_ BitVec 8)) ((as const (Array (_ BitVec 2) (_ BitVec 8))) x)) +(define-fun A1 () (Array (_ BitVec 2) (_ BitVec 8)) ((as const (Array (_ BitVec 2) (_ BitVec 8))) y)) +(declare-const i0 (_ BitVec 2)) +(declare-const e0 (_ BitVec 8)) +(declare-const i1 (_ BitVec 2)) +(declare-const e1 (_ BitVec 8)) +(assert (distinct x y)) +(assert (= (store A0 i0 e0) (store A1 i1 e1))) +(check-sat) +)"; + std::string resp = Z3_eval_smtlib2_string(ctx, script); + ENSURE(resp.find("unsat") != std::string::npos); + Z3_del_config(cfg); + Z3_del_context(ctx); +} + +void tst_mod_factor() { + test_mod_factor_mod_path(); + test_mod_factor_idiv_path(); + test_const_array_store_chain_unsat(); +} diff --git a/src/test/nla_intervals.cpp b/src/test/nla_intervals.cpp index 5cea168ee..1207c746c 100644 --- a/src/test/nla_intervals.cpp +++ b/src/test/nla_intervals.cpp @@ -207,13 +207,82 @@ void test_nla_intervals_fractional() { VERIFY(true); // Placeholder } +void test_fetch_normalized_term_column() { + std::cout << "test_fetch_normalized_term_column\n"; + + lp::lar_solver s; + + // Create some variables + lpvar x = s.add_var(0, true); // j0 + lpvar y = s.add_var(1, true); // j1 + lpvar z = s.add_var(2, true); // j2 + + // Add a term t = 2*x + 3*y and register it + lp::lar_term t; + t.add_monomial(rational(2), x); + t.add_monomial(rational(3), y); + s.add_term(t.coeffs_as_vector(), UINT_MAX); + s.register_existing_terms(); + + // Now build the same term independently and look it up + lp::lar_term query; + query.add_monomial(rational(2), x); + query.add_monomial(rational(3), y); + lp::mpq a; + lp::lar_term norm_query = query.get_normalized_by_min_var(a); + + std::pair result; + bool found = s.fetch_normalized_term_column(norm_query, result); + VERIFY(found); + std::cout << " round-trip lookup: " << (found ? "PASS" : "FAIL") << "\n"; + + // Build query with variables added in reverse order + lp::lar_term query_rev; + query_rev.add_monomial(rational(3), y); + query_rev.add_monomial(rational(2), x); + lp::lar_term norm_rev = query_rev.get_normalized_by_min_var(a); + + bool found_rev = s.fetch_normalized_term_column(norm_rev, result); + VERIFY(found_rev); + std::cout << " reverse-order lookup: " << (found_rev ? "PASS" : "FAIL") << "\n"; + + // Test a 3-variable term: x - y + 5*z + lp::lar_term t2; + t2.add_monomial(rational(1), x); + t2.add_monomial(rational(-1), y); + t2.add_monomial(rational(5), z); + s.add_term(t2.coeffs_as_vector(), UINT_MAX); + s.register_existing_terms(); + + lp::lar_term query2; + query2.add_monomial(rational(1), x); + query2.add_monomial(rational(-1), y); + query2.add_monomial(rational(5), z); + lp::lar_term norm2 = query2.get_normalized_by_min_var(a); + + found = s.fetch_normalized_term_column(norm2, result); + VERIFY(found); + std::cout << " 3-variable term lookup: " << (found ? "PASS" : "FAIL") << "\n"; + + // Test that a non-registered term is NOT found + lp::lar_term query3; + query3.add_monomial(rational(7), x); + query3.add_monomial(rational(11), y); + lp::lar_term norm3 = query3.get_normalized_by_min_var(a); + + bool found_missing = s.fetch_normalized_term_column(norm3, result); + VERIFY(!found_missing); + std::cout << " non-existent term not found: " << (!found_missing ? "PASS" : "FAIL") << "\n"; +} + void test_nla_intervals() { test_nla_intervals_basic(); - test_nla_intervals_negative(); + test_nla_intervals_negative(); test_nla_intervals_zero_crossing(); test_nla_intervals_power(); test_nla_intervals_mixed_signs(); test_nla_intervals_fractional(); + test_fetch_normalized_term_column(); } } // namespace nla diff --git a/src/test/simplifier.cpp b/src/test/simplifier.cpp index a4da49861..3b2abf7b2 100644 --- a/src/test/simplifier.cpp +++ b/src/test/simplifier.cpp @@ -6,6 +6,7 @@ Copyright (c) 2015 Microsoft Corporation #include "api/z3.h" #include "api/z3_private.h" +#include #include #include "util/util.h" #include "util/trace.h" @@ -211,6 +212,26 @@ static void test_array() { Z3_del_context(ctx); } +static void test_sat_smt_ufbv_predicate_model_validation() { + Z3_context ctx = Z3_mk_context(nullptr); + const char* result = + Z3_eval_smtlib2_string(ctx, + "(set-logic QF_UFBV)\n" + "(set-option :sat.smt true)\n" + "(set-option :model_validate true)\n" + "(declare-fun p ((_ BitVec 4)) Bool)\n" + "(declare-const x (_ BitVec 4))\n" + "(declare-const y (_ BitVec 4))\n" + "(assert (xor (p x) (p y)))\n" + "(assert (bvuge x (_ bv1 4)))\n" + "(assert (bvult y (_ bv1 4)))\n" + "(check-sat)\n" + "(get-model)\n"); + ENSURE(std::strstr(result, "sat") != nullptr); + ENSURE(std::strstr(result, "invalid model") == nullptr); + Z3_del_context(ctx); +} + void tst_simplifier() { test_array(); @@ -218,4 +239,5 @@ void tst_simplifier() { test_datatypes(); test_bool(); test_skolemize_bug(); + test_sat_smt_ufbv_predicate_model_validation(); } diff --git a/src/test/smt2print_parse.cpp b/src/test/smt2print_parse.cpp index 76b169a4a..83920cabd 100644 --- a/src/test/smt2print_parse.cpp +++ b/src/test/smt2print_parse.cpp @@ -160,6 +160,39 @@ void test_repeated_eval() { Z3_del_context(ctx); } +void test_ho_curried_application() { + char const* spec = + "(set-logic HO_ALL)\n" + "(declare-fun transfer () (-> (-> Int Bool) (-> Int Bool)))\n" + "(assert (forall ((P (-> Int Bool))) (=> (P 0) ((transfer P) 0))))\n" + "(declare-fun top () (-> Int Bool))\n" + "(assert (forall ((x Int)) (top x)))\n" + "(assert (not ((transfer top) 0)))\n" + "(check-sat)\n"; + + Z3_context ctx = Z3_mk_context(nullptr); + Z3_set_error_handler(ctx, setError); + test_eval(ctx, spec, false); + Z3_del_context(ctx); +} + +void test_ho_choice_expression() { + char const* spec = + "(set-logic HO_ALL)\n" + "(declare-sort U 0)\n" + "(declare-fun P () (-> U Bool))\n" + "(assert (exists ((x U)) (P x)))\n" + "(declare-fun witness () U)\n" + "(assert (= witness (choice ((x U)) (P x))))\n" + "(assert (not (P witness)))\n" + "(check-sat)\n"; + + Z3_context ctx = Z3_mk_context(nullptr); + Z3_set_error_handler(ctx, setError); + test_eval(ctx, spec, false); + Z3_del_context(ctx); +} + void test_name(Z3_string spec, Z3_string expected_name) { Z3_context ctx = Z3_mk_context(nullptr); Z3_set_error_handler(ctx, setError); @@ -289,6 +322,8 @@ void tst_smt2print_parse() { // Test ? test_repeated_eval(); + test_ho_curried_application(); + test_ho_choice_expression(); test_symbol_escape(); diff --git a/src/test/tptp.cpp b/src/test/tptp.cpp new file mode 100644 index 000000000..a96f31568 --- /dev/null +++ b/src/test/tptp.cpp @@ -0,0 +1,126 @@ +#include +#include +#include +#include +#include "util/debug.h" +#include "util/error_codes.h" +#include "cmd_context/tptp_frontend.h" + +struct tptp_case { + char const* name; + char const* input; + char const* expected_status; +}; + +static unsigned run_tptp(char const* input, std::string& out, std::string& err) { + std::streambuf* old_out = std::cout.rdbuf(); + std::streambuf* old_err = std::cerr.rdbuf(); + std::ostringstream out_buf; + std::ostringstream err_buf; + std::cout.rdbuf(out_buf.rdbuf()); + std::cerr.rdbuf(err_buf.rdbuf()); + unsigned code = read_tptp_string(input); + std::cout.rdbuf(old_out); + std::cerr.rdbuf(old_err); + out = out_buf.str(); + err = err_buf.str(); + return code; +} + +static std::string run_tptp(char const* input) { + std::string out, err; + unsigned code = run_tptp(input, out, err); + ENSURE(code == 0); + return out; +} + +extern bool g_display_statistics; +extern bool g_display_model; + +void tst_tptp() { + g_display_statistics = false; + g_display_model = false; + std::vector cases = { + {"agatha-butler", +R"(fof(ax1,axiom, lives(agatha)). +fof(ax2,axiom, lives(butler)). +fof(ax3,axiom, lives(charles)). +fof(ax4,axiom, ! [X] : (lives(X) => (X = agatha | X = butler | X = charles))). +fof(ax5,axiom, ! [X,Y] : (killed(X,Y) => hates(X,Y))). +fof(ax6,axiom, ! [X,Y] : (killed(X,Y) => ~ richer(X,Y))). +fof(ax7,axiom, ! [X] : (hates(agatha,X) => ~ hates(charles,X))). +fof(ax8,axiom, ! [X] : (X != butler => hates(agatha,X))). +fof(ax9,axiom, ! [X] : (~ richer(X,agatha) => hates(butler,X))). +fof(ax10,axiom, ! [X] : (hates(agatha,X) => hates(butler,X))). +fof(ax11,axiom, ! [X] : (? [Y] : ~ hates(X,Y))). +fof(ax12,axiom, agatha != butler). +fof(ax13,axiom, ? [X] : killed(X,agatha)). +fof(conj,conjecture, ~ killed(butler,agatha)).)", + "% SZS status Theorem"}, + {"socrates-theorem", +R"(fof(a1,axiom, ! [X] : (human(X) => mortal(X))). +fof(a2,axiom, human(socrates)). +fof(c1,conjecture, mortal(socrates)).)", + "% SZS status Theorem"}, + {"simple-sat", +R"(fof(a1,axiom, p(a)).)", + "% SZS status Satisfiable"}, + {"fof-implicit-forall", +R"(fof(a1,axiom, p(X)). +fof(c1,conjecture, p(a)).)", + "% SZS status Theorem"}, + {"cnf-implicit-forall", +R"(cnf(c1,axiom, p(X)). +cnf(c2,axiom, ~ p(a)).)", + "% SZS status Unsatisfiable"}, +// {"fof-bare-constant-equality", +// R"(fof(a1,axiom, ! [X] : (X = a)). +//fof(c1,conjecture, b = a).)", +// "% SZS status Theorem"}, + {"tff-negative-literal", +R"(tff(c1,conjecture, $less(-2,2)).)", + "% SZS status Theorem"}, + {"tff-rational-literal", +R"(tff(c1,conjecture, $less(1/2,2/3)).)", + "% SZS status Theorem"}, + {"tff-type-decl-arrow", +R"(tff(p_type,type, p: $int > $o ). +tff(a1,axiom, p(1)). +tff(c1,conjecture, p(1)).)", + "% SZS status Theorem"}, + {"tff-typed-int-quantifier", +R"(tff(c1,conjecture, ? [X: $int] : $less(12,X)).)", + "% SZS status Theorem"}, + {"tff-lesseq-built-in", +R"(tff(c1,conjecture, $lesseq(2,2)).)", + "% SZS status Theorem"}, + {"tff-bare-integer-equality", +R"(tff(c1,conjecture, 31 != 12).)", + "% SZS status Theorem"}, + {"tff-decimal-literal", +R"(tff(c1,conjecture, ~ $less(-3.25,-8.69)).)", + "% SZS status Theorem"}, + {"tff-uminus-built-in", +R"(tff(c1,conjecture, $less($uminus(2),0)).)", + "% SZS status Theorem"}, + {"tff-let-single-binding", +R"(tff(c1,conjecture, $let(a: $int, a := 3, $less(a,4))).)", + "% SZS status Theorem"}, + {"tff-let-multiple-bindings", +R"(tff(c1,conjecture, $let([a: $int, b: $int], [a := 1, b := 2], $less($sum(a,b),4))).)", + "% SZS status Theorem"}, + {"tff-let-nested", +R"(tff(c1,conjecture, $let(a: $int, a := 5, $let(b: $int, b := 3, $less(b,a)))).)", + "% SZS status Theorem"} + }; + for (auto const& c : cases) { + std::string out = run_tptp(c.input); + std::cout << c.name << " status: " << c.expected_status << " out: " << out << "\n"; + ENSURE(out.find(c.expected_status) != std::string::npos); + } + + std::string out, err; + unsigned code = run_tptp("tff(c1,conjecture, $less(1/0,1)).", out, err); + ENSURE(code == ERR_PARSER); + ENSURE(err.find("denominator of rational literal cannot be zero") != std::string::npos); +} diff --git a/src/test/udoc_relation.cpp b/src/test/udoc_relation.cpp index c9c6555c5..57017f03c 100644 --- a/src/test/udoc_relation.cpp +++ b/src/test/udoc_relation.cpp @@ -6,7 +6,9 @@ Copyright (c) 2015 Microsoft Corporation #include "muz/rel/udoc_relation.h" #include "util/trace.h" +#include "util/gparams.h" #include "util/vector.h" +#include "util/gparams.h" #include "ast/ast.h" #include "ast/ast_pp.h" #include "ast/reg_decl_plugins.h" @@ -35,6 +37,7 @@ class udoc_tester { struct init { init(ast_manager& m) { + gparams::set("fp.engine", "datalog"); reg_decl_plugins(m); } }; @@ -44,6 +47,7 @@ class udoc_tester { bv_util bv; expr_ref_vector m_vars; smt_params m_smt_params; + params_ref m_fp_params; datalog::register_engine m_reg; datalog::context m_ctx; datalog::rel_context rc; @@ -113,7 +117,7 @@ class udoc_tester { public: udoc_tester(): - m_init(m), bv(m), m_vars(m), m_ctx(m, m_reg, m_smt_params), rc(m_ctx), + m_init(m), bv(m), m_vars(m), m_ctx(m, m_reg, m_smt_params, m_fp_params), rc(m_ctx), p(dynamic_cast(*rc.get_rmanager().get_relation_plugin(symbol("doc")))), cr(dynamic_cast(*rc.get_rmanager().get_relation_plugin(symbol("check_relation")))) { diff --git a/src/util/mpq.cpp b/src/util/mpq.cpp index ddc227847..456508403 100644 --- a/src/util/mpq.cpp +++ b/src/util/mpq.cpp @@ -289,6 +289,25 @@ void mpq_manager::power(mpq const & a, unsigned p, mpq & b) { set(b, 1); return; } + if (eq(a, 1)) { + set(b, 1); + return; + } + if (eq(a, -1)) { + if (p % 2 == 0) + set(b, 1); + else + set(b, -1); + return; + } + if (eq(a, 0)) { + set(b, 0); + return; + } + + if (p > (1 << 20)) + throw default_exception("power is too large to compute"); + unsigned mask = 1; mpq power; set(power, a); diff --git a/src/util/mpz.h b/src/util/mpz.h index 505bb177e..7b714b9be 100644 --- a/src/util/mpz.h +++ b/src/util/mpz.h @@ -314,13 +314,14 @@ class mpz_manager { else { cell = reserve; cell->m_size = 1; + digit_t* cell_digits = reinterpret_cast(cell + 1); if (a.value() < 0) { sign = -1; - cell->m_digits[0] = -a.value(); + cell_digits[0] = -a.value(); } else { sign = 1; - cell->m_digits[0] = a.value(); + cell_digits[0] = a.value(); } } } diff --git a/src/util/scoped_numeral_vector.h b/src/util/scoped_numeral_vector.h index b5cfb69cb..44e625550 100644 --- a/src/util/scoped_numeral_vector.h +++ b/src/util/scoped_numeral_vector.h @@ -28,7 +28,7 @@ public: _scoped_numeral_vector(const _scoped_numeral_vector & other) : m_manager(other.m_manager) { for (unsigned i = 0, e = other.size(); i != e; ++i) { - push_back((*this)[i]); + push_back(other[i]); } } diff --git a/src/util/search_tree.h b/src/util/search_tree.h index 04a222066..85294d550 100644 --- a/src/util/search_tree.h +++ b/src/util/search_tree.h @@ -12,14 +12,19 @@ Abstract: Nodes can be in one of three states: open, closed, or active. - Closed nodes are fully explored (both children are closed). - - Active nodes have no children and are currently being explored. - - Open nodes either have children that are open or are leaves. + - Active nodes are currently assigned to a worker. + - Open nodes are unsolved and available for future activation. - A node can be split if it is active. After splitting, it becomes open and has two open children. + Tree activation follows an SMTS-style policy: prefer nodes in lower + accumulated-attempts bands, and then prefer deeper nodes within the same band. + + Tree expansion is also SMTS-inspired: a timeout does not force an immediate + split. Instead, expansion is gated to avoid overgrowing the tree and prefers + shallow timed-out leaves so that internal nodes can be revisited. Backtracking on a conflict closes all nodes below the last node whose atom is in the conflict set. - Activation searches an open node closest to a seed node. + Activation selects a best-ranked open node using accumulated attempts and depth. Author: @@ -27,9 +32,9 @@ Author: --*/ +#pragma once #include "util/util.h" #include "util/vector.h" -#pragma once namespace search_tree { @@ -41,6 +46,11 @@ namespace search_tree { node *m_left = nullptr, *m_right = nullptr, *m_parent = nullptr; status m_status; vector m_core; + unsigned m_num_activations = 0; + unsigned m_effort_spent = 0; + unsigned m_round_max_effort = 0; + unsigned m_active_workers = 0; + unsigned m_cancel_epoch = 0; public: node(literal const &l, node *parent) : m_literal(l), m_parent(parent), m_status(status::open) {} @@ -58,9 +68,6 @@ namespace search_tree { literal const &get_literal() const { return m_literal; } - bool literal_is_null() const { - return Config::is_null(m_literal); - } void split(literal const &a, literal const &b) { SASSERT(!Config::literal_is_null(a)); SASSERT(!Config::literal_is_null(b)); @@ -70,12 +77,13 @@ namespace search_tree { SASSERT(!m_right); m_left = alloc(node, a, this); m_right = alloc(node, b, this); - m_status = status::open; } node* left() const { return m_left; } node* right() const { return m_right; } node* parent() const { return m_parent; } + bool is_leaf() const { return !m_left && !m_right; } + unsigned depth() const { unsigned d = 0; node* p = m_parent; @@ -86,27 +94,15 @@ namespace search_tree { return d; } - node *find_active_node() { - if (m_status == status::active) - return this; - if (m_status == status::closed) - return nullptr; - node *nodes[2] = {m_left, m_right}; - for (unsigned i = 0; i < 2; ++i) { - auto res = nodes[i] ? nodes[i]->find_active_node() : nullptr; - if (res) - return res; - } - if (m_left->get_status() == status::closed && m_right->get_status() == status::closed) - m_status = status::closed; - return nullptr; - } - void display(std::ostream &out, unsigned indent) const { for (unsigned i = 0; i < indent; ++i) out << " "; Config::display_literal(out, m_literal); - out << (get_status() == status::open ? " (o)" : get_status() == status::closed ? " (c)" : " (a)"); + switch (get_status()) { + case status::open: out << " (o)"; break; + case status::closed: out << " (c)"; break; + case status::active: out << " (a)"; break; + } out << "\n"; if (m_left) m_left->display(out, indent + 2); @@ -123,6 +119,41 @@ namespace search_tree { void clear_core() { m_core.clear(); } + unsigned num_activations() const { + return m_num_activations; + } + void mark_new_activation() { + set_status(status::active); + ++m_num_activations; + ++m_active_workers; + } + void dec_active_workers() { + if (m_active_workers > 0) + --m_active_workers; + if (m_active_workers == 0 && m_status == status::active) { + m_round_max_effort = 0; + m_status = status::open; + } + } + bool has_active_workers() const { + return m_active_workers > 0; + } + unsigned effort_spent() const { + return m_effort_spent; + } + void update_round_max_effort(unsigned effort) { + if (effort <= m_round_max_effort) + return; + m_effort_spent -= m_round_max_effort; + m_round_max_effort = effort; + m_effort_spent += m_round_max_effort; + } + unsigned get_cancel_epoch() const { + return m_cancel_epoch; + } + void inc_cancel_epoch() { + ++m_cancel_epoch; + } }; template class tree { @@ -130,28 +161,114 @@ namespace search_tree { scoped_ptr> m_root = nullptr; literal m_null_literal; random_gen m_rand; + unsigned m_expand_factor = 2; + unsigned m_effort_unit = 1000; + + // Used for tree expansion throttling policy in should_split() + // SMTS says set to num workers, but our experiments show a big regression + // Leaving at 0 for now, but making it configurable for future experimentation + unsigned m_min_tree_size = 0; - // return an active node in the subtree rooted at n, or nullptr if there is none - node *activate_from_root(node *n) { - if (!n) - return nullptr; - if (n->get_status() != status::open) - return nullptr; - auto left = n->left(); - auto right = n->right(); - if (!left && !right) { - n->set_status(status::active); - return n; + struct candidate { + node* n = nullptr; + unsigned scaled_effort = UINT_MAX; + unsigned depth = 0; + }; + + // A measure of how much effort has been spent on the node, used for activation prioritization and expansion decisions + // The effort unit is the workers' initial conflict budget, and effort spent grows by a factor defined in smt_parallel.h on each split attempt + unsigned scaled_effort(node const* n) const { + return n->effort_spent() / std::max(1, m_effort_unit); + } + + // Node selection policy: prefer lower effort bands, then deeper nodes within the same band, and break ties randomly + bool better(candidate const& a, candidate const& b) const { + if (!a.n) + return false; + if (!b.n) + return true; + if (a.scaled_effort != b.scaled_effort) + return a.scaled_effort < b.scaled_effort; + if (a.depth != b.depth) + return a.depth > b.depth; + return false; + } + + void select_next_node(node* cur, status target_status, candidate& best) const { + if (!cur || cur->get_status() == status::closed) + return; + + if (cur->get_status() == target_status) { + candidate cand; + cand.n = cur; + cand.scaled_effort = scaled_effort(cur); + cand.depth = cur->depth(); + + if (better(cand, best)) + best = cand; } - node *nodes[2] = {left, right}; - unsigned index = m_rand(2); - auto child = activate_from_root(nodes[index]); - if (child) - return child; - child = activate_from_root(nodes[1 - index]); - if (child) - return child; - return nullptr; + + select_next_node(cur->left(), target_status, best); + select_next_node(cur->right(), target_status, best); + } + + bool has_unvisited_open_node(node* cur) const { + if (!cur || cur->get_status() == status::closed) + return false; + if (cur->get_status() == status::open && cur->num_activations() == 0) + return true; + return has_unvisited_open_node(cur->left()) || has_unvisited_open_node(cur->right()); + } + + unsigned count_unsolved_nodes(node* cur) const { + if (!cur || cur->get_status() == status::closed) + return 0; + return 1 + count_unsolved_nodes(cur->left()) + count_unsolved_nodes(cur->right()); + } + + unsigned count_active_nodes(node* cur) const { + if (!cur || cur->get_status() == status::closed) + return 0; + return (cur->get_status() == status::active ? 1 : 0) + + count_active_nodes(cur->left()) + + count_active_nodes(cur->right()); + } + + // Find the depth of the shallowest leaf node that at least 1 worker has timed out on + // Used for tree expansion policy + void find_shallowest_timed_out_leaf_depth(node* cur, unsigned& best_depth) const { + if (!cur || cur->get_status() == status::closed) + return; + + if (cur->is_leaf() && cur->effort_spent() > 0) + best_depth = std::min(best_depth, cur->depth()); + + find_shallowest_timed_out_leaf_depth(cur->left(), best_depth); + find_shallowest_timed_out_leaf_depth(cur->right(), best_depth); + } + + bool should_split(node* n) { + if (!n || n->get_status() != status::active || !n->is_leaf()) + return false; + + unsigned num_active_nodes = count_active_nodes(m_root.get()); + unsigned unsolved_tree_size = count_unsolved_nodes(m_root.get()); + + // If the tree is already large compared to the number of active nodes, be more aggressive about splitting to encourage exploration + if (unsolved_tree_size >= num_active_nodes * m_expand_factor) + return false; + + // ONLY throttle when tree is "large enough" + if (unsolved_tree_size >= m_min_tree_size) { + if (has_unvisited_open_node(m_root.get())) // Do not expand if there are still unvisited open nodes (prioritize exploration before expansion) + return false; + if (m_rand(2) != 0) // Random throttling (50% rejection) + return false; + } + + unsigned shallowest_timed_out_leaf_depth = UINT_MAX; + find_shallowest_timed_out_leaf_depth(m_root.get(), shallowest_timed_out_leaf_depth); + return n->depth() == shallowest_timed_out_leaf_depth; } // Bubble to the highest ancestor where ALL literals in the resolvent @@ -223,6 +340,7 @@ namespace search_tree { void close(node *n, vector const &C) { if (!n || n->get_status() == status::closed) return; + n->inc_cancel_epoch(); n->set_status(status::closed); n->set_core(C); close(n->left(), C); @@ -246,8 +364,10 @@ namespace search_tree { node *p = n->parent(); - // The conflict does NOT depend on the decision literal at node n, so n’s split literal is irrelevant to this conflict - // thus the entire subtree under n is closed regardless of the split, so the conflict should be attached higher, at the nearest ancestor that does participate + // The conflict does NOT depend on the decision literal at node n, so n’s decision literal is irrelevant to this conflict + // thus the entire subtree under n is closed, so the conflict should be attached higher, at the nearest ancestor that does participate + // NOTE: I think this is dead code because the backtrack function already walks up to the nearest ancestor whose literal is in the conflict, which is the only place where this is called + // Keep for now since it does generalize this function to be used for arbitrary conflict attachment if (p && all_of(C, [n](auto const &l) { return l != n->get_literal(); })) { close_with_core(p, C); return; @@ -314,20 +434,38 @@ namespace search_tree { m_rand.set_seed(seed); } - void reset() { - m_root = alloc(node, m_null_literal, nullptr); - m_root->set_status(status::active); + void set_effort_unit(unsigned effort_unit) { + m_effort_unit = std::max(1, effort_unit); } - // Split current node if it is active. - // After the call, n is open and has two children. - void split(node *n, literal const &a, literal const &b) { - n->split(a, b); + void reset() { + m_root = alloc(node, m_null_literal, nullptr); + } + + // On timeout, either expand the current leaf or reopen the node for a + // later revisit, depending on the tree-expansion heuristic. + bool try_split(node *n, unsigned cancel_epoch, literal const &a, literal const &b, unsigned effort) { + if (is_lease_canceled(n, cancel_epoch)) + return false; + + // Record at most one effort contribution per concurrent round on this node. + // Stale workers still contribute, but only via the round-local maximum. + n->update_round_max_effort(effort); + bool did_split = false; + + if (should_split(n)) { + n->split(a, b); + did_split = true; + } + + return did_split; } // conflict is given by a set of literals. // they are subsets of the literals on the path from root to n AND the external assumption literals void backtrack(node *n, vector const &conflict) { + if (!n) + return; if (conflict.empty()) { close_with_core(m_root.get(), conflict); return; @@ -349,6 +487,8 @@ namespace search_tree { // Walk upward to find the nearest ancestor whose decision participates in the conflict while (n) { + // Does the UNSAT core contain the decision literal at node n? + // If yes, i.e. if the core contains n->literal, then the conflict depends on the decision made at node n. if (any_of(conflict, [&](auto const &a) { return a == n->get_literal(); })) { // close the subtree under n (preserves core attached to n), and attempt to resolve upwards close_with_core(n, conflict); @@ -360,48 +500,52 @@ namespace search_tree { UNREACHABLE(); } - // return an active node in the tree, or nullptr if there is none - // first check if there is a node to activate under n, - // if not, go up the tree and try to activate a sibling subtree - node *activate_node(node *n) { - if (!n) { - if (m_root->get_status() == status::active) - return m_root.get(); - n = m_root.get(); + // Try to select an open node using the select_next_node policy + // If there are no open nodes, try to select an active node for portfolio solving + node* activate_best_node() { + candidate best; + select_next_node(m_root.get(), status::open, best); + if (!best.n) { + IF_VERBOSE(1, verbose_stream() << "NO OPEN NODES, trying active nodes for portfolio solving\n";); + select_next_node(m_root.get(), status::active, best); // If no open nodes, only then consider active nodes for selection } - auto res = activate_from_root(n); - if (res) - return res; - auto p = n->parent(); - while (p) { - if (p->left() && p->left()->get_status() == status::closed && - p->right() && p->right()->get_status() == status::closed) { - if (p->get_status() != status::closed) - return nullptr; // inconsistent state - n = p; - p = n->parent(); - continue; - } - if (n == p->left()) { - res = activate_from_root(p->right()); - if (res) - return res; - } - else { - VERIFY(n == p->right()); - res = activate_from_root(p->left()); - if (res) - return res; - } - n = p; - p = n->parent(); - } - return nullptr; + if (!best.n) + return nullptr; + best.n->mark_new_activation(); + return best.n; } - node *find_active_node() { - return m_root->find_active_node(); + node* activate_root() { + if (m_root->get_status() == status::closed) + return nullptr; + m_root->mark_new_activation(); + return m_root.get(); + } + + void find_nonclosed_nodes_with_literal(literal const& lit, ptr_vector>& out) { + find_nonclosed_nodes_with_literal_rec(m_root.get(), lit, out); + } + + void find_nonclosed_nodes_with_literal_rec(node* n, literal const& lit, ptr_vector>& out) { + if (!n) + return; + + if (!Config::literal_is_null(n->get_literal()) && n->get_literal() == lit && n->get_status() != status::closed) + out.push_back(n); + + find_nonclosed_nodes_with_literal_rec(n->left(), lit, out); + find_nonclosed_nodes_with_literal_rec(n->right(), lit, out); + } + + void dec_active_workers(node* n) { + if (!n) + return; + n->dec_active_workers(); + } + + bool is_lease_canceled(node* n, unsigned cancel_epoch) const { + return !n || n->get_status() == status::closed || n->get_cancel_epoch() != cancel_epoch; } vector const &get_core_from_root() const { diff --git a/src/util/timeout.cpp b/src/util/timeout.cpp index c030c2ec6..f640db1a5 100644 --- a/src/util/timeout.cpp +++ b/src/util/timeout.cpp @@ -19,6 +19,7 @@ Revision History: --*/ #include +#include #include "util/util.h" #include "util/timeout.h" #include "util/error_codes.h" @@ -29,15 +30,28 @@ Revision History: static scoped_timer * g_timeout = nullptr; static void (* g_on_timeout)() = nullptr; +static void do_timeout() { + std::cout << "timeout\n"; + std::cout.flush(); + if (g_on_timeout) + g_on_timeout(); +} + +#ifdef SIGXCPU +// React to SIGXCPU (an external CPU limit, e.g. ulimit -t) like a -T timeout. +static void STD_CALL on_sigxcpu(int) { + signal(SIGXCPU, SIG_DFL); + do_timeout(); + raise(SIGXCPU); +} +#endif + namespace { class g_timeout_eh : public event_handler { public: void operator()(event_handler_caller_t caller_id) override { m_caller_id = caller_id; - std::cout << "timeout\n"; - std::cout.flush(); - if (g_on_timeout) - g_on_timeout(); + do_timeout(); throw z3_error(ERR_TIMEOUT); } }; @@ -56,4 +70,8 @@ void disable_timeout() { void register_on_timeout_proc(void (*proc)()) { g_on_timeout = proc; +#ifdef SIGXCPU + // Handle external CPU limits (SIGXCPU) like our own timeouts. + signal(SIGXCPU, on_sigxcpu); +#endif } diff --git a/src/util/trace_tags.def b/src/util/trace_tags.def index 67adb62c9..d343b2304 100644 --- a/src/util/trace_tags.def +++ b/src/util/trace_tags.def @@ -56,6 +56,7 @@ X(ctx_propagate_assertions, assert_eq_bug, "assert eq bug") X(ctx_solver_simplify_tactic, ctx_solver_simplify_tactic, "ctx solver simplify tactic") X(default_qm_plugin, default_qm_plugin, "default qm plugin") +X(default_qm_plugin, ho_matching, "ho matching") X(default_qm_plugin, mam_stats, "mam stats") X(default_qm_plugin, quantifier, "quantifier")